概要: なぜ 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適用 |
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 です。global を指定してください。us-central1 を指定すると404エラーになります。セットアップ手順
GASエディタを開く
スプレッドシートの「拡張機能」メニューから「Apps Script」を選択します。
コード.gs を配置
GASエディタの「コード.gs」に、Vertex AI版のコードを貼り付けます。ファイル構成は appsscript.json、コード.gs、index.html の3つです。
プロジェクト設定を確認
歯車アイコン(設定)を開き、以下を確認します:
- タイムゾーン: (GMT+09:00) 日本標準時 - 東京
- Chrome V8 ランタイムを有効にする にチェック
- 「appsscript.json」マニフェスト ファイルをエディタで表示する にチェック
GCPプロジェクトを紐付け
設定画面の下部「Google Cloud Platform(GCP)プロジェクト」セクションで、GCPプロジェクト番号を入力します。
プロジェクト番号: 1041693762523
プロジェクトID: ai-project001-486207
appsscript.json を設定
マニフェストファイルを編集し、OAuthスコープを正しく設定します。特に cloud-platform と userinfo.email が重要です。
Drive サービスを追加
左メニューの「サービス」横の「+」をクリックし、Drive API v3 を追加します。GCPコンソール側でも Drive API を有効化してください。
コード全文
コード.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"
]
}
よくあるエラーと対策
| エラー | 原因 | 対策 |
|---|---|---|
| 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 を有効化 |
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 へ | ||
gemini-3.1-flash-preview → 正しくは gemini-3.1-flash-lite-previewgemini-3-flash → 正しくは gemini-3-flash-preview
スマホ対応: GAS WebApp 化
名刺解析をスマホからワンタップで実行できるWebアプリに拡張します。
GAS の doGet() + google.script.run を使い、追加コストゼロで実現できます。
スマホブラウザ → GAS WebアプリURL →
doGet() → index.html表示 → ボタンタップ → google.script.run.runFromWeb() → 解析実行 → 結果表示
GAS側のコードを確認
前セクションで実装した code.gs には、すでにスマホ対応用の関数が含まれています。
/**
* スマホ用画面の表示(WebApp のエントリポイント)
*/
function doGet() {
return HtmlService.createHtmlOutputFromFile("index")
.setTitle("名刺解析リモコン")
.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
}
/**
* HTML から呼び出される解析関数
* UI ダイアログを使わないモード(isUiEnabled = false)
*/
function runFromWeb() {
return mainEngine(false);
}
SpreadsheetApp.getUi() が使えないため、mainEngine(false) で UI 操作をスキップしています。
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>
WebApp としてデプロイ
GAS エディタから以下の手順でデプロイします。
| 設定項目 | 値 | 説明 |
|---|---|---|
| 種類 | ウェブアプリ | 「新しいデプロイ」→ 歯車アイコンから選択 |
| 次のユーザーとして実行 | 自分 | Drive/Sheets へのアクセス権限は自分のアカウントで実行 |
| アクセスできるユーザー | 組織内の全員 | 社内限定にする場合。外部公開なら「全員」 |
https://script.google.com/macros/s/…/exec)をスマホのブラウザで開けば、すぐに名刺解析リモコンが使えます。
ホーム画面に追加(任意)
デプロイ 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) が呼ばれる |
コードを修正した場合は「デプロイを管理」→「新しいバージョン」で再デプロイしてください。
URL は変わりません。