01

概要: なぜ Vertex AI に移行するのか

!
gemini-3-pro-preview は 2026年3月9日をもって廃止(サンセット)されました。
後継モデルは gemini-3.1-pro-preview です。なお gemini-3-flash-preview は引き続き利用可能です。
本資料ではセキュリティ・管理性の観点から、モデル更新と同時に Vertex AI 方式への移行を推奨しています。

移行の背景

gemini-3-pro-preview が廃止され、後継の 3.1 系への移行が必要になりました。3.1 系モデルはAPIキー方式でも利用できますが、Vertex AI 経由でのアクセスが推奨されます。以下はその比較です。

項目 APIキー方式(旧) Vertex AI方式(新)
認証 APIキーをURLパラメータで送信 OAuth トークン(Bearer)
セキュリティ キー漏洩リスクあり GCPのIAMで一元管理
課金 AI Studio経由 GCPプロジェクト課金
モデル管理 利用制限あり エンタープライズ向け全モデル利用可
SLA なし GCP SLA適用
i
この資料は名刺OCRアプリの移行実績に基づいています。他のGASプロジェクトにも同じ手順が適用できます。
02

5つの主要変更点

# 項目 旧(APIキー方式) 新(Vertex AI方式)
1 エンドポイント generativelanguage.googleapis.com aiplatform.googleapis.com
2 認証方式 URLパラメータ ?key=xxx ヘッダー Authorization: Bearer TOKEN
3 モデルID gemini-3-flash-preview gemini-3.1-flash-lite-preview
4 リージョン 不要 global(3.x系必須)
5 リクエスト形式 role 省略可能 "role": "user" が必須
!
重要: gemini-3.1-flash-preview というモデルIDは存在しません。正しくは gemini-3.1-flash-lite-preview です。
!
重要: 3.x系モデルのリージョンは必ず global を指定してください。us-central1 を指定すると404エラーになります。
03

セットアップ手順

1

GASエディタを開く

スプレッドシートの「拡張機能」メニューから「Apps Script」を選択します。

拡張機能メニューからApps Scriptを選択
2

コード.gs を配置

GASエディタの「コード.gs」に、Vertex AI版のコードを貼り付けます。ファイル構成は appsscript.jsonコード.gsindex.html の3つです。

GASエディタにコードを配置
3

プロジェクト設定を確認

歯車アイコン(設定)を開き、以下を確認します:

  • タイムゾーン: (GMT+09:00) 日本標準時 - 東京
  • Chrome V8 ランタイムを有効にする にチェック
  • 「appsscript.json」マニフェスト ファイルをエディタで表示する にチェック
プロジェクト設定画面
4

GCPプロジェクトを紐付け

設定画面の下部「Google Cloud Platform(GCP)プロジェクト」セクションで、GCPプロジェクト番号を入力します。

プロジェクト番号: 1041693762523

プロジェクトID: ai-project001-486207

GCPプロジェクト紐付け
5

appsscript.json を設定

マニフェストファイルを編集し、OAuthスコープを正しく設定します。特に cloud-platformuserinfo.email が重要です。

appsscript.json の設定
6

Drive サービスを追加

左メニューの「サービス」横の「+」をクリックし、Drive API v3 を追加します。GCPコンソール側でも Drive API を有効化してください。

Drive APIサービスの追加
!
GCP側のAPI有効化も必要: GCPコンソール → APIとサービス → 「Google Drive API」と「Vertex AI API」の両方を有効化してください。
*
最後に: スコープ変更後は、一度スクリプトを実行して再承認ダイアログを通す必要があります。
04

コード全文

コード.gs

メインのGASスクリプト。Vertex AI経由でGeminiを呼び出します。

/**
 * 名刺OCR自動化システム — Vertex AI版
 */
var PROJECT_ID      = "ai-project001-486207";
var LOCATION        = "global";
var MODEL_ID        = "gemini-3.1-flash-lite-preview";
var FOLDER_INPUT_ID = "1U1yYHjPfPwCIYC-ZbxFeRXpBa2taBxjo";
var FOLDER_DONE_ID  = "1css9rBa9gWXRu2Efh2LBpk5pNvEdiXC-";
var ORG_DOMAIN      = "lcreator.co.jp";

/**
 * PC用メニューの作成
 */
function onOpen() {
  try {
    var ui = SpreadsheetApp.getUi();
    ui.createMenu("★名刺解析メニュー")
      .addItem("🚀 名刺の解析を開始", "startFromUi")
      .addToUi();
  } catch (e) {}
}

/**
 * スマホ用画面の表示
 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index")
    .setTitle("名刺解析リモコン")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

function startFromUi() { return mainEngine(true); }
function runFromWeb()  { return mainEngine(false); }

/**
 * メイン解析エンジン
 */
function mainEngine(isUiEnabled) {
  var currentUser = Session.getActiveUser().getEmail();
  var ss = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
  var clean = function(val) { return Array.isArray(val) ? val.join(", ") : (val || ""); };

  if (isUiEnabled && (!currentUser || !currentUser.endsWith("@" + ORG_DOMAIN))) {
    var ui = SpreadsheetApp.getUi();
    var res = ui.alert("警告", "組織外アカウントです。続行しますか?", ui.ButtonSet.YES_NO);
    if (res == ui.Button.NO) return "中断されました";
  }

  if (ss.getLastRow() === 0) {
    ss.appendRow(["日時", "会社名", "氏名", "役職", "電話番号", "メールアドレス", "住所", "画像URL", "実行者"]);
  }

  try {
    var listResponse = Drive.Files.list({
      q: "'" + FOLDER_INPUT_ID + "' in parents and trashed = false",
      supportsAllDrives: true,
      includeItemsFromAllDrives: true,
      fields: "files(id, name, mimeType)"
    });

    var files = listResponse.files;
    if (!files || files.length === 0) {
      var msg = "対象の画像が見つかりません。";
      if (isUiEnabled) SpreadsheetApp.getUi().alert(msg);
      return msg;
    }

    var token = ScriptApp.getOAuthToken();
    var url = "https://aiplatform.googleapis.com/v1/projects/"
      + PROJECT_ID + "/locations/" + LOCATION
      + "/publishers/google/models/" + MODEL_ID
      + ":generateContent";

    files.forEach(function(file) {
      if (!file.mimeType.includes("image")) return;
      var fileBlob = DriveApp.getFileById(file.id).getBlob();

      var payload = {
        "contents": [{
          "role": "user",
          "parts": [
            { "text": "名刺情報を解析しJSONで返してください。company, name, title, tel, email, address。複数の値がある場合は配列で返してください。Markdown不要。" },
            { "inlineData": { "mimeType": file.mimeType, "data": Utilities.base64Encode(fileBlob.getBytes()) } }
          ]
        }]
      };

      var apiRes = UrlFetchApp.fetch(url, {
        "method": "post",
        "contentType": "application/json",
        "headers": { "Authorization": "Bearer " + token },
        "payload": JSON.stringify(payload),
        "muteHttpExceptions": true
      });

      var statusCode = apiRes.getResponseCode();
      if (statusCode !== 200) {
        throw new Error("Vertex AI API エラー (" + statusCode + "): " + apiRes.getContentText());
      }

      var data = JSON.parse(
        JSON.parse(apiRes.getContentText()).candidates[0].content.parts[0].text
          .replace(/```json|```/g, "").trim()
      );

      var originalFile = DriveApp.getFileById(file.id);
      var copiedFile = originalFile.makeCopy(file.name, DriveApp.getFolderById(FOLDER_DONE_ID));

      ss.appendRow([
        new Date(),
        clean(data.company),
        clean(data.name),
        clean(data.title),
        clean(data.tel),
        clean(data.email),
        clean(data.address),
        copiedFile.getUrl(),
        currentUser || "外部ユーザー"
      ]);

      Drive.Files.remove(file.id, { supportsAllDrives: true });
    });

    var finishMsg = "✅ 全ての解析が完了しました!";
    if (isUiEnabled) SpreadsheetApp.getUi().alert(finishMsg);
    return finishMsg;

  } catch (e) {
    var errorMsg = "❌ エラー: " + e.toString();
    if (isUiEnabled) SpreadsheetApp.getUi().alert(errorMsg);
    return errorMsg;
  }
}

index.html

スマートフォン用のリモコンUI。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: 'Helvetica Neue', 'Noto Sans JP', sans-serif;
      background: #e0e5ec;
      min-height: 100vh;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 20px;
    }
    .card {
      background: #e0e5ec;
      border-radius: 20px;
      padding: 40px 30px;
      width: 100%;
      max-width: 400px;
      box-shadow: 8px 8px 15px #a3b1c6, -8px -8px 15px #ffffff;
      text-align: center;
    }
    h1 { font-size: 1.4rem; color: #2d3748; margin-bottom: 8px; }
    .subtitle { font-size: 0.85rem; color: #5a6a82; margin-bottom: 30px; }
    .btn {
      display: block; width: 100%; padding: 16px;
      font-size: 1.1rem; font-weight: 600; color: #fff;
      border: none; border-radius: 12px; cursor: pointer;
      background: linear-gradient(135deg, #4a90d9, #357abd);
      box-shadow: 4px 4px 10px #a3b1c6, -4px -4px 10px #ffffff;
      transition: all 0.2s;
    }
    .btn:active {
      box-shadow: inset 4px 4px 8px #a3b1c6, inset -4px -4px 8px #ffffff;
      transform: scale(0.98);
    }
    .btn:disabled { opacity: 0.6; cursor: not-allowed; }
    #status {
      margin-top: 24px; padding: 16px; border-radius: 12px;
      font-size: 0.95rem; color: #2d3748; background: #e0e5ec;
      box-shadow: inset 6px 6px 10px #a3b1c6, inset -6px -6px 10px #ffffff;
      min-height: 50px; display: flex; align-items: center;
      justify-content: center; word-break: break-all;
    }
    .spinner {
      display: inline-block; width: 18px; height: 18px;
      border: 3px solid #a3b1c6; border-top-color: #4a90d9;
      border-radius: 50%; animation: spin 0.8s linear infinite;
      margin-right: 8px;
    }
    @keyframes spin { to { transform: rotate(360deg); } }
  </style>
</head>
<body>
  <div class="card">
    <h1>📇 名刺解析リモコン</h1>
    <p class="subtitle">入力フォルダの名刺画像を一括解析します</p>
    <button class="btn" id="runBtn" onclick="run()">🚀 解析スタート</button>
    <div id="status">待機中...</div>
  </div>
  <script>
    function run() {
      var btn = document.getElementById('runBtn');
      var status = document.getElementById('status');
      btn.disabled = true;
      status.innerHTML = '<span class="spinner"></span>解析中...';
      google.script.run
        .withSuccessHandler(function(msg) {
          status.textContent = msg;
          btn.disabled = false;
        })
        .withFailureHandler(function(err) {
          status.textContent = '❌ ' + err.message;
          btn.disabled = false;
        })
        .runFromWeb();
    }
  </script>
</body>
</html>

appsscript.json

マニフェストファイル。OAuthスコープの設定が最重要です。

{
  "timeZone": "Asia/Tokyo",
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "Drive",
        "version": "v3",
        "serviceId": "drive"
      }
    ]
  },
  "webapp": {
    "executeAs": "USER_ACCESSING",
    "access": "DOMAIN"
  },
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/cloud-platform",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/userinfo.email"
  ]
}
05

よくあるエラーと対策

エラー 原因 対策
404 The requested URL was not found エンドポイントのホスト名が間違い(global-aiplatform 等) aiplatform.googleapis.com を使用(プレフィックスなし)
404 Publisher Model not found モデルIDが存在しない、またはリージョンが違う 3.x系は global を指定。gemini-3.1-flash-lite-preview を使用
400 Please use a valid role Vertex AI では role フィールドが必須 contents 内に "role": "user" を追加
403 Session.getActiveUser 権限不足 OAuthスコープに userinfo.email がない appsscript.json にスコープを追加し、再承認
403 Drive API has not been used GCPコンソール側で Drive API が未有効化 GCPコンソールで Drive API を有効化
06

Gemini モデル・リージョン対応表

2026年3月29日時点の対応状況です。

モデル APIキー Vertex AI global Vertex AI us-central1 備考
gemini-2.5-flash 対応
gemini-2.5-pro 対応
gemini-3-flash-preview 対応 対応 非対応
gemini-3.1-pro-preview 対応 対応 非対応 最高精度
gemini-3.1-flash-lite-preview 対応 対応 非対応 推奨(コスパ最良)
gemini-3-pro-preview 廃止 2026/3/9 廃止 → 3.1-pro-preview へ
!
存在しないモデルID(よくある間違い):
gemini-3.1-flash-preview → 正しくは gemini-3.1-flash-lite-preview
gemini-3-flash → 正しくは gemini-3-flash-preview
07

スマホ対応: GAS WebApp 化

名刺解析をスマホからワンタップで実行できるWebアプリに拡張します。
GAS の doGet() + google.script.run を使い、追加コストゼロで実現できます。

全体フロー:
スマホブラウザ → GAS WebアプリURL → doGet() → index.html表示 → ボタンタップ → google.script.run.runFromWeb() → 解析実行 → 結果表示
1

GAS側のコードを確認

前セクションで実装した code.gs には、すでにスマホ対応用の関数が含まれています。

/**
 * スマホ用画面の表示(WebApp のエントリポイント)
 */
function doGet() {
  return HtmlService.createHtmlOutputFromFile("index")
    .setTitle("名刺解析リモコン")
    .setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}

/**
 * HTML から呼び出される解析関数
 * UI ダイアログを使わないモード(isUiEnabled = false)
 */
function runFromWeb() {
  return mainEngine(false);
}
!
WebApp では SpreadsheetApp.getUi() が使えないため、mainEngine(false) で UI 操作をスキップしています。
2

index.html を作成

GAS エディタで「ファイル +」→「HTML」→ ファイル名を index にして、以下を貼り付けます。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      font-family: -apple-system, 'Noto Sans JP', sans-serif;
      background: #f5f5f5;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      padding: 20px;
    }
    .card {
      background: #fff;
      border-radius: 16px;
      padding: 32px 24px;
      max-width: 360px;
      width: 100%;
      box-shadow: 0 4px 20px rgba(0,0,0,0.08);
      text-align: center;
    }
    h1 { font-size: 20px; margin-bottom: 8px; }
    .sub { color: #888; font-size: 13px; margin-bottom: 24px; }
    #btn {
      background: #4a90d9;
      color: #fff;
      border: none;
      border-radius: 12px;
      padding: 16px 32px;
      font-size: 16px;
      font-weight: 600;
      width: 100%;
      cursor: pointer;
      transition: opacity 0.2s;
    }
    #btn:disabled { opacity: 0.5; cursor: not-allowed; }
    #status {
      margin-top: 20px;
      padding: 12px;
      border-radius: 8px;
      font-size: 14px;
      display: none;
    }
    .success { background: #e8f5e9; color: #2e7d32; }
    .error   { background: #fbe9e7; color: #c62828; }
    .loading { background: #e3f2fd; color: #1565c0; }
  </style>
</head>
<body>
  <div class="card">
    <h1>📇 名刺解析リモコン</h1>
    <p class="sub">Google Driveの名刺画像を一括解析します</p>
    <button id="btn" onclick="run()">🚀 解析スタート</button>
    <div id="status"></div>
  </div>

  <script>
    function run() {
      var btn = document.getElementById('btn');
      var status = document.getElementById('status');

      btn.disabled = true;
      btn.textContent = '⏳ 解析中…';
      status.style.display = 'block';
      status.className = 'loading';
      status.textContent = '処理中です。しばらくお待ちください…';

      google.script.run
        .withSuccessHandler(function(result) {
          btn.disabled = false;
          btn.textContent = '🚀 解析スタート';
          status.className = result.indexOf('✅') >= 0 ? 'success' : 'error';
          status.textContent = result;
        })
        .withFailureHandler(function(err) {
          btn.disabled = false;
          btn.textContent = '🚀 解析スタート';
          status.className = 'error';
          status.textContent = '❌ ' + err.message;
        })
        .runFromWeb();
    }
  </script>
</body>
</html>
3

WebApp としてデプロイ

GAS エディタから以下の手順でデプロイします。

設定項目 説明
種類 ウェブアプリ 「新しいデプロイ」→ 歯車アイコンから選択
次のユーザーとして実行 自分 Drive/Sheets へのアクセス権限は自分のアカウントで実行
アクセスできるユーザー 組織内の全員 社内限定にする場合。外部公開なら「全員」
デプロイ後に生成される URL(https://script.google.com/macros/s/…/exec)を
スマホのブラウザで開けば、すぐに名刺解析リモコンが使えます。
4

ホーム画面に追加(任意)

デプロイ URL を Safari / Chrome で開き、「ホーム画面に追加」すると
ネイティブアプリのように使えます。

iPhone (Safari)

共有ボタン(□↑)→「ホーム画面に追加」

Android (Chrome)

メニュー(⋮)→「ホーム画面に追加」

仕組みの解説: google.script.run

API 役割 備考
google.script.run HTML → GAS 関数を非同期呼び出し GAS WebApp 専用の組み込み API
.withSuccessHandler(fn) GAS 関数の return 値を受け取る 引数にGAS側の戻り値が渡される
.withFailureHandler(fn) GAS 側で例外が発生した場合のハンドラ 引数に Error オブジェクト
.runFromWeb() GAS 側の runFromWeb() を実行 mainEngine(false) が呼ばれる
コード修正時の注意: WebApp の URL はデプロイごとに固定されます。
コードを修正した場合は「デプロイを管理」→「新しいバージョン」で再デプロイしてください。
URL は変わりません。