駅すぱあと x Cognitive Services で 画像DE路線当てBOT を作ろう！ [BOT実装(Node.js ver) 編]

[Microsoft Cognitive Services](https://azure.microsoft.com/ja-jp/services/cognitive-services/) の [Custom Vision Service](https://azure.microsoft.com/ja-jp/services/cognitive-services/custom-vision-service/) は、オリジナルの画像判定エンジンを作成して API で推定値を取得できるサービスです。また、[駅すぱあとwebサービス](https://ekiworld.net/service/lp/webservice/) は、国内初の乗換案内ソフト [駅すぱあと](https://ekiworld.net/about/index.html) がもつ様々な情報をWebAPIで提供しているサービスです。

今回は Custom Vision Service で作成した画像判定エンジンを利用して、
鉄道の路線を画像から判別し、 駅すぱあとwebサービス を利用して路線に含まれる駅を検索し、それを返す BOT を作成する方法を紹介します。

>Microsoft Cognitive Services の Custom Vision Service は、オリジナルの画像判定エンジンを作成して API で推定値を取得できるサービスです。
機械学習などで画像判定ロジックを構築しなくても、画像をアップロードしてタグ付けを行うことで、画像判定エンジンを構築できます。

>[駅すぱあとwebサービス](https://ekiworld.net/service/lp/webservice/) は、国内初の乗換案内ソフト [駅すぱあと](https://ekiworld.net/about/index.html) がもつ様々な情報をWebAPIで提供しているサービスです。
経路探索はもちろん、駅データや時刻表などの情報を取得することができます。 
無料で使えるフリープランを用意しており、企業にかぎらず個人でもご利用いただけます。

この BOT アプリは Bot Framework Channel Emulator を使ってローカル環境で稼働確認することが可能です。また、Web 公開 & Bot Framework に登録すると、埋め込み可能な Web Chat が利用できます。

>**駅すぱあと x Cognitive Services で 画像DE路線当てBOT を作ろう！**
>(1) [Custom Vision 編](https://qiita.com/hmaruyama/items/fd28cdee19a55fb510b1)
>(2) BOT実装 編
>- **Node.js ver 編** ※このページ
>- [C# ver 編](https://qiita.com/annie/private/c4bcc3dff1fa17deea2b)

Custom Vision 編、BOT実装 編を通して作成できる 画像DE路線当てBOT↓

![スクリーンショット 2017-09-22 18.26.46.png](https://qiita-image-store.s3.amazonaws.com/0/79577/a3e17890-02bc-2a71-44bb-c8cbab83bf1f.png)

この資料では、 Bot Builder を用いてBOTを実装する部分を紹介します。


# 必要な環境、サブスクリプションの準備

## 開発環境
### Bot Framework Channel Emulator
BOTの挙動をローカルで確認する際に必要となります。
https://github.com/Microsoft/BotFramework-Emulator/releases/ からダウンロードすることができます。

### Node.js
Node.jsを利用してBOTを開発します。 
https://nodejs.org/en/ からダウンロードすることができます。

### ngrok
ローカルホストを外部からでも叩けるようにするツールです。
今回は、BOTを経由して Custom Vision API に　画像を添付してPOSTリクエストを行うのですが、その際に必要となります。
https://ngrok.com/download からダウンロードできます。

##駅すぱあと Webサービス のアクセスキー
[駅すぱあと Webサービス](https://ekiworld.net/service/sier/webservice/index.html) のフリープランをお申込みいただき、アクセスキーを取得してください。
https://ekiworld.net/service/sier/webservice/free_provision.html
（即時発行ではないので少しばかしお時間かかります・・すいませんorz）

## Custom Vision 編 を実施していない場合
[Custom Vision 編](http://qiita.com/annie/items/293525901020685ad5f6) を実施していない場合はMicrosoft アカウントを準備してください。([Custom Vision 編] と同じもので問題ありません。)

### Microsoft アカウント
Cognitive Services (と必要に応じて Azure) サブスクリプションの申し込みに必要ですので、持っていない場合は取得しておきます。
>[Microsoft アカウント登録手続き](https://www.microsoft.com/ja-jp/msaccount/signup/default.aspx)

#BOT アプリの実装

## BOTを動かす

まず最初に、BOTが自分の環境でちゃんと動くかどうかを確認します。
BOTを実装する作業ディレクトリに移動し、以下のコマンドを実行してください。

~~~sh
$ npm init
~~~

ぽちぽちエンターを押して設定していきますが、*entry point*の項目だけは、デフォルトではなく`app.js`に設定することをオススメします。
設定が完了したら、BOT開発に必要な以下のモジュールをインストールします。

~~~sh
$ npm install --save botbuilder restify
~~~

* botbuilder...MicrosoftBotFrameworkで動くBOTを作成できるSDKです。
* restify...REST形式のwebサービス構築に特化したフレームワークです。


作業ディレクトリ直下に、*app.js*ファイルを作成します。
その中に、以下のコードをコピーして保存してください。
最低限の機能を揃えたサンプルコードです。

~~~javascript
var restify = require('restify');
var builder = require('botbuilder');

var server = restify.createServer();
server.listen(process.env.port || process.env.PORT || 3978, function () {
   console.log('%s listening to %s', server.name, server.url); 
});

var connector = new builder.ChatConnector({
    appId: process.env.MICROSOFT_APP_ID,
    appPassword: process.env.MICROSOFT_APP_PASSWORD
});

server.post('/api/messages', connector.listen());

var bot = new builder.UniversalBot(connector, function (session) {
    session.send("You said: %s", session.message.text);
});
~~~

ファイルを保存したら、挙動の確認を行います。
以下のコマンドを打ち込み、サービスを起動させます。

~~~sh
$ node app.js
~~~

サービスが起動すると、リクエストを受け付けるポート番号がターミナルに表示されます。

エミュレータを立ち上げます。
ウィンドウ上部の*Enter your endpoint URL*をクリックし、以下のURLを打ち込みます。
*{{ポート番号}}*の部分は、サービスを起動した時に表示されるポート番号に置き換えてください。

~~~
http://localhost:{{ポート番号}}/api/messages
~~~

*Microsoft App ID*と*Microsoft App Password*を指定する画面が表示されますが、それは無視して、*Connect*をクリックします。
メッセージ入力欄に適当に何かメッセージを書き込んでみましょう。
例えば*Hello*と入力したら、*You said: Hello*と返れば成功です。

![20170901102355.png](https://qiita-image-store.s3.amazonaws.com/0/79577/185c583b-e16f-3722-54c1-84c092e41363.png)


## Custom Vision の呼び出し

さて、自分の環境で無事にBOTが動かせることが確認できたら、Custom Vision APIをBOT内で呼び出してみましょう。

必要なモジュールをインストールします。

~~~sh
$ npm install --save dotenv request
~~~

* dotenv...環境変数を扱う際に便利なモジュールです。
* request...HTTPリクエストを扱う際に便利なモジュールです。

先ほど作成した*app.js*を開きます。
*app.js*の上部に、app.js内で*dotenv*、*request*が使用できるように、モジュールの読み込み記述を行います。

~~~javascript
var restify = require('restify');
var builder = require('botbuilder');

// 追記する部分 ここから
require('dotenv').config();
var request = require('request');
// ここまで

:
中略
:
~~~

画像をURLとして取得し、Custom Vision を呼び出します。
`var bot = new builder.UniversalBot(connector, function (session) {`から以下の行を、まるっと下記のコードに書き換えます。

~~~javascript
:
中略
:

var bot = new builder.UniversalBot(connector, function (session) {

  // メディア(画像)が添付されているか
  if(session.message.attachments.length > 0) {

    var tag = ""; // custom vision で定義したカテゴリータグ

    var customVisionApiRequestOptions = {
      uri: process.env.CUSTOM_VISION_API_URI,
      headers: {
        "Content-Type": "application/json",
        "Prediction-Key": process.env.CUSTOM_VISION_PREDICTION_KEY
      },
      json: {
        "Url": session.message.attachments[0].contentUrl
      }
    }

    // Custom Vision API へのPOSTリクエスト
    request.post(customVisionApiRequestOptions, function (error, response, body) {
      if(!error && response.statusCode == 200) {

        // Probability (≒信頼度) の高いものからリストされるため、0番目を取得
        tag = response.body.Predictions[0].Tag;

        session.send("%s に、いちばんにてるね！", tag);

      } else {
        console.log("error: " + error);
        session.send("はんていにしっぱいしちゃった。もういちどがぞうを送ってください。");
      }
    })
  } else {
    session.send("でんしゃのしゃしんを送ってね！");
  }
});
~~~

`process.env.環境変数名`で、指定された環境変数名から値を取得することができます。
Custom Vision API のPrediction-KeyやURLは、環境変数で扱いますので、環境変数に値をセットします。

*app.js*と同じ階層に、*.env*という名前でファイルを生成し、その中に環境変数を記述していきます。

~~~
CUSTOM_VISION_API_URI={{ご自身の Custom Vision API のURL}}
CUSTOM_VISION_PREDICTION_KEY={{ご自身の Custom Vision API のPrediction Key}}
~~~

※windows10の場合、拡張子の前に名前がないとファイルを作らせてくれないようで、ファイルエクスプローラから*.env*ファイルを新規作成できないことが分かりました。こちら( [[Win10] Windows上で拡張子だけのファイルを作成する方法](https://shnk38.com/pc/how-to-win/win10-dot-htaccess/) )のページにコマンドプロンプトで作成する方法が挙げられています。

*{{ご自身の Custom Vision API のURL}}*と*{{ご自身の Custom Vision API のPrediction Key}}*の値は、[Custom Vision 編](https://qiita.com/hmaruyama/items/fd28cdee19a55fb510b1)のAPIアクセス情報画面から取得できます。
*Prediction URL*の画面の、*If you have an image URL:*と書かれた方の値を利用します。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e616d617a6f6e6177732e636f6d2f302f37393537372f63373531333030352d333661342d643261322d363662392d3065346263383137366433312e706e67.png](https://qiita-image-store.s3.amazonaws.com/0/79577/8992fd93-bce8-8a58-baff-cc0fa9ddbb39.png)

挙動を確認します。
エミュレータを開き、メニューボタンから、*App Settings*を開きます。

![スクリーンショット 2017-09-22 18.34.06.png](https://qiita-image-store.s3.amazonaws.com/0/79577/0628a331-ea4a-0439-03fd-892d59ae97bd.png)

ngrokをダウンロードしてzipファイルを解凍すると、*ngrok*という名の実行ファイルが入っています。
その実行ファイルが置いてあるパスを指定します。
*Bypass ngrok for local addresses*と書かれたチェックボックスがありますが、チェックを外してください(チェックを入れてしまうと、意図しないエラーが発生してしまいます)。
最後に*SAVE*をクリックします。

![スクリーンショット 2017-09-22 18.54.45.png](https://qiita-image-store.s3.amazonaws.com/0/79577/f6371f5b-ee6c-628a-0dc0-0451b4e59784.png)

サービスを起動します。

~~~sh
$ node app.js
~~~

入力エリアの画像アイコンをクリックして、分析した画像を選択し、BOTに送信すると、判定されたタグが返信されるのを確認してください。

![スクリーンショット 2017-09-26 11.12.15.png](https://qiita-image-store.s3.amazonaws.com/0/79577/abc3f7e9-13e9-8ebb-a500-d2f296d7d213.png)

## 駅すぱあとwebサービス の呼び出し

画像を判定して取得できたタグに応じてメッセージを変更します。
今回は、 駅すぱあとWebサービス を利用して、路線名から停車駅を取得し、表示するようにします。

駅すぱあとwebサービスの詳しい使い方は、[駅すぱあとwebサービス フリープラン ドキュメント](http://docs.ekispert.com/v1/le/)に掲載しています。

>路線名、停車駅はこちらのAPIを利用して取得できます。
>今回は予め路線コードはアプリ内に保持し、停車駅のみをAPIで取得しています。
>- 路線名の取得: http://api.ekispert.jp/v1/xml/operationLine?name=路線名&key=アクセスキー
>- 停車駅の取得: http://api.ekispert.jp/v1/json/station?&operationLineCode=路線コード&key=アクセスキー

*app.js*を編集します。
`var bot = new builder.UniversalBot(connector, function (session) {`から以下の行を、まるっと下記のコードに書き換えます。

~~~javascript
:
中略
:

var bot = new builder.UniversalBot(connector, function (session) {

  // メディア(画像)が添付されているか
  if(session.message.attachments.length > 0) {

    var lineName = ""; // 駅すぱあと運航路線名
    var lineCode = ""; // 駅すぱあと運航路線コード
    var tag = ""; // custom vision で定義したカテゴリータグ
    var stations = []; // 路線停車駅情報(名前、緯度経度、都道府県..etc)リスト
    var stationNames = []; // 路線停車駅名リスト

    var customVisionApiRequestOptions = {
      uri: process.env.CUSTOM_VISION_API_URI,
      headers: {
        "Content-Type": "application/json",
        "Prediction-Key": process.env.CUSTOM_VISION_PREDICTION_KEY
      },
      json: {
        "Url": session.message.attachments[0].contentUrl
      }
    }

    // Custom Vision API へのPOSTリクエスト
    request.post(customVisionApiRequestOptions, function (error, response, body) {
      if(!error && response.statusCode == 200) {

        // Probability (≒信頼度) の高いものからリストされるため、0番目を取得
        tag = response.body.Predictions[0].Tag;

        switch (tag) {
          case "Chuo_Sobu":
            lineName = "ＪＲ中央・総武線各駅停車";
            lineCode = "110";
            break;
          case "Chuo_Ex":
            lineName = "ＪＲ中央線快速";
            lineCode = "109";
            break;
          case "Keihin-Tohoku":
            lineName = "ＪＲ京浜東北線";
            lineCode = "115";
            break;
          case "Tokaido":
            lineName = "ＪＲ東海道本線";
            lineCode = "117";
            break;
          case "Yamanote":
            lineName = "ＪＲ山手線";
            lineCode = "113";
            break;
          case "Yokosuka_SobuEx":
            lineName = "ＪＲ横須賀線";
            lineCode = "116";
            break;
        }

        session.send("%s に、いちばんにてるね！", lineName);

        var ekispertRequestOptions = {
          url: encodeURI("https://api.ekispert.jp/v1/json/station?operationLineCode=" + lineCode + "&key=" + process.env.EKISPERT_ACCESS_KEY),
          json: true
        }

        // 駅すぱあとWebサービスへのGETリクエスト
        request.get(ekispertRequestOptions, function(error, response, body) {
          if (!error && response.statusCode == 200) {

            session.send("%s は、以下のえきをはしるよ。", lineName);

            stations = response.body.ResultSet.Point;

            // 駅名称を列挙した配列を作成
            for(var i=0; i<stations.length; i++) {
              stationNames.push(stations[i].Station.Name);
            }
            session.send("%s", stationNames.join(' → '));
          } else {
            console.log("error: " + error);
          }
        })
      } else {
        console.log("error: " + error);
        session.send("はんていにしっぱいしちゃった。もういちどがぞうを送ってください。");
      }
    })
  } else {
    session.send("でんしゃのしゃしんを送ってね！");
  }
});
~~~

*.env*ファイルに、 駅すぱあとwebサービス のアクセスキーを追加します。

```
:
中略
:

EKISPERT_ACCESS_KEY={{ご自身の 駅すぱあとwebサービス のアクセスキー}}
```

*{{ご自身の 駅すぱあとwebサービス のアクセスキー}}*は、駅すぱあとwebサービスにアクセスする為に必要な値です。
[駅すぱあとwebサービス　フリープラン 申し込みページ](https://ekiworld.net/free_provision/index.php)から無料でお申し込みいただけます。

挙動を確認します。

~~~sh
$ node app.js
~~~

入力エリアの画像アイコンをクリックして、分析した画像を選択、BOTに送信します。
設定したメッセージが返答されたら完成です。

![スクリーンショット 2017-09-22 18.26.46.png](https://qiita-image-store.s3.amazonaws.com/0/79577/a3e17890-02bc-2a71-44bb-c8cbab83bf1f.png)

# Appendix
完成形のソースコードを GitHub にて公開しました。
https://github.com/hmaruyama/train-finder-bot

# 終わりに

様々なサービスを組み合わせることで、簡単にBOTを実装することができました。
作成したBOTをベースに、画像判定の精度をあげるもよし、対応路線のバリエーションを増やすもよし、会話のバリエーションを増やすもよし・・。
パブリックに公開するなら、こちらの手順([Microsoft Bot Framework、Azureを利用したBot開発！Bot作成からWeb上で公開するまで - hi, hikaru](http://hiiiiiiihikaru.hatenadiary.com/entry/2017/09/01/132921))が参考になると思います。