注目されるAlexa、そして開発中引っかかった話

![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/9feaa986-e7fb-6717-db41-560e473666b5.png)
[出處](https://www.livewirelabs.co/voice-user-interface-vuis-future-in-mobile-app-an-overview/)
最近Alexa, Cortana, Siri, Assistant などVUIサービスが多様にリリースされています。
その中で、自分なりのチャレンジで盛り上がっています。

今年は特に色んな機会を通じて良くチャレンジができました。
VUIを企画しながらびっくりするぐらい効果を高める道が見える時はすごく盛り上がります。
(まだ、作って壊してを繰り返しているが・・・ -_)
ざっくりこのような機会がありました。

- 引き取っている自社サービスのVUI導入企画・設計・デモ実装
- 1年前から本格的に活動しているゴスペルに何か役になるVUIサービスを企画
- [「Alexa Salon special @福岡 powered by Fusic」](https://fusic.co.jp/doings/226?fbclid=IwAR3AueIu6-s3MXNiIgzeZic6ol85m-2EaHVsRBefeMxTu3ig_U4ENreZkxM)
- VUI関連実務案件
- エンジニア合宿
- など

今のところで一番感じているのは、VUI設計の段階で作りたいパフォーマンスが**開発プラットフォームで本当に実現可能か**を先に調べたほうが重要だということです。以前、多分できると思って確認せずに要件定義・設計を進めましたが、結局構築の段階で頻繁な制限との衝突で設計を変えたり結構カオスになってしまったことがあります。

そして、言語によってサポートされて無い部分があったりしますので、特に先に確認しないといけません。
例えばAlexaの日本語にした場合は、*AMAZON.FallbackIntent*, *Follow-Up Mode*, *AMAZON.LITRAL*がサポートされないです。
Google Assistantの場合は、利用してするディバイスがスピーカーじゃないと色々サポートできない部分があります。
結構痛いところですね。
作りたいインタフェースの動作に関してはできるだけミニマムでモックを作ってみて実装可能を確認した上で設計することをおすすめします。

とりあえず、VUI実装してみた話を3日間の記事で喋っていきたいと思います。
日本語が下手なので読みにくいと思いますが、よろしくお願いいたします！

- [ ] 「言葉の帰還、消えたインターフェイス」 
- [x] 「注目されるAlexa、そして開発中引っかかった話」 
- [ ] 「Assistantの実務開発中引っかかった話」   

#概要
さて、今日はAmazon Alexaの話です。
AmazonのAlexaはEcho, Echo Dot, Spotなどのハードを利用したクラウド基盤の人工機能AUIサービスです。内部的には装着された7つのマイクロ音声を受信して曖昧な自然言語をAIで正確に認識します。変換されたバイナリーを「Speech to Text」でテキストに変換し、分析後、構造化して意図判断(Intent)とスロット(Slot)を割り付けます。という眠くなる話は、[Advent Calendar 2017の記事](https://qiita.com/leedohyung-dba/items/a7ae594e1d455e2f962c)で書いた書いた気がいしますのでやめます。

ま、スマホで画面をタッチインタフェースを理論的にちゃんと理解した上で開発と利用をしてはいないように、一つのインタフェースに利用するほうが良いと思います。ただし、スマホが起動されてホームボタンを押して画面がActiveになった上でタッチができるように、息の力とかハンマーの手段で利用できないというのと同じく基本的な制約とルールは認知していなければなりません。

#目次
 - 注目されるAlexa
 - 開発中引っかかった話

#注目されるAlexa
最近 Cortana, Siri, Assistant などVUIサービスが多様にリリースされています。
この状態の上で、なぜAlexaは他に比べてすごく注目されているか気になると思います。
自分なりで理由を申し上げますと2つあります。

1番目は、Alexaは開発プラットフォームがプラットフォームが開放されています。
つまり、自社の音声認識技術を積極的に開放していることですね。
それによってサードパーティ企業が活躍することができ、どんどんAlexaの魅力は高くんなりました。

2番目は、Amazon.comの大きなECサービスが背景であることだと思います。
とりあえず、2018年6月基準でアメリカには4万個以上、日本には1500個以上のスキールが開発されているらしいです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/a7224492-e16a-2fd3-7c84-894abaefe129.png)
[出處](https://voicebot.ai/2018/09/27/alexa-skills-officially-pass-40000-in-us-and-1500-in-japan/)

#開発中引っかかった話
次は開発(ASK Custom)を進めながら引っかかった話をさせて頂きます。

##スキールBuildツール
Alexaデブコンソール画面であちこちで移動しながらVUIリソースを設定したりすることは面倒くさいです。そして、何をどう設定したか、毎回エキスポートしておかないといけないし管理も面倒くさいです。最悪、手順書も書かないと行けなくなります。

その理由で、自分も最初はコンソールでやってましたが、すぐ管理ツールを入れて開発しています。自分は[**ask-cli**](https:///www.npmjs。com/package/ask-cli)を使ってから, Alexa Skill開発をServerless Frameworkで統合管理するために[**serverless-alexa-skills**](https://github.com/marcy-terui/serverless-alexa-skills)プラグインに乗り換えました。両方ともスキルとモデルをコードで管理できます。
入門は[関連ブログ](http://marcy.hatenablog.com/entry/2017/12/14/000047)を参考にしていただければ簡単に使えます。

するとウェブページを一切開かずに 

```log
- EndpointのLambda関数を生成して、
 ➔ 関数をデプロイする。

- Skillを生成して、
 ➔ 基本設定を行って、
 ➔ 設計したモデルを設定して、
 ➔ スキルをビルドする
```
が完結できます。👍

やってみましょうかね。

> Serverlessプロジェクト生成とserverless-alexa-skills設置する

![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/c0f245fa-c186-6ff9-31ca-a8154524fd93.png)
> EndpointのLambda関数を生成してデプロイする

```serverless.yml
service: sigdemo

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1
  environment:
    APP_ID: ${env:ALEXA_SKILL_ID}  # empty
    AIPO_KEY: ${env:AIPO_KEY}

package:
  include:
    - node_modules/

functions:
  skillHandler:
    name: sigdemo
    description: sig demo
    handler: src/index.handler
    memorySize: 128
    timeout: 60
    events:
      - alexaSkill:
          appId: ${env:ALEXA_SKILL_ID} # empty
          enabled: true
```
```index.js
'use strict'
const Alexa = require('alexa-sdk');
const APP_ID = process.env.APP_ID;

const defaultHandler = require('./handlers/default_handler.js');

module.exports.handler = (event, context, callback) => {
  let alexa = Alexa.handler(event, context, callback);
  alexa.dynamoDBTableName = 'sigdemo';
  alexa.registerHandlers(defaultHandler);
  alexa.APP_ID = APP_ID;
  alexa.execute();
}
```
```default_handler.js
const commonLib = require('../lib/common.js');
const Aipo = require('../lib/Aipo.js');
const commonConst = require('../consts/common.js');

const AIPO_KEY = process.env.AIPO_KEY;

module.exports = {
  'LaunchRequest': function () {
    this.emit('AskWelcome');
  },

  'AskWelcome': function() {
    let speakValue = commonLib.voiceEffect.effect('こんにちは！');
    this.emit(':ask', speakValue, '次のオーダーしてください！');
  },

  'SkillIntroduceIntent': function() {
    let speakValue = '私はいろいろできるよ！ｗ';
    this.response.speak(speakValue).listen('次のオーダーしてください！');
    this.emit(':responseReady');
  },

  'AskForTheMeetingRoomReservation': function() {
    const mr = this.event.request.intent.slots.meeting_room || false;
    const t = this.event.request.intent.slots.time || false;
    const aipoObj = Aipo.new(AIPO_KEY);
    let speakValue = commonLib.voiceEffect.slow(aipoObj.isEmpty(mr, t));
    this.emit(':tell', speakValue);
  },

  'AMAZON.StopIntent': function () {
    this.response.speak('さよなら！');
    this.emit(':responseReady');
  },

  'AMAZON.HelpIntent': function() {
    let speakValue = '時間と会議室を聞いてみてください。';
    this.response.speak(speakValue).listen('次のオーダーしてください！');
    this.emit(':responseReady');
  },

  'AMAZON.CancelIntent': function() {
    this.response.speak('さよなら！');
    this.emit(':responseReady');
  },

  'AMAZON.FallbackIntent': function() {
    this.emit(':ask', "すみません、質問が分かりません。", 'もう一度！');
  },
};
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/17282534-0ec4-24e4-6573-bc24703d4cc2.png)

> Alexa Skillを生成する

![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/1d6c7bb9-9544-bb8a-cc21-108851b81385.png)

>基本設定を行って、
>設計したモデルを設定して、
>スキルをビルドする

```
service: sigdemo

provider:
  name: aws
  runtime: nodejs6.10
  stage: dev
  region: ap-northeast-1
  environment:
    APP_ID: ${env:ALEXA_SKILL_ID}
    AIPO_KEY: ${env:AIPO_KEY}

package:
  include:
    - node_modules/

functions:
  skillHandler:
    name: sigdemo
    description: sig demo
    handler: src/index.handler
    memorySize: 128
    timeout: 60
    events:
      - alexaSkill:
          appId: ${env:ALEXA_SKILL_ID}
          enabled: true

plugins:
  - serverless-alexa-skills

custom:
  alexa:
    vendorId: ${env:YOUR_AMAZON_VENDOR_ID}
    clientId: ${env:YOUR_AMAZON_CLIENT_ID}
    clientSecret: ${env:YOUR_AMAZON_CLIENT_SECRET}
    skills:
      - id: ${env:ALEXA_SKILL_ID}
        skillManifest:
          publishingInformation:
            locales:
              ja-JP:
                name: sample
          apis:
            custom: {
              "endpoint": {
                "uri": "${env:LAMBDA_FUNCTION_URI}"
              }
            }
          manifestVersion: '1.0'
        models:
          ja-JP:
            interactionModel:
              languageModel:
                invocationName: sigmon
                intents:
                  - name: SkillIntroduceIntent
                    samples:
                      - '何ができますか'
                  - name: AskForTheMeetingRoomReservation
                    slots:
                        - name: meeting_room
                          type: MEETING_ROOM
                        - name: time
                          type: AMAZON.TIME
                    samples:
                      - "{time} に {meeting_room} の会議室は空いていますか"
                      - "{meeting_room} の会議室は {time} に空いていますか"
                      - "会議室 {meeting_room} は {time} に空いていますか"
                      - "会議室 {meeting_room} はいつ空きますか"
                      - "{time} に空いている会議室を教えてください"
                types:
                  - values:
                      - name:
                          value: 'mercury'
                          synonyms: 
                            - 'マーキュリー'
                      - name:
                          value: 'mars'
                          synonyms: 
                            - 'マース'
                      - name:
                          value: 'neptune'
                          synonyms: 
                            - 'ネプチューン'
                      - name:
                          value: 'jupiter'
                          synonyms: 
                            - 'ジュピター'
                      - name:
                          value: 'pluto'
                          synonyms: 
                            - 'プルートー'
                    name: MEETING_ROOM
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/5af9e3d9-ae85-ff68-e36f-e4ddd3d38113.png)

>呼び出し名を変更して再ビルド

![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/64bf8212-15cf-e753-1c54-896e6ffa4eaf.png)
これで、自分のスキルの生成、モデル定義、ビルドが全部終わりです！
✍：テストもCLIでできないかな？！

良くできているか確認するため、デブコンソールにアクセスして確認します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/47875942-24f4-0325-fdbe-4b17c5b8a8e7.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/01e04fbc-c117-520a-840a-d405bd7d8f55.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/ab7b83a8-13be-e4ac-419d-64d839bf8cf8.png)
いい感じですね！👏👏👏
動作テストもやってみます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/6d03e129-c5bc-ac10-82dd-f05dd60ed400.png)
いい感じですね！👏👏👏

##セッション情報を継続的に維持したい
基本対話が終了されたらの対話のセッション(sessionAttributes)は削除されます。だが、対話のセッションを維持して行きたい時がありますね。その方法は悩むことすら要らないぐらい素敵な方法がありまして紹介します。

多分Alexa-SDで良く開発している方はすでにご存知だと思ってますが、セッション情報をDynamoDBに継続的に保存して行く方法です。DynamoDBに対話中のセッションを入れ込むのは、方法の説明も要らないぐらいすごく簡単です。

権限がちゃんと当たってるなら、Alexa-SDK Ver1の場合、

```index.js
module.exports.handler = (event, context, callback) => {
  let alexa = Alexa.handler(event, context, callback);
  alexa.dynamoDBTableName = 'sigdemo';
  alexa.registerHandlers(defaultHandler);
  alexa.APP_ID = APP_ID;
  alexa.execute();
}
```
のように1行のテーブル名を定義したおけば、テーブル生成とともにセッションデータのsessionAttributesがそのままテーブルに埋め込みます。
もちろん、対話が終わって、再開しするときも、特にDynamoDBから読み込み処理は必要はありません。自動にsessionAttributesにセットされて、いつも通り、セションを利用したらOKです。さらに、追加されたレコードは自動にuserIdの固有キーでColumnが追加されるので、Alexa-SDK以外のところでも識別が可能です。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/bae34833-28f0-5e24-bee1-4e02268f5a98.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/eef2f5b9-3f95-fdcd-989e-c59332ce3633.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/660cb1d0-abe4-8bc4-280c-2ebb310c6289.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/c38eb721-d225-4a99-a3c4-15e597f8bc65.png)
これで、対話のセッション情報をの継続に維持して活用したら色んなことができそうですね！（ﾄﾞｷﾄﾞｷ）
分かりやすく説明されているこの[ブログ](https://dev.classmethod.jp/cloud/alexa-sdk-dynamodb/)を参考してください。
また、Alexa-SDK Ver2を利用している方は[こちら](https://dev.classmethod.jp/cloud/alexa-sdk-v2-dynamodb-tips/)を参考してください。

一つ注意点は、暗黙的に全てのセッション情報がDynamoDBに入れ込んでしまうので、**無意識で入って継続的に維持されるセッション** 情報のためバグが発生される可能性もあります。ちゃんと設計の段階で継続的に持っていく対話セッションデータを設計した方が良いと思います。

##WEBサイトユーザー情報との連携
Endpointで、スキル識別キー、アカウント識別キー、ハード識別キーを簡単に取得できます。
詳しくには調査した方の[ブログ](https://dev.classmethod.jp/cloud/alexa-skill-applicationid-deviceid-userid/)を参考してください。
自分の場合はuserIdを利用して以下の流れでWEBサービスとのアカウント連携を実装してみました。
※ userId取得 : `event.context.System.user.userId`

```log
1．専用Skillで自分のハード登録リクエスト ➔ ユーザーコード、生年月日をスピーチ
 ㄴ Skill ➔ 一致するユーザーに対するメールに認証コード送信
2．専用Skillで自分のハード登録認証コード確認 ➔ メールから確認した認証コードをスピーチ
 ㄴ Skill ➔ userIdを取得してDBに入力
3．専用Skillで一般機能使う
 ㄴ 対話中のuserIdに対するUser情報を取得
 ㄴ そのuserIdに対するセッション情報(sessionAttributes)のDynamoDBレコードにUser情報を入れ込む
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/9b53afef-5d32-157b-6ae6-31346985cafc.png)

##処理時間の制限
Alexa Skillにも、処理時間の制限があります。
Google Assistantスキルを構築する時は、10秒の短い処理制限時間がありまして、結局開発サポートセンターに問い合わせ、10秒以上伸ばせる方法はありませんと言われて設計を変えたことがありました。

反面、他の開発プラットフォームより圧倒的に時間が長いです。重い外部データを取得してくるときや、色んなパフォーマンスを再現することにはとても嬉しいところでしょう。そして、Progressive ResponseでSSMLのaudioタグやbreakタグを利用したら結構長い処理時間を確保できます。
> 1回の応答に含めることのできるすべての音声ファイルの合計再生時間は240秒まで

```default_handler.js
...
'AskForTheMeetingRoomReservation': function() {
    const mr = this.event.request.intent.slots.meeting_room || false;
    const t = this.event.request.intent.slots.time || false;
    const aipoObj = Aipo.new(AIPO_KEY);
 
    const requestId = this.event.request.requestId;
    const token = this.event.context.System.apiAccessToken;
    const endpoint = this.event.context.System.apiEndpoint;
    const ds = new Alexa.services.DirectiveService();
 
    const audio = 'https://s3-ap-northeast-1.amazonaws.com/devio-blog-data/output.mp3'; //30秒
    const ssmlTA = '<speak>確認しますので、少々お待ち下さい<audio src="' + audio + '" /></speak>';
    const ssmlBA = '<speak><break time="10s"/><audio src="' + audio + '" /></speak>';
 
    const directive1 = new Alexa.directives.VoicePlayerSpeakDirective(requestId, ssmlTA);
    const directive2 = new Alexa.directives.VoicePlayerSpeakDirective(requestId, ssmlBA);
    const directive3 = new Alexa.directives.VoicePlayerSpeakDirective(requestId, ssmlBA);
    const directive4 = new Alexa.directives.VoicePlayerSpeakDirective(requestId, ssmlBA);
    const directive5 = new Alexa.directives.VoicePlayerSpeakDirective(requestId, ssmlBA);
    const progressiveRes1 = ds.enqueue(directive1, endpoint, token);
    const progressiveRes2 = ds.enqueue(directive2, endpoint, token);
    const progressiveRes3 = ds.enqueue(directive3, endpoint, token);
    const progressiveRes4 = ds.enqueue(directive4, endpoint, token);
    const progressiveRes5 = ds.enqueue(directive5, endpoint, token);
    const serviceCall = aipoObj.isEmpty(mr, t); // use Promise

    Promise.race([progressiveRes1, progressiveRes2, progressiveRes3, progressiveRes4, progressiveRes5, serviceCall])
    .then( speakValue => {
        this.emit(':tell', speakValue);
    });
  },
...
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/b3e2b728-ccc3-7e09-1a03-40058a3342c8.png)

😱あら、テストコンソールではうまくできましたが、時間あるとき Echoハードで実際動作を確認してみます。
参考してた[ブログ](https://dev.classmethod.jp/cloud/alexa-progressive-response/)で以下の内容を確認しました。
> なお、Progressive Responseを使用しても、レスポンスの制限時間（8秒以内）を超えることは出来ませんので注意して下さい。
>> まさか、、、8秒はだめ！！！！！！！

少し長くなりまして、以下からはササッといきます。

##反応待機時間の制限
次はユーザーの反応を待つ時間の話です。
制限時間は8秒ですごく短いです。
※ [10秒だとする方](https://forums.developer.amazon.com/questions/113737/alexa%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20.html)もいるらしいですが、自分は間違いなく8秒でした。
設計しながら、たまに利用者が8秒以内で回答できない可能性が高い部分が生まれます。
なぜかとすると、ユーザーは対話の中で情報を取得して、それに基づいた次の回答をするからですね。
理解する時間は置いておいて、悩む時間が発生されやすいです。
###### *間違って設計してしまうと、いつ切れるかはらはらされながら使う最悪のUIになってしまいます。*

とりあえず、この制限を乗り越えるため、結局自分は以下の方針で設計しました。
①. Audioで BGMを流して 利用者の反応をスタンバイする。音楽再生アプリを実装して音楽の途中にストップをかけるパターンと同じ
②. スタンバイの最大時間を超えて終了される場合、それを許して、DynamoDBに保存されている 対話ステータスセッション値を参照、次の対話の開始に利用する。

##Push Notification申請
やっぱり、VUIを設計しながらは、色んなところで Push通知が必要になる場合がたくさん出てきます。
とりあえず申請しましたが、3っヶ月間回答なし… 他に申請した方に聞いてみると半年間回答なしだと…( ﾟдﾟ;)彡
※ [申し込みページ](https://alexa.au1.qualtrics.com/jfe/form/SV_72lplGGegxNZ9Ln)
※ [申請受けて設定したブログ](https://blog.r3it.com/alexa-day-2018-73928e3b3923)

##キャッチボール方式の対話の形は必須なのか
まだ、もうちょっと試してみるところはありますが、現時点では必須になると思います。
つまり、ボールを継続で一般的にもらいっぱなしの方法はありませんでした。もちろん、Push Notificationsで実現可能ですが、上端で話を申し上げましたとおり、申請がなかなか受け取れませんので…
※ Google Assistantの方はPush Notificationsが特に申請せずに設定だけで可能でしたが、一応開発サポートセンターに問い合わせしたら、実現不可能だと回答もらいました。

##そのほか！
大変申し訳ございませんが、アドベントカレンダーの日付を超えてしまって、
その他に引っかかった内容はタイトルだけ貼って置きます！_(._.)_
#### 発音が良くない自分はコマンドが間違った場合、すぐ対話終了されちゃうことは不面
#### 毎回スキールを呼び出しするのは面倒くさい
#### Alexaにスピーチを録音させて他の利用者のハードに投げること
#### SSML面白い！
#### Follow-Up Mode

以上、ご覧くださって、ありがとうございました！