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

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, 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, developers can implement batch generation for card sets—such as the Twelve Gold Saints example documented in docs/gallery-part-2.md—without modifying the core credit management or UI grid layout defined in 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 at line 20. The interface already supports grid layouts through CSS classes defined in 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 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) 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 already supports the layout; only the data payload requires batching.

New API endpoint: Add 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. 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 in the project root to handle multiple prompt submissions. This implementation re-exports helpers from the existing generate-image.js to maintain consistency:

// 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, implement a batch request helper that sends the prompt array to the new endpoint and maps results to the existing card grid UI:

// 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 handle the visual layout automatically.

Key Files Reference

Summary

  • Reuse existing helpers: Import reserveGeneration, generateImage, and completeReservation from 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 and 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 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 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 and CSS grid definitions in 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).

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →