7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

LINE Bot + GASでCloud Vision APIを使う

Posted at

前書き

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

そんなある日、たいていのメンバーがランアプリのスクショを上げてくる中、ランニングウォッチの表示を写メしてくれるメンバーが出現。これも試しにとAPIデモのページでアップしてみたら、
image.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を受ける→画像のコンテンツを取得する→テキスト抽出する→距離・タイムを検出する→返信する という流れになります。

コード.gs
var CHANNEL_ACCESS_TOKEN = 'LINE Developerで作成したチャンネルのアクセストークンを貼ります';
var GOOGLE_API_KEY = 'GCPプロジェクトで作成したAPIキーを貼ります';

スクリプトプロパティを使ってみようとしたのですが、デプロイしたWebhookとして動作するときはもう一手間必要なようで、いったん直書き(公開するときはキレイにします)

コード.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で抽出します。

コード.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された形で返すようにしています。

コード.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からそれっぽいテキストを検出します。

コード.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;
}

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

コード.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
やったー \(^o^)/

TODO

  • 返信と合わせてSpreadsheetに記録
  • 誰の記録かわかるようLINEのuserIDから名前を追加
  • 毎日定刻に、24時間分の集計をLINEに送信
  • FBメッセンジャーから引っ越してもらう

参考記事・サイト

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?