1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AppSheetでGeminiを使ってみる(無料で動かせる構成)

1
Posted at

はじめに

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シートを作成する。

image.png

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を選択する

image.png

アプリ名とカテゴリを指定する
image.png

Select data sourceでGoogle Sheetsを選択し、先ほど作成したスプレッドシートを選ぶ
image.png

アプリが作成される
スクリーンショット 2026-09-20 11.23.48.png

4-2.データ型の確認

アプリの+(Add)→Photo欄のEditを選択する

image.png

Photoの横のEdit this columnを選択し、PhotoがImage型になっていることを確認する

image.png

同様にDateTime列がDate型になっていることも確認する

4-3.動作確認

ここまでの設定で、スマートフォンからカメラを起動して写真を登録することができる。

スマートフォンにAppSheetアプリを入れ、AppSheetを作成したGoogleアカウントを利用してログインする

アプリを起動し、写真を撮影する

image.png

Google Sheetsにレコードが追加されていることを確認する

image.png

画像がドライブに保存されていることも確認する
image.png

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を選択する

image.png

画面右上のAPIキーを作成→キーを作成
※Google Cloudプロジェクトの指定が必要なので、既存のものを選ぶか新規作成する(請求先アカウントの紐づけは不要)

image.png

作成したキーをコピーする
image.png

5-3.Apps Scriptコード作成

Google DriveからGoogle Apps Scriptを新規作成する

image.png

コードを作成して保存する
image.png

サンプルコード
/****************************************************
 * 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)

image.png

5-4.Apps Scriptテスト

testFirstRowを選択した状態で実行をクリックする

image.png

実行ログに以下のような写真の解析結果が出て、スプレッドシートが更新されていることを確認する

{
  "brand": "Apple",
  "productName": "AirPods Pro",
  "modelNumber": "",
  "category": "ワイヤレスイヤホン",
  "visibleText": "",
  "overview": "Appleのワイヤレスイヤホン「AirPods Pro」の充電ケースです。",
  "features": "・MagSafeまたはワイヤレス充電対応・コンパクトで持ち運びやすいホワイトのケース",
  "useCase": "イヤホンの充電および持ち運び",
  "confidence": "高"
}

image.png

6.AppSheetのトリガー設定

AppSheetのAutomation→Create my first automationを選択する
image.png

Create a new eventを選択する
image.png

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

Add a step → Create a new stepを選択する
image.png

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

Function Nameに processPendingRowsを設定する
Run asynchronously?にチェックを入れる

image.png

7.アプリ動作確認

スマートフォンから先ほどと同じように写真を撮影するとAppsScriptが動き、撮影した物の情報が自動で表示されるようになる
image.png

スプレッドシートも更新される
image.png

AppsScriptの動作状況は"実行数"で確認できる
image.png

ここまでで一通りアプリを動かすことができた

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格納・表示までを一括で連携させる考え方は、多様な業務プロセスの自動化に応用可能だと思いますのでぜひご活用ください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?