Alexa Skill Kitの紹介＆GitHubの音声認識インタフェース能力実装

## **・概要**
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/7ffce5a8-e099-2cd8-7975-903ea8766d25.png)
※ 画嬢出所 ： [Wink Amazon Alexa](https://www.wink.com/products/amazon-alexa/)

Amazonが2017年11月8日(水), クラウド基盤の音声サービスAmazon Alexaの**日本語も使えるように**なりました。
これで、英語が弱い人も使えるようになりましたね！素晴らしい！
そして3種類のデバイスAmazon Echo, Echo Plus, Echo Dotが国内で販売を開始しました。
今年の年末にAlexaのニュースが🔥 ホット🔥しますね。

装置を安価に配布してサービスで取り巻くアマゾンの勝利の方程式に画期的なサービスを出したいと思いました！
ちなみに、`何ヶ月前まで国内ではまだ技適に対応しておらず、通常利用では電波法違反となる可能性があるので、禁止されたらしいでした。`

どこで見たか忘れましたが、ある記事で以下の画嬢がありました。
何かピンと来ましてするキャプチャー取りました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/2ceac932-33ce-1256-504d-65c6d1e3623e.png)
皆さんはどうですか？
何かピンときますか？
何か軍で揮官が兵士に命令をしてるような…ウハハハ

ということで、弊社の**エンジニア合宿**の機会で開発してみました。
それは**Amazon Echo Dot**を利用して**業務の効率性を高めるASK(Alexa Skill Kit)**です。
（モニターとキーボードをなるべくIDEから離れないことを希望してそれに役に立つ…）
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/00be66e1-9043-f3a6-4a7d-367ed402b40a.png)

**※ 弊社の合宿**
   - 仕事関係は一切禁止、あくまで興味を高めるための面白いチャレンジを最優先でやる合宿

楽しみで作りたかったのは以下の通りでした。
・スキルにGithubの音声認識インタフェース
・Aipoのスケジュール(自分以外)を取得した他人の位置案内
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/a9744624-4536-c923-6cd2-0177cd2fd1e2.png)

## **・Alexaの基本原理を理解**
詳しい内容は公式ホームページのチュートリアルや色んな記事がいっぱいあるので、
今回の実装内容を理解するため、ざっくり基本原理を説明させていただきたいと思っております。

まず、「Alexa」とはクラウドベースの音声対話エージェントのことを指し、
「Echo」は、Alexaを利用するためのホームスピーカーでございます。

また、Alexaはサードパーティー会社が使用できるようにSDKが公開されています。
一つが音声認識や自然言語解釈等を行うAlexa Voice Service(AVS)、
他の一つが様々な機能をSkillという形でパッケージングしてAlexa ServiceとつなげるAlexa Skill Kit(ASK)です。
なので、自分はAlexaに新たな機能を与えるASKを利用しました。

どういう流れで動くんでしょうかざっくり説明させて頂きます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/ec4cf1b8-095f-f019-bc31-c2cc1cc5eda8.png)※ 画嬢出所 ： [Introduction to building alexa skills and putting your amazon echo to work](https://www.slideshare.net/abediaz/introduction-to-building-alexa-skills-and-putting-your-amazon-echo-to-work
)

1．使用者が命令すると、
2．Alexaが利用者の要請を分析して構造化した後、サービスに転送して、
3．サービスは要請を処理してテキストまたはグラフィカルな応答をします。

4．Alexaが返してもらったテキストを音声に変換したあと装置でストリームして、
5．装置はその回答をプレイします。

つまり、どのような音声に反応するか、
どのような単語をパラメータにしてどの機能を実行するのか、
返ってきた答えをどのようにAlexaに戻すのか、
これらを定義したパッケージ、API群をAlexa Skill Kitと呼び、
そのうち実際に実行される機能の部分をSkillと言います。
ASKを使ってSkillを作る、という理解がわかりやすいと思います。

### ▶ **対話構文**
次は人がAlexaと対話するための**構文**です。

| Wake Word|       Launch |    Skill Name    |    Utterance    |
|:-----------------|------------------:|:------------------|------------------:|
|       Echo|           ask|        she|    How is the weather today?|
| Alexa       |       launch |    she    |    Play the music.    |
| Amazon       |       load |    she    |    Please tell me the bug issue.    |

それぞれの文句を説明します。
### ▶ **Wake Word（Alexaを起こす）**
この文句をリクエストするとデバイスは青色がくるりと回ってレスポンスし、
これで対話の始まりになります。
このキーワードは[Alexa Device Manager](https://alexa.amazon.com/spa/index.html)ページからamazonやechoなど、他の名に切り替えます。

### ▶ **Launch（Skillを選ぶ）**
この次に来る文句がSkill名になります。
複数のSkillが入ってますので、どのSkillにリクエストを投げるかを選びます。
Launchの文句は 様々の種類がありますので、[ドキュメント](https://developer.amazon.com/docs/custom-skills/understanding-how-users-invoke-custom-skills.html
)を参考してください。
また、他の接続詞を利用して順番が後ろになる場合もあります。
例えば、`「Echo, please tell me the bug issue from Anime Facts」` で
呼んだ場合も、`from` からその後Skill名だと判断します。

### ▶ **Skill Name**
Alexa Device Managerから有効にしたスキル名でございます。
例えば自分が利用している以下のスキルでしたら、
「Echo open Top Music Chart」にします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/f0b56bf5-9bbc-8d9a-5427-62c435011047.png)

### ▶ **Utterance**
意図を実行する単語及び入力構文です。
「ホテルを予約してくれよ」、「花を注文してくれよ」ようなことです。
自分は発音が悪いですが、中にDeep Learningが走ってるので、いい感じに聞き取れます。

ざっくり説明すると言いながら、少し長くなってしまいました。
一応説明がこのぐらいでお終いします。
その後は、実装の話をしながら必要なところに説明差し上げます。

## **・挨拶できる"SHE"スキル実装**
まず、こういう単純なスキルを世の中に送り出して見て
生成の流れを理解しましょう。

### **▶実装ステップ ①**
スキルを構築する最初のステップは以下の項目を設計することです。
 それぞれの設計項目に挨拶できる"SHE"の通り付けてました。(\☺/)

#### **Intent**
 – 達成しようとする目標自体でございます。
 – 「ホテルの予約」、「花の注文」ようなことです。
(\☺/) 友達になって挨拶する 

#### **Utterance**
 – 意図をを実行する単語及び入力構文です。
 – 「ホテルを予約してくれよ」、「花を注文してくれよ」ようなことです。
(\☺/) 友達になってくれ ！ ・ 調子大丈夫？

#### **Slots**
 – 各Slotは利用者の意図を達成するため提供する小さいデータでございます。
 – 旅行アナウンス関連スキルなら都市、空港などの情報をリストアップした後、利用することができます。
(\☺/) FriendCode

#### **Prompt**
 – 利用者が意図を達成するための一部データを提供するようにリクエストする質問でございます。
(\☺/) Lambda Linux 6.10

これで単純な挨拶が出来るスキル**【she】**に対して
**達成目標(Intent)を定義し、それに対する意図(Utterance)を追加してそれに対するアクションを定義**しました。

### **▶実装ステップ ②**
次はAWS Lambda関数を新規登録します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/91121007-da57-6efd-3b54-cdd3f1d1cb67.png)
以下はそのソースコードでございます。

```js:aisatu.js
'use strict';

// レスポンスサポートする

function buildSpeechletResponse(title, output, repromptText, shouldEndSession) {
    return {
        outputSpeech: {
            type: 'PlainText',
            text: output,
        },
        card: {
            type: 'Simple',
            title: `SessionSpeechlet - ${title}`,
            content: `SessionSpeechlet - ${output}`,
        },
        reprompt: {
            outputSpeech: {
                type: 'PlainText',
                text: repromptText,
            },
        },
        shouldEndSession,
    };
}

function buildResponse(sessionAttributes, speechletResponse) {
    return {
        version: '1.0',
        sessionAttributes,
        response: speechletResponse,
    };
}


// スキルの動作を制御する

/**
 * スキルがLuncchされた時のレスポンス
 */
function getHelloResponse(callback) {
    const sessionAttributes = {};
    const cardTitle = 'Hello';
    const speechOutput = 'Hello, my name is she.';
    const repromptText = 'sorry, I can not understand.';

    // 次の会話にセッションをリセットするか
    const shouldEndSession = false;

    callback(sessionAttributes,
        buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

/**
 * 対話のセッションが切れた時のレスポンス
 */
function handleSessionEndRequest(callback) {
    const cardTitle = 'Session Ended';
    const speechOutput = 'Thank you., Have a nice day!';
    const shouldEndSession = true;

    callback({}, buildSpeechletResponse(cardTitle, speechOutput, null, shouldEndSession));
}

function createFriendCodeAttributes(code) {
    return {
        code,
    };
}

/**
 * コードセッションを設定し、利用者に回答する動作定義
 */
function setCodeInSession(intent, session, callback) {
    const cardTitle = intent.name;
    const friendCodeSlot = intent.slots.FriendCode;
    let repromptText = '';
    let sessionAttributes = {};
    const shouldEndSession = false;
    let speechOutput = '';

    if (friendCodeSlot) {
        const friendCode = friendCodeSlot.value;
        sessionAttributes = createFriendCodeAttributes(friendCode);
        speechOutput = `Hello ${friendCode}., I now know your friend code is ${friendCode}. You can ask me, Do you know me?`;
    } else {
        speechOutput = "I'm not sure what your friend code is. Please try again.";
    }

    callback(sessionAttributes,
         buildSpeechletResponse(cardTitle, speechOutput, repromptText, shouldEndSession));
}

/**
 * 友達コードを回答する
 */
function getCodeFromSession(intent, session, callback) {
    let friendCode;
    const repromptText = null;
    const sessionAttributes = {};
    let shouldEndSession = false;
    let speechOutput = '';

    if (session.attributes) {
        friendCode = session.attributes.friendCode;
    }

    if (friendCode) {
        speechOutput = `Hello, Your are ${friendCode}, Are you okay?`;
        shouldEndSession = false;
    } else {
        speechOutput = "sorry, I don't know you, Goodbye.";
    }

    callback(sessionAttributes,
         buildSpeechletResponse(intent.name, speechOutput, repromptText, shouldEndSession));
}


// 各イベント定義

/**
 * セッションが呼ばれた時のイベント
 */
function onSessionStarted(sessionStartedRequest, session) {
    console.log(`onSessionStarted requestId=${sessionStartedRequest.requestId}, sessionId=${session.sessionId}`);
}

/**
 * Launchした時のイベント
 */
function onLaunch(launchRequest, session, callback) {
    console.log(`onLaunch requestId=${launchRequest.requestId}, sessionId=${session.sessionId}`);

    // Dispatch to your skill's launch.
    getHelloResponse(callback);
}

/**
 * 利用者の命令の意図を指定した時のイベント
 */
function onIntent(intentRequest, session, callback) {
    console.log(`onIntent requestId=${intentRequest.requestId}, sessionId=${session.sessionId}`);

    const intent = intentRequest.intent;
    const intentName = intentRequest.intent.name;

    // Dispatch to your skill's intent handlers
    if (intentName === 'HelloIAmIntent') {
        setCodeInSession(intent, session, callback);
    } else if (intentName === 'YouKnowMeIntent') {
        getCodeFromSession(intent, session, callback);
    } else if (intentName === 'AMAZON.HelpIntent') {
        getHelloResponse(callback);
    } else if (intentName === 'AMAZON.StopIntent' || intentName === 'AMAZON.CancelIntent') {
        handleSessionEndRequest(callback);
    } else {
        throw new Error('Invalid intent');
    }
}

/**
 * セッションが終わった時のイベント
 */
function onSessionEnded(sessionEndedRequest, session) {
    console.log(`onSessionEnded requestId=${sessionEndedRequest.requestId}, sessionId=${session.sessionId}`);
    // Add cleanup logic here
}


// メインハンドラー

exports.handler = (event, context, callback) => {
    try {
        console.log(`event.session.application.applicationId=${event.session.application.applicationId}`);

        /**
         * Uncomment this if statement and populate with your skill's application ID to
         * prevent someone else from configuring a skill that sends requests to this function.
         */
        /*
        if (event.session.application.applicationId !== 'amzn1.echo-sdk-ams.app.[unique-value-here]') {
             callback('Invalid Application ID');
        }
        */

        if (event.session.new) {
            onSessionStarted({ requestId: event.request.requestId }, event.session);
        }

        if (event.request.type === 'LaunchRequest') {
            // 特に命令を指定せず、スキルが呼ばれた時
            onLaunch(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'IntentRequest') {
            // UtteranceとマッピングされたIntentが呼ばれた時
            onIntent(event.request,
                event.session,
                (sessionAttributes, speechletResponse) => {
                    callback(null, buildResponse(sessionAttributes, speechletResponse));
                });
        } else if (event.request.type === 'SessionEndedRequest') {
            onSessionEnded(event.request, event.session);
            callback();
        }
    } catch (err) {
        callback(err);
    }
};
```
実装内容は意外と単純です。
Alexaからのリクエストタイプは以下の通りです。

| LaunchRequest       |       IntentRequest |    SessionEndedRequest    |
|:-----------------|:------------------|:------------------|
| 特に命令を指定せず、スキルが呼ばれた時             |              UtteranceとマッピングされたIntentが呼ばれた時 |        ユーザーが対話を終了したり、一定時間対話がされなくてセッション有効時間が切れた時        |
スキル構築の重要ポイントは**IntentRequest**です。
  後スキル登録の時定義するIntent(達成目標)とそれに対する処理を**マッピング**します。
  より細かい説明は[ドキュメント](https://developer.amazon.com/docs/custom-skills/custom-interaction-model-reference.html)をご覧ください。

### **▶実装ステップ ③**
次は、達成する目標、**Intent**を定義します。
データ形式はjson形式です。

```json:intent.json
{
  "intents": [
    {
      "intent": "HelloIAmIntent",
      "slots": [
        {
          "name": "FriendCode",
          "type": "LIST_OF_SIGNS"
        },
        {
          "name": "Date",
          "type": "AMAZON.DATE"
        }
      ]
    },
    {
      "intent": "YouKnowMeIntent"
    }
  ]
}
```

次は、**Slot**たちを定義します。
カスタムスロットタイプ**LIST_OF_SIGNS**で定義します。
Sortのタイプを参照したい方は[ドキュメント](https://developer.amazon.com/docs/custom-skills/slot-type-reference.html)をご覧ください。

```yaml:FriendCodeSolts
lee
dohyung
sakura
hamano
```

意図を実行するための構文、**Utterance**を定義します。
`サンプル` と書いて居るけど、この構文を元にリスポンスします。

```yaml:Utterance
HelloIAmIntent Hello, I am {FriendCode}
HelloIAmIntent do you know {FriendCode}
YouKnowMeIntent do you know me?
```

### **▶実装ステップ ④**
残りは、[Amazon Deveroper Console](https://developer.amazon.com/edw/home.html)で**ASKを登録**することです。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/7037da22-97d2-e390-dfc8-3738bb85cdff.png)
👆に入って、Add a New Skillボタンを押すと👇の登録画面ができます。
`Test` まで行うと、公開はできませんが、[Alexa Device Manager](https://alexa.amazon.com/spa/index.html)ページから
自分のスキルを登録してデバイスからテストすることが できます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/07c224d7-d5d5-76a8-f78c-acf7d4554956.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/cab245a9-628e-8437-f5cc-b3f946f7a09b.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/b526fbb4-333d-ce05-72df-8cfc26bd4126.png)

また、Privacy & Complianceまですると**公開可能**ですが、
Amazonから検証が行った後可能です。(けっこう時間掛かりますー！)

各ステップのおもな設定項目としては以下の通りです。

#### - **Skill Information**
**スキル名**と**呼び出し名**を設定
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/53ff2b49-4017-d263-652e-e94bac53c830.png)

#### - **Interation Model**
**インテントスキーマ**と**サンプル発話(Utterance)**を設定します。
また、必要であれば**カスタムスロットタイプ**も設定します。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/5427ed4d-0b0b-837a-5b56-bc30a1260fbd.png)

#### - **Configuration**
**エンドポイント**を指定します。
自分はLambda関数を指定しました。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/68e0451b-14f2-e107-ca16-8d0e01ed4340.png)

#### - **Test**
**設定内容のテスト**を行うことが出来ます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/88cb54e5-0c71-77d0-2e0d-4ac0bf0cf9b1.png)

Listenボタンを押すと、結果をヒアリングすることができます。

そして、Serviceの Request & Responseの内容も確認できますのですごく便利です。
なぜ便利かとすると、以下のようにエラー内容を詳しく確認することができない時、
**Postman**や**Lambda関数コンソルのテスト**でその **Request(json)**値をそのまま
設定してリクエストすると、細かいレスポンスボディを確認することができます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/2fcd180c-993b-2a78-172e-ce9997a61a6f.png)

あらら、、、すみませんー
上端のキャプチャーが最初挨拶スキルではなく、
本番のGithubの音声認識インタフェース実行内容の方を取っちゃいました。

## **・"SHE"スキルにGitHubの音声認識インタフェース能力を与える**
Github API を利用してIssue及びPull requestを操作ができるようにしました。
操作できるのは以下の通りです。

### **▶ 利用の流れ**
1. 友達コードで認証した後、自分が持っているリポジトリリストを案内もらう
2. 自分のリポジトリであるリポジトリにアクセスする
3. アクセスしたリポジトリでissueとpull request操作が可能

### 🌺🌺🌺**▶ 実装内容**🌺🌺🌺
コードの内容を見せることは、まだ改修が色々必要なので、
以後にコードのGithubリポジトリを案内させて頂きます。

動作姿は[Youtubeにアップ](https://youtu.be/_DZZea488ao)しておきましたので、
気になる方はご覧ください。
英語発音頑張って練習しているんですが、
まだ下手なんで、Google音声からー.....


## ・ 「SHE」スキルにAipoアナウンス能力を与える
crxを作って、S3にPUTすることまで終わって、一応スキップしましたー！
継続的にすすめます！
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/33e44e96-ecb7-1fb3-90e5-a5b7b78333fd.png)


## 大変だったこと
 - ASKのセッション時間と持続的なセッション管理
 - シングルページアプリケーションでAPI提供してない(有料はある)Aipoのデータ自動取得



## 【参考】デバイスの設定の方法
[Alexa Device Manager](https://alexa.amazon.com/spa/index.html)ページから
設定➜新しいデバイスをセットアップするとその後は、優しく手順を案内してくれます。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/8e945b7a-92fc-eea4-39bb-dfc0bba73683.png)

## 【参考】Alexa Device Managerページでデバイス別に対話した履歴が残る
(スキル生成ページのTestに対する対話は含めません)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/108364/85e9bf1d-6367-fa2e-6e30-b00fb2ccd56d.png)