Open15

ChatGPTの画像入力を使ったマークアップの自動生成とE2E自動化に関する考察

mizchimizchi

tl;dr

  • ChatGPT の画像入力でラフスケッチから簡単なマークアップを生成させることができる
  • 生HTML はプロンプトとして弱い。 typescript(tsx) や headless-ui でコンテキスト情報を足すといい感じになる。
  • ChatGPT にスクリーンショットを入力し、 ChatGPT に puppeteer に操作させれて E2E 生成を自動化したり無限の猿BOT が作れるのでは。

マークアップエンジニアではなく、プログラマ視点でCSSを生成させたい。ロジックはこちらが考えるから、それに合わせたものを作ってくれ、という感じ。

まだ figma 連携とかは考えておらず、雑なラフを食わせたり、一般的なパターンを生成してもらえれば良いと思って試している。

とか考えてるうちに、うちの ChatGPT に画像入力が来た。まだAPIが来てないが、これが来ることを前提にマークアップ補助ツールを考える

これの続き
https://zenn.dev/mizchi/scraps/c212ed98ea97df

mizchimizchi

Excalidraw のラフスケッチから CSS Grid を生成させてみる

いい感じ

mizchimizchi

既存のウェブサイトのレイアウト分析は可能だろうか

出力

.container {
  display: grid;
  grid-template-rows: auto auto 1fr;
  grid-template-columns: 2fr 1fr;
  grid-template-areas: 
    "header header"
    "nav nav"
    "main sidebar"
    "footer footer";
}

.header {
  grid-area: header;
}

.nav {
  grid-area: nav;
}

.main {
  grid-area: main;
}

.sidebar {
  grid-area: sidebar;
}

.footer {
  grid-area: footer;
}

大枠は拾ってくれたが、それが固定幅要素なのか、可変長なのかの判断は難しそう。main が入れ子になっててその中身も... と指示すればいいだろうか。

画像より excalidraw のラフのがほどよく特徴量に落ちてるから使いやすいからっぽい。この路線で考えてみる。

mizchimizchi

CSS Grid をサブグリッド情報込みで生成させる。このとき固定幅の情報も与える。

出力

:root {
  --header-height: 50px;
  --sidebar-title-height: 40px;
  --main-title-height: 30px;
  --content-footer-height: 25px;
  --footer-height: 45px;
}

.container {
  display: grid;
  grid-template-rows: var(--header-height) 1fr var(--footer-height);
  grid-template-columns: 1fr 3fr;
  grid-template-areas:
    "header header"
    "sidebar main"
    "footer footer";
}

.header {
  grid-area: header;
}

.sidebar {
  grid-area: sidebar;
  display: grid;
  grid-template-rows: var(--sidebar-title-height) 1fr;
  grid-template-columns: 1fr;
  grid-template-areas:
    "sidebar-title"
    "menu-items";
}

.sidebar-title {
  grid-area: sidebar-title;
}

.menu-items {
  grid-area: menu-items;
}

.main {
  grid-area: main;
  display: grid;
  grid-template-rows: var(--main-title-height) 1fr var(--content-footer-height);
  grid-template-columns: subgrid;
  grid-template-areas:
    "main-title"
    "content"
    "content-footer";
}

.main-title {
  grid-area: main-title;
}

.content {
  grid-area: content;
}

.content-footer {
  grid-area: content-footer;
}

.footer {
  grid-area: footer;
}

感想

悪くない

mizchimizchi

画像を認識させてSVG化できないか

出力


<svg width="410" height="372" xmlns="http://www.w3.org/2000/svg">
    <!-- 背景色 -->
    <rect width="410" height="372" fill="white" />

    <!-- ウサギの本体 -->
    <ellipse cx="205" cy="186" rx="150" ry="190" fill="white" stroke="black" stroke-width="2" />

    <!-- ウサギの顔 -->
    <ellipse cx="205" cy="100" rx="90" ry="80" fill="white" stroke="black" stroke-width="2" />

    <!-- 左の目 -->
    <ellipse cx="175" cy="80" rx="10" ry="15" fill="black" />
    <!-- 右の目 -->
    <ellipse cx="235" cy="80" rx="10" ry="15" fill="black" />

    <!-- 鼻 -->
    <ellipse cx="205" cy="100" rx="5" ry="8" fill="red" />

    <!-- リボン -->
    <polygon points="205,140 185,120 225,120" fill="green" />

    <!-- 口 -->
    <path d="M 190 110 Q 205 130 220 110" fill="none" stroke="red" stroke-width="2" />

    <!-- 左の耳 -->
    <path d="M 130 10 Q 150 50 170 10" fill="white" stroke="black" stroke-width="2" />
    <!-- 右の耳 -->
    <path d="M 240 10 Q 260 50 280 10" fill="white" stroke="black" stroke-width="2" />
</svg>

感想

このあと色々クエリを追加してみたが、結局これ以上元画像に近づけることができず。内部の認識モデルではそうなって固定されてしまっているっぽい。

直接画像を渡していると言うより、伝言ゲームを経た結果と思ったほうが良さそう。

結局画像の入力モデルがあるだけで、画像の出力モデルは専用のものを使ったほうがよさそう。

mizchimizchi

つまり、(雑な)マークアップ補助ツールを作るには、これを実現できるといいのでは。

  • 作りたい対象のラフスケッチを書く
  • プロンプトで html+tailwind でコードを生成するように指示して Chat Complession API を叩く
  • 生成された HTML をプレビュー
  • 生成された DOM を HTML2Canvas かなにかで画像化してアップロードする
  • レンダリングした画像と修正指示で再度生成する
  • ↑ を繰り返す

CSS は一回生成したら終わりではなく、繰り返し指示を与えて修正していけるとよさそう。

これをやるツールを途中まで実装する。
とはいえ、まだ 画像入力API が開放されてないので、それを待つことになるが...

mizchimizchi

というわけで、途中までつくったやつ

OpenAI API と Excardraw の埋め込みコンポーネントで実装した。
Excridraw の中身を使えてないので、そこはまだハリボテ。

初期プロンプトはこんな感じ

あなたはマークアップエンジニアです。指示されたコードを HTMLElement + tailwind を使って実装します。reset.css は適用済みです。
出力には必ず ```html の markdown のコードブロックを必ず一つだけ含んでください。コードブロック以外の出力は可能な限り省いてください。

左側でラフを書き、真ん中でプロンプトを記述し、右側で生成されたHTMLをプレビューする。

ラフを svg で出力することはできているが、現状これをアップロードしてプロンプトにする方法がない。そのAPIが来るまで寝かせる。

というかこれを作る過程で別の課題が見つかったのでそれを倒す。

mizchimizchi

生HTML+Tailwind はメタ情報が足りない!

HTML と CSS を2つに分けると生成したコードを取り出したり適用するのが面倒、と思ってポータブルなTailiwind 前提の 生HTML を出力ターゲットにしてみた。

が、 tsx + radix-ui と比べるとコードの生成精度がだいぶ落ちる印象を受けた。これは途中からモデルを gpt-3.5 から gpt-4 にしてみたが変わらず。

たぶん、こういうことが考えられる。

  • tsx だとコンポーネント名や className を名付ける過程でメタ情報が発生している。typechat で見た現象
  • tsx で記述するとコンポーネント名や props でおそらく役割がある程度定まる。props でプレースホルダの代入なんかも AI が実装してくれる。
  • radix-ui(というか headless系フレームワーク) で記述するとアクセシビリティを記述することになるのと等価なので、役割から装飾が決まる
  • className への tailwind のユーティリティクラスの羅列は「どのように実装するか」はわかるが、AI からすると「ユーティリティクラスの羅列が何の役割を意味しているか」という推論が挟まることになる
    • 再生成に過去のプロンプトを与えているが、このとき役割が失われている可能性がある
    • 今回は試さないが、 Human Readable / Machine Readable な data 属性も生成させたらどうだろう?
  • tailwind を使わないセレクター命名とルールセット定義は意味論が与えられるが、直接 html+tailwind だとその情報が失われる。

というわけで、React に限らず、AI に生成させる高水準なコンポーネントのソースコードの状態で装飾を生成させるのがよい気がする。

mizchimizchi

というわけで、前にやったものの復習だが、 headlessui/react + tailwind で生成するように指示してみた。

入力

これは headless ui の TabGroup の実装です。明るい感じで tailwind を用いて装飾してください。

import { Tab } from '@headlessui/react';
function MyTabs(props: { tabs: Array<{title: React.ReactNode, content: React.ReactNode}>}) {
  return (
    <Tab.Group>
      <Tab.List>
        {props.tabs.map((tab, idx) => <Tab key={idx}>{tab.title}</Tab>)}
      </Tab.List>
      <Tab.Panels>
        {props.tabs.map((tab, idx) => <Tab.Panel key={idx}>{tab.content}</Tab.Panel>)}
      </Tab.Panels>
    </Tab.Group>
  )
}

出力

import { Tab } from '@headlessui/react';

interface TabProps {
  tabs: Array<{
    title: React.ReactNode;
    content: React.ReactNode;
  }>;
}

const MyTabs: React.FC<TabProps> = (props) => {
  return (
    <Tab.Group>
      <Tab.List className="flex p-1 space-x-1 bg-gray-100">
        {props.tabs.map((tab, idx) => (
          <Tab
            key={idx}
            className={({ selected }) =>
              selected
                ? 'py-2 px-4 bg-blue-500 text-white rounded-lg'
                : 'py-2 px-4 bg-white hover:bg-gray-200 text-gray-800 rounded-lg'
            }
          >
            {tab.title}
          </Tab>
        ))}
      </Tab.List>
      <Tab.Panels className="mt-2">
        {props.tabs.map((tab, idx) => (
          <Tab.Panel 
            key={idx}
            className="p-4 bg-white shadow rounded-lg"
          >
            {tab.content}
          </Tab.Panel>
        ))}
      </Tab.Panels>
    </Tab.Group>
  );
}

export default MyTabs;

感想

こんな感じで手を加えていける。
というか、やはり関数コンポーネントの props の型ぐらいは自分で書く必要がある。

実際に対話的に修正するには、画像以外にプレビューコンテナとルート要素のコンテナモデルを指定する必要がありそうな気がする。

  • プレビュー用のコンテナサイズ (eg. 800/800)
  • プレビューの展開に使った props の実例
  • ルート要素のコンポーネントが inline か box か、box の場合はサイズも (eg box[100%,100%] )
mizchimizchi

前回は headless-ui の公式ドキュメントのサンプルのテンプレートを与えたが、chatgpt が headless-ui 自体を知ってそうな気がしたので、別スレッド作って忘れてしまってワンショットのコード生成させてみた。

入力

この MyTabs の テンプレートを Tailwind で装飾しながら実装してください。

import { Tab } from '@headlessui/react';

interface TabProps {
  tabs: Array<{
    title: React.ReactNode;
    content: React.ReactNode;
  }>;
}

const MyTabs: React.FC<TabProps> = (props) => {
  return < />
}

出力

import React from 'react';
import { Tab } from '@headlessui/react';

interface TabProps {
  tabs: Array<{
    title: React.ReactNode;
    content: React.ReactNode;
  }>;
}

const MyTabs: React.FC<TabProps> = ({ tabs }) => {
  return (
    <Tab.Group>
      <div className="flex justify-center">
        <Tab.List className="flex p-1 space-x-1 bg-blue-900 rounded-xl">
          {tabs.map((tab, idx) => (
            <Tab
              key={idx}
              className={({ selected }) =>
                selected
                  ? 'py-2.5 px-6 bg-blue-600 text-white rounded-lg'
                  : 'py-2.5 px-6 bg-white text-gray-500 hover:bg-blue-200 rounded-lg'
              }
            >
              {tab.title}
            </Tab>
          ))}
        </Tab.List>
      </div>
      <Tab.Panels className="mt-2">
        {tabs.map((tab, idx) => (
          <Tab.Panel
            key={idx}
            className="p-4 border rounded-xl border-blue-200"
          >
            {tab.content}
          </Tab.Panel>
        ))}
      </Tab.Panels>
    </Tab.Group>
  );
};

export default MyTabs;

(プレビューは省略)

感想

およそ期待した通りのコードを生成してくれた。つまり、props の型が十分に意味をなしているなら、その実装はAIに丸投げできる。もちろんボックスモデル等は自分で考えることになるが...

これは一番使われてそうなライブラリを選択するインセンティブになりそう。

mizchimizchi

CSS生成ツールの雛形としては、 headless-ui や radix-ui のビルトインのUI生成の初期プロンプトとして与えれば十分そうで、その後細かい修正はプレビューしつつ対話的に行えば良さそう。

tailwind, chakra, panda-css のユーティリティクラスは、基本的にdiv や span の末端で使うものとして、AI がそれを再解釈するのはあまり期待できない。

プログラマがコンポーネントのロジックを考えたら、残りが決まる、という感じにするのが筋がよい気がする。

mizchimizchi

次に生成物に対する E2E テストの自動生成を試した

一応動きそうなコードを吐いた。

これを puppeteer に渡して、そのスクリーンショットを再度 ChatGPT に画像入力として渡す、というのを繰り返すことで、対話的なブラウザ操作が可能になるはず。

とはいえ、やはりセレクタに無理があるので、DOM に data-test-id を降っておいてそれを可視化する API とか混ぜたほうがいいかも。ブラックボックス感は薄れるが、HTML そのものを全部渡すと精度は上がる。

つまりこういう事ができる。

  • コード生成
  • 初期テストシナリオ生成
  • テストシナリオ失敗時、どういう理由で失敗したかを推論させて、人間の修正をまつことなく再実行を試み、成功したらPR を出す。
  • あいまいな指示を与えて動線に気づけるか確かめさせる

昔ジョークで「人間を docker コンテナ化してE2E化できねーかなー」と言っていたのが現実的になりそう。

mizchimizchi

無限の猿AI

そもそもだが、E2E というか、ランダム無限巡回の無限猿BOTを賢くするのに使える。

ステージング環境を開いた pupetter の操作を与え、適当な指示を与えて行動を取らせ続ける。これによって裏でエラーがでないかメトリクスを確認する。(金かかりそう)

行動制限するのが難しそうだし、ブラウザの自動操縦は悪用する方法が無限に思いつくな...

mizchimizchi

今後のTODO

  • OpenAI API に画像入力APIが来たら本当に使いやすいか確認
  • Headless UI でコンポーネントを生成する初期プロンプトを考察する
  • chatgpt plugin の実装方法を探る
mizchimizchi

CSS修正の対話的UI、雑に 3ペーンで作ってたけど、使うのは主に初回と微修正を加えたいときだし、初回プロンプト生成ウィザード的なやつにして、それ以外はモーダルで出すとかのほうがいいか。

あと自力で実装することを考えてたけど、もしかして ChatGPT プラグインで HTML のレンダリングができるなら、そっちの路線もあるかもしれない。