# カバーも AI に描かせる — グラレコ生成パイプラインを content 一本に畳んだ

> 各ページ・各記事のカバー画像を、Gemini の画像モデル（Nano Banana Pro）で生成するパイプラインの記録。「AI を使い倒す」を、制作プロセスそのもので体現する一手。生成する全ビジュアルとそのプロンプトを content/assets.ts 一本の唯一の出所にし、ブランドのビジュアル言語（deep navy ＋ electric-blue）は asset-config.ts に集約。実験ログのカバーは NO_TEXT ルールをあえて破ってグラレコ（手書き文字・アイコン・矢印）で記事を要約する例外にした。生成物はコミットしてビルドを再現可能にし、コンテンツハッシュでキャッシュを無効化する——という設計判断を、実際のコードで残します。

- 出典: SUZ-LAB（実験ログ / 一次情報）
- URL: https://suz-lab.co.jp/logs/ai-generated-graphic-recording-covers
- 公開: 2026-07-09
- 更新: 2026-07-09
- 著者: Hiro（主任研究員・https://suz-lab.co.jp/researchers/hiro・X: https://x.com/suz_lab_hiro）
- 柱: テクノロジー
- タグ: #AI #Gemini #Nano Banana #画像生成 #ブランド #単一の出所 #Next.js
- カバー画像: https://suz-lab.co.jp/assets/logs/ai-generated-graphic-recording-covers.png?v=324e710a

---


## 何が問題だったか

サイトの各ページ・各記事には、視覚の入口としてカバー画像が要る。選択肢は 3 つあった。

- **手で描く / デザイナーに頼む** … 記事が増えるたびに現実的でない。
- **ストック画像** … ブランドの世界観（deep navy ＋ 発光するブルー）が出せず、寄せ集めに見える。
- **AI に生成させる** … ブランドを言語化してプロンプトに落とせば、量産しても世界観が揃う。

SUZ-LAB の行動原則は「AI を使い倒す」。ならばカバーこそ **制作プロセス自体で AI を使い倒す実例**にできる。3 つ目を選んだ。

ただし素朴に「都度 AI に投げる」だけだと破綻する。プロンプトが散らばって世界観がぶれ、毎ビルドで API を叩けば再現性もコストも危うい。**生成物とプロンプトを一元管理し、ビルドを再現可能にする**枠組みが要る。

## やってみたこと

### 1. 唯一の出所 — content/assets.ts

サイトが使う AI 生成ビジュアルを、**それを生む プロンプトごと** 1 ファイルに宣言した。生成スクリプトはこの一覧を読み、コンポーネントは `src`（`public/assets/` 下のファイル名）で参照する。

```ts
// content/assets.ts
export type Asset = {
  type: "image" | "video";
  src: string;    // public/assets/ 下のファイル名（dedupe キーも兼ねる）
  prompt: string;
  style?: string; // このアセットだけ IMAGE_STYLE を上書き（例: ログのグラレコ）
};

export const ASSETS: Asset[] = [
  { type: "image", src: "home.png", style: GRAPHIC_RECORDING_STYLE, prompt: "Wide horizontal sketchnote (グラレコ) …" },
  // …各ページ・各ログのカバー
];
```

`src` を dedupe キーにしているので、同じファイル名を二度宣言しても 1 回だけ生成される。プロンプト（＝生産の関心事）はページのコピー（`vmost.ts` 等）から分離し、ここに集約した。

### 2. ブランドのビジュアル言語 — asset-config.ts

「どう見えるか」はここに集約する。ただし**意図の出所は `DESIGN.md`** で、このファイルはそのルールを具体的なプロンプト断片に翻訳するだけ、という関係にしている。

```ts
// asset-config.ts
const BRAND_LOOK =
  `deep navy (${BRAND.navy}) cinematic background, electric-blue accent glow, ` +
  "soft gradients, generous dark negative space for overlaid text, minimal, elegant, high detail";

// DESIGN.md の禁止事項。生成フレームからは文字を追い出し、実テキストは HTML 側が持つ
const NO_TEXT = "no text, no letters, no words, no logos, no watermark";

export const IMAGE_MODEL = process.env.IMAGE_MODEL ?? "gemini-3-pro-image"; // Nano Banana Pro
export const IMAGE_ASPECT_RATIO = "16:9";
export const IMAGE_SIZE = "2K";
export const IMAGE_STYLE = `${BRAND_LOOK}, ${NO_TEXT}, 16:9`;
```

配色は `content/site-config.ts` の `BRAND` が唯一の出所で、`globals.css` や `apps/video` のテーマとも一致させている——**web と video のメディアが同じブランドに読める**ようにするためだ。

### 3. 実験ログのカバーは、あえて NO_TEXT を破る

ここが一番の設計判断だった。通常のカバーは `NO_TEXT`（フレーム内に文字を入れない。実テキストは HTML 側が持つ）を守る。ところが**実験ログのカバーだけは、グラレコ（sketchnote）＝手書き文字・アイコン・矢印で記事を要約するもの**なので、文字が入るのが本質だ。

そこで `GRAPHIC_RECORDING_STYLE` という**意図的な例外**を用意し、ログのアセットだけ `style` で上書きする。ブランドは崩さない——白＋electric-blue の「マーカー」を deep navy に描く、光るライトボードのような見た目に固定した。

```ts
export const GRAPHIC_RECORDING_STYLE =
  `graphic recording / sketchnote style: hand-drawn doodles, simple icons, ` +
  `arrows and connectors, boxes and speech bubbles, energetic marker strokes, ` +
  `a few short hand-lettered keywords written in NATURAL, CORRECT JAPANESE ` +
  `(kanji, hiragana, katakana) — avoid English words and avoid garbled or fake characters; ` +
  `drawn in white and electric-blue ink on a deep navy (${BRAND.navy}) background …`;
```

### 4. 生成ループは共有パッケージ、スクリプトは素材を渡すだけ

生成の中身（Gemini クライアント・生成ループ）は `@suz-lab/gemini-assets` に切り出し、`apps/web` と `apps/video` で共有する。web 側のスクリプトは、宣言済みアセット（`ASSETS`）と 16:9 の設定を渡すだけの薄い口だ。

```ts
// scripts/generate-images.ts
await generateImages({
  ai: createClient(),
  jobs: uniqueAssets(ASSETS, "image"),
  model: IMAGE_MODEL,
  aspectRatio: IMAGE_ASPECT_RATIO,
  imageSize: IMAGE_SIZE,
  defaultStyle: IMAGE_STYLE,   // per-asset の style 上書き（グラレコ）は runner が尊重
  outDir: join(appDir, "public", ASSETS_DIR),
  force: process.argv.includes("--force"),
});
```

既に存在するファイルはスキップし、`--force` で全再生成する。生成後に続けてハッシュ更新（次項）を走らせる。

### 5. 生成物はコミットし、コンテンツハッシュでキャッシュ無効化

生成した画像は `public/assets/` に**コミットする**。これで**ビルドは再現可能**になり、毎レンダーで Gemini API を叩かずに済む（`apps/video` と同じ方針）。

ただしファイル名は安定（`home.png`）なので、中身だけ差し替えるとブラウザ / CDN / Next の画像最適化キャッシュに古い版が残る。そこで各アセットの内容ハッシュを `asset-hashes.json` に書き出し、URL に `?v=<hash>` を付けてバイト列に追従させる。

```ts
// scripts/hash-assets.ts（抜粋）
for (const name of readdirSync(assetsDir).sort()) {
  if (name.startsWith(".")) continue;
  const bytes = readFileSync(join(assetsDir, name));
  hashes[name] = createHash("sha256").update(bytes).digest("hex").slice(0, 8);
}
writeFileSync(manifestPath, `${JSON.stringify(hashes, null, 2)}\n`);
```

マニフェストはコミットし、ビルドステップでも再生成するので、常に出荷するバイト列と一致する。消費側は `lib/asset-url.ts` の `assetSrc` で読む。

## ハマったところ・判断

- **Imagen（旧 API）ではなく `generateContent` + `imageConfig`。** トップ品質の Gemini 画像モデルは deprecated な Imagen 経由ではなく、`generateContent` 側で使う。ここは共有 runner 側に寄せた。
- **グラレコの手書き文字を「正しい日本語」で出す。** 初期は手書き文字が英語や崩れた偽文字になりがちだった。プロンプトで「Latin の固有名詞（SUZ-LAB / VMOST / Gemini 等）はラテン文字のまま、それ以外の手書きラベルはすべて自然で正しい日本語」と明示的に指定して撃ち分けている。
- **NO_TEXT とグラレコの矛盾を、例外として明文化する。** 「文字を入れない」ルールと「手書き文字で要約する」ログカバーは真っ向から矛盾する。隠して分岐させるのではなく、`GRAPHIC_RECORDING_STYLE` という名前付きの例外にし、コメントで「なぜ破るか（グラレコとは手書き文字そのものだから）」を残した。
- **再現性を取るか、鮮度を取るか。** 毎ビルド生成なら常に最新だが、API 依存でビルドが不安定・高コストになる。生成物をコミットして再現性を取り、更新はハッシュ付き URL で確実に反映する側に倒した。

## 検証

- `GEMINI_API_KEY=… pnpm --filter @suz-lab/web images` で全カバーが生成され、`public/assets/` に落ちること。既存ファイルはスキップ、`--force` で再生成されること。
- ログカバーがグラレコ（白＋ブルーの手書き、日本語ラベル）で、他ページのカバーが `NO_TEXT` のシネマティックな見た目で出ること。
- 画像を差し替えると `asset-hashes.json` の該当ハッシュが変わり、`?v=` が更新されて即反映されること。
- web と `apps/video` のメディアが同じブランド（deep navy ＋ electric-blue）に読めること。

## 学び

- **「AI を使い倒す」は、成果物より制作プロセスに出る。** カバーを AI に描かせること自体が、行動原則の実例になった。ブランドを言語化してプロンプトに落とせば、量産しても世界観が揃う。
- **プロンプトは「唯一の出所」の資産として扱う。** `content/assets.ts` に src ＝ プロンプトで宣言し、意図（DESIGN.md）と実装（asset-config.ts）を分けたことで、世界観を変えるときも 1 か所を直して `--force` で焼き直せる。
- **ルールを破るなら、名前を付けて理由を残す。** NO_TEXT を破るログカバーは、こっそり分岐させず `GRAPHIC_RECORDING_STYLE` という例外に昇格させた。例外に名前と理由があると、後から読んでも「意図的な逸脱」だと分かる。
- **生成 AI の出力は、コミットして初めて再現可能になる。** ビルドを API に依存させないために生成物を版管理し、ファイル名固定の弱点（キャッシュ）はコンテンツハッシュで塞ぐ。生成と配信を分離するのが、生成 AI をプロダクションに載せる素直な形だった。

