個人でアプリ開発をしていて、もっとも判断に迷う瞬間のひとつが「このデザインは出せるレベルか」という問いだと思います。
機能が動くかどうかはテストで確認できます。バグがないかもコードレビューで確認できます。でも、「このボタンの配置は直感的か」「このフォントサイズは読みやすいか」「全体的なトーンはアプリのコンセプトと合っているか」——こういった審美的な判断は、コードで書きにくく、テストで確認しにくく、見落とされがちです。
2026 年初頭から Antigravity の Agent Mode と Gemini Vision の組み合わせを試してみた結果、この「曖昧な判断」をある程度コードに落とし込めることが分かりました。完全な主観的判断をAIに委ねることは今もできませんが、「基本的な UI 品質基準を満たしているか」を定量的にチェックし、問題箇所に対して改善提案を自動生成する仕組みは、現時点でも実用的なレベルで動きます。
ここでは実際に私が運用しているパイプラインの全設計を公開します。
なぜ「審美眼をコードにする」のか
前提として、AI が人間のデザイナーの感性を完全に再現できるとは思っていません。私自身、アート活動を通じてデザインに向き合ってきた経験から言えば、美しさの本質は文脈と意図の産物であり、アルゴリズムが完全に把握できるものではない、という立場です。
ただ、「美しいかどうか」と「使いやすいかどうか」は少し違います。後者には、ある程度普遍的に評価できる基準が存在します。
- ターゲットのコントラスト比が WCAG 2.1 AA 基準(4.5:1)を満たしているか
- タッチ可能な要素が 44×44pt 以上のサイズを確保しているか
- 視線誘導の流れが左上から右下へ自然につながっているか
- 重要な情報がファーストビューに収まっているか
これらは数値化できます。Gemini Vision はスクリーンショットを読み取り、ピクセルレベルの分析ができます。Antigravity はその分析結果を受け取り、修正コードを生成できます。
三者を組み合わせれば、「テストでは通るけど、実際に触るとなんか使いにくい」という問題を、CI/CD のパイプラインで自動検知できるようになります。
構築するシステムの全体像
今回作るパイプラインは以下の流れです。
[アプリのスクリーン]
↓
[Playwright / appium でスクリーンショット取得]
↓
[Gemini Vision API で評価リクエスト送信]
↓
[構造化スコアとして JSON で受信]
↓
[Antigravity エージェントがスコアを読み、改善コード生成]
↓
[GitHub Actions で自動コミット or PR 作成]
それぞれのステップを順番に実装します。
Step 1: 評価ルーブリックの設計
審美的評価を定量化するためには、まず「何を評価するか」を明文化する必要があります。私が使っているルーブリックは以下の 5 軸です。
// src/lib/ui-evaluator/rubric.ts
export type EvaluationAxis = {
name: string;
weight: number; // 合計 1.0 になるよう設定
criteria: string[];
};
export const UI_RUBRIC: EvaluationAxis[] = [
{
name: "accessibility",
weight: 0.30,
criteria: [
"テキストと背景のコントラスト比が 4.5:1 以上か",
"インタラクティブ要素のサイズが 44pt 以上か",
"フォーカスインジケーターが視認できるか",
],
},
{
name: "visual_hierarchy",
weight: 0.25,
criteria: [
"見出し・本文・補足の三層構造が明確か",
"主要なアクションが視覚的に目立つか",
"余白が情報のグルーピングを助けているか",
],
},
{
name: "consistency",
weight: 0.20,
criteria: [
"同種の要素に統一されたスタイルが使われているか",
"ブランドカラーが一貫して使われているか",
"タイポグラフィスケールが統一されているか",
],
},
{
name: "information_density",
weight: 0.15,
criteria: [
"一画面の情報量が認知負荷の範囲内か",
"重要情報がファーストビューに収まっているか",
"スクロールなしでコア機能が把握できるか",
],
},
{
name: "motion_feedback",
weight: 0.10,
criteria: [
"ユーザーアクションへのフィードバックが明確か",
"ローディング状態が適切に表示されているか",
],
},
];ここで大切なのは、すべての軸を定性的な記述として列挙するのではなく、AI が画像を見て「判定できる問い」に変換することです。「美しいか」ではなく「コントラスト比は十分か」という形に落とし込むことで、Gemini Vision が答えやすくなります。
Step 2: スクリーンショット取得の自動化
Playwright を使って対象画面のスクリーンショットを取得します。モバイルアプリの場合は Appium、または iOS Simulator のスクリーンショット API を使います。
// src/lib/ui-evaluator/capture.ts
import { chromium, type Page } from "playwright";
import * as fs from "fs";
import * as path from "path";
export type ScreenConfig = {
url: string;
viewport: { width: number; height: number };
screenshotPath: string;
waitForSelector?: string;
theme?: "light" | "dark";
};
export async function captureScreen(config: ScreenConfig): Promise<string> {
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({
viewport: config.viewport,
colorScheme: config.theme ?? "light",
deviceScaleFactor: 2, // Retina 解像度でキャプチャ
});
const page: Page = await context.newPage();
await page.goto(config.url, { waitUntil: "networkidle" });
if (config.waitForSelector) {
await page.waitForSelector(config.waitForSelector, { timeout: 5000 });
}
const dir = path.dirname(config.screenshotPath);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
await page.screenshot({
path: config.screenshotPath,
fullPage: false, // ファーストビューのみ評価
});
await browser.close();
return config.screenshotPath;
}
// 複数スクリーンをまとめてキャプチャするユーティリティ
export async function captureAllScreens(
screens: ScreenConfig[]
): Promise<string[]> {
const results: string[] = [];
for (const screen of screens) {
const screenshotPath = await captureScreen(screen);
results.push(screenshotPath);
console.log(`✅ Captured: ${screen.url} → ${screenshotPath}`);
}
return results;
}iOS アプリのスクリーンショット取得は Simulator 経由でも可能です。
# iOS Simulator からスクリーンショットを取得するスクリプト
#!/bin/bash
SIMULATOR_ID=$(xcrun simctl list --json | \
python3 -c "import json,sys; d=json.load(sys.stdin); \
[print(v['udid']) for runtimes in d['devices'].values() \
for v in runtimes if v['state']=='Booted']" | head -1)
if [ -z "$SIMULATOR_ID" ]; then
echo "❌ 起動中のシミュレーターが見つかりません"
exit 1
fi
OUTPUT_DIR="./screenshots/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUTPUT_DIR"
xcrun simctl io "$SIMULATOR_ID" screenshot "$OUTPUT_DIR/screen.png"
echo "✅ Screenshot saved to $OUTPUT_DIR/screen.png"Step 3: Gemini Vision API で評価リクエストを構築する
Gemini Vision に UI 評価を依頼するプロンプトの設計が、このパイプラインの核心です。一般的な感想を返させるのではなく、JSON として構造化されたスコアを返させる点が肝心です。
// src/lib/ui-evaluator/evaluator.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
import * as fs from "fs";
import * as path from "path";
import type { EvaluationAxis } from "./rubric";
import { UI_RUBRIC } from "./rubric";
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
export type AxisScore = {
axis: string;
score: number; // 0-10
issues: string[];
suggestions: string[];
};
export type UIEvaluationResult = {
screenshotPath: string;
totalScore: number; // 0-100 (weighted)
axes: AxisScore[];
criticalIssues: string[];
summary: string;
timestamp: string;
};
function buildEvaluationPrompt(rubric: EvaluationAxis[]): string {
const axisDescriptions = rubric
.map(
(axis) =>
`- ${axis.name} (weight: ${axis.weight}): ${axis.criteria.join(" / ")}`
)
.join("\n");
return `
あなたは UI/UX の品質評価を行う専門家です。
提供されたスクリーンショットを以下のルーブリックに基づいて評価してください。
## 評価軸と基準
${axisDescriptions}
## 出力形式
以下の JSON 形式で、コードブロックなしで出力してください。
{
"axes": [
{
"axis": "軸名",
"score": 0-10の数値,
"issues": ["発見された問題点1", "発見された問題点2"],
"suggestions": ["具体的な改善提案1", "具体的な改善提案2"]
}
],
"criticalIssues": ["即時対応が必要な問題(アクセシビリティ違反等)"],
"summary": "全体的な評価を2-3文で"
}
評価は客観的・技術的に行い、主観的な美的判断は最小限にしてください。
アクセシビリティ基準(WCAG 2.1)の違反は必ず critical として報告してください。
`.trim();
}
export async function evaluateUIScreenshot(
screenshotPath: string
): Promise<UIEvaluationResult> {
const model = genAI.getGenerativeModel({ model: "gemini-2.0-flash" });
const imageBuffer = fs.readFileSync(screenshotPath);
const base64Image = imageBuffer.toString("base64");
const mimeType = screenshotPath.endsWith(".png") ? "image/png" : "image/jpeg";
const prompt = buildEvaluationPrompt(UI_RUBRIC);
const result = await model.generateContent([
prompt,
{
inlineData: {
mimeType,
data: base64Image,
},
},
]);
const responseText = result.response.text();
let parsedAxes: AxisScore[];
let criticalIssues: string[];
let summary: string;
try {
const parsed = JSON.parse(responseText);
parsedAxes = parsed.axes;
criticalIssues = parsed.criticalIssues;
summary = parsed.summary;
} catch (e) {
// JSON パースに失敗した場合のフォールバック
parsedAxes = UI_RUBRIC.map((axis) => ({
axis: axis.name,
score: 5,
issues: ["評価の取得に失敗しました"],
suggestions: [],
}));
criticalIssues = [];
summary = "評価の取得に失敗しました: " + String(e);
}
// 重み付きスコアを計算
const totalScore = parsedAxes.reduce((acc, axisScore) => {
const rubricAxis = UI_RUBRIC.find((r) => r.name === axisScore.axis);
const weight = rubricAxis?.weight ?? 0.2;
return acc + axisScore.score * 10 * weight;
}, 0);
return {
screenshotPath,
totalScore: Math.round(totalScore),
axes: parsedAxes,
criticalIssues,
summary,
timestamp: new Date().toISOString(),
};
}Step 4: Antigravity エージェントに評価結果を渡して修正を依頼する
ここが今回のパイプラインで最も面白い部分です。Gemini Vision から返ってきた JSON スコアを、Antigravity のエージェントが読み取り、実際のコードを修正する仕組みを作ります。
AGENTS.md に評価エージェントの振る舞いを定義します。
# AGENTS.md — UI品質評価エージェントの設定
## ui-quality-fixer エージェントの役割
このエージェントは `evaluation-results.json` を読み取り、
UI/UX の問題を修正するコードを生成する専門エージェントです。
### 動作原則
- score が 7.0 未満の軸について、必ず具体的な修正を行う
- criticalIssues に含まれる問題は最優先で修正する
- 修正はコンポーネント単位で行い、他の動作に影響しない最小変更にする
- アクセシビリティ修正は WCAG 2.1 AA 基準に準拠する
### 修正後に行うこと
1. 修正したファイルのリストを `fix-report.md` に記録する
2. 同じスクリーンのスクリーンショットを再取得して評価スコアを確認する
3. スコアが改善されていない場合は、別のアプローチで再修正する(最大3回)
### 修正を行わないこと
- ブランドカラーの変更(ブランド定義ファイルの確認が必要)
- レイアウトの抜本的な変更(デザインシステムとの整合性確認が必要)
- アニメーション・トランジションの追加(パフォーマンス影響のため)評価結果を Antigravity エージェントに渡すスクリプトです。
// src/lib/ui-evaluator/fix-runner.ts
import * as fs from "fs";
import { type UIEvaluationResult } from "./evaluator";
export type FixRequest = {
evaluationResult: UIEvaluationResult;
sourceDir: string;
targetComponent?: string;
};
export async function generateFixPrompt(request: FixRequest): Promise<string> {
const { evaluationResult } = request;
const lowScoreAxes = evaluationResult.axes.filter(
(axis) => axis.score < 7.0
);
if (
lowScoreAxes.length === 0 &&
evaluationResult.criticalIssues.length === 0
) {
return ""; // 修正不要
}
const issueDetails = lowScoreAxes
.map(
(axis) =>
`【${axis.axis} — スコア: ${axis.score}/10】\n` +
` 問題: ${axis.issues.join(" / ")}\n` +
` 提案: ${axis.suggestions.join(" / ")}`
)
.join("\n\n");
const criticalSection =
evaluationResult.criticalIssues.length > 0
? `\n\n## ⚠️ 緊急対応が必要な問題\n${evaluationResult.criticalIssues.map((i) => `- ${i}`).join("\n")}`
: "";
return `
UI 品質評価エージェントからの修正依頼です。
## 評価サマリー
総合スコア: ${evaluationResult.totalScore}/100
対象スクリーン: ${evaluationResult.screenshotPath}
${criticalSection}
## 改善が必要な軸(スコア 7.0 未満)
${issueDetails}
## 対象ソースディレクトリ
${request.sourceDir}
上記の問題を修正してください。AGENTS.md の「修正を行わないこと」に記載の変更は行わず、
最小限の差分で最大の改善を目指してください。
修正後、fix-report.md に変更ファイルのリストと修正内容を記録してください。
`.trim();
}
export function writeEvaluationResults(
results: UIEvaluationResult[],
outputPath: string
): void {
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2), "utf-8");
console.log(`📊 Evaluation results written to ${outputPath}`);
}Step 5: Before/After で見る実際の効果
このパイプラインを自分のアプリ(引き寄せ系 iOS アプリ)に適用したときの例を紹介します。
Before(修正前の状態)
// 修正前: 薄いグレーのテキストでコントラスト比が基準未満だった
struct AffirmationCard: View {
let text: String
var body: some View {
VStack {
Text(text)
.font(.body)
.foregroundColor(Color.gray.opacity(0.6)) // ← コントラスト比 2.1:1(基準: 4.5:1)
.padding()
}
.background(Color.white)
}
}Gemini Vision による評価結果:
{
"axes": [
{
"axis": "accessibility",
"score": 3.0,
"issues": [
"テキスト '#999999' と背景 '#FFFFFF' のコントラスト比が約 2.1:1 で WCAG AA 基準(4.5:1)を大幅に下回っています"
],
"suggestions": [
"テキストカラーを '#767676' 以上の暗さに変更してください(最小限の修正で AA 基準達成)",
"推奨: '#555555' でコントラスト比 7.4:1 を確保できます"
]
}
],
"criticalIssues": [
"アクセシビリティ違反: メインコンテンツのコントラスト比が WCAG 2.1 AA 基準を満たしていません"
]
}Antigravity エージェントが生成した修正コード(After):
// 修正後: Antigravity エージェントによる自動修正
// Fix: WCAG 2.1 AA コントラスト比を満たすカラーに変更(2.1:1 → 7.4:1)
struct AffirmationCard: View {
let text: String
var body: some View {
VStack {
Text(text)
.font(.body)
.foregroundColor(Color(hex: "#555555")) // ← コントラスト比 7.4:1
.padding()
}
.background(Color.white)
}
}この修正は、エージェントが評価 JSON を読んだあと 40 秒ほどで自動生成されました。私が手動でコントラスト比を計算する必要も、カラーピッカーを開く必要もありませんでした。
Step 6: GitHub Actions に組み込む
PR が作成されるたびに UI 評価を自動実行し、スコアが閾値を下回った場合はマージをブロックする設定です。
# .github/workflows/ui-quality-check.yml
name: UI Quality Check
on:
pull_request:
branches: [main]
paths:
- "src/components/**"
- "src/screens/**"
- "src/styles/**"
jobs:
ui-evaluation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install chromium
- name: Start dev server
run: npm run dev &
env:
PORT: 3000
- name: Wait for server
run: npx wait-on http://localhost:3000 --timeout 30000
- name: Capture screenshots
run: npx ts-node src/lib/ui-evaluator/capture-all.ts
- name: Run UI evaluation
run: npx ts-node src/lib/ui-evaluator/evaluate-all.ts
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
- name: Check quality thresholds
run: |
node -e "
const results = require('./evaluation-results.json');
const failures = results.filter(r => r.totalScore < 65);
const criticals = results.filter(r => r.criticalIssues.length > 0);
if (criticals.length > 0) {
console.error('❌ Critical accessibility issues found:');
criticals.forEach(r => r.criticalIssues.forEach(i => console.error(' -', i)));
process.exit(1);
}
if (failures.length > 0) {
console.warn('⚠️ Low quality scores:');
failures.forEach(r => console.warn(\` \${r.screenshotPath}: \${r.totalScore}/100\`));
// 警告のみ(ブロックはしない)
}
console.log('✅ UI quality check passed');
"
- name: Upload evaluation report
uses: actions/upload-artifact@v4
if: always()
with:
name: ui-evaluation-report
path: |
evaluation-results.json
screenshots/このパイプラインを 3 ヶ月運用して気づいたこと
実際に使い続けると、公式ドキュメントや技術記事では書かれていない発見がいくつかありました。
1. Gemini Vision が苦手な評価軸がある
「情報密度」の評価は比較的安定していますが、「一貫性」の評価はばらつきが大きいです。同じスクリーンショットを 3 回送っても、スコアが 6〜9 の間でばらつくことがあります。コントラスト比のように数値化できる軸は安定していますが、「統一感があるか」のような文脈依存の判断は、モデルのサンプリングに左右されます。
対策として、temperature を 0.1 以下に設定し、同じスクリーンを 2 回評価して平均を取るようにしています。
2. CI でコストが積み上がりやすい
Gemini Vision はリクエスト単価が低いですが、PR ごとに 20 スクリーン評価すると 1 日 50 PR で 1,000 リクエスト/日になります。月額にすると無視できないコストになるため、paths: フィルターを適切に設定して UI に影響するファイルが変更されたときだけ実行するようにしています。
3. エージェントが過度に修正しすぎる場合がある
AGENTS.md の「修正を行わないこと」の定義を最初は甘くしていたため、エージェントがブランドカラーを突然変えたり、レイアウトを大幅に変更したりすることがありました。「最小変更で最大改善」という原則を AGENTS.md に明記することと、変更スコープをコンポーネント単位に限定することで、この問題はほぼ解消しました。
AIに「審美眼」は宿るか — 開発を続ける中で見えてきたこと
技術的な話の締めに、少し立ち止まって考えていることを書かせてください。
このパイプラインを作りながら、「AI が UI を評価できる」という事実と向き合い続けました。正直、最初は懐疑的でした。アートを長く続けてきた立場から言えば、審美的な判断は文脈の中にあり、アルゴリズムに収まるものではないという感覚があったからです。
実際に運用してみて感じているのは、「AI は規則に基づく品質を判断できます。人間は意図に基づく美しさを判断する」という役割分担が自然に生まれた、ということです。
コントラスト比が基準を満たしているかどうかは、AI の方が正確に速く判断できます。でも「このアプリの世界観にこの余白は合っているか」という問いは、今も人間が担う必要があります。
AI に任せることで、人間が本来考えるべき問いに集中できる時間が増える——このパイプラインは、そういう性格のものだと思っています。
このアプローチが、自分のアプリをより丁寧に育てたいと考えている方にとって、参考になれば幸いです。
n8n + Antigravity で構築する自律型ワークフロー自動化の完全ガイド も、エージェントをパイプラインに組み込む文脈で参考になります。