LINE Botで感情分析して返答してみる

LINE Botで返答するときに、Botに投げかけられたメッセージを感情分析して、テキストとスタンプで返答してみました。

# LINE Botの準備
CodeZineの「日本一わかりやすいLINE Bot開発講座」を参考にして準備しました。
http://codezine.jp/article/corner/686

利用した言語とサーバも同様です。

| 利用言語  | サーバ  |
|:---------|:-------|
| PHP      | Heroku |

# 利用した感情分析API
Google Cloud PlatformのCLOUD NATURAL LANGUAGE APIを利用。
https://cloud.google.com/natural-language/

※現在ベータ期間中で、テキスト レコードと呼ばれる測定単位を利用して無料、有料を判断しています。以下のページをご覧ください。
https://cloud.google.com/natural-language/pricing

# 感情分析APIを利用するための準備 1
まずはGoogle Cloud Plaformサイト上で準備が必要です。

1. Google Cloud Platformへの申し込み ※1
2. LINE Bot向けのプロジェクトの作成 ※1
3. サービスアカウントの作成とダウンロード ※2

※1 1と2の参考サイト
http://www.apps-gcp.com/gcp-startup/

※2 3の参考サイト
https://www.magellanic-clouds.com/blocks/guide/create-gcp-service-account-key/
この例の通り、JSONファイルを保存します。

# 「3. サービスアカウントの作成とダウンロード」の補足
3で用意したJSONファイルをBotの処理を記述するPHP上で、環境変数に設定することで、CLOUD NATURAL LANGUAGE APIを利用できるようになります。

JSONファイルを、サーバ上のBotの処理を行うPHPが読み取れる場所にアップロードしてください。

# 感情分析APIを利用するための準備 2
CLOUD NATURAL LANGUAGE APIをPHP上から呼び出すために、Google Cloud Client Library for PHPを利用します。
https://github.com/GoogleCloudPlatform/google-cloud-php

ダウンロードは、上記サイトのREADME.mdのQuick Startにあるように、ローカルPC上のBotのPHPファイルがあるディレクトリで、composerで行いました。

```BotのPHPファイルがあるディレクトリ上
$ composer require google/cloud
```

# 感情分析APIを盛り込んだソース
CodeZineの「日本一わかりやすいLINE Bot開発講座」2回目「LINE Bot開発の概要と実装の基本」で紹介されている、スタンプとメッセージを返答するソースを元に作成しました。感情分析用に追加したのは主に1〜5の処理です。

```php
<?php

require_once __DIR__ . '/vendor/autoload.php';

# 1. Google Cloud client libraryをインポート
use Google\Cloud\Language\LanguageClient;

# 2. Google Cloud Natural Languageのクライアントを生成
$language = new LanguageClient([
    'projectId' => '[Google Cloud Platformのプロジェクト名を記載]'
]);

# 3. Google Cloud Platformのプロジェクトのサービスアカウントに関する設定
# 環境変数にサービスアカウントのJSONファイルの置き場所を指定する
putenv('GOOGLE_APPLICATION_CREDENTIALS='.dirname(__FILE__) . '[アップロードしたサービスアカウントのJSONファイルを指定]');

# LINE BOTの処理
$httpClient = new \LINE\LINEBot\HTTPClient\CurlHTTPClient(getenv('CHANNEL_ACCESS_TOKEN'));
$bot = new \LINE\LINEBot($httpClient, ['channelSecret' => getenv('CHANNEL_SECRET')]);

$signature = $_SERVER["HTTP_" . \LINE\LINEBot\Constant\HTTPHeader::LINE_SIGNATURE];
try {
  $events = $bot->parseEventRequest(file_get_contents('php://input'), $signature);
} catch(\LINE\LINEBot\Exception\InvalidSignatureException $e) {
  error_log("parseEventRequest failed. InvalidSignatureException => ".var_export($e, true));
} catch(\LINE\LINEBot\Exception\UnknownEventTypeException $e) {
  error_log("parseEventRequest failed. UnknownEventTypeException => ".var_export($e, true));
} catch(\LINE\LINEBot\Exception\UnknownMessageTypeException $e) {
  error_log("parseEventRequest failed. UnknownMessageTypeException => ".var_export($e, true));
} catch(\LINE\LINEBot\Exception\InvalidEventRequestException $e) {
  error_log("parseEventRequest failed. InvalidEventRequestException => ".var_export($e, true));
}

foreach ($events as $event) {
  if (!($event instanceof \LINE\LINEBot\Event\MessageEvent)) {
    error_log('Non message event has come');
    continue;
  }
  if (!($event instanceof \LINE\LINEBot\Event\MessageEvent\TextMessage)) {
    error_log('Non text message has come');
    continue;
  }
  $profile = $bot->getProfile($event->getUserId())->getJSONDecodedBody();
  $text = $event->getText();

  # 4. BOTに送られてきたテキストの感情を調べる
  $annotation = $language->analyzeSentiment($text);
  $sentiment = $annotation->sentiment();
  $score = $sentiment['score']; //scoreの範囲は-1から1

  # 5. scoreに応じて返信するテキストとスタンプを設定する
  if ($score >= 0) {
    $message = "楽しそうですね！";
    $stkid = 138;
  }
  else {
    $message = "悲しそうですね…";
    $stkid = 111;
  }
  $message .= "(スコア:".$score.")";

  # 回答する
  $bot->replyMessage($event->getReplyToken(),
    (new \LINE\LINEBot\MessageBuilder\MultiMessageBuilder())
      ->add(new \LINE\LINEBot\MessageBuilder\TextMessageBuilder($message))
      ->add(new \LINE\LINEBot\MessageBuilder\StickerMessageBuilder(1, $stkid))
  );
}

?>
```
# CLOUD NATURAL LANGUAGE APIのレスポンス
感情分析した結果については以下を参照してください。
https://cloud.google.com/natural-language/docs/reference/rest/v1beta1/Sentiment
今のところ利用できるのはscoreだけかなと思いました。

# LINE Botの実行結果
Botにメッセージを送ると以下のように返答が来ます。
![linebot.jpg](https://qiita-image-store.s3.amazonaws.com/0/179035/26797a1b-9932-f394-444b-23bdbe956a59.jpeg "linebot.jpg")


# 補足:Herokuを利用する場合
CodeZineの記事の通り、サーバにHerokuを利用する場合、「Google Cloud Client Library for PHP」内のRequestBuilder.phpでエラーが発生しました。

デバッグしたところ、Herokuであるディレクトリ階層以下でPHPの関数file_get_contentsがエラーとなっていたため、読み込もうとしていたJSONファイルの場所と、RequestBuilder.phpの処理を一部変更することで対応しました。

### RequestBuilder.phpの場所(composerを利用した場合)
[BotのPHPを置いてある場所]/vendor/google/cloud/src/Core/RequestBuilder.php

### エラー内容
```
2017-05-14T08:03:55.072470+00:00 app[web.1]: [14-May-2017 08:03:55 UTC] PHP Warning:  file_get_contents(/app/vendor/google/cloud/src/language/Connection/ServiceDefinition/language-v1.json): failed to open stream: No such file or directory in /app/vendor/google/cloud/src/Core/RequestBuilder.php on line 139
```


### 対応内容1
デバッグの結果、[BotのPHPを置いてある場所]/vendor/google/cloud/src/languageまではfile_get_contentsが正常に動いていたので、[BotのPHPを置いてある場所]/vendor/google/cloud/src/language/Connection/ServiceDefinition/language-v1.jsonを、[BotのPHPを置いてある場所]/vendor/google/cloud/src/languageにコピーしました。

### 対応内容2
RequestBuilder.phpで対応内容1のJSONファイルを読み込むように以下のように変更しました。

```php:RequestBuilder.php
    /**
     * @param string $servicePath
     * @return array
     */
    private function loadServiceDefinition($servicePath)
    {
        if ($servicePath == "/app/vendor/google/cloud/src/language/Connection/ServiceDefinition/language-v1.json") {
          $servicePath = str_replace("language/Connection/ServiceDefinition", "Language", $servicePath);
        }

        return $this->jsonDecode(
            file_get_contents($servicePath, true),
            true
        );
    }

```