[GAS Gemini]受信したFAXを要約したメールを送ろう

GasでGeminiAI

どうも。Kenny(tsujikenzo)です。このシリーズでは、 「GASでGeminiを使って受信したFAXを要約したメールを送ろう」 シリーズをお送りしています。全3回でお送りします。

はじめに

前回のブログで、Googleの生成AIであるGemini(ジェミナイ)を使って、画像認識ができることをお届けしました。

この応用として、受信したFAXをGeminiに要約してもらいメールを送信する 、というアプリを作りたいと思います。

引き続き、対象となる読者は以下です。

  • Google WorkSpaceのBussiness Starter以上のプランを利用している
  • 前回のブログ記事のgenerateImageCaption()関数を実行し、結果を得ることができた
  • 動かないエラーが発生するなどはまずググるなどして解決を試みるスキルがある

FAXそれはPDF

前回のブログでは、Gemini APIのVision Capability(画像認識処理)を使って、画像のキャプションを生成したり、OCRに成功しました。

復習ですが、「画像」というのは、ファイルタイプが以下のファイルを指します。

  • PNG – image/png
  • JPEG – image/jpeg
  • WEBP – image/webp
  • HEIC – image/heic
  • HEIF – image/heif

今後のGeminiのバージョンアップにより、PDFも対応可、ということになる可能性もあります。ご注意ください。

今回は、画像ではなく、PDFの中身を解析したいと思います。

実際の利用場面

弊社でよく使うPDFといえば、受信FAXです。ネットFAXサービスを利用しており、FAXの中身は都度従業員が目で確認しています。

今までは「FAXなんて廃止」だと思っていましたが、実はFAXはすごい技術なので保守した方がいいです。

それと前提として、FAXを受信するときに、PDFではなく画像で保存することができないか、ネットFAXサービス会社に確認中です。

以下のように、GeminiはまだPDFの内容解析に対応していません。 

開発の流れ(要件定義)

以下が、アプリ開発の流れです。(要件定義や外部設計というやつです。軽めの。)

用意するもの(フォルダ)

  • 未処理の生PDFを保存するフォルダ
  • PDFを1枚ずつ分割したOCR未処理フォルダ
  • OCR処理済みフォルダ
  • ゴミ箱フォルダ

用意するもの(スプレッドシート)

  • 1ファイル1レコードに対応したスプレッドシート
  • 1ページ1レコードに対応したスプレッドシート
  • このスプレッドシートのコンテナバインドスクリプトにGASを書く

用意するもの(実行環境)

  • 未処理のPDFが1枚か複数枚かチェックする10分置きの時限トリガー(GAS)
  • OCR未処理のPDFをチェックする5分置きの時限トリガー(GAS)
  • Gmailアカウント(GASの実行者から送信で構いません)

用意するもの(開発環境)は任意です。

  • VS Code(Copilot)
  • GitHub リモートリポジトリ
  • GitHub Assistant
  • Gemini Chat
  • Chat GPT 4o

GASのエディタにGeminiが降りてくるのが、次のお祭りだと思います。

開発の流れ(フローチャート)

以下が、アプリケーションを利用する際の、流れです。

  1. 受信したメールに添付ファイルがあれば、Google DriveにPDFを保存する
  2. (このフォルダには手作業でPDFをアップロードしてもかまわない)
  3. PDFファイルにスターがついていないものは、単ページか複数ページかを判定する
  4. 単ページのものは分割処理済みフォルダへ、複数ページのものは分割して分割処理積みフォルダへ
  5. (元のPDFファイルにはStarを付ける)
  6. 分割処理済みフォルダの中にある、Starのついていないファイルを画像に変換して、画像化処理済みフォルダへ移動する
  7. (元のPDFファイルにはStarを付ける)
  8. 画像化処理済みフォルダの中にある、Starのついていないファイルを、画像分析処理する
  9. (元のPDFファイルにはStarを付ける)
  10. スプレッドシートの列「要約処理済み」が未処理のレコードを、要約する
  11. メール送信する

ファイルに対して、処理をしたかどうかという判定は、Starを使うのが便利です。

Star管理ではなく、スプレッドシートのレコードとして処理済みかどうかを判定する方法もありますので、今回はどちらも併用します。

なお、受信したメールに添付されているPDFをフォルダに自動で保存するアプリは、別の記事でご確認ください(かんたんなのでブログはたくさんあると思います)。

下準備

要件定義したフォルダ、スプレッドシートなどを準備し、フォルダIDなどをメモしておきましょう。

PDF分割未処理フォルダには、サンプルのPDFを入れておきます。PDFのファイルIDもメモしておきましょう。 

開発をはじめよう

アプリは、最終的な目的を達成するパーツから作り始めます。

今回の最終的な目的は「PDFの要約をメールする」なので、メールするパーツから作ります。

スクリプトファイルは、どこに書いても構いません。

function main() {
 
  // 分析結果をメール送信する
  sendExcerptedGmail();

}

それでは、sendExcerptedGmail()関数の中身を書いていきましょう。

メール送信する関数の中身(AI編)

せっかくなので、AIがどれほどの実力なのか、試してみましょう。

先ほどの、要件定義とフローチャートを、AIにぶっこんで、コーディングしてもらいます。

これからGoogle Apps Scriptでアプリを作成します。要件定義とフローチャートは以下です。最新のV8エンジンでコーディングしてください。

## 開発の流れ(要件定義)
以下が、アプリ開発の流れです。

#### 用意するもの(フォルダ)
- 未処理の生PDFを保存するフォルダ
- PDFを1枚ずつ分割したOCR未処理フォルダ
- OCR処理済みフォルダ
- ゴミ箱フォルダ

#### 用意するもの(スプレッドシート)
- 1ファイル1レコードに対応したスプレッドシート
- 1ページ1レコードに対応したスプレッドシート
- このスプレッドシートのコンテナバインドスクリプトにGASを書く

#### 用意するもの(実行環境)
- 未処理のPDFが1枚か複数枚かチェックする10分置きの時限トリガー(GAS)
- OCR未処理のPDFをチェックする5分置きの時限トリガー(GAS)
- Gmailアカウント(GASの実行者から送信で構いません)

## 開発の流れ(フローチャート)
以下が、アプリケーションを利用する際の、流れです。
1. 受信したメールに添付ファイルがあれば、Google DriveにPDFを保存する
2. (このフォルダには手作業でPDFをアップロードしてもかまわない)
3. PDFファイルにスターがついていないものは、単ページか複数ページかを判定する
4. 単ページのものは分割処理済みフォルダへ、複数ページのものは分割して分割処理積みフォルダへ
5. (元のPDFファイルにはStarを付ける)
6. 分割処理済みフォルダの中にある、Starのついていないファイルを画像に変換して、画像化処理済みフォルダへ移動する
7. (元のPDFファイルにはStarを付ける)
8. 画像化処理済みフォルダの中にある、Starのついていないファイルを、画像分析処理する
9. (元のPDFファイルにはStarを付ける)
10. スプレッドシートの列「要約処理済み」が未処理のレコードを、要約する
11. メール送信する

Geminiは、アウトラインを出力してくれました。これはこれで便利だと思います。

// グローバル変数 (フォルダID、スプレッドシートIDなどを設定)

// PDFファイル取得処理
function getPDFs() {
  // ... (メール受信、Drive保存処理)
}

// PDF分割判定処理
function checkPDFs() {
  // ... (PDFページ数取得、分割処理)
}

// 画像変換処理
function convertImages() {
  // ... (PDF to Image変換処理)
}

// OCR処理
function runOCR() {
  // ... (Cloud Vision API連携、OCR処理)
}

// 要約処理
function summarize() {
  // ... (要約処理)
}

// メール送信処理
function sendEmail() {
  // ... (メール送信処理)
}

// トリガー設定
function createTriggers() {
  // ... (時限トリガー設定)
}

Chat GPT 4oは、全文を出力してくれました。しゅごい。

function checkNewPDFs() {
  const folder = DriveApp.getFolderById('RAW_PDF_FOLDER_ID');
  const files = folder.getFiles();
  
  while (files.hasNext()) {
    const file = files.next();
    if (!file.isStarred()) {
      processPDF(file);
      file.setStarred(true);
    }
  }
}

function processPDF(file) {
  const numPages = countPDFPages(file);
  if (numPages > 1) {
    splitPDF(file);
  } else {
    moveToProcessedFolder(file, 'SINGLE_PAGE_FOLDER_ID');
  }
}

function countPDFPages(file) {
  // PDFページ数を判定する処理 (Google PDF APIやサードパーティライブラリを活用)
  return 1; // 仮の値
}

function splitPDF(file) {
  const splitFolder = DriveApp.getFolderById('SPLIT_PDF_FOLDER_ID');
  // PDF分割処理を実装
  moveToProcessedFolder(file, splitFolder.getId());
}

function moveToProcessedFolder(file, folderId) {
  const folder = DriveApp.getFolderById(folderId);
  file.moveTo(folder);
}

function convertToImage() {
  const folder = DriveApp.getFolderById('SPLIT_PDF_FOLDER_ID');
  const files = folder.getFiles();
  
  while (files.hasNext()) {
    const file = files.next();
    if (!file.isStarred()) {
      const image = pdfToImage(file);
      moveToProcessedFolder(image, 'IMAGE_FOLDER_ID');
      file.setStarred(true);
    }
  }
}

function pdfToImage(file) {
  // PDFを画像に変換する処理 (OCRサービスと連携)
  return file; // 仮の戻り値
}

function performOCR() {
  const folder = DriveApp.getFolderById('IMAGE_FOLDER_ID');
  const files = folder.getFiles();
  
  while (files.hasNext()) {
    const file = files.next();
    if (!file.isStarred()) {
      const text = extractTextFromImage(file);
      saveToSheet(file, text);
      file.setStarred(true);
    }
  }
}

function extractTextFromImage(file) {
  // OCR処理 (Google Vision APIなどを使用)
  return 'Sample OCR text';
}

function saveToSheet(file, text) {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('OCR_RESULTS');
  sheet.appendRow([file.getName(), text]);
}

function sendSummaryEmail() {
  const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('OCR_RESULTS');
  const data = sheet.getDataRange().getValues();
  
  let message = '';
  data.forEach(row => {
    message += `File: ${row[0]}\nSummary: ${row[1]}\n\n`;
  });
  
  MailApp.sendEmail({
    to: 'recipient@example.com',
    subject: 'OCR Summary Report',
    body: message
  });
}

何がなんだか分からない状態ですが、使えるパーツもあるようなので、これは、このままメモ帳代わりのスクリプトエディタにコピペしておきましょう。

今回は、ここまでです。

まとめ

以上で、「GASでGeminiを使って受信したFAXを要約したメールを送ろう」の1回目をお送りしました。

普段やってる開発と頭の中をツラツラを書いたので、間違ってるところも多いと思います。

要件定義も甘いです。

そして、AIでどこまで書けるのだろう、というのもテストしました。

Geminiが関数のアウトラインを出したに過ぎないのにくらべて、Chat GPT 4oは、できる限りの実装を出力したのはすごいと思います。

肝心の処理も、テキストで示すなど、頭いい子ちゃんだなと思います。

  • // PDFを画像に変換する処理 (OCRサービスと連携)
  • // OCR処理 (Google Vision APIなどを使用)

次回は、「PDFを分割しよう」をお届けします。お楽しみに。

このシリーズの目次

タイトルとURLをコピーしました