top of page

GAS × Gemini で動画をAIに読ませる ― スマホで撮って喋るだけ、営農記録をスプレッドシートに自動入力

  • 2 日前
  • 読了時間: 13分

2026年7月15日(水)、高知県の「IoP技術者コミュニティ」第3回講座を、オンライン(Teams)と弊社高知オフィスのハイブリッドで開催しました。PROMPT-XはIoP技術者コミュニティの事務局を務めています。テーマは「動画 × AI で営農記録をデジタル化」。前回(6月)の「GAS(Google Apps Script) × AI で手書き帳票を電子化」の続編で、今回は入力を「動画」に広げます。スマホで圃場を撮りながらその日の様子を喋るだけで、AIが映像と音声の両方を読み取り、スプレッドシートに営農記録として書き込みます。参加者全員が手元でコードを動かすハンズオン形式で行いました。


この記事では、当日のハンズオンから「スマホの動画をGASとGeminiでスプレッドシートに自動入力する」実装を、動くコードとともに紹介します。Googleアカウントさえあれば追加費用ゼロで試せます。


地方拠点から全国レベルの開発に挑戦してみたい方は、記事末尾の採用情報もご覧ください。


まず、GASで動画をAIに読ませる


前回の講座では、GASからGeminiに「文章」や「画像(手書き帳票)」を渡して読み取らせました。今回渡すのは「動画」です。ポイントは、動画を渡す方法が画像のときとほとんど変わらないことです。Geminiは1回のAPI呼び出しで、映像(作物の色・着果・しおれ・写り込んだ文字など)と音声(撮影者がその場で喋ったナレーション)の両方をまとめて読み取ってくれます。


GASは、Googleアカウントがあれば無料で使えるサーバーレスの実行環境です。`UrlFetchApp`で外部APIを叩けるので、Gemini APIもそのまま呼べます。APIキーはGoogle AI Studioで発行し、コードに直書きせず`PropertiesService`(スクリプトプロパティ)に保存します。動画はGoogleドライブに置くだけでよく、`DriveApp`で読み込んで`Utilities.base64Encode`し、`inline_data`としてプロンプトと一緒に`parts`へ入れて渡します。画像のときと違うのは、`inline_data`の中身が動画になるだけです。


const GEMINI_MODEL = 'gemini-3.1-flash-lite';
const VIDEO_FILE   = '01_field-video.mp4';   // ドライブに置いた動画のファイル名

// 実行用:メニューの ▶ から実行すると、ログに読み取り結果が出る
function readVideoFromDrive() {
  const fileId = findFileId(VIDEO_FILE);
  const prompt = 'この動画は、撮影者がその場の様子を撮りながら口頭で説明したものです。'
    + '映像(映っているもの・その状態・写り込んだ文字)と'
    + '音声(ナレーション)の両方を読み取って、わかったことを説明してください。';
  Logger.log(askGeminiWithVideo(fileId, prompt));
}

function askGeminiWithVideo(fileId, prompt) {
  // APIキーはコードに直書きせず、スクリプトプロパティから読む
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  const blob = DriveApp.getFileById(fileId).getBlob();   // ドライブの動画を読む
  const url = 'https://generativelanguage.googleapis.com/v1beta/models/'
    + GEMINI_MODEL + ':generateContent';

  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-goog-api-key': apiKey },
    payload: JSON.stringify({ contents: [{ parts: [
      { text: prompt },                                     // 質問(プロンプト)
      { inline_data: { mime_type: blob.getContentType(),    // mp4 / mov などを自動判定
                       data: Utilities.base64Encode(blob.getBytes()) } }  // 動画
    ] }] }),
    muteHttpExceptions: true
  });
  return JSON.parse(res.getContentText()).candidates[0].content.parts[0].text;
}

// 動画を「ファイル名」で探す(IDを手で貼らなくてよい)
function findFileId(fileName) {
  const files = DriveApp.getFilesByName(fileName);
  if (!files.hasNext()) throw new Error('ファイルが見つかりません: ' + fileName);
  return files.next().getId();
}


モデルには、安価・高速な軽量モデル「gemini-3.1-flash-lite」を指定しています。1回の呼び出しでも数百トークン程度と安く、無料枠も充実しているので、受講者も気兼ねなく何度も試せます。なお、動画は画像よりずっとデータが大きいため、この方法でそのまま埋め込めるのは20MB程度(短い動画で15秒くらい)が目安です。長い動画は後述のWebアプリ側で弾きます。


動画をそのまま渡せるのは、いまはGeminiだけ


前回の手書き帳票(画像)は、実はどのAIでもだいたい読めるようになっています。ところが「動画」は事情が違います。2026年7月15日時点で、主要3社の本体モデル(API)にファイルをそのまま渡せるかを整理すると、次のようになります(各社の公式ドキュメントで確認)。


  • Gemini(Google):画像 ○ / 音声 ○ / 動画 ○

  • GPT-5.5(OpenAI):画像 ○ / 音声 △(別の専用モデルなら可)/ 動画 ✕

  • Claude(Anthropic):画像 ○(PDFも可)/ 音声 ✕ / 動画 ✕


Geminiの動画読み取りは、映像(1コマ/秒)と音声を同時に処理します。1時間程度までの動画や、YouTubeのURLをそのまま渡すこともできます。画像はどのAIも読めるようになりましたが、動画をそのまま渡せるのは、いまはGeminiだけ。今回の講座でGeminiを選んだ理由のひとつがこれです。


読み取り結果を「営農記録の列」にする


前の関数では、読み取った内容が「ひとかたまりの文章」で返ってきます。集計やグラフ化をしたいなら、これでは足りません。そこで、6月講座の構造化出力と同じ発想で、AIに「決まった形のJSONで答えて」とお願いします。`generationConfig`に`responseMimeType: 'application/json'`(JSONで返して)と`temperature: 0`(毎回ぶれない)を指定するのがコツです。


もうひとつの工夫が、記録日時をAIに書かせないことです。日付や時刻は、シートに登録するときにGAS側の`new Date()`(サーバ時刻・JST)で自動的に付けます。こうすると利用者は日時を喋らなくてよく(負担が減り)、記録の信頼性も上がります。


// ▶ で実行:動画を読み取り、営農記録1行としてシートに追記する
function recordVideoToSheet() {
  const fileId = findFileId(VIDEO_FILE);
  const json = analyzeVideoAsRecord(fileId);
  const data = JSON.parse(json);
  const rec  = data.record || {};

  const sh = getRecordSheet().getSheets()[0];
  if (sh.getLastRow() === 0) {
    sh.appendRow(['記録日時', '天気', '給液量', '排液量', '作物の見た目', '備考', '文字起こし', '映像']);
  }
  // ★ 記録日時はここで自動付与(AIの出力ではなくサーバ時刻・JST)
  const ts = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm');
  sh.appendRow([
    ts,
    rec['天気'] || '', rec['給液量'] || '', rec['排液量'] || '',
    rec['作物の見た目'] || '', rec['備考'] || '',
    data.transcription || '', data.visual || ''
  ]);
}

// 動画を読み取り、「決まった形のJSON文字列」で返す
function analyzeVideoAsRecord(fileId) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  const blob = DriveApp.getFileById(fileId).getBlob();
  const url  = 'https://generativelanguage.googleapis.com/v1beta/models/'
    + GEMINI_MODEL + ':generateContent';

  const prompt = 'この動画は、営農記録をつけるために撮影者がその場の様子を撮りながら口頭で述べたものです。'
    + '映像(作物や物の状態・写り込んだ文字)と音声(ナレーション)の両方を読み取ってください。'
    + '日付や時刻は出力に含めないでください(登録時にシステムが自動付与します)。'
    + '出力は次のJSONのみ: '
    + '{"transcription":"音声の文字起こし","visual":"映像から読み取れたこと",'
    + '"record":{"天気":,"給液量":,"排液量":,"作物の見た目":,"備考":}}。該当なしはnull。';

  const res = UrlFetchApp.fetch(url, {
    method: 'post',
    contentType: 'application/json',
    headers: { 'x-goog-api-key': apiKey },
    payload: JSON.stringify({
      contents: [{ parts: [
        { text: prompt },
        { inline_data: { mime_type: blob.getContentType(),
                         data: Utilities.base64Encode(blob.getBytes()) } }
      ] }],
      generationConfig: {
        responseMimeType: 'application/json',   // ← 「JSONで返して」
        temperature: 0                          // ← ぶれない抽出に
      }
    }),
    muteHttpExceptions: true
  });
  return JSON.parse(res.getContentText()).candidates[0].content.parts[0].text;
}

// 記録用シートを用意する(一度作ったらIDを覚えて使い回す)
function getRecordSheet() {
  const p  = PropertiesService.getScriptProperties();
  const id = p.getProperty('SHEET_ID');
  if (id) { try { return SpreadsheetApp.openById(id); } catch (e) {} }
  const ss = SpreadsheetApp.create('営農記録(動画AI)');
  p.setProperty('SHEET_ID', ss.getId());
  return ss;
}


動画 → 営農記録(表)まで、GASだけで一気通貫です。あとはこの関数を時間トリガーで回したり、次に紹介するWebアプリ化をすれば、日々の記録がそのまま溜まっていきます。


こうした仕組みを、お客さまの現場の課題に合わせて設計し、実装まで持っていく仕事に興味がある方は、PROMPT-Xの採用情報もあわせてご覧ください。


スマホで撮って、その場で記録するWebアプリにする


ここまでは「ドライブに動画を置いて▶で実行」でした。これを「スマホで撮ってその場で登録」できるように、GASのWebアプリ機能(`doGet`)で画面を配ります。デプロイの手順は6月講座のWebアプリと同じで、URLをスマホで開けば動きます。


実装で一番のハマりどころが、カメラの開き方です。ブラウザでカメラを使う`getUserMedia`は、GASの画面(HtmlServiceのiframe)ではブロックされて動きません。そこで、HTMLの`<input type="file" accept="video/*" capture>`を使います。これはスマホでタップすると標準のカメラが開き、撮った動画をそのままファイルとして受け取れる仕組みで、iframeの制約と関係なく動きます。


もうひとつ入れているのが、登録前のワンクッションです。AIの読み取りは完璧ではないので、シートに書き込む前に人が中身を一度確認し、「✅これで登録/🔄撮り直す」を選べるようにしています。実務で安心して使うための工夫です。


function doGet() {
  const html = `<!DOCTYPE html><html><head>
<meta name="viewport" content="width=device-width,initial-scale=1"></head>
<body style="font-family:system-ui;font-size:18px;padding:20px;line-height:1.7">
  <h2>🎥 営農記録AI(動画)</h2>
  <p>圃場を撮りながら、その日の記録を喋ってください(5〜15秒)</p>
  <!-- ★ getUserMediaは使わず、capture付きfile inputで標準カメラを開く -->
  <input type="file" accept="video/*" capture id="f" style="font-size:18px"><br><br>
  <div id="st" style="color:#666"></div>

  <div id="preview" style="display:none">
    <pre id="out" style="white-space:pre-wrap;background:#f4f4f4;padding:12px;border-radius:8px"></pre>
    <p>この内容で登録していいですか?(日時は登録時に自動で記録されます)</p>
    <button id="ok" style="font-size:18px;padding:8px 20px;background:#1b5e20;color:#fff;border:none;border-radius:8px">✅ これで登録</button>
    <button id="redo" style="font-size:18px;padding:8px 16px;margin-left:8px">🔄 撮り直す</button>
  </div>
  <div id="done" style="display:none;color:#1b5e20;font-weight:bold"></div>

<script>
  var f = document.getElementById('f'), st = document.getElementById('st');
  var out = document.getElementById('out'), pv = document.getElementById('preview');
  var dn = document.getElementById('done'), pending = null;

  // ① 撮る → AIに読ませる → 確認エリアに表示
  f.addEventListener('change', function(e){
    var file = e.target.files[0]; if (!file) return;
    if (file.size > 18 * 1048576) { st.textContent = '動画が大きすぎます。15秒以内で撮り直してください'; return; }
    st.textContent = '解析中...';
    var r = new FileReader();
    r.onload = function(){
      var b64 = (r.result.split(',')[1] || '');
      google.script.run
        .withSuccessHandler(function(res){ pending = res; out.textContent = res; pv.style.display = 'block'; st.textContent = '内容を確認してください'; })
        .withFailureHandler(function(err){ st.textContent = 'エラー: ' + err.message; })
        .processMedia(b64, file.type || 'video/mp4');
    };
    r.readAsDataURL(file);
  });

  // ②「✅ これで登録」→ シートに追記
  document.getElementById('ok').addEventListener('click', function(){
    if (!pending) return; st.textContent = '登録中...'; pv.style.display = 'none';
    google.script.run
      .withSuccessHandler(function(url){ dn.innerHTML = '✅ 登録しました <a href="' + url + '" target="_blank">シートを開く</a>'; dn.style.display = 'block'; st.textContent = ''; })
      .registerRecord(pending);
  });
</script></body></html>`;
  return HtmlService.createHtmlOutput(html);
}

// 動画(base64)をGeminiに送り、読み取り結果(JSON文字列)を返す
function processMedia(b64, mime) {
  const apiKey = PropertiesService.getScriptProperties().getProperty('GEMINI_API_KEY');
  const url = 'https://generativelanguage.googleapis.com/v1beta/models/'
    + GEMINI_MODEL + ':generateContent';
  const prompt = 'この動画は圃場を撮影しながら口頭でその日の営農記録を述べたものです。'
    + '映像と音声の両方を読み取ってください。日付や時刻は出力に含めないでください。'
    + '出力は次のJSONのみ: {"transcription":"音声の文字起こし","visual":"映像から読み取れたこと",'
    + '"record":{"天気":,"給液量":,"排液量":,"作物の見た目":,"備考":}}。該当なしはnull。';

  const res = UrlFetchApp.fetch(url, {
    method: 'post', contentType: 'application/json',
    headers: { 'x-goog-api-key': apiKey },
    payload: JSON.stringify({
      contents: [{ parts: [{ text: prompt }, { inline_data: { mime_type: mime, data: b64 } }] }],
      generationConfig: { responseMimeType: 'application/json', temperature: 0 }
    }),
    muteHttpExceptions: true
  });
  return JSON.parse(res.getContentText()).candidates[0].content.parts[0].text;
}

// 確認後に呼ばれる:読み取り結果(JSON)をシートの1行に追記(記録日時は自動付与)
function registerRecord(jsonText) {
  const data = JSON.parse(jsonText), rec = data.record || {};
  const sh = getRecordSheet().getSheets()[0];
  if (sh.getLastRow() === 0) {
    sh.appendRow(['記録日時', '天気', '給液量', '排液量', '作物の見た目', '備考', '文字起こし', '映像']);
  }
  const ts = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd HH:mm');
  sh.appendRow([ts, rec['天気'] || '', rec['給液量'] || '', rec['排液量'] || '',
    rec['作物の見た目'] || '', rec['備考'] || '', data.transcription || '', data.visual || '']);
  return getRecordSheet().getUrl();
}


これで「撮る → AIが読む → 確認 → 記録」がスマホだけで完結します。現場で手がふさがっていても、撮って一言喋るだけ。構造化はAIに任せ、日時は自動、登録の前に人が一度確認する。無理なく続けられる記録の形になりました。


AIには「画面」も書かせられる


講座の後半では、この入力画面のHTMLそのものをAIに書かせる、という一歩踏み込んだ内容も扱いました。コツは、AIに「つなぎ目(呼ぶ関数の約束事)」と「環境の制約」をきちんと伝えることです。具体的には、「`google.script.run`で`processMedia`と`registerRecord`を呼ぶこと」「カメラは`getUserMedia`ではなく`capture`付きのfile inputを使うこと」「18MBを超える動画は弾くこと」をプロンプトに書きます。これだけ伝えれば、GAS上で正しく動く画面のHTMLが返ってきます。コードが読めなくても「ボタンを大きく」「色を緑系に」と言葉で画面を直せる、という体験まで踏み込みました。


どのくらい正確に読めるのか ― 精度は「実測」で選ぶ


手書き文字は、長いあいだ従来型OCRの「宿敵」でした。ところが汎用AIの読み取り精度は、ここ数年で大きく上がっています。研究ベンチマークでは汎用LLMが手書き認識の専用ツールを上回って誤字率5%未満(arXiv 2025)、最前線のマルチモーダルAIの手書き読み取りは93〜95%(AIMultiple 2026)という報告もあります。文字の「形」そのものではなく、前後の言葉や帳票の意味から「ここに入るのはこの言葉」と推測できるのが、従来OCRとの本質的な違いです。


ただし、日本語の手書きや天気記号のような「現場のクセ」は、こうしたベンチマークには出てきません。結局は自分のタスクで測るしかない。そこで、手書きの作業日報1枚を最安クラスの3モデルに同じ条件で読ませてみました(2026年7月・実測)。


  • Gemini 3.1 Flash-Lite:精度 100% / 1枚あたり $0.0015

  • GPT-5.4 nano:精度 47% / $0.0014

  • GPT-5.4 mini:精度 94% / $0.0051


nanoとほぼ同じ値段で精度は53ポイント差、3倍高いminiにも勝ちました。「高い=正確」とは限りません。値段や知名度でなく、自分のタスクの実測で選ぶ、というのがモデル選びの勘所です(モデルは数ヶ月で入れ替わります)。



もうひとつ注意したいのが、安いAIの「それっぽい嘘」です。読めない字を空欄にせず、もっともらしい別の言葉で埋めてくることがあります。実測では「山田太郎」→「よだまち部」、「東町マンション」→「東側マンション」、天気記号の◎(くもり)も○(快晴)も一律「晴れ」と誤読、といった例が出ました。出力のJSONの形は整っているので、一見すると正しく見えてしまう。だからこそ、AIの出力には目視チェックを残すこと ―― 次の「3つのコツ」の3つ目につながります。


精度を上げる、3つのコツ


最後に、AIの読み取り精度を上げる3つのコツを、今日の実装と結びつけて整理します。


  • ① プロンプトに「現場の言葉」を教える:説明なしだと3モデルとも「晴れ/くもり」を「晴れ」止まりでしたが、「『/』=のち、『|』=時々」と一文足すだけで「晴れのちくもり」まで正しく読めました(実測)。

  • ② 出力の「形」を固定する:`responseMimeType: 'application/json'` + `temperature: 0`。今日のstep2でやったことです。

  • ③ 人の確認を、ひと呼吸はさむ:AIは間違える前提で、登録の前に「✅これで登録/🔄撮り直す」。今日のstep4の確認画面がまさにこれです。


精度は「モデル × プロンプト × 仕組み」の掛け算です。かつては高いモデルに複雑なプロンプトを書かなければ読めなかったものが、安いモデルにシンプルな指示で、動画の映像も音声もここまで読めるようになりました。特別な投資なしに、現場の手で記録のデジタル化を始められる時代になってきたことを、参加者全員で確かめられた回になりました。


講座をふりかえって


ハンズオンの後の意見交換では、動画×AIの応用アイデアが次々と出ました。「作業手順を撮っておけば、そのままマニュアルに使えるのではないか」「話すだけで作業日誌がつけられる」といった声のほか、「まず動画で残しておいて、後からAIでデータ化する」という発想の転換も共有されました。記録するその瞬間はスマホで撮るだけにして、構造化は後工程に回す ―― 現場で手がふさがる人ほど、この“撮っておく”の気軽さが効いてきます。機器のメンテナンス記録を動画といっしょに残す、という現場ならではのアイデアもありました。


前回の手書き帳票(画像)から、今回の動画(映像+音声)へ。入力の形は変わっても、「GASだけで、無料の範囲で、現場の人が自分で小さく作れる」という芯は同じです。書籍やオンライン記事には残りにくい試行錯誤や、実際に動くコードを、地域のエンジニアや企業が直接交換できる実践的な場は、高知で実務に携わる私たちにとっても貴重です。PROMPT-Xは引き続き本コミュニティの事務局として、高知県の施設園芸産業のデジタル化・スマート化を支えていきます。


なお次回(8月)は、この延長で「フィジカルAI」編を予定しています。今回スマホが担っていた“センサー”の役割を、ESP32などの小さなデバイスに置き換えて「声で記録するデバイス」を自作する回です。


手を動かしてAI活用を学ぶ ― Technovation Workshop


今回のように「実際にコードを動かしながら学ぶ」スタイルで、最先端のテクノロジーをビジネスの現場で活かすスキルを身につけられる体験型講座として、PROMPT-XはTechnovation Workshopを提供しています。「IoTとAI」「画像とAI」「データとAI」の3つのハンズオンプログラムで構成され、今回の記事で扱った動画・画像のAI読み取りや時系列データの活用なども、基礎から体系立てて学べます。エンジニアの方はもちろん、ビジネスの現場で課題解決にAIを役立てたい方にもおすすめです。


PROMPT-Xでは仲間を募集しています


PROMPT-Xは東京・鹿児島・高知に拠点を置いています。お客さまの現場に入り込み、業務課題に対して生成AIを含む最新技術をどう活かすかを一緒に考え、実装まで伴走するエンジニアを募集しています。地方拠点から全国の先進企業・自治体と直接取引を行い、上流設計から実装まで一気通貫で手がける環境です。現場の課題を、技術で形にするところまで一緒にやりたい方を歓迎します。


PROMPT-Xで働くことに興味がある方は、採用情報ページをご覧ください。鹿児島での募集はジョブアンテナ鹿児島もあわせてご覧いただけます。


関連リンク





 
 
 

コメント


bottom of page