Admin tool for comparing AI models, prompts, and hyperparameters through the production merch pipeline
The Image Generation Benchmark is an admin-only tool inside Merch Admin for running controlled experiments on merch AI art generation. Each benchmark session sweeps a cartesian product of:
selfies × design text × prompts × models × quality × input fidelity
Design text is optional. A run with no names stays selfies × prompts × models × quality × input fidelity, the same as before.
Every generation runs through the same production pipeline as live merch:
Background removal is deliberately never applied in the benchmark — sessions compare raw model output, so a remover would obscure what the prompt and model actually produced.
Results are stored in PostgreSQL and uploaded to S3 so admins can compare outputs side-by-side, like/dislike individual cells, and rerun past sessions.
| Environment | URL |
|---|---|
| Local dev | http://localhost:3004/admin/merch/benchmark |
| Production | Merch Admin → Benchmark tab in the top nav |
The Benchmark tab is admin-only. Supplier users are routed to /orders and cannot access it.
Routes:
| Page | Path |
|---|---|
| New Benchmark (setup) | /benchmark |
| Run history | /benchmark/results |
| Session detail | /benchmark/results/:id |
apps/zooly-app/app/(pages)/admin/merch/ (Host — mounts the admin UI)
[[...path]]/page.tsx Lazy-imports @zooly/merch-admin-client App (basename /admin/merch)
Benchmark tab mounts pages from @zooly/merch-benchmark-client
BenchmarkPathsProvider base="/benchmark"
packages/merch-benchmark/client/ (@zooly/merch-benchmark-client)
dashboard-page.tsx Setup form + sticky Run Benchmark bar
results-page.tsx Session list
session-detail-page.tsx Comparison grid, config, rerun
components/ Image upload, selfie groups modal, results grid
packages/merch-benchmark/srv/ (@zooly/merch-benchmark-srv)
create-session.ts Cartesian product → DB rows
run-generation.ts Full pipeline: GPT Image → mask → overlay → S3
selfies.ts Western / Japan selfie corpus helpers
packages/merch/img-gen/ (@zooly/merch-img-gen)
generateAIArt, removeBackgroundWithBria, downloadAndProcessAIImage, applyTemplateMask
Shared with production /api/merch/ai/generate and render steps
apps/zooly-app/app/api/merch/admin/benchmark/
sessions/ GET list, POST create
sessions/[id]/ GET detail, PATCH rename
generate/ POST run one generation
generations/[id]/like/ PATCH like/dislike
selfies/ GET selfie corpus
packages/db/
schema/merchBenchmarkTables.ts
access/merch/merch-benchmark.ts
Same pattern as the rest of merch admin:
GET /api/merch/admin/* with credentials: "include"requireAdmin() from @zooly/merch-admin-srvVITE_AUTH_URL)After creating a session, the dashboard fires a client-side job queue (run-loop.ts) that POSTs to /api/merch/admin/benchmark/generate with bounded concurrency. The session detail page polls every 3 seconds while status is running.
Auto-generated timestamp name; editable. Required to run.
Two corpora:
| Set | Count | Notes |
|---|---|---|
| Western | 110 | Default dry-run pool |
| Japan | 442 | Larger corpus |
Selection modes:
| Mode | Behavior |
|---|---|
| all | Every selfie in the corpus |
| range | Inclusive index range (e.g. 1–5) |
| random | N random selfies at run time |
| group | Selfies from a saved test group — can mix Western, Japan, and uploaded selfies |
Selfie groups are shared with the Simulation tab. Use Manage groups to create, edit, or delete groups from the full pool (Western + Japan + uploaded selfies). The group editor also supports uploading new selfies — they are stored on S3 and become available to every group.
One or more named prompt variants. At least one prompt with non-empty text is required.
Some designs ask the shopper for a name or other words and place that writing in the picture. A benchmark can test several of those words against the same selfies.
Add one or more lines. Each line has a label (what you see on the results) and the words that go into the picture. The run multiplies: every selfie is generated once for each line.
Leave the section empty when the design does not collect words. Older runs without this section keep working.
The prompt must include the same placeholder the shop uses, written as <USER-INPUT>. A named placeholder such as <USER-INPUT:company> receives the same words. The model never sees the placeholder itself.
You can turn on the same text check the shop uses. Each result then shows whether those words would have been allowed. The picture is still generated, and the check is recorded the same way Issue Catcher records pass or fail.
Issue Catcher checks can name the words for that result. For example: "the company name ACME appears above ACQUIRED in the same font." To write one check that follows every line, put <USER-INPUT> in the check where the name should be.
Drag-and-drop image upload fields (or paste a URL). These map to production design assets:
Models come from GET /api/merch/admin/models (same catalog as production). Only active models appear.
| Setting | Options |
|---|---|
| Quality | low, medium, high |
| Input fidelity | low, high |
| Concurrency | Parallel generation jobs (default 5) |
A sticky bar at the bottom shows total generations, estimated cost, and a Run Benchmark button. The button is disabled with a hint when required fields are missing.
/benchmark/results)Lists past sessions with name, date, status, and models used. New Benchmark returns to the setup page.
/benchmark/results/:id)Header
running, completed, or failed?from=sessionId)Stats
Progress, failed count, cost, average duration. Progress bar while running.
Configuration
Expandable section showing prompts (read-only textarea, max ~500px height), template/overlay/mask thumbnails, and run parameters.
Results grid
Defined in packages/db/src/schema/merchBenchmarkTables.ts. Migration: 0128_merch_benchmark_tables.sql.
merch_benchmark_sessionOne row per benchmark run.
| Column | Purpose |
|---|---|
prompts | JSON array of { name, text } |
texts | Optional design-text lines { name, value }[]. Empty on older runs |
screen_design_text | When true, each result records the shop text check |
template_image_url, overlay_url, mask_image_url | Asset URLs |
selfie_test_set | western or japan |
total_generations, completed_generations, failed_generations | Counters |
status | running | completed | failed |
estimated_cost, actual_cost | Cost tracking |
merch_benchmark_generationOne row per (selfie, design text, prompt, model, quality, fidelity) combination. Design text is omitted when the run has no lines.
| Column | Purpose |
|---|---|
selfie_url, selfie_index | Input selfie |
model_endpoint, model_display_name | From model catalog |
prompt, prompt_name | Prompt text and variant name |
text_name, text_value | Design text this row used, when any |
resolved_prompt | Prompt after that text was written in |
design_text_screen | Shop text-check pass or fail, when the run asked for it |
params | { quality, inputFidelity, selfieFilter } |
art_url | Post-mask + post-overlay result (S3) |
masked_art_url | Post-background-removal result (S3) |
liked | true / false / null human review |
status | pending | running | completed | failed |
duration_ms, cost_per_image, error | Metrics |
Access layer: packages/db/src/access/merch/merch-benchmark.ts
All routes under apps/zooly-app/app/api/merch/admin/benchmark/. All require admin auth + CORS.
List all sessions with aggregated model names.
Create a session and bulk-insert pending generation rows.
Body (key fields):
{
"name": "Benchmark 2026-06-10",
"prompts": [{ "name": "Default", "text": "..." }],
"texts": [{ "name": "ACME", "value": "ACME" }],
"screenDesignText": false,
"selfies": [{ "index": 1, "url": "https://..." }],
"models": [{ "endpointId": "gpt-image", "displayName": "GPT" }],
"templateImageUrl": null,
"overlayUrl": null,
"maskImageUrl": null,
"concurrency": 5,
"selfieTestSet": "western",
"qualities": ["low"],
"inputFidelities": ["high"],
"selfieFilter": null
}
Session + all generations.
Rename session: { "name": "..." }
Run one generation: { "generationId": "..." }. maxDuration = 300.
Set review: { "liked": true | false | null }
Returns the selfie corpus for the chosen test set.
Merch admin is served by zooly-app at /admin/merch, same-origin with the API. Open it at http://localhost:3004/admin/merch/benchmark.
Run migration 0128_merch_benchmark_tables against your local database before first use.
?templateV2=On This Page
What is the Benchmark?Where to AccessArchitectureAuthClient-side generation loopSetup PageSession nameSelfiesPromptsDesign textTemplate, overlay, and maskModels and hyperparametersRun summary barResultsRun history (,[object Object],)Session detail (,[object Object],)Database Schema[object Object][object Object]API ReferenceGET /api/merch/admin/benchmark/sessionsPOST /api/merch/admin/benchmark/sessionsGET /api/merch/admin/benchmark/sessions/[id]PATCH /api/merch/admin/benchmark/sessions/[id]POST /api/merch/admin/benchmark/generatePATCH /api/merch/admin/benchmark/generations/[id]/likeGET /api/merch/admin/benchmark/selfies?set=western|japanEnvironmentRelated Documentation