ChatGPTとの会話遍歴 バイブコーディング すべて見せます。

2025年5月2日金曜日

ChatGPT GAS Gemini API Google Apps Script VibeCoding

【実録】Gemini API × GASで領収書OCRから勘定科目を自動分類

投稿日:

目次


はじめに

この記事では、「領収書のOCRテキストから、最適な勘定科目をAI判定し、スプレッドシートに自動入力する」までの実装過程を、実際に起きたエラー対応も含めて紹介します。

使用技術:

  • Google Apps Script(GAS)
  • Gemini API(Generative Language API v1beta)
  • モデル:Gemini 1.5 Pro

ステップ1|要求仕様まとめ

  • Gemini APIキーを定数管理
  • 対象スプレッドシートIDを指定
  • 2行目以降を処理(1行目はヘッダー)
  • L列OCRあり、M列フラグなしの行だけ処理
  • 勘定科目をK列へ、完了をM列へ記載
  • エラー時にはメール通知

ステップ2|試行錯誤の記録

問題点原因解決策
401エラーAuthorizationヘッダー誤りURLにAPIキー付与
404エラーモデル名ミス正しいモデルに変更
400エラーpayload形式ミスcontents形式に修正

コーディング知識のある方には何のこと無いと思いますが、試行錯誤を繰り返しました。

ステップ3|最終版スクリプト(Gemini 1.5 Pro対応版)

<!-- GASスクリプト -->
const GEMINI_API_KEY = 'XXXX';
const SPREADSHEET_ID = 'YYYY';
const SHEET_NAME = '経費明細';
const ERROR_EMAIL = 'your-email@example.com';
const MODEL_NAME = 'gemini-1.5-pro-002';
const ENDPOINT = 'https://generativelanguage.googleapis.com/v1beta/models/' + MODEL_NAME + ':generateContent?key=' + encodeURIComponent(GEMINI_API_KEY);

function processReceiptCategory() {
  const ss = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);
  const values = sheet.getDataRange().getValues();

  for (let i = 1; i < values.length; i++) {
    const ocrText = values[i][11];
    const doneFlag = values[i][12];
    if (ocrText && !doneFlag) {
      try {
        const prompt = ['以下は領収書のOCRテキストです(ノイズが含まれています)。', ocrText, '', '会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'].join('\n');
        const payload = JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] });
        const options = { method: 'post', contentType: 'application/json', payload, muteHttpExceptions: true };
        const response = UrlFetchApp.fetch(ENDPOINT, options);
        const code = response.getResponseCode();
        const body = response.getContentText();
        if (code === 200) {
          const json = JSON.parse(body);
          const parts = json.candidates[0].content.parts;
          const category = parts.map(p => p.text).join('').trim();
          sheet.getRange(i + 1, 11).setValue(category);
          sheet.getRange(i + 1, 13).setValue('完了');
        } else {
          throw new Error(`Gemini API Error (${code}): ${body}`);
        }
      } catch (e) {
        Logger.log(e);
        MailApp.sendEmail({ to: ERROR_EMAIL, subject: '【GASエラー通知】領収書処理エラー', body: e.toString() });
      }
      break;
    }
  }
}

ステップ4|チャット履歴まとめ

今回はChatGPT 4oを使用

初回プロント

以下の手順を Google Apps Script のスクリプトにしてほしい。 

1. 定数として「自分の Gemini API キー」を設定できる変数を用意する  
 キーは”XXXX"

2. 対象のスプレッドシート ID を指定して開き、アクティブなシートを取得する  
 IDは”YYYY” シート”経費明細”

3. シートの1行目はヘッダー行であり、データ処理の対象外とする。   
4. 2行目以降の全データを対象に、
・処理完了フラグ M2:Mが空欄
かつ
・領収書写真OCR L2:Lが空欄ではない最初の1行だけを処理する   
5. 当該行の領収書写真OCR L2;Lを GeminiAPIを経由し伝送。

以下は領収書のOCRテキストです(ノイズが含まれています)。
この中から、**会計処理に必要な情報のみ抽出して**、最も適切な「勘定科目」を1つだけ日本語で出力してください。 と指示

6. AI から返ってきた勘定科目をKK2:K 勘定科目 に書き込み、M2:M 処理完了フラグに"完了"を書き込む   
7. AI 呼び出しでエラーが発生した場合は、エラーメッセージをログに書き出すとともに、任意のEメールアドレス○○にエラー内容を送付する
  
8. 1行だけ処理したら必ずループを抜ける  

生成結果

/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理
 */

const GEMINI_API_KEY    = 'XXXX';                  // ご自身の Gemini API キー
const SPREADSHEET_ID    = 'YYYY';                  // 対象のスプレッドシート ID
const SHEET_NAME        = '経費明細';               // 対象シート名
const ERROR_EMAIL       = 'your-email@example.com'; // エラー通知先メールアドレス

function processReceiptCategory() {
  // スプレッドシートとシートを取得
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);
  }

  // 全データを取得(1行目ヘッダーを含む)
  const values = sheet.getDataRange().getValues();

  // 2行目以降をループ
  for (let i = 1; i < values.length; i++) {
    const ocrText    = values[i][11]; // L列 (0-based index 11)
    const doneFlag   = values[i][12]; // M列 (0-based index 12)

    // 未処理かつOCRテキストがある行だけを対象
    if (!doneFlag && ocrText) {
      try {
        // Gemini API 呼び出し用ペイロード
        const url = 'https://generativelanguage.googleapis.com/v1beta/models/text-bison-001:generateText';
        const prompt = 
          '以下は領収書のOCRテキストです(ノイズが含まれています)。\n' +
          ocrText + '\n\n' +
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。';

        const payload = {
          prompt: {
            text: prompt
          },
          temperature: 0,
          candidateCount: 1
        };

        const options = {
          method: 'post',
          contentType: 'application/json',
          headers: {
            Authorization: 'Bearer ' + GEMINI_API_KEY
          },
          payload: JSON.stringify(payload),
          muteHttpExceptions: true
        };

        const response = UrlFetchApp.fetch(url, options);
        const code     = response.getResponseCode();
        const body     = response.getContentText();

        if (code === 200) {
          const json     = JSON.parse(body);
          const category = json.candidates[0].output.trim();

          // K列(勘定科目)と M列(処理完了フラグ)に書き込み
          sheet.getRange(i + 1, 11).setValue(category);   // K 列
          sheet.getRange(i + 1, 13).setValue('完了');     // M 列
        } else {
          throw new Error(`Gemini API Error (${code}): ${body}`);
        }
      } catch (e) {
        // エラーをログに出力し、メールで通知
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }

      // 必ず1行だけ処理したらループを抜ける
      break;
    }
  }
}

ここで一度GAS実行も401エラー 下記の通りエラー修正指示

	Error: Gemini API Error (401): {
  "error": {
    "code": 401,
    "message": "Request had invalid authentication credentials. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
    "status": "UNAUTHENTICATED"
  }
}

こんなエラー出たよ。
/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理(APIキー認証版)
 */

const GEMINI_API_KEY    = 'XXXX';                  // ご自身の Gemini API キー
const SPREADSHEET_ID    = 'YYYY';                  // 対象のスプレッドシート ID
const SHEET_NAME        = '経費明細';               // 対象シート名
const ERROR_EMAIL       = 'your-email@example.com'; // エラー通知先メールアドレス

function processReceiptCategory() {
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);

  const values = sheet.getDataRange().getValues();
  for (let i = 1; i < values.length; i++) {
    const ocrText  = values[i][11]; // L列
    const doneFlag = values[i][12]; // M列
    if (!doneFlag && ocrText) {
      try {
        // APIキーをクエリにセット
        const url = 
          'https://generativelanguage.googleapis.com/v1beta/models/text-bison-001:generateText'
          + '?key=' + encodeURIComponent(GEMINI_API_KEY);

        const prompt = [
          '以下は領収書のOCRテキストです(ノイズが含まれています)。',
          ocrText,
          '',
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'
        ].join('\n');

        const payload = {
          prompt: { text: prompt },
          temperature: 0,
          candidateCount: 1
        };

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

        const response = UrlFetchApp.fetch(url, options);
        if (response.getResponseCode() === 200) {
          const category = JSON.parse(response.getContentText())
                               .candidates[0].output.trim();
          sheet.getRange(i + 1, 11).setValue(category);  // K列
          sheet.getRange(i + 1, 13).setValue('完了');    // M列
        } else {
          throw new Error(`Gemini API Error (${response.getResponseCode()}): `
                          + response.getContentText());
        }
      } catch (e) {
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }
      break;  // 1行だけ処理したら抜ける
    }
  }
}

生成結果をまた実行→エラー404→エラー内容をコピペし修正依頼

Error: Gemini API Error (404): {
  "error": {
    "code": 404,
    "message": "Requested entity was not found.",
    "status": "NOT_FOUND"
  }
}

またでた


/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理(Generative Language API v1beta2)
 */

// ——— 定数設定 ———
const GEMINI_API_KEY = 'XXXX';                     // ご自身の Gemini API キー
const SPREADSHEET_ID = 'YYYY';                     // 対象のスプレッドシート ID
const SHEET_NAME     = '経費明細';                  // 対象シート名
const ERROR_EMAIL    = 'your-email@example.com';   // エラー通知先メールアドレス

// モデル名 & エンドポイント(v1beta2 + text-bison@001)
const MODEL_NAME = 'text-bison@001';
const ENDPOINT   = 'https://generativelanguage.googleapis.com/v1beta2/models/'
                   + encodeURIComponent(MODEL_NAME)
                   + ':generateText?key=' + encodeURIComponent(GEMINI_API_KEY);

/**
 * main: 未処理のレシートOCR行をひとつだけ処理する
 */
function processReceiptCategory() {
  // スプレッドシートとシートを取得
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);
  }

  // 全データを取得(1行目ヘッダー行を含む)
  const values = sheet.getDataRange().getValues();

  // 2行目以降をループして、最初の未処理行を処理
  for (let i = 1; i < values.length; i++) {
    const ocrText  = values[i][11]; // L列 (0-based index 11)
    const doneFlag = values[i][12]; // M列 (0-based index 12)

    // OCRテキストあり & 未処理 の行だけを対象
    if (ocrText && !doneFlag) {
      try {
        // プロンプト作成
        const prompt = [
          '以下は領収書のOCRテキストです(ノイズが含まれています)。',
          ocrText,
          '',
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'
        ].join('\n');

        // API 呼び出し用ペイロード
        const payload = {
          prompt: { text: prompt },
          temperature: 0,
          candidateCount: 1
        };

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

        // Gemini API 呼び出し
        const response = UrlFetchApp.fetch(ENDPOINT, options);
        const code     = response.getResponseCode();
        const body     = response.getContentText();

        if (code === 200) {
          const json     = JSON.parse(body);
          const category = json.candidates[0].output.trim();

          // K列(勘定科目)・M列(処理完了フラグ)に書き込み
          sheet.getRange(i + 1, 11).setValue(category); // K列 (1-based)
          sheet.getRange(i + 1, 13).setValue('完了');   // M列 (1-based)
        } else {
          throw new Error(`Gemini API Error (${code}): ${body}`);
        }
      } catch (e) {
        // エラー時はログ & メール通知
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }

      // 1行だけ処理したら必ずループを抜ける
      break;
    }
  }
}

/**
 * デバッグ用: 利用可能なモデル一覧を取得してログに出力する
 */
function listGeminiModels() {
  const listUrl = 'https://generativelanguage.googleapis.com/v1beta2/models?key='
                  + encodeURIComponent(GEMINI_API_KEY);
  const res     = UrlFetchApp.fetch(listUrl);
  Logger.log(res.getContentText());
}

ここで、よくよく見ると、見慣れないGeminiモデルがでてきたので、

現在利用可能なモデル一覧を調べたうえで、最適なものを提案してほしい

と改めて依頼すると

以下に、Generative Language API(v1beta2)および Gemini API で現在利用可能な主なモデルをまとめ、その上で「領収書OCRテキストから勘定科目を分類・抽出する」用途に最適と思われるモデルを提案します。

現在利用可能なモデル一覧

モデル選定のポイント

  1. タスク適合性: 単一行のテキストから「勘定科目」を抽出し分類する用途では、汎用テキスト生成モデルが最適です。

  2. コスト効率: 頻繁にセルごとに呼び出すため、高レイテンシ・高コストなプロモデルよりライトモデルを優先します。

  3. スループットとレイテンシ: シート処理はバッチ的に1行ずつ行うため、100ms程度のレスポンスタイムを維持できるモデルが望ましいです。

  4. 精度: 分類精度を担保するため、プロファイルされた調整済みモデルを利用するか、zero/few-shot プロンプトで安定した出力を得られるモデルを選びます。

推奨モデル

models/text-bison@001 を第一候補とします。

もし将来的にマルチモーダル対応(例えば領収書画像の直接解析など)を検討する場合は、gemini-2.0-flash または gemini-2.0-flash-lite の利用を検討するとよいでしょう (Gemini models  |  Gemini API  |  Google AI for Developers).


と返答あり。自らもGEMINI API 公式documentをななめ読みしたうえで、Gemini 1.5 Proが良いかと思われたので、

1.5proは?


と質問。 返答は以下

Gemini 1.5 Pro の概要と利用可否についてまとめます。

1. モデルの特徴

  • リリース時期:2024年2月に “Gemini 1.5” として公開され、その後「Pro」モデル(Gemini 1.5 Pro-002)が2024年9月24日にリリースされました (Gemini (language model), Gemini (language model)).

  • アーキテクチャ:新アーキテクチャ+Mixture-of-Experts により、前世代(Gemini 1.0 Pro)よりも幅広い推論や大規模コンテキスト処理が可能。最大 1,000,000トークン のコンテキストウィンドウをサポートしています (Gemini (language model)).

  • Vertex AI 呼び出し

2. 利用上の制約

3. 用途別の選択ポイント

  • 領収書OCRテキストから「勘定科目」を一行ずつ抽出するような 軽量・単純分類タスク には、むしろ text-bison@001 がコスト・性能のバランスに優れ、安定した分類結果を得やすいです。

  • ただし、

    • 大規模コンテキスト(長文や複数ドキュメントの同時解析)、

    • 複雑な推論(マルチステップのビジネスロジックを含む要約や分類)、

    • あるいは既に Gemini 1.5 Pro を利用中で高い精度を追求したい場合

    Gemini 1.5 Pro が威力を発揮します。


結論

  • 新規プロジェクトかつ経費分類のみtext-bison@001 をおすすめ。

  • 既存プロジェクトで1.5 Proを有効化済、かつ複雑タスクにも拡張予定gemini-1.5-pro を利用検討。

  • これから新規に高性能モデルを使いたい場合gemini-2.0-flashgemini-2.5-pro(プレビュー)も合わせてご検討ください。


1.5proをお願いしたのに、text-bison@001 をおすすめしてくるので、やむなく、

text-bison@001 で実装をお願い。
全scriptを提示して?

と依頼。返答結果は下記。


/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理(Generative Language API v1beta2 + text-bison@001)
 */

// ——— 定数設定 ———
const GEMINI_API_KEY = 'XXXX';                   // ご自身の Gemini API キー
const SPREADSHEET_ID = 'YYYY';                   // 対象のスプレッドシート ID
const SHEET_NAME     = '経費明細';                // 対象シート名
const ERROR_EMAIL    = 'your-email@example.com'; // エラー通知先メールアドレス

// ——— モデル名 & エンドポイント ———
const MODEL_NAME = 'text-bison@001';
const ENDPOINT   = 'https://generativelanguage.googleapis.com/v1beta2/models/'
                   + encodeURIComponent(MODEL_NAME)
                   + ':generateText?key=' + encodeURIComponent(GEMINI_API_KEY);

/**
 * メイン関数:
 * 未処理の行を1行だけ処理し、
 * 勘定科目をK列に、完了フラグをM列に書き込む
 */
function processReceiptCategory() {
  // スプレッドシート・シート取得
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);
  }

  // 全データ取得(1行目はヘッダー)
  const values = sheet.getDataRange().getValues();

  // 2行目以降をループ
  for (let i = 1; i < values.length; i++) {
    const ocrText  = values[i][11]; // L列 (0-based index 11)
    const doneFlag = values[i][12]; // M列 (0-based index 12)

    // OCRテキストありかつ未処理の行を対象
    if (ocrText && !doneFlag) {
      try {
        // プロンプト作成
        const prompt = [
          '以下は領収書のOCRテキストです(ノイズが含まれています)。',
          ocrText,
          '',
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'
        ].join('\n');

        // API 呼び出し用ペイロード
        const payload = {
          prompt: { text: prompt },
          temperature: 0,
          candidateCount: 1
        };

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

        // Gemini API 呼び出し
        const response = UrlFetchApp.fetch(ENDPOINT, options);
        const code     = response.getResponseCode();
        const body     = response.getContentText();

        if (code === 200) {
          const category = JSON.parse(body)
                               .candidates[0].output
                               .trim();

          // K列(勘定科目)に出力、M列(処理完了フラグ)に「完了」を書き込み
          sheet.getRange(i + 1, 11).setValue(category); // K列 (1-based)
          sheet.getRange(i + 1, 13).setValue('完了');   // M列 (1-based)
        } else {
          throw new Error(`Gemini API Error (${code}): ${body}`);
        }
      } catch (e) {
        // エラー時はログ & メール通知
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }

      // 1行だけ処理したら必ずループを抜ける
      break;
    }
  }
}

/**
 * デバッグ用: 利用可能なモデル一覧を取得してログ出力
 */
function listGeminiModels() {
  const listUrl = 'https://generativelanguage.googleapis.com/v1beta2/models?key='
                  + encodeURIComponent(GEMINI_API_KEY);
  const res     = UrlFetchApp.fetch(listUrl);
  Logger.log(res.getContentText());
}


でもうまくいかない。。。 下記エラーを修正依頼


	Error: Gemini API Error (400): {
  "error": {
    "code": 400,
    "message": "* GenerateTextRequest.model: unexpected model name format\n",
    "status": "INVALID_ARGUMENT"
  }
}

/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理(Generative Language API v1beta2 + text-bison-001)
 */

// ——— 定数設定 ———
const GEMINI_API_KEY = 'XXXX';                   // ご自身の Gemini API キー
const SPREADSHEET_ID = 'YYYY';                   // 対象のスプレッドシート ID
const SHEET_NAME     = '経費明細';                // 対象シート名
const ERROR_EMAIL    = 'your-email@example.com'; // エラー通知先メールアドレス

// ——— モデル名 & エンドポイント ———
// モデル名を "@001" ではなく "-001" に修正
const MODEL_NAME = 'text-bison-001';
const ENDPOINT   = 'https://generativelanguage.googleapis.com/v1beta2/models/'
                   + MODEL_NAME
                   + ':generateText?key=' + encodeURIComponent(GEMINI_API_KEY);

/**
 * メイン関数:
 * 未処理の行を1行だけ処理し、
 * 勘定科目をK列に、完了フラグをM列に書き込む
 */
function processReceiptCategory() {
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);
  }

  const values = sheet.getDataRange().getValues();
  for (let i = 1; i < values.length; i++) {
    const ocrText  = values[i][11]; // L列
    const doneFlag = values[i][12]; // M列

    if (ocrText && !doneFlag) {
      try {
        const prompt = [
          '以下は領収書のOCRテキストです(ノイズが含まれています)。',
          ocrText,
          '',
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'
        ].join('\n');

        const payload = {
          prompt: { text: prompt },
          temperature: 0,
          candidateCount: 1
        };

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

        const response = UrlFetchApp.fetch(ENDPOINT, options);
        const code     = response.getResponseCode();
        const body     = response.getContentText();

        if (code === 200) {
          const category = JSON.parse(body)
                               .candidates[0].output
                               .trim();
          sheet.getRange(i + 1, 11).setValue(category); // K列
          sheet.getRange(i + 1, 13).setValue('完了');   // M列
        } else {
          throw new Error(`Gemini API Error (${code}): ${body}`);
        }
      } catch (e) {
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }
      break;
    }
  }
}

/**
 * デバッグ用: 利用可能なモデル一覧を取得してログ出力
 */
function listGeminiModels() {
  const listUrl = 'https://generativelanguage.googleapis.com/v1beta2/models?key='
                  + encodeURIComponent(GEMINI_API_KEY);
  const res     = UrlFetchApp.fetch(listUrl);
  Logger.log(res.getContentText());
}

修正してもさらにエラー。我慢できず、モデルが適切でないことを下記通り指摘。
Error: Gemini API Error (404): {
  "error": {
    "code": 404,
    "message": "Requested entity was not found.",
    "status": "NOT_FOUND"
  }
}

モデルがおかしいんじゃない?


まだ違うモデルでスクリプトを書いてくる・・・

/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理
 *
 * ── フロー ──
 * 1) listModels() で利用可能モデルを取得
 * 2) "text-bison" を含むモデル名を自動選択
 * 3) 選択したモデルで generateText を呼び出し
 * 4) K列に勘定科目、M列に"完了"をセット
 */

const GEMINI_API_KEY = 'XXXX';                   // ご自身の Gemini API キー
const SPREADSHEET_ID = 'YYYY';                   // 対象のスプレッドシート ID
const SHEET_NAME     = '経費明細';                // 対象シート名
const ERROR_EMAIL    = 'your-email@example.com'; // エラー通知先メールアドレス

/**
 * 利用可能モデル一覧を取得し、
 * name に "text-bison" を含む最初のモデルID を返す
 */
function getTextBisonModel() {
  const url = 'https://generativelanguage.googleapis.com/v1beta2/models'
            + '?key=' + encodeURIComponent(GEMINI_API_KEY);
  const res  = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (res.getResponseCode() !== 200) {
    throw new Error('モデル一覧取得エラー(' + res.getResponseCode() + '): ' + res.getContentText());
  }
  const data = JSON.parse(res.getContentText());
  if (!data.models || !Array.isArray(data.models)) {
    throw new Error('models フィールドが見つかりませんでした。');
  }
  // "models/text-bison-001" などの形で返ってくる
  for (let m of data.models) {
    if (m.name.indexOf('text-bison') !== -1) {
      // "models/text-bison-001" → ["models","text-bison-001"][1]
      return m.name.split('/')[1];
    }
  }
  throw new Error('利用可能な "text-bison" モデルが見つかりませんでした。');
}

/**
 * メイン関数:
 * 未処理の行を1行だけ処理し、
 * 勘定科目をK列に、完了フラグをM列に書き込む
 */
function processReceiptCategory() {
  // スプレッドシート & シート取得
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);

  // モデル名を自動取得
  const modelId  = getTextBisonModel();  
  const endpoint = 'https://generativelanguage.googleapis.com/v1beta2/models/'
                   + modelId
                   + ':generateText?key=' + encodeURIComponent(GEMINI_API_KEY);

  // 全データ取得(1行目ヘッダー)
  const values = sheet.getDataRange().getValues();
  for (let i = 1; i < values.length; i++) {
    const ocrText  = values[i][11]; // L列
    const doneFlag = values[i][12]; // M列

    if (ocrText && !doneFlag) {
      try {
        // プロンプト作成
        const prompt = [
          '以下は領収書のOCRテキストです(ノイズが含まれています)。',
          ocrText,
          '',
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'
        ].join('\n');

        const payload = {
          prompt: { text: prompt },
          temperature: 0,
          candidateCount: 1
        };
        const options = {
          method: 'post',
          contentType: 'application/json',
          payload: JSON.stringify(payload),
          muteHttpExceptions: true
        };

        // API 呼び出し
        const res  = UrlFetchApp.fetch(endpoint, options);
        const code = res.getResponseCode();
        const txt  = res.getContentText();

        if (code === 200) {
          const category = JSON.parse(txt).candidates[0].output.trim();
          sheet.getRange(i + 1, 11).setValue(category); // K列
          sheet.getRange(i + 1, 13).setValue('完了');   // M列
        } else {
          throw new Error(`Gemini API Error (${code}): ${txt}`);
        }
      } catch (e) {
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }
      break;  // 1行だけ処理したら抜ける
    }
  }
}

/**
 * デバッグ用: 実際にモデル一覧を Logger に出力する
 */
function listModels() {
  const url = 'https://generativelanguage.googleapis.com/v1beta2/models'
            + '?key=' + encodeURIComponent(GEMINI_API_KEY);
  const res = UrlFetchApp.fetch(url);
  Logger.log(res.getContentText());
}

ので、ここで根本治療。
Gemini 1.5 Pro で全script修正をお願い


と指示。

/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理(Generative Language API v1beta + Gemini 1.5 Pro)
 */

// ——— 定数設定 ———
const GEMINI_API_KEY = 'XXXX';                   // ご自身の Gemini API キー
const SPREADSHEET_ID = 'YYYY';                   // 対象のスプレッドシート ID
const SHEET_NAME     = '経費明細';                // 対象シート名
const ERROR_EMAIL    = 'your-email@example.com'; // エラー通知先メールアドレス

// ——— モデル名 & エンドポイント ———
const MODEL_NAME = 'gemini-1.5-pro-latest';
const ENDPOINT   = 'https://generativelanguage.googleapis.com/v1beta/models/'
                   + MODEL_NAME
                   + ':generateContent?key=' + encodeURIComponent(GEMINI_API_KEY);

/**
 * メイン関数:
 * 未処理の行を1行だけ処理し、
 * 勘定科目をK列に、完了フラグをM列に書き込む
 */
function processReceiptCategory() {
  // スプレッドシート・シート取得
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);
  }

  // 全データ取得(1行目はヘッダー行)
  const values = sheet.getDataRange().getValues();

  // 2行目以降をループし、最初の未処理行を処理
  for (let i = 1; i < values.length; i++) {
    const ocrText  = values[i][11]; // L列 (0-based index 11)
    const doneFlag = values[i][12]; // M列 (0-based index 12)

    if (ocrText && !doneFlag) {
      try {
        // プロンプト組み立て
        const prompt = [
          '以下は領収書のOCRテキストです(ノイズが含まれています)。',
          ocrText,
          '',
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'
        ].join('\n');

        // API 呼び出し用ペイロード
        const payload = {
          prompt: { text: prompt },
          temperature: 0,
          candidateCount: 1
        };
        const options = {
          method: 'post',
          contentType: 'application/json',
          payload: JSON.stringify(payload),
          muteHttpExceptions: true
        };

        // Gemini 1.5 Pro API 呼び出し
        const res  = UrlFetchApp.fetch(ENDPOINT, options);
        const code = res.getResponseCode();
        const body = res.getContentText();

        if (code === 200) {
          const category = JSON.parse(body)
                               .candidates[0].output
                               .trim();
          // K列(勘定科目)・M列(処理完了フラグ)に書き込み
          sheet.getRange(i + 1, 11).setValue(category); // K列
          sheet.getRange(i + 1, 13).setValue('完了');   // M列
        } else {
          throw new Error(`Gemini API Error (${code}): ${body}`);
        }
      } catch (e) {
        // エラー時はログ & メール通知
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }
      // 1行だけ処理したらループを抜ける
      break;
    }
  }
}

/**
 * デバッグ用: 実際に利用可能なモデル一覧をログ出力
 */
function listModels() {
  const url = 'https://generativelanguage.googleapis.com/v1beta/models'
            + '?key=' + encodeURIComponent(GEMINI_API_KEY);
  const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  Logger.log(res.getContentText());
}

再度実行処理を行うも、まだエラー


Error: Gemini API Error (400): {
  "error": {
    "code": 400,
    "message": "Invalid JSON payload received. Unknown name \"prompt\": Cannot find field.\nInvalid JSON payload received. Unknown name \"temperature\": Cannot find field.\nInvalid JSON payload received. Unknown name \"candidateCount\": Cannot find field.",
    "status": "INVALID_ARGUMENT",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.BadRequest",
        "fieldViolations": [
          {
            "description": "Invalid JSON payload received. Unknown name \"prompt\": Cannot find field."
          },
          {
            "description": "Invalid JSON payload received. Unknown name \"temperature\": Cannot find field."
          },
          {
            "description": "Invalid JSON payload received. Unknown name \"candidateCount\": Cannot find field."
          }
        ]
      }
    ]
  }
}


エラー修正後下記スクリプトを実行


/**
 * Google Apps Script: 領収書OCRテキストから勘定科目を取得し、
 * シートに書き込む自動処理(Generative Language API v1beta + Gemini 1.5 Pro)
 *
 * モデル呼び出し時の JSON ペイロードには `contents` フィールドを使います :contentReference[oaicite:0]{index=0}。
 * レスポンスからは最初の候補の content.parts[0].text を抽出します :contentReference[oaicite:1]{index=1}。
 */

// ——— 定数設定 ———
const GEMINI_API_KEY = 'XXXX';                   // ご自身の Gemini API キー
const SPREADSHEET_ID = 'YYYY';                   // 対象のスプレッドシート ID
const SHEET_NAME     = '経費明細';                // 対象シート名
const ERROR_EMAIL    = 'your-email@example.com'; // エラー通知先メールアドレス

// ——— モデル名 & エンドポイント ———
const MODEL_NAME = 'gemini-1.5-pro-002';
const ENDPOINT   =
  'https://generativelanguage.googleapis.com/v1beta/models/'
  + MODEL_NAME
  + ':generateContent?key='
  + encodeURIComponent(GEMINI_API_KEY);

/**
 * メイン関数:
 * 2行目以降の未処理行を1行だけ処理し、
 * K列に勘定科目、M列に「完了」を書き込む
 */
function processReceiptCategory() {
  const ss    = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    throw new Error(`シート「${SHEET_NAME}」が見つかりません。`);
  }

  // データ取得(1行目はヘッダー)
  const values = sheet.getDataRange().getValues();
  for (let i = 1; i < values.length; i++) {
    const ocrText  = values[i][11]; // L列 (0-based index 11)
    const doneFlag = values[i][12]; // M列 (0-based index 12)

    // OCRテキストあり & 未処理 の行を処理
    if (ocrText && !doneFlag) {
      try {
        // プロンプト作成
        const prompt = [
          '以下は領収書のOCRテキストです(ノイズが含まれています)。',
          ocrText,
          '',
          'この中から、会計処理に必要な情報のみ抽出して、最も適切な「勘定科目」を1つだけ日本語で出力してください。'
        ].join('\n');

        // リクエストペイロード
        const payload = JSON.stringify({
          contents: [
            { parts: [ { text: prompt } ] }
          ]
          // generation_config などを追加したい場合はここに
        });

        const options = {
          method: 'post',
          contentType: 'application/json',
          payload,
          muteHttpExceptions: true
        };

        // API 呼び出し
        const response = UrlFetchApp.fetch(ENDPOINT, options);
        const code     = response.getResponseCode();
        const body     = response.getContentText();

        if (code === 200) {
          const json = JSON.parse(body);
          // 最初の候補のテキストを抽出
          const parts   = json.candidates[0].content.parts;
          const category = parts.map(p => p.text).join('').trim();

          // K列(勘定科目)と M列(処理完了フラグ)に書き込み
          sheet.getRange(i + 1, 11).setValue(category); // K列 (1-based)
          sheet.getRange(i + 1, 13).setValue('完了');   // M列 (1-based)
        } else {
          throw new Error(`Gemini API Error (${code}): ${body}`);
        }
      } catch (e) {
        // エラー時はログ & メール通知
        Logger.log(e);
        MailApp.sendEmail({
          to: ERROR_EMAIL,
          subject: '【GAS エラー通知】領収書処理エラー',
          body: e.toString()
        });
      }
      // 1行だけ処理したら必ずループを抜ける
      break;
    }
  }
}

/**
 * デバッグ用: 利用可能なモデル一覧を Logger に出力
 */
function listModels() {
  const url = 'https://generativelanguage.googleapis.com/v1beta/models'
            + '?key=' + encodeURIComponent(GEMINI_API_KEY);
  const res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  Logger.log(res.getContentText());
}

通った!!!!!


最後に

今回のポイント:

  • エラー文を冷静に読む
  • API仕様変更に注意
  • レスポンス構造を把握

Generative Language APIは進化が非常に早いため、公式ドキュメントを常にチェックすることをおすすめします!