【改良】送られてきた画像の種類を疑似判定し、保存およびLINE Bot返信に対応した話とフィードバックについて

# 画像で判定できないなら、テキストデータを使えばいいじゃない
おはようございます、こんにちは、こんばんは！
いつでも笑顔、いけちゃんです。

つい先日、`Google Apps Script（以下、GASと表記）`を活用して`LINE Bot`を作り、業者さんから送られてくる画像を`Google Spread Sheet`に保存することができた記事を公開しました。

https://qiita.com/ikeuchi-kota/items/549f24a0065e870cae01

その時に一つ、解決できていない大きな課題がありました。それが下記の通り。

:::note warn
`Teachable Machine`で書類を判定するGASのプログラムコードが書けていない
☞何度も`ChatGPT`も駆使して試しているのですが、実装に至っておりません。
これが実装できると、書類の仕分け、催促も非常にスムーズなのですが・・・
:::

実はその後、どうすれば画像を判定できるかを悩み続けていたのですが、あるアドバイスがきっかけで驚くほど早く解決に導くことが出来ました。
そのアドバイスとは以下の通り（原文ママ）
![image (73).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/03ce8d97-47f3-13ac-86fd-2cc49958d8dc.png)

ぼんやりとテキストからの判定は考えていましたが、`Teachable Machine`が自分の中で衝撃的過ぎてどうにかして使いたいと思っていた矢先、このようなフィードバックを受け考え直すこととしました。

早速取り掛かってみます！

# ChatGPTに聞いたら瞬時に課題解決した！
### まずは結果から、動作状況は下記動画をご確認ください。
※動画には載せていませんが、`Google Spread Sheet`上には判定結果が画像保存先URLやテキストデータとともに保存されています

<blockquote class="twitter-tweet" data-media-max-width="560"><p lang="ja" dir="ltr">ふっふっふ・・・LINEBotでOCRと画像データを保存するだけじゃなく、テキスト内容から仕分けもできるようになったぞ！しかも相手にそれが返信されるオマケ付！<a href="https://twitter.com/hashtag/protoout?src=hash&amp;ref_src=twsrc%5Etfw">#protoout</a><a href="https://twitter.com/hashtag/%E3%83%96%E3%83%83%E3%82%AF%E3%82%AA%E3%83%95%E3%81%A7%E3%83%97%E3%83%AD%E3%83%9D%E3%83%BC%E3%82%BA%E3%81%99%E3%82%8B%E3%83%80%E3%82%A4%E3%82%A8%E3%83%83%E3%83%88%E4%B8%AD%E3%81%AE%E3%83%A4%E3%83%9E%E3%83%88%E9%81%8B%E8%BC%B8?src=hash&amp;ref_src=twsrc%5Etfw">#ブックオフでプロポーズするダイエット中のヤマト運輸</a><a href="https://twitter.com/hashtag/LINEBot?src=hash&amp;ref_src=twsrc%5Etfw">#LINEBot</a><a href="https://twitter.com/hashtag/GAS?src=hash&amp;ref_src=twsrc%5Etfw">#GAS</a> <a href="https://twitter.com/hashtag/GoogleAppsScript?src=hash&amp;ref_src=twsrc%5Etfw">#GoogleAppsScript</a> <a href="https://t.co/HasOdVCY3W">pic.twitter.com/HasOdVCY3W</a></p>&mdash; いけちゃん (@since2023_kota) <a href="https://twitter.com/since2023_kota/status/1765032559187001621?ref_src=twsrc%5Etfw">March 5, 2024</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 上記を実現したGASのプログラムコード

:::note warn
コード使用時の注意点
1. "自分のチャネルアクセストークンを入れてください"に、<font color="Red">自分のLINE Messaging APIのチャネルアクセストークン</font>を入力してください
1. "自分のGoogle DriveのフォルダIDを入れてください"に、<font color="Red">LINEから送られてきた画像を保存したいGoogle DriveのフォルダID</font>を入力してください

1. 中段以下に、<font color="Red">// 以下を追加：テキストに「a」が含まれている場合は「A」、「b」という表示がある場合は「B」、「c」という表示がある場合は「C」、どれも含まれていない場合は「不明」として分類する</font>という項目がありますが、ここからがテキストデータによる書類判定箇所となります。<br>`text.indexOf`という項目の後ろの文書が画像に含まれているテキストデータ、`classification`がGoogle Spread SheetやLINE Botで記録・返信される判定結果となりますので自由に変更してください。
1. テキスト判定する項目を増やしたい場合は上記のコードをコピーして下の行に追加していけば良いです。
:::

```javascript
//★★LINE Messaging APIのチャネルアクセストークン★★
var LINE_ACCESS_TOKEN = "自分のチャネルアクセストークンを入れてください";

//★★画像を保存するフォルダーID★★
var GOOGLE_DRIVE_FOLDER_ID = "自分のGoogle DriveのフォルダIDを入れてください";

//ファイル名に使う現在日時をMomentライブラリーを使って取得
var date = Moment.moment(); //現在日時を取得
var formattedDate = date.format("YYYYMMDD_HHmmss");

//LINE Messaging APIからPOST送信を受けたときに起動する
// e はJSON文字列
function doPost(e){
  if (typeof e === "undefined"){
    //eがundefinedの場合動作を終了する
    return;
  } 

  //JSON文字列をパース(解析)し、変数jsonに格納する
  var json = JSON.parse(e.postData.contents);

  //受信したメッセージ情報を変数に格納する
  var reply_token = json.events[0].replyToken; //reply token
  var messageId = json.events[0].message.id; //メッセージID
  var messageType = json.events[0].message.type; //メッセージタイプ

  //LINEで送信されたものが画像以外の場合、LINEで返信し動作を終了する
  if(messageType !== "image"){
    var messageNotImage = "画像を送信してください"
    //変数reply_tokenとmessageNotImageを関数sendMessageに渡し、sendMessageを起動する
    sendMessage(reply_token, messageNotImage)
    return;
  }

  var LINE_END_POINT = "https://api-data.line.me/v2/bot/message/" + messageId + "/content";
  
  //変数LINE_END_POINTとreply_tokenを関数get_ocr_Imageに渡し、get_ocr_Imageを起動する
  getImage(LINE_END_POINT, reply_token);
}

//Blob形式で画像を取得する
function getImage(LINE_END_POINT, reply_token){
  try {
    var url = LINE_END_POINT;

    var headers = {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN
    };

    var options = {
      "method" : "get",
      "headers" : headers,
    };

    var res = UrlFetchApp.fetch(url, options);

    //Blob形式で画像を取得し、ファイル名を設定する
    //ファイル名: LINE画像_YYYYMMDD_HHmmss.png
    var imageBlob = res.getBlob().getAs("image/png").setName("LINE画像_" + formattedDate + ".png")

    //変数imageBlobとreply_tokenを関数saveImageに渡し、saveImageを起動する
    save_ocr_Image(imageBlob, reply_token)

  } catch(e) {
    //例外エラーが起きた時にログを残す
    Logger.log(e.message);
  }
}

//画像をGoogle Driveのフォルダーに画像を保存（アップロード）、OCRをかけテキストを取得。
//保存された画像、作成されたドキュメントファイルは残す。
//OCRで得られたテキストをLine Botへ返信する。
function save_ocr_Image(imageBlob, reply_token){
  try{
    //画像をGoogle Driveのフォルダーに画像を保存（アップロード）
    var folder = DriveApp.getFolderById(GOOGLE_DRIVE_FOLDER_ID);
    var file = folder.createFile(imageBlob);

    //ここからOCRをかけていく
    var files = folder.getFiles();
    while (files.hasNext()) {
      var file = files.next();
      // ファイルの種類が画像であることを確認する
      if (file.getMimeType().indexOf('image/') !== -1) {
        var resource = {
          title: "OCR結果_" + formattedDate, // 生成されるGoogleドキュメントのファイル名
          mimeType: "application/vnd.google-apps.document" // Googleドキュメント形式
        };
        var option = {
          "ocr": true,// OCRを行うかの設定です
          "ocrLanguage": "ja",// OCRを行う言語の設定です
        };
        var image = Drive.Files.copy(resource, file.getId(), option);

        //テキストを加工する
        var text_O = DocumentApp.openById(image.id).getBody().getText();
        var text_R01 = text_O.replace(/([^ -~｡-ﾟ]),/g, '$1，')   
                             .replace(/([^ -~｡-ﾟ])\./g, '$1．')   
                             .replace(/(\d)\．/g, '$1\.')        
                             .replace(/([ -~｡-ﾟ])、/g, '$1, ')    
                             .replace(/([ -~｡-ﾟ])。/g, '$1. ')    
                             .replace(/([^ -~｡-ﾟ])\(/g, '$1（')   
                             .replace(/([^ -~｡-ﾟ])\)/g, '$1）')   
                             .replace(/\(([^ -~｡-ﾟ])/g, '（$1')   
                             .replace(/\)([^ -~｡-ﾟ])/g, '）$1')   
                             .replace(/([^ -~｡-ﾟ])\:/g, '$1：')  
                             .replace(/([^ -~｡-ﾟ]) /g, '$1');     
        var text_R02 = text_R01.match(/(，|．)/) ? text_R01.replace(/([^ -~｡-ﾟ])、/g, '$1，').replace(/([^ -~｡-ﾟ])。/g, '$1．') : text_R01;
        var text = text_R02;

       // 以下を追加：テキストに「a」が含まれている場合は「A」、「b」という表示がある場合は「B」、「c」という表示がある場合は「C」、どれも含まれていない場合は「不明」として分類する
      var classification = "不明"; // デフォルトは「不明」

      if (text.indexOf('a') !== -1) {
        classification = "A";
      } else if (text.indexOf('b') !== -1) {
        classification = "B";
      } else if (text.indexOf('c') !== -1) {
        classification = "C";
      }

      // OCRで得られたテキストを、現在のスプレッドシートの一番最終行+1行に張り付ける。
      var ss = SpreadsheetApp.getActiveSpreadsheet()
      var sheet = ss.getActiveSheet()
      var lastRow = sheet.getLastRow();

      // OCRで得られたテキストを、現在のスプレッドシートの一番最終行+1行に張り付ける。
      sheet.getRange(lastRow+1, 2).setValue(text);

      if (classification === "不明") {
        // OCRで得られたテキストを、Line Botへ返信する。
        var message = "画像が粗い、または無関係の画像のため判定できませんでした、お手数ですがもう一度送付してください。";
        //変数reply_tokenとmessageを関数sendMessageに渡し、sendMessageを起動する
        sendMessage(reply_token, message);
      } else {
        // 画像の保管先URLも記録する
        sheet.getRange(lastRow+1, 1).setValue(classification);
        sheet.getRange(lastRow+1, 3).setValue(file.getUrl());
        
        // OCRで得られたテキストを、Line Botへ返信する。
        var message = "画像を" + classification + "として分類し、保存しました。";
        //変数reply_tokenとmessageを関数sendMessageに渡し、sendMessageを起動する
        sendMessage(reply_token, message);
      }
        // アップロードした写真、作成したOCRファイルが不要であれば、以下のコードをここに書いて削除する（先頭の"//"を消す）
        //file.setTrashed(true); //アップロードした写真を削除
        Drive.Files.remove(image.id); //作成したOCRファイルを削除

        break; // 一つだけ処理するためループを抜ける
      }
    }
  } catch(e){
    //例外エラーが起きた時にログを残す
    Logger.log(e);
  }
}

//ユーザーにメッセージを送信する
function sendMessage(reply_token, text){
  //返信先URL
  var replyUrl = "https://api.line.me/v2/bot/message/reply";

  var headers = {
    "Content-Type": "application/json; charset=UTF-8",
    "Authorization": "Bearer " + LINE_ACCESS_TOKEN
  };
  
  var postData = {
    "replyToken": reply_token,
    "messages": [{
                  "type": "text",
                  "text": text
                  }]
  };

  var options = {
    "method" : "post",
    "headers" : headers,
    "payload" : JSON.stringify(postData)
  };

  //LINE Messaging APIにデータを送信する
  UrlFetchApp.fetch(replyUrl, options);
}
```

今回はGASコードの追加のみのため、以前のコードからそのまますべて上書きし、デプロイすれば完了です！
これもChatGPTに一瞬で書いてもらいました！時代は変わったね！

### 解決したことまとめ
前回の記事で掲載した「どこまでできたか」図を更新しました。
![やりたいこと図4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/40f55f3e-f1fb-b3ba-9e6f-a9e63754304c.jpeg)

どこが変わったかって・・・？ここです！

![やりたいこと図4集中.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3637980/0753634f-c48a-d985-96ee-8269706bcef1.jpeg)

やりたいことがほぼできたため、業者さんにもさらに試してもらいます！

# 同僚からのフィードバック
さて、業者さんに試していただく傍ら、書類を`Google Spread Sheet`上に仕分けて保存していくことに関する同僚からの声も伺いました。
まだ一言二言程度の内容ですが、その中でも特に改善してほしいという要望・課題がありました。

:::note warn
業者さんから送られてくる資料は有効期限がある書類がありますが、有効期限が切れる前に未提出の業者さんがわかるようになりますか？
あると業者さんへの確認がスムーズにできるのですが・・・
:::

保存するだけでなく、その先の業務を考えている同僚に感謝です！
紙のゼロ化に向け大きな前進を果たせた今、作業オペレーションも改善していきたいと思います。

# まとめ
やりたいことはすべて盛り込んできました。
あとは、使ってもらう業者さんや同僚から意見をとにかく聞いて、紙のゼロ化を果たすために改善し続けていきます。
最後までご覧いただき、ありがとうございました！