本記事はPROMPT-X技術ブログの7月講座記事(動画×AIで記録アプリを作る)の続編です。サーバ側の実装はそちらに、本記事は画面(UI)をAIに作らせる部分に絞ります。
GAS(Google Apps Script)は doGet でHTMLを返すだけでWebアプリになります。スプレッドシート連携もAPIも無料枠で動く。ただ、正直に言うと画面のHTMLを書くのが一番面倒です。ロジックは数十行なのに、見栄えの良いUIを作ろうとするとCSSとの格闘が始まります。
そこで、UIデザイン専用のAI「Google Stitch」(無料・Googleアカウントで利用可)に画面を作らせて、そのままGASで動くのかを試しました。最終的には動いたのですが、一発では動きませんでした。「AIに何を渡すか」で結果がここまで変わるのかと思うくらい差が出たので、3回の試行を検証データごと残しておきます。
題材: 動画で記録するスマホWebアプリ
題材は前回作った「スマホで撮って喋るだけで記録が残る」アプリです。サーバ側(GAS)はすでに動いています。
-
processMedia(base64, mimeType)— 動画をGemini APIに渡し、映像+音声を読み取ってJSON文字列を返す -
registerRecord(jsonText)— 確認後にスプレッドシートへ1行追記(記録日時はサーバ側で自動付与) -
doGet()—HtmlService.createHtmlOutputFromFile('index')で画面を返す
つまりAIに作らせたいのは index.html の1枚だけ。GASのファイルは .gs と .html の2種類しかなく、CSSは<style>、JSは<script>でHTML内に書くため、画面は自己完結の1ファイルで完結します。この「差し替え単位が1ファイル」という性質が、AI生成と相性抜群です。
AI生成UIがGASで動くための「契約」
ただし、どんなに美しいUIでも次を守らないとGASでは動きません。
-
つなぎ目(関数の契約): サーバ呼び出しは
google.script.run.withSuccessHandler(...).processMedia(...)の形。fetchやXMLHttpRequestは書かない -
カメラ:
getUserMediaはGASの画面(HtmlServiceのiframe)でブロックされる。<input type="file" accept="video/*" capture>を使う -
サイズ制限:
inline_dataの上限があるため18MB超の動画は送らない -
データの形: サーバが返すJSONのキー(今回は日本語キー
"天気"など)をそのまま扱う - 自己完結: 外部CDN・フォント・アイコンフォントを読み込まない(1枚のHTMLで完結)
この「契約と制約のリスト」をどうAIに伝えるかが今回の本題です。
1回目: 動いているHTMLを丸投げ → デザインは変わるが中身は全滅
まず、現在動いているindex.html(92行)をそのまま貼り、「同じ機能・同じサーバ連携を維持したままデザインを作り直して」と指示しました。動くコードを見せれば、呼び出し方も制約も読み取ってくれるだろう、と。
デザインは見事にリニューアルされて返ってきました。高コントラスト、大きなボタン、指定した緑(#45B035)。おっ、と思いながら生成コードをgrepすると——
| 検査項目 | 結果 |
|---|---|
processMedia / registerRecord の呼び出し |
0回 ❌ |
capture付きfile input |
○ |
| 18MB判定 | なし ❌ |
| サーバのJSONキーの扱い | なし ❌ |
| 外部CDN読み込み | 4件 ❌ |
サーバ連携がまるごと落ちていました。Stitchは「デザインのリニューアル」として解釈し、見た目だけ作り直したわけです。動いているコードを見せるだけでは、そのどこを「守るべき仕様」と見なすかはAI任せになる。考えてみれば当然でした。
2回目: 契約を明文化して+HTMLも見せる → 惜しい、2つ残る
次に、チャットで契約を箇条書きで明文化し、その下に動作中のHTMLを「参考実装」として添付しました。
コードの修正をお願いします。デザインは今のまま維持し、
サーバ連携の実装だけを実際の契約に合わせてください。
【厳守する技術契約】
1) 外部のCDN・CSS・JS・フォント・画像を一切読み込まない。1枚の自己完結HTMLにする
2) カメラ起動は <input type="file" accept="video/*" capture="environment">(getUserMedia不可)
3) 動画はFileReaderでbase64にし、
google.script.run.withSuccessHandler(...).withFailureHandler(...).processMedia(base64, mimeType)
でサーバに渡す(fetchやXMLHttpRequestは使わない)
4) 18MBを超える動画は送信せず「15秒以内で撮り直してください」と表示
5) 登録は google.script.run で registerRecord(jsonText) を呼び、返るシートURLをリンク表示
6) processMediaの戻り値はJSON「文字列」で、形は
{"transcription":"...","visual":"...",
"record":{"天気":"","給液量":"","排液量":"","作物の見た目":"","備考":""}}
【参考】以下が、実際にGAS上で動作しているHTMLです。
サーバとのつなぎ方はこれに合わせてください。
--- 動作中のHTML ---
(92行のindex.htmlを全文貼付)
Stitchは「サーバー連携機能は完全に維持します」「ご提示いただいた形式に完全準拠させています」と自信満々です。実際、grepしてみると google.script.run・processMedia・registerRecord・capture・18MB判定・JSON.parse——主要なつなぎ目はすべて入っていて、この時点ではかなり期待しました。
しかし2つ残りました。
-
JSONキーが英語に正規化されていた: 契約に日本語キーを明記したのに、生成コードは
data.record.weather/feed_amount/crop_statusを参照。サーバはrecord['天気']を返すので、このままでは画面に値が出ません - Tailwind CDNとGoogle Fontsの読み込みが混入: 「外部を読み込まない」の指示より、Stitchのデフォルトの作り方が勝った形
怖いのは、この状態でも見た目は完璧に動いているように見えることです。JSONの形も整っている。でも中身のキーが違うから値が入らない。「完全準拠しました」という宣言と、実際に準拠していることは、別物でした。危うく信じるところでした。
3回目: 残り2点を名指しで修正 → 全項目クリア
最後は具体的に名指しします。「サーバのキーは日本語で固定されており変更できない。現在 data.record.weather と参照しているが、実際は data.record['天気']」「cdn.tailwindcss.com と fonts.googleapis.com を削除し、素のCSSとシステムフォントに置き換え、アイコンは絵文字かインラインSVGに」。
生成された最終版の検査結果です。
| 検査項目 | 結果 |
|---|---|
processMedia / registerRecord
|
2回 / 1回 ✅ |
google.script.run + withSuccessHandler
|
✅ |
capture付きfile input(getUserMedia 0回) |
✅ |
18MB判定(const MAX_SIZE = 18 * 1024 * 1024) |
✅ |
日本語キー record['天気'] 〜 record['備考']
|
5つすべて ✅ |
| 外部読み込み | 0件 ✅ |
3回目でようやく全部通りました。
GASに入れて、実際に動かす
エクスポートしたHTMLを新規GASプロジェクトの index.html に貼り、サーバ側 code.gs と合わせてWebアプリとしてデプロイしました。
画面は問題なく表示(コンソールエラー0)。サーバ側も、テスト動画(トマト温室を撮りながら record を口頭で述べた6.8MBのmp4)で通しました。
13:10:55 動画: video/mp4 6862KB
13:11:08 AI応答JSON: {
"transcription": "トマトの様子を記録します。今日は晴れ。給液は10リットル、
排液は3割くらい。葉の色は良好、しおれはありません。",
"visual": "ビニールハウス内で栽培されているトマトの様子。多くの実が赤く熟しており…",
"record": { "天気": "晴れ", "給液量": "10リットル", "排液量": "3割",
"作物の見た目": "葉の色は良好、しおれなし", "備考": null }
}
13:11:10 シートURL: https://docs.google.com/spreadsheets/d/…
13:11:10 実行完了
動画1本が「天気・給液量・排液量…」の列に整い、文字起こしと一緒にシートに入りました。Stitchが作った画面から投げた動画を、Geminiが読んでシートに書く。ここが通ったときは素直にうれしかったです。
やってみて痛感したこと: AIの「やりました」は検査する
一番の実感は、AIの「準拠しました」は疑ってかかったほうがいい、ということです。疑うといっても目視でコードを読み込む必要はなくて、私はこのくらいの雑なgrepで受け入れ検査をしていました。
# 守るべき契約が入っているか
grep -c 'google.script.run' generated.html # 期待: 2
grep -c 'processMedia' generated.html # 期待: >0
grep -c 'registerRecord' generated.html # 期待: >0
grep -c 'capture' generated.html # 期待: >0
# あってはいけないもの
grep -c 'getUserMedia' generated.html # 期待: 0
grep -cE 'src="https?://|href="https?://' generated.html # 期待: 0
# データの形
grep -oE "record\['[^']+'\]" generated.html # 期待: サーバと同じキー
数十秒で終わるこの検査が、1回目の「サーバ連携全滅」も2回目の「英語キー化」も一発で可視化してくれました。目視でコードを読むより先に、まずgrepです。
3回の試行を振り返って残ったのはこの4つです。
- 動いているHTMLを見せるのは有効。ただし単独では不十分 — どこを守るべきかはAIが判断してしまう。契約の明文化とセットで渡す
- 明文化しても落ちるものはある — 日本語キーは英語に正規化され、外部CDNは混入した。ドメイン固有の約束事ほど名指しで確認する
- 受け入れ検査を機械化する — grepで数十秒。「動いているように見える」と「約束を守っている」は別物
- 最後は必ず実機で動かす — デプロイして、実データで、シートの中身まで見る
とはいえ、この4点を押さえた見返りは大きくて、CSSを1行も書かずに実用的なGASアプリの画面が手に入りました。ロジックはGAS+Gemini、画面はStitch、人間はつなぎ目の設計と検査。この分担は今後も使うと思います。
おわりに
Stitchはまだ日本語の実践情報が少ないので、この記録が試す方の参考になれば。次は履歴・分析タブ(Stitchが勝手に生やしたナビゲーション)に実体を持たせるところを試そうと思っています。試した結果や、もっと良い渡し方があれば、ぜひコメントで教えてください。
こうした仕組みをお客さまの現場の課題に合わせて設計・実装する仕事に興味のある方は、エンジニア採用もご覧ください。






