# How to Implement Batch Generation for Card Sets like Twelve Gold Saints

> Learn to implement batch generation for card sets like Twelve Gold Saints. Reuse existing helpers to efficiently generate multiple images with credit accounting and error handling.

- Repository: [苍何/awesome-gpt-image-2](https://github.com/freestylefly/awesome-gpt-image-2)
- Tags: how-to-guide
- Published: 2026-08-26

---

**You can implement batch generation by creating a new `/api/generate-batch` endpoint that loops over an array of prompts, reuses the existing `reserveGeneration` and `generateImage` helpers from [`api/generate-image.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-image.js), and returns aggregated results while preserving per-image credit accounting and error handling.**

The freestylefly/awesome-gpt-image-2 repository provides a React-based GPT image generation platform that currently handles single images through a Node.js API backed by Supabase. By extending the existing reservation architecture in [`api/generate-image.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-image.js), developers can implement batch generation for card sets—such as the Twelve Gold Saints example documented in [`docs/gallery-part-2.md`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/docs/gallery-part-2.md)—without modifying the core credit management or UI grid layout defined in [`src/styles.css`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/styles.css).

## Understanding the Current Architecture

The repository builds image generation around three distinct layers that remain unchanged during batch implementation.

### Front-End Layer (React + Vite)

UI components assemble prompts and invoke the back-end API at **`/api/generate-image`**. The prompt structure for a 12-card grid (the "Twelve Gold Saints") is described in the documentation file [`docs/gallery-part-2.md`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/docs/gallery-part-2.md) at line 20. The interface already supports grid layouts through CSS classes defined in [`src/styles.css`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/styles.css) (specifically the `.cardActions` and animation rules between lines 1360-1407), meaning the UI requires no structural changes to display multiple generated cards.

### Back-End Layer (Node API)

The file [`api/generate-image.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-image.js) handles single `POST` requests through a strict reservation pattern:

1. **Reservation**: Calls `reserveGeneration` (or `reserveCreditGeneration` for administrators) via Supabase RPC `reserve_generation_usage` to record the user, case, and prompt
2. **Generation**: Calls the external CIYUAN image service through `generateImage`
3. **Completion**: Executes `completeReservation` to finalize the transaction or `releaseReservation` on failure

This three-phase pattern ensures each generation deducts a credit or respects the free-usage quota before any external API costs are incurred.

### Usage Tracking (Supabase RPC)

The `reserve_generation_usage` RPC stored in the Supabase migrations (specifically [`20260509061039_fix_generation_usage_ambiguous_columns.sql`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/20260509061039_fix_generation_usage_ambiguous_columns.sql)) handles atomic credit deduction. The `profileFromReservation` helper merges reservation data with user profiles to return updated quota information in the API response.

## Batch Implementation Strategy

To generate a batch of cards (one image per saint, or a 12-grid collage), extend the existing flow without altering core reservation logic:

**Front-end batch builder**: Collect an array of prompts (one per card) and send them together. The existing UI for card grids in [`src/styles.css`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/styles.css) already supports the layout; only the data payload requires batching.

**New API endpoint**: Add [`api/generate-batch.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-batch.js) that loops over the received prompt list, re-using the existing `reserveGeneration` and `generateImage` helpers. Each iteration creates its own reservation, guaranteeing credit accounting per image.

**Shared reservation logic**: Inside the batch handler, call the same `reserveGeneration` (or `reserveCreditGeneration` for admins) as [`generate-image.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/generate-image.js). Keep the `profileFromReservation` merge logic unchanged to ensure batch generation respects the same quota and free-usage rules as single-image calls.

**Response aggregation**: Return an array of `{ ok: true, image, user }` objects, matching the single-image response shape. The front-end can map this onto the card grid UI without component modifications.

**Optional parallelism**: Use `Promise.allSettled` to run all generations concurrently, falling back to sequential retries for any failures. This improves latency for large batches while handling partial failures gracefully.

## Building the Batch Endpoint

Create [`api/generate-batch.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-batch.js) in the project root to handle multiple prompt submissions. This implementation re-exports helpers from the existing [`generate-image.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/generate-image.js) to maintain consistency:

```javascript
// File: api/generate-batch.js
import {
  getAuthContext,
  getProfileById,
  isSupabaseServerConfigured
} from './_lib/supabase.js';
import { reserveGeneration, reserveCreditGeneration,
         generateImage, completeReservation,
         releaseReservation, profileFromReservation } from './generate-image.js';

// Re-use the same JSON helper
function json(res, status, payload) {
  res.status(status).json(payload);
}

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    res.setHeader('Allow', 'POST');
    return json(res, 405, { ok: false, error: 'METHOD_NOT_ALLOWED' });
  }

  // -----------------------------------------------------------------
  // 1️⃣ Authenticate (same as generate-image.js)
  // -----------------------------------------------------------------
  const auth = await getAuthContext(req, { allowAnonymous: false });
  if (auth.error) return json(res, auth.status || 401, { ok: false, error: auth.error });

  // -----------------------------------------------------------------
  // 2️⃣ Parse body: expect { prompts: [{caseId, prompt}, …] }
  // -----------------------------------------------------------------
  const { prompts } = req.body ?? {};
  if (!Array.isArray(prompts) || prompts.length === 0) {
    return json(res, 400, { ok: false, error: 'INVALID_PAYLOAD' });
  }

  // -----------------------------------------------------------------
  // 3️⃣ Process each prompt – reservation → generation → completion
  // -----------------------------------------------------------------
  const results = await Promise.allSettled(
    prompts.map(async ({ caseId, prompt }) => {
      const cleanPrompt = String(prompt ?? '').trim();
      if (!cleanPrompt) throw new Error('EMPTY_PROMPT');

      const reservation = auth.profile.isSuperAdmin
        ? await reserveCreditGeneration(auth.client, auth.user.id, caseId, cleanPrompt)
        : await reserveGeneration(auth.client, auth.user.id, caseId, cleanPrompt);

      const image = await generateImage(cleanPrompt);
      await completeReservation(auth.client, reservation.reservationId);
      return {
        ok: true,
        image,
        user: profileFromReservation(auth.profile, reservation)
      };
    })
  );

  // -----------------------------------------------------------------
  // 4️⃣ Build final payload: successes + errors (with refreshed profile)
  // -----------------------------------------------------------------
  const refreshedProfile = await getProfileById(auth.user.id).catch(() => null);
  const payload = results.map(r => (r.status === 'fulfilled')
    ? r.value
    : { ok: false, error: r.reason?.message || 'GENERATION_FAILED', user: refreshedProfile });

  return json(res, 200, payload);
}

```

This endpoint preserves the atomic reservation pattern from the single-image flow: each prompt triggers its own `reserveGeneration` call, ensuring that if a user exhausts credits mid-batch, subsequent requests fail gracefully without consuming external API resources.

## Frontend Integration

In [`src/main.jsx`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/main.jsx), implement a batch request helper that sends the prompt array to the new endpoint and maps results to the existing card grid UI:

```javascript
// src/main.jsx – batch request helper
async function generateCardSet(cardPrompts) {
  const resp = await fetch('/api/generate-batch', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompts: cardPrompts })
  });
  const results = await resp.json(); // [{ok, image, user}, …]
  // Attach each image to the UI grid (e.g., <img src={result.image} …/>)
  return results;
}

// Example call for the Twelve Gold Saints
const goldSaints = [
  { caseId: 101, prompt: 'Gold Saint of Aries – Chinese name "白羊座金圣"' },
  { caseId: 102, prompt: 'Gold Saint of Taurus – Chinese name "金牛座金圣"' },
  // … 10 more …
];
generateCardSet(goldSaints).then(renderGrid);

```

The `results` array maintains the same shape as individual generation responses, allowing existing React components to render each card without modification. The CSS grid classes in [`src/styles.css`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/styles.css) handle the visual layout automatically.

## Key Files Reference

- **[`api/generate-image.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-image.js)**: Core single-image endpoint containing reservation logic and CIYUAN integration
- **[`api/generate-batch.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-batch.js)**: New batch handler (implementation detailed above)
- **[`src/main.jsx`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/main.jsx)**: Front-end entry point for prompt assembly and API calls
- **[`src/styles.css`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/styles.css)**: Contains `.cardActions` and animation rules (lines 1360-1407) for grid layouts
- **[`docs/gallery-part-2.md`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/docs/gallery-part-2.md)**: Documentation example of the Twelve Gold Saints 12-grid request at line 20
- **[`supabase/migrations/20260509061039_fix_generation_usage_ambiguous_columns.sql`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/supabase/migrations/20260509061039_fix_generation_usage_ambiguous_columns.sql)**: Defines the `reserve_generation_usage` RPC for credit tracking

## Summary

- **Reuse existing helpers**: Import `reserveGeneration`, `generateImage`, and `completeReservation` from [`api/generate-image.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-image.js) to maintain consistent credit accounting and error handling.
- **Process in parallel**: Use `Promise.allSettled` to generate multiple images concurrently while capturing individual failures without aborting the entire batch.
- **Preserve response shapes**: Return arrays of `{ ok, image, user }` objects to ensure compatibility with existing UI components in [`src/main.jsx`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/main.jsx) and [`src/styles.css`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/styles.css).
- **Maintain atomic reservations**: Each prompt triggers its own Supabase reservation via `reserve_generation_usage`, ensuring credit limits are enforced per image rather than per batch.
- **Support Twelve Gold Saints**: The documented 12-card grid example from [`docs/gallery-part-2.md`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/docs/gallery-part-2.md) works immediately with this implementation by sending 12 prompt objects to `/api/generate-batch`.

## Frequently Asked Questions

### How does credit reservation work when generating a batch of cards?

Each prompt in the batch triggers an individual call to `reserveGeneration` (or `reserveCreditGeneration` for administrators) before calling the external CIYUAN service. This atomic approach ensures that if a user has only 5 credits remaining but requests 12 images, the sixth reservation attempt fails with an insufficient quota error, preventing unpaid API usage. The `reserve_generation_usage` RPC in Supabase handles this enforcement atomically.

### Should I process batch generations sequentially or in parallel?

Use `Promise.allSettled` for parallel processing as shown in the [`api/generate-batch.js`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/api/generate-batch.js) implementation. Parallel execution significantly reduces latency for card sets like the Twelve Gold Saints, while `allSettled` ensures that one failed generation (due to network issues or content policy violations) does not reject the entire batch. The approach gracefully handles partial failures by returning specific error messages for individual cards alongside successful results.

### How does the UI handle displaying multiple generated cards?

The existing UI components in [`src/main.jsx`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/main.jsx) and CSS grid definitions in [`src/styles.css`](https://github.com/freestylefly/awesome-gpt-image-2/blob/main/src/styles.css) (specifically the `.cardActions` class and associated animation rules) already support multi-card layouts. The batch endpoint returns an array of standard response objects that map directly to the existing card components. No modifications to the React component structure or styling are required to display a 12-card grid.

### What authentication requirements apply to batch generation?

The batch endpoint uses the same `getAuthContext` helper as the single-image endpoint, requiring valid authentication and disallowing anonymous requests (`allowAnonymous: false`). Each generation within the batch inherits the user's admin status from `auth.profile.isSuperAdmin`, determining whether to call `reserveCreditGeneration` (for unlimited admin users) or standard `reserveGeneration` (for credit-limited accounts).