0
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?

GASアプリの画面はAIに描かせる ― Google Stitchに「動いているHTML+守るべき契約」を渡して、3往復で実機が動くまで【2026年7月】

0
Last updated at Posted at 2026-07-23

本記事は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回の試行を検証データごと残しておきます。

Stitchのキャンバス。左から渡した元HTML、デザインシステム、生成された新UI

題材: 動画で記録するスマホ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では動きません。

  1. つなぎ目(関数の契約): サーバ呼び出しは google.script.run.withSuccessHandler(...).processMedia(...) の形。fetchXMLHttpRequest は書かない
  2. カメラ: getUserMedia はGASの画面(HtmlServiceのiframe)でブロックされる。<input type="file" accept="video/*" capture> を使う
  3. サイズ制限: inline_data の上限があるため18MB超の動画は送らない
  4. データの形: サーバが返すJSONのキー(今回は日本語キー "天気" など)をそのまま扱う
  5. 自己完結: 外部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.runprocessMediaregisterRecordcapture・18MB判定・JSON.parse——主要なつなぎ目はすべて入っていて、この時点ではかなり期待しました。

チャットで契約+動作HTMLを渡して修正させているところ

しかし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.comfonts.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回目でようやく全部通りました。

エクスポートは「コードをクリップボードにコピー」または.zipで

GASに入れて、実際に動かす

エクスポートしたHTMLを新規GASプロジェクトの index.html に貼り、サーバ側 code.gs と合わせてWebアプリとしてデプロイしました。

GASエディタ。コード.gsとindex.htmlの2ファイル構成

デプロイしたWebアプリ。Stitchが作った画面がそのまま動く

画面は問題なく表示(コンソールエラー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  実行完了

実行ログ。動画→Gemini→JSON→シートまで流れている

スプレッドシートに1行追記。日本語キーが正しく列に入っている

動画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つです。

  1. 動いているHTMLを見せるのは有効。ただし単独では不十分 — どこを守るべきかはAIが判断してしまう。契約の明文化とセットで渡す
  2. 明文化しても落ちるものはある — 日本語キーは英語に正規化され、外部CDNは混入した。ドメイン固有の約束事ほど名指しで確認する
  3. 受け入れ検査を機械化する — grepで数十秒。「動いているように見える」と「約束を守っている」は別物
  4. 最後は必ず実機で動かす — デプロイして、実データで、シートの中身まで見る

とはいえ、この4点を押さえた見返りは大きくて、CSSを1行も書かずに実用的なGASアプリの画面が手に入りました。ロジックはGAS+Gemini、画面はStitch、人間はつなぎ目の設計と検査。この分担は今後も使うと思います。

おわりに

Stitchはまだ日本語の実践情報が少ないので、この記録が試す方の参考になれば。次は履歴・分析タブ(Stitchが勝手に生やしたナビゲーション)に実体を持たせるところを試そうと思っています。試した結果や、もっと良い渡し方があれば、ぜひコメントで教えてください。


こうした仕組みをお客さまの現場の課題に合わせて設計・実装する仕事に興味のある方は、エンジニア採用もご覧ください。

0
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
0
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?