はじめに
Appsheet×Geminiのお試しとして撮影した画像の情報をGeminiによって取得するアプリを作ってみる。
利用するサービスは以下
- AppSheet:スマートフォンアプリ、カメラ、一覧・詳細画面
- Google Drive:撮影画像の保存
- Google Sheets:商品データベース
- Google Apps Script:各サービス間の処理連携
- Gemini:画像解析、製品識別、情報抽出
- Google Search:公式サイト・製品情報の検索
AppSheetの有料機能であるAI Taskを使用するとより簡単にAI機能を利用することができるが、ここでは無料でお試しできるApps ScriptからGemini APIを呼び出す構成とする。
1. つくるもの
写真から製品情報を自動登録するアプリを作る
スマートフォンで商品を撮影すると、以下の情報を自動取得する。
- メーカー
- 製品名
- 型番・モデル番号
- カテゴリ
- 写真から読み取った文字
- 製品概要
- 主な特徴
- 用途
- 情報の信頼度
- 公式サイトURL
最終的な処理フローは以下の通り。
スマートフォン
│
│ 写真撮影
▼
AppSheet
│
▼
Google Sheets
│
▼
Apps Script
│
▼
Gemini
│
├── 製品識別
├── OCR
└── 情報抽出
│
▼
Google Search
│
└── 公式情報確認
│
▼
Google Sheets
│
▼
AppSheet
│
▼
商品図鑑
【スクリーンショット①:完成した商品図鑑】
ここにAppSheetの完成画面を掲載
写真、メーカー、製品名、概要、公式サイトへのリンクなどが表示される商品一覧・詳細画面。
2. 構成
画像の理解はGemini、データ保存はSheets、UIはAppSheet、処理連携はApps Scriptを利用する
| サービス | 役割 |
|---|---|
| AppSheet | UI、カメラ、商品一覧、商品詳細 |
| Google Drive | 撮影画像の保存 |
| Google Sheets | 商品データベース |
| Apps Script | API呼び出し、データ加工、処理制御 |
| Gemini | 画像解析、製品識別、情報抽出 |
| Google Search | 製品情報・公式サイト検索 |
3. Google Sheets(データストア)
まずGoogle SheetsにProductsシートを作成する。
1行目には以下の項目を設定する
| 列 | 項目 | 内容 |
|---|---|---|
| A | ID | 商品ID |
| B | DateTime | 登録日時 |
| C | Photo | 商品写真 |
| D | Brand | メーカー |
| E | ProductName | 製品名 |
| F | ModelNumber | 型番 |
| G | Category | カテゴリ |
| H | VisibleText | 写真から取得した文字 |
| I | Overview | 製品概要 |
| J | Features | 主な特徴 |
| K | UseCase | 主な用途 |
| L | Confidence | 識別信頼度 |
| M | OfficialURL | 公式サイト |
| N | Source | 情報源 |
| O | AI_Processed | AI処理済み |
| P | AI_Error | エラー情報 |
ID DateTime Photo Brand ProductName ModelNumber Category VisibleText Overview Features UseCase Confidence OfficialURL Source AI_Processed AI_Error
4. AppSheet
Google SheetsをデータソースとしてAppSheetアプリを作成する。
4-1.AppSheet作成
https://www.appsheet.com/Home/Apps にアクセスし、Create > App > Start with existing dataを選択する
Select data sourceでGoogle Sheetsを選択し、先ほど作成したスプレッドシートを選ぶ

4-2.データ型の確認
アプリの+(Add)→Photo欄のEditを選択する
Photoの横のEdit this columnを選択し、PhotoがImage型になっていることを確認する
同様にDateTime列がDate型になっていることも確認する
4-3.動作確認
ここまでの設定で、スマートフォンからカメラを起動して写真を登録することができる。
スマートフォンにAppSheetアプリを入れ、AppSheetを作成したGoogleアカウントを利用してログインする
アプリを起動し、写真を撮影する
Google Sheetsにレコードが追加されていることを確認する
5. Apps Script × Geminiで画像処理
5-1.全体像
Apps Scriptが未処理レコードを検出し、AIによる解析結果を追加するようにする
処理の流れは以下の通り
未処理レコードを取得
↓
Photoのパスを取得
↓
Google Driveから画像を取得
↓
Gemini APIへ画像を送信
↓
JSON形式の解析結果を取得
↓
Google Sheetsへ書き戻し
AI処理済みかどうかはAI_Processed列で管理することで、同じ商品を繰り返し処理することを防止する
5-2.APIキーの取得
Geminiを利用するためにAPIキーを取得する。ここではGoogle AI StudioでAPIキーを作成する。
↑にアクセスし、画面左下のGet API Keyを選択する
画面右上のAPIキーを作成→キーを作成
※Google Cloudプロジェクトの指定が必要なので、既存のものを選ぶか新規作成する(請求先アカウントの紐づけは不要)
5-3.Apps Scriptコード作成
Google DriveからGoogle Apps Scriptを新規作成する
サンプルコード
/****************************************************
* AppSheet × Gemini
* 写真 → 製品識別 → Google Sheets
****************************************************/
// プロパティキーの定数定義
const PROPERTY_KEYS = {
GEMINI_API_KEY: 'GEMINI_API_KEY',
SPREADSHEET_ID: 'SPREADSHEET_ID',
LLM_MODEL: 'LLM_MODEL'
};
const CONFIG = {
SHEET_NAME: 'Products', // 対象シート
MAX_ROWS: 5, // 1回の実行で処理する最大件数
};
/**
* プロパティストアから値を取得(存在しない場合はエラーをスルー)
*/
function getProperty_(key) {
const value = PropertiesService.getScriptProperties().getProperty(key);
if (!value) {
throw new Error(
`スクリプト プロパティ「${key}」が設定されていません。プロジェクトの設定 → スクリプト プロパティから設定してください。`
);
}
return value;
}
/**
* プロパティから取得した SPREADSHEET_ID を元にスプレッドシートを開く
*/
function getSpreadsheet_() {
const spreadsheetId = getProperty_(PROPERTY_KEYS.SPREADSHEET_ID);
return SpreadsheetApp.openById(spreadsheetId);
}
/**
* プロパティから取得した LLM_MODEL と API_KEY を使用して API URL を生成
*/
function getApiUrl_() {
const model = getProperty_(PROPERTY_KEYS.LLM_MODEL);
const apiKey = getProperty_(PROPERTY_KEYS.GEMINI_API_KEY);
return `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(apiKey)}`;
}
/**
* 初回実行・動作確認
*/
function setup() {
// 必須プロパティの存在確認
const apiKey = getProperty_(PROPERTY_KEYS.GEMINI_API_KEY);
const spreadsheetId = getProperty_(PROPERTY_KEYS.SPREADSHEET_ID);
const model = getProperty_(PROPERTY_KEYS.LLM_MODEL);
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
throw new Error(
`シート「${CONFIG.SHEET_NAME}」が見つかりません。`
);
}
Logger.log(`セットアップ確認OK (Model: ${model}, Sheet ID: ${spreadsheetId})`);
}
/**
* 未処理の商品を検索してGeminiで解析
*
* AppSheetから新しい商品が登録された後、
* この関数をトリガーから実行する。
*/
function processPendingRows() {
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SHEET_NAME);
if (!sheet) {
throw new Error(
`シート「${CONFIG.SHEET_NAME}」がありません。`
);
}
const values = sheet.getDataRange().getValues();
if (values.length <= 1) {
Logger.log('処理対象なし');
return;
}
const headers = values[0];
const column = createColumnMap_(headers);
validateColumns_(column);
let processed = 0;
// 2行目から処理
for (let i = 1; i < values.length; i++) {
if (processed >= CONFIG.MAX_ROWS) { break; }
const row = values[i];
const processedFlag = String(row[column.AI_Processed] || '').toUpperCase();
// 処理済みならスキップ
if (processedFlag === 'TRUE') { continue; }
// 写真がなければスキップ
const photoPath = String(row[column.Photo] || '').trim();
if (!photoPath) { continue; }
const rowNumber = i + 1;
try {
Logger.log(
`処理開始: row=${rowNumber}, photo=${photoPath}`
);
// ------------------------------------------
// 1. Driveから画像取得
// ------------------------------------------
const imageFile = findImageFile_(photoPath);
if (!imageFile) {
throw new Error(
`画像ファイルが見つかりません: ${photoPath}`
);
}
const blob = imageFile.getBlob();
const mimeType = blob.getContentType();
// ------------------------------------------
// 2. Gemini画像解析
// ------------------------------------------
const result = analyzeProductWithGemini_(blob, mimeType);
// ------------------------------------------
// 3. Google Sheetsへ書き込み
// ------------------------------------------
writeResult_(sheet, rowNumber, column, result);
// 処理済み
sheet.getRange(rowNumber, column.AI_Processed + 1).setValue(true);
// エラー欄をクリア
sheet.getRange(rowNumber, column.AI_Error + 1).clearContent();
processed++;
Logger.log(`処理完了: row=${rowNumber}`);
} catch (error) {
Logger.log(`処理エラー: row=${rowNumber}: ${error.message}`);
// エラー内容を保存
sheet.getRange(rowNumber, column.AI_Error + 1).setValue(error.message);
}
}
Logger.log(`処理終了: ${processed}件`);
}
/**
* Geminiに商品画像を送信
*/
function analyzeProductWithGemini_(blob, mimeType) {
const base64Image = Utilities.base64Encode(blob.getBytes());
const prompt = `
あなたは製品識別アシスタントです。
添付された商品写真を分析し、写真から判別できる製品情報を抽出してください。
目的:スマートフォンで撮影した写真から、商品図鑑に登録するための製品情報を作成すること。
重要なルール:
1. 写真から確認できる情報を優先する
2. 型番が読める場合は正確に抽出する
3. 型番が確認できない場合は推測しない
4. 製品名が不明な場合は空文字にする
5. メーカーが不明な場合は空文字にする
6. 写真に写っている文字はOCRとして抽出する
7. 製品を断定できない場合はconfidenceを「低」にする
8. 架空の製品名・型番を作成しない
9. 説明文は日本語で記載する
以下のJSON形式だけを返してください。JSON以外の文章は返さないでください。
`;
const responseSchema = {
type: 'OBJECT',
properties: {
brand: {
type: 'STRING',
description: 'メーカー名。不明なら空文字'
},
productName: {
type: 'STRING',
description: '正式な製品名。不明なら空文字'
},
modelNumber: {
type: 'STRING',
description: '型番・モデル番号。不明なら空文字'
},
category: {
type: 'STRING',
description: '製品カテゴリ。不明なら空文字'
},
visibleText: {
type: 'STRING',
description: '写真から読み取れる文字'
},
overview: {
type: 'STRING',
description: '製品の簡潔な概要'
},
features: {
type: 'STRING',
description: '主な特徴。複数ある場合は「・」で区切る'
},
useCase: {
type: 'STRING',
description: '主な用途'
},
confidence: {
type: 'STRING',
enum: [
'高',
'中',
'低'
]
}
},
required: [
'brand',
'productName',
'modelNumber',
'category',
'visibleText',
'overview',
'features',
'useCase',
'confidence'
]
};
const payload = {
contents: [
{
role: 'user',
parts: [
{
inlineData: {
mimeType: mimeType,
data: base64Image
}
},
{
text: prompt
}
]
}
],
generationConfig: {
responseMimeType: 'application/json',
responseSchema: responseSchema,
temperature: 0.1,
maxOutputTokens: 2048
}
};
const response = UrlFetchApp.fetch(getApiUrl_(), {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const statusCode = response.getResponseCode();
const responseText = response.getContentText();
if (statusCode < 200 || statusCode >= 300) {
throw new Error(
`Gemini API Error ${statusCode}: ${responseText}`
);
}
const responseJson = JSON.parse(responseText);
const text = responseJson?.candidates?.[0]?.content?.parts?.[0]?.text;
if (!text) {
throw new Error(
'Geminiから解析結果を取得できませんでした。'
);
}
let result;
try {
result = JSON.parse(text);
} catch (error) {
throw new Error(
'GeminiのJSON解析に失敗しました: ' +
text
);
}
return result;
}
/**
* AppSheetのPhoto列から
* Google Drive上の画像ファイルを探す。
*/
function findImageFile_(photoPath) {
// パスからファイル名だけ取得
const fileName = photoPath.split('/').pop();
if (!fileName) { return null; }
const files = DriveApp.getFilesByName(fileName);
if (!files.hasNext()) { return null; }
return files.next();
}
function writeResult_(sheet, rowNumber, column, result) {
setCell_(sheet, rowNumber, column.Brand, result.brand);
setCell_(sheet, rowNumber, column.ProductName, result.productName);
setCell_(sheet, rowNumber, column.ModelNumber, result.modelNumber);
setCell_(sheet, rowNumber, column.Category, result.category);
setCell_(sheet, rowNumber, column.VisibleText, result.visibleText);
setCell_(sheet, rowNumber, column.Overview, result.overview);
setCell_(sheet, rowNumber, column.Features, result.features);
setCell_(sheet, rowNumber, column.UseCase, result.useCase);
setCell_(sheet, rowNumber, column.Confidence, result.confidence);
}
// ==================================================
// セル書き込み
// ==================================================
function setCell_(sheet, row, zeroBasedColumn, value) {
sheet.getRange(row, zeroBasedColumn + 1).setValue(value || '');
}
// ==================================================
// ヘッダーから列番号を取得
// ==================================================
function createColumnMap_(headers) {
const map = {};
headers.forEach((header, index) => {
if (header) {
map[String(header).trim()] = index;
}
});
return map;
}
// ==================================================
// 必須列チェック
// ==================================================
function validateColumns_(column) {
const requiredColumns = [
'ID',
'DateTime',
'Photo',
'Brand',
'ProductName',
'ModelNumber',
'Category',
'VisibleText',
'Overview',
'Features',
'UseCase',
'Confidence',
'AI_Processed',
'AI_Error'
];
const missing = requiredColumns.filter(name => column[name] === undefined);
if (missing.length > 0) {
throw new Error(
'Productsシートに必要な列がありません: ' + missing.join(', ')
);
}
}
/**
* シート上の最初の未処理レコードだけを処理
*
* 初回動作確認用
*/
function testFirstRow() {
const sheet = getSpreadsheet_().getSheetByName(CONFIG.SHEET_NAME);
const values = sheet.getDataRange().getValues();
const headers = values[0];
const column = createColumnMap_(headers);
validateColumns_(column);
for (let i = 1; i < values.length; i++) {
const row = values[i];
const processed = String(row[column.AI_Processed] || '').toUpperCase();
const photo = String(row[column.Photo] || '').trim();
if (processed !== 'TRUE' && photo) {
processSingleRow_(sheet, i + 1, column); // 最初の1件だけ処理
return;
}
}
Logger.log('処理対象のデータがありません。');
}
// ==================================================
// 1行だけ処理
// ==================================================
function processSingleRow_(sheet, rowNumber, column) {
try {
const photoPath = String(sheet.getRange(rowNumber, column.Photo + 1).getValue()).trim();
if (!photoPath) { throw new Error('Photoが空です。'); }
const file = findImageFile_(photoPath);
if (!file) { throw new Error('Google Drive上に画像が見つかりません: ' + photoPath); }
const blob = file.getBlob();
const result = analyzeProductWithGemini_(blob, blob.getContentType());
writeResult_(sheet, rowNumber, column, result);
sheet.getRange(rowNumber, column.AI_Processed + 1).setValue(true);
sheet.getRange(rowNumber, column.AI_Error + 1).clearContent();
Logger.log(JSON.stringify(result, null, 2));
} catch (error) {
sheet.getRange(rowNumber, column.AI_Error + 1).setValue(error.message);
throw error;
}
}
プロジェクトの設定→スクリプトプロパティから以下プロパティを設定する
| プロパティ | 値 |
|---|---|
GEMINI_API_KEY |
AI Studioから取得したAPIキー |
LLM_MODEL |
モデル名を入れる (例:gemini-3.8-flash) ※https://ai.google.dev/gemini-api/docs/models?hl=ja のエンドポイント名を入れる |
SPREADSHEET_ID |
スプレッドシートIDを入れる ※( https://docs.google.com/spreadsheets/d/{ここがID}/edit) |
5-4.Apps Scriptテスト
testFirstRowを選択した状態で実行をクリックする
実行ログに以下のような写真の解析結果が出て、スプレッドシートが更新されていることを確認する
{
"brand": "Apple",
"productName": "AirPods Pro",
"modelNumber": "",
"category": "ワイヤレスイヤホン",
"visibleText": "",
"overview": "Appleのワイヤレスイヤホン「AirPods Pro」の充電ケースです。",
"features": "・MagSafeまたはワイヤレス充電対応・コンパクトで持ち運びやすいホワイトのケース",
"useCase": "イヤホンの充電および持ち運び",
"confidence": "高"
}
6.AppSheetのトリガー設定
AppSheetのAutomation→Create my first automationを選択する

任意のEvent nameを設定し、SettingsでAdds/Updatesを選択する

Add a step → Create a new stepを選択する

Settingsの画面でCall a scriptを選択し、先程作成したApps Scriptを指定する

Function Nameに processPendingRowsを設定する
Run asynchronously?にチェックを入れる
7.アプリ動作確認
スマートフォンから先ほどと同じように写真を撮影するとAppsScriptが動き、撮影した物の情報が自動で表示されるようになる

ここまでで一通りアプリを動かすことができた
AppsScript実行時に以下のようなエラーが出る場合があります。エラーが出た際は時間を空けて再実行するか別のモデルに変更してみてください。(プレビュー版のモデルやリリース直後のモデルを使うと出やすいです。)
Gemini API Error 503: {
"error": {
"code": 503,
"message": "This model is currently experiencing high demand. Spikes in demand are usually temporary. Please try again later.",
"status": "UNAVAILABLE"
}
}
8.ポイント
今回作成したアプリのポイントを紹介します
8-1. AIの回答をJSONとして扱う
AIに文章として回答させると、人間には理解しやすくてもプログラムで再利用する際に「メーカー名の抽出」などの自然言語処理が別途必要になります。
出力をJSON形式に固定(構造化)することで、API経由で取得したデータをそのままGoogle Sheetsの各列(brand, productNameなど)へ機械的に直接マッピング・自動保存できるようになります。
8-2. 画像認識の限界をWeb検索で補う
外観がほぼ同じマイナーチェンジモデルなど、画像解析だけでは判断できないものの認識には精度の限界があります。
Geminiによる画像解析で得た結果を仮説(検索クエリ)とし、Google Searchで公式サイトを照会するステップを挟むことで、型番・スペックの正確性と公式URL(信頼できる情報源)を補完しています。
8-3. 既存のGoogleエコシステムによる役割分担
AIだけに全てを行わせるのではなく、得意分野に応じた適切な役割分担がなされています。
- UI(入力・表示): スマホから直感的に操作できる AppSheet
- ストレージ・DB: 画像保持の Google Drive と 構造化データの Google Sheets
- バックエンド制御: 処理の自動化と連携を担う Apps Script (GAS)
- 知能・検索: 画像解析の Gemini と 実データの担保をする Google Search
このように「UI・データ保持・AI・検索」を連携させることで、写真を撮るだけで自動的にデータが蓄積・補正され、視覚的な商品図鑑として閲覧できるシステムを実現しています。
9. まとめ
本記事では、無料枠の範囲で構築可能なAIアプリケーションの作成手順を紹介しました。
単にGeminiのチャット上で画像を解析するだけでも商品情報の取得は可能ですが、UIにAppSheet、画像理解にGemini、連携処理にApps Script、データベースにGoogle スプレッドシートを採用することで、実用的なAI搭載型商品データベースを極めてシンプルな構成で実現できます。
画像入力
↓
AI画像解析
↓
製品識別
↓
Web検索
↓
公式データ照合
↓
構造化処理
↓
DB格納
↓
UI表示
このような入力から解析・検索・構造化・DB格納・表示までを一括で連携させる考え方は、多様な業務プロセスの自動化に応用可能だと思いますのでぜひご活用ください!




















