LINE Bot + GASでCloud Vision APIを使う

# 前書き
先々週、FBメッセンジャーで共有しているラン記録の読み取りをできないかと試してみた記事を書いたのだけど、どうもFBメッセンジャーのグループではBotが使えなさそうで、少し意欲減退していました。

https://qiita.com/quwaji/items/172369dd03d045a81e87

そんなある日、たいていのメンバーがランアプリのスクショを上げてくる中、ランニングウォッチの表示を写メしてくれるメンバーが出現。これも試しにとAPIデモのページでアップしてみたら、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3381/de4aa950-0ec4-45b7-8b88-0894d1afcd9f.png)
スゲェ！　逆さまでもいけた！！
俄然、やる気が再燃してきました。

# 用意したもの
Cloud Vision APIを使うので、Googleアカウントにクレカを登録し、課金を有効にしたプロジェクトを作成します。
使用するAPIに Cloud Vision APIを指定し、APIキーを作成しておきます。

Cloud Vision APIの呼び出しは、GASを使ってみます。のちのち使うため、SpreadsheetにApp Scriptを追加し、上記プロジェクトに紐付けます。

LINE Botにするため、LINE Developerでプロバイダとチャンネルを作成します。グループチャットに参加できるように設定をしておきます。
こちらからは、チャンネルアクセスキーを用意しておきます。

# 作るもの
LINE Botとして実装するので、お友達登録してチャットでも、グループに招待してグループチャットでも使えます。
画像が送信されたら、Cloud Vision APIを使って画像の中のテキスト抽出を行います。
その中に、タイムと距離が含まれていたら、ラン報告としてチャットで返します。
検出結果はSpreadsheetに記録し、毎日定刻に集計してチャットに通知します。（今後実装）

# 書いたもの

大きく、Webhookを受ける→画像のコンテンツを取得する→テキスト抽出する→距離・タイムを検出する→返信する　という流れになります。

```js:コード.gs
var CHANNEL_ACCESS_TOKEN = 'LINE Developerで作成したチャンネルのアクセストークンを貼ります';
var GOOGLE_API_KEY = 'GCPプロジェクトで作成したAPIキーを貼ります';
```
スクリプトプロパティを使ってみようとしたのですが、デプロイしたWebhookとして動作するときはもう一手間必要なようで、いったん直書き（公開するときはキレイにします）

```js:コード.gs
function doPost(e) {

  Logger.log('webhook received: ' + e.postData.contents);

  var userId = JSON.parse(e.postData.contents).events[0].source.userId;
  var type = JSON.parse(e.postData.contents).events[0].source.type;
  var groupId = '';
  var replyTo = '';
  if(type == 'group') {
    groupId = JSON.parse(e.postData.contents).events[0].source.groupId;
    replyTo = groupId;
  }
  else {
    replyTo = userId;
  }
  var msgType = JSON.parse(e.postData.contents).events[0].message.type;
  var messageText = '';
  switch(msgType) {
    case 'image':
      var messageId = JSON.parse(e.postData.contents).events[0].message.id;
      // コンテンツをGETして解析、ラン画像なら返信。
      var image = getContent(messageId);
      var result = analyzeImage(image);
      var duration = detectTime(result);
      var distance = detectDistance(result);
      Logger.log('image analyzed: ' + result + String.fromCharCode(10) + 'distance: ' + distance + ', duration: ' + duration);
      if(duration != null && distance != null) {
        messageText = 'ナイスラン！' + String.fromCharCode(10);
        messageText += '距離' + String.fromCharCode(9) + distance + String.fromCharCode(10);
        messageText += 'タイム' + String.fromCharCode(9) + duration;
        sendLine(replyTo, messageText);
      }
      break;
    default:
  }

  return JSON.stringify({});

}
```
Webhookの処理になります。メッセージのソースがユーザかグループかにより返信の送信先が変わるため、判定してreplyToにとっておきます。
メッセージの種類が画像（image）の場合のみ処理します。messageIDを引数に、LINE Messaging APIで画像イメージを取得し、テキストをcloud Vision APIで抽出します。

```js:コード.gs
function getContent(messageId) {
  var url = 'https://api-data.line.me/v2/bot/message/' + messageId + '/content';
  var response;
  try {
    response = UrlFetchApp.fetch(url, {
      'headers': {
        'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN
      },
      "muteHttpExceptions" : true,
      "validateHttpsCertificates" : false,
      "followRedirects" : false
    });
    Logger.log('getContent(' + messageId + ') : ' + response.getResponseCode());
    return Utilities.base64Encode(response.getBlob().getBytes());
  } catch(e) {
    // 例外エラー処理
    Logger.log('Error:')
    Logger.log(e)
    return null;
  }
}
```
LINE Messaging APIで画像を取得します。Cloud Vision APIに渡す、base64encodeされた形で返すようにしています。

```js:コード.gs
// Vision APIで画像を解析して結果を取得
function analyzeImage(image) {

   const url = 'https://vision.googleapis.com/v1/images:annotate?key=' + GOOGLE_API_KEY;

   // 画像からテキストの検出
   const body = {
       "requests": [
           {
               "image": {
                   "content": image
               },
               "features": [
                   {
                       "type": "DOCUMENT_TEXT_DETECTION",
                   }
               ]
           }
       ]
   };

   const head = {
       "method": "post",
       "contentType": "application/json",
       "payload": JSON.stringify(body),
       "muteHttpExceptions": true
   };

   const response = UrlFetchApp.fetch(url, head);
   const obj = JSON.parse(response.getContentText());
   const result = obj.responses[0].textAnnotations[0].description;

   return result;
}
```
Cloud Vision APIでは多様な画像解析ができるのですが、今回はテキストの抽出のみで「DOCUMENT_TEXT_DETECTION」だけを指定しています。
APIデモのサイトで確認できるように、画像上の配置から関連性を構造化して参照もできるのですが、まずは動かしてみたいというところで、雑に抽出全テキストが改行で結合されたdescriptionからそれっぽいテキストを検出します。

```js:コード.gs
// 距離を見つける
function detectDistance(result) {
  // 8.260 km
  a = result.match('([0-9]+).([0-9]+)[ ]*km');
  if ( a != null) {
    return a[1] + '.' + a[2];
  }
  // 7.67
  // 距離(km)
  a = result.match('([0-9]+).([0-9]+)\n距離');
  if ( a != null) {
    return a[1] + '.' + a[2];
  }

  return null;
}

// タイムを見つける
function detectTime(result) {
  // 00:55:29.3
  a = result.match('([0-9]+):([0-9]+):([0-9]+.[0-9]+)');
  if ( a != null) {
    return a[1] + ':' + a[2] + ':' + a[3];
  }
  // 0:51:45
  a = result.match('([0-9]+):([0-9]+):([0-9]+)');
  if ( a != null) {
    return a[1] + ':' + a[2] + ':' + a[3];
  }
  // 0:49'01" LAP
  a = result.match('([0-9]+):([0-9]+)\'([0-9]+)\" LAP');
  if ( a != null) {
    return a[1] + ':' + a[2] + ':' + a[3];
  }
  // 01:36:08
  // タイム
  a = result.match('([0-9]+):([0-9]+):([0-9]+)\nタイム');
  if ( a != null) {
    return a[1] + ':' + a[2] + ':' + a[3];
  }
  // 36:08
  // タイム
  a = result.match('([0-9]+):([0-9]+)\nタイム');
  if ( a != null) {
    return '0:' + a[1] + ':' + a[2];
  }

  return null;
}
```
「距離っぽいもの」「タイムっぽいもの」も、たぶん機械学習でできそうなものですが、現時点ではベタにアップされた様々なスクショや写真からパターンを書いています。。

```js:コード.gs
// LINEに送信
function sendLine(to, strMessage){
   
  //Lineに送信するためのトークン
  var strToken = CHANNEL_ACCESS_TOKEN;
  var options =
   {
     "method"  : "post",
     "payload" : JSON.stringify({
       'messages': [{
         'type': 'text',
         'text': strMessage,
        }],
       'to': to,
      }),
     "headers" : {"Authorization" : "Bearer " + strToken, 
       "Content-Type" : "application/json"
     }
 
   };
 
   UrlFetchApp.fetch("https://api.line.me/v2/bot/message/push",options);
   console.log(to + String.fromCharCode(10) + strMessage)
}

```

# 動かしてみた
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3381/4655cdcb-c6f3-d4e7-db31-521159e6e7f7.png)
やったー　＼(^o^)／
# TODO
- 返信と合わせてSpreadsheetに記録
- 誰の記録かわかるようLINEのuserIDから名前を追加
- 毎日定刻に、２４時間分の集計をLINEに送信
- FBメッセンジャーから引っ越してもらう

# 参考記事・サイト

https://cloud.google.com/functions/docs/tutorials/ocr

https://note.com/thediaryof/n/n1f9148c5252a

https://qiita.com/ttyokoyama/items/4c4d08bbde6b8a8dd243