Admin-only staged pipeline for building reusable personalized-merch templates — display image, blue/green dummy template, replace prompts, talent bake, benchmark, and activation
Template Studio (Template v2) is the admin tool for creating reusable personalized-merch templates. A finished template is a generic base that talents (or admins) later pick to build talent-specific designs: the studio produces a dummy composition (blue = talent stand-in, green = fan stand-in) plus the prompts that swap those dummies at runtime.
This replaces the old manual merch_template (v1) editor. Design Requests (ZLY-1420) remain a separate talent→admin pipeline; Template Studio shares generation UI components and a low-level generation core with that flow, but stores data in its own table.
| Side | Who | Where | Purpose |
|---|---|---|---|
| Template Studio | Admins | Merch Admin → Templates (/templates) | Build, verify, and activate reusable templates |
| Talent picker | Talents / sellers | Dashboard → Create your design / Yourself store | Choose an active template (public fields only) |
| Runtime | System | Yourself-store bake + per-fan generation | Swap blue dummy → talent photo, green dummy → fan selfie |
Assets (references, guides, styles, notes)
│
▼
Stage 1 — Display image candidates → pick displayImageUrl
│
▼
Stage 2 — Dummy template candidates → pick templateImageUrl
│ (blue dummy = talent, green dummy = fan; clothing/pose preserved)
▼
Replace prompts (stored on the template)
│
▼
Stage 3 — Bake talent (optional) → approve bakedTemplateUrl
│
▼
Benchmark (prefilled, linked back)
│
▼
Activate (isActive = true) → appears in talent picker
Bake + benchmark stay available after activation so admins can later bake a real talent photo into a ready template and take a benchmark result into a store via the existing create-design dialog.
castMode controls which figures the dummy template and replace prompts cover:
| Mode | Dummies | Replace prompts | Use case |
|---|---|---|---|
both (default) | Blue + green | Talent + fan | Full personalization |
talent_only | Blue only | Talent only | Branded template (no per-fan swap) |
fan_only | Green only | Fan only | Personalized without talent IP |
Cast mode drives stage-2 default prompts, which UI fields are shown, activation validation, and benchmark prefill.
A single-cast template never mentions the excluded figure. talent_only prompts never name the fan or the green dummy, and fan_only prompts never name the talent or the blue dummy — including the stage-2 image manifest sent alongside the prompt, which only names the dummy color(s) that mode uses.
Cast mode sets the placeholder count, not the head count. It says nothing about the other people in a reference image: a talent_only template built from a two-person reference still has two people, one of them the placeholder. Stage 1 is told explicitly to keep every non-placeholder person and to match the reference's number of people, and stage 2 is told the output must keep the same number of people as the display image. Earlier wording (a bare "do NOT include a fan figure") was read as "delete the second person", which silently dropped figures that every later stage then inherited.
Changing cast mode resets the prompts. A PATCH that changes castMode re-derives dummyTemplatePrompt, replaceTalentPrompt and replaceFanPrompt from getTemplateV2PromptDefaults(newMode), clearing the replace prompt the new mode does not use. Otherwise the previous mode's wording would survive into every later stage. Prompt fields sent in the same PATCH are treated as deliberate admin edits and win over the defaults.
Templates whose stored dummy prompt is still a verbatim older default are healed to the current one on save and on generate, so a stale prompt cannot outlive a change to the defaults. A hand-edited prompt is always left alone.
When activated, a template exposes the same public shape the old v1 table had (and what the talent picker / yourself-store still consume):
| Field | Meaning |
|---|---|
displayImageUrl | Marketing / “final look” image shown when picking a template |
templateImageUrl | Dummy composition the model works from at runtime |
replaceTalentPrompt | Swap the blue dummy for the talent’s photo (once, when adopting the template) |
replaceFanPrompt | Swap the green dummy for the fan selfie (per store session / purchase) |
Stage 1 does not use a real talent photo — the display image is a generic design with placeholder figures. Talent-specific work happens later (bake step, yourself-store, or design-request processing).
Stage 2 recreates the chosen display image and replaces human figures with mannequins:
Generation always includes a shared dummy example image on S3:
https://fr-development.s3.us-east-2.amazonaws.com/merch/templates-v2/dummy.jpeg
(constant DUMMY_REFERENCE_IMAGE_URL in packages/merch/srv/src/template-v2.ts; source file assets/dummy.jpeg).
The dummy example shows a single figure on a plain background, so the manifest also tells the model to take only the mannequin material from it and to ignore its pose, framing, background and figure count — otherwise it drags multi-person designs towards one person on an empty backdrop.
The default dummy prompt requires:
Prompts are best-effort defaults and remain admin-editable.
Template Studio assets are for a generic template. The UI collects:
| Asset | Required? | Notes |
|---|---|---|
| Reference images | Strongly recommended | Exact vs inspiration mode |
| Design guides / colors | Optional | Brand palettes / guidelines |
Styles (merch_filter ids) | Optional | Style prompts appended to generation |
| Notes + figure placement text | Optional but useful | Placement / mood for the prompt writer |
| Template info (name, description, tags, cast mode) | Name required to activate |
Not collected in the Assets section (by design):
(DB columns talent_image_urls / overlay_image_url may still exist for compatibility; the studio UI does not edit them for this flow.)
generateTemplateV2DisplayPrompt).generateTemplateV2DisplayCandidates) using the shared generation core.displayImageUrl.Figures in the prompt are described as generic placeholders (talent and/or fan depending on cast mode), not real people.
Requires a chosen display image.
templateImageUrl.Textareas for replaceTalentPrompt / replaceFanPrompt (shown per cast mode), prefilled with blue/green-dummy defaults.
The raw dummy template is not ideal for fan-swap benchmarking when a talent figure exists. Before Run benchmark, the admin chooses a talent source:
| Option | Source |
|---|---|
| Default female influencer | First selfie in GET /benchmark/selfies?set=western |
| Default male influencer | Second selfie in that list |
| Upload your own | Admin upload → bake |
| No image | Keep the blue dummy; clear bakedTemplateUrl |
Replace talent runs the template’s replace-talent prompt against the dummy template, appends a talent_bake generation, and the admin approves one result → bakedTemplateUrl.
/benchmark?templateV2=<id>, prefilled from getTemplateV2BenchmarkPrefill.
bakedTemplateUrl if set, else raw templateImageUrl.GET /benchmark/sessions?templateV2Id=….template_v2_id (parallel to design-request linking).isActive and status = ready.merch_template_v2Schema: packages/db/src/schema/merchTemplateV2Table.ts
Access: packages/db/src/access/merch/merch-template-v2.ts
Migrations: 0191_merch_template_v2.sql, 0192_merch_template_v1_to_v2.sql (copied v1 rows preserving ids, then dropped merch_template).
| Area | Columns |
|---|---|
| Metadata | name, description, tags, cast_mode |
| Assets | reference_image_urls, reference_mode, design_guide_urls, style_filter_ids, notes, replace_talent_description, replace_fan_description (+ legacy-capable talent_image_urls, overlay_image_url) |
| Production | display_prompt, dummy_template_prompt, generations (jsonb) |
| Staging | baked_template_url |
| Output | display_image_url, template_image_url, replace_talent_prompt, replace_fan_prompt |
| Lifecycle | status (in_progress | ready), is_active, created_by, timestamps |
Indexes: merch_template_v2_created_idx, merch_template_v2_active_idx.
generations jsonb){
id: string;
step: "display" | "dummy_template" | "talent_bake";
prompt: string;
imageUrl: string;
talentImageUrl?: string; // talent_bake only
cost: number;
createdAt: string; // ISO
}
Appends are atomic (appendMerchTemplateV2Generation using jsonb ||) so parallel candidate runs do not clobber each other.
merch_benchmark_session.template_v2_id (nullable) + index merch_benchmark_session_template_v2_idx. Created when the Benchmark dashboard is started with ?templateV2=.
packages/merch/srv/src/generation-core.ts — used by both Template Studio and Design Requests:
generateArtCandidates — parallel GPT Image 2 batches, BRIA background removal, S3 uploadgeneratePromptFromImages — multimodal Gemini prompt writeraspectRatioPromptSuffix — square (1:1) by defaultDesign-request entity logic stays in design-request.ts / design-request-template-prompt.ts; they call the core instead of duplicating mechanics.
packages/merch/srv/src/template-v2.ts (@zooly/merch-srv exports):
| Function | Role |
|---|---|
createTemplateV2 / listTemplatesV2 / getTemplateV2 / updateTemplateV2 / deleteTemplateV2 | CRUD |
generateTemplateV2DisplayPrompt | Stage 1 LLM prompt from assets |
generateTemplateV2DisplayCandidates | Stage 1 image batch |
generateTemplateV2DummyCandidates | Stage 2 dummy batch |
bakeTemplateV2Talent | Stage 3 talent bake |
getTemplateV2BenchmarkPrefill | Benchmark form payload |
setTemplateV2Active | Activate / deactivate with validation |
getTemplateV2PromptDefaults | Cast-mode default prompts |
DUMMY_REFERENCE_IMAGE_URL | Public dummy example |
Validation errors throw TemplateV2ValidationError → HTTP 400.
All under /api/merch/admin/templates-v2, admin-only (requireAdmin), CORS-enabled. Response shape { status: "success", data: { ... } }.
| Endpoint | Method | Purpose |
|---|---|---|
/templates-v2 | GET | List all (?active=true for published only) |
/templates-v2 | POST | Create empty studio row (default prompts for both) |
/templates-v2/:id | GET | One template + promptDefaults for current cast mode |
/templates-v2/:id | PATCH | Update assets / prompts / chosen images / cast mode; returns the template + promptDefaults for the resulting cast mode |
/templates-v2/:id | DELETE | Delete |
/templates-v2/:id/display-prompt | POST | LLM display prompt from assets (maxDuration 120) |
/templates-v2/:id/generate-display | POST | { prompt, count? } → display candidates (maxDuration 300) |
/templates-v2/:id/generate-dummy-template | POST | { prompt, count? } → dummy candidates (maxDuration 300) |
/templates-v2/:id/bake-talent | POST | { talentImageUrl } → talent bake (maxDuration 300) |
/templates-v2/:id/benchmark-prefill | GET | Prefill payload for Benchmark dashboard |
/templates-v2/:id/activate | POST | { active: boolean } |
Benchmark sessions:
| Endpoint | Change |
|---|---|
GET /benchmark/sessions?templateV2Id= | Filter sessions for a template |
POST /benchmark/sessions | Accepts templateV2Id on create |
Routes live under apps/zooly-app/app/api/merch/admin/templates-v2/. Routers stay thin (auth, parsing, errors); business logic is in @zooly/merch-srv.
packages/merch-admin/client/src/app/pages/templates-page.tsx — route /templates (Merch Admin sidebar → Templates).
Layout:
+ create, deep link ?template=<id>.Text fields auto-save with a debounced PATCH; image/mode/style changes PATCH immediately. Uploads use uploadProcessedImage → S3 under merch/templates-v2/<id>/….
Extracted from Design Requests into
packages/merch-admin/client/src/components/design-request/production-shared.tsx:
PromptCard, TemplateCandidateCard, ImageGroup, SectionLabelCandidateHistoryModal, CandidateDetailsModalPROD_TEAL accentDesign Requests and Template Studio both import these; behavior of Design Requests is unchanged.
packages/merch-benchmark/client/src/pages/dashboard-page.tsx:
?templateV2=<id> loads /templates-v2/:id/benchmark-prefilltemplateV2Id on session create/templates?template=<id>GET /api/merch/seller/templates lists active v2 rows, mapped to the public (v1-equivalent) shape used by CreateDesignPage and the Yourself store builder (packages/offers/client).
Template-sourced requests resolve chosen template ids via getMerchTemplateV2ById in:
getDesignRequestBenchmarkPrefillgenerateDesignRequestArt (reference images)Admin Design Requests dashboard template lookup uses GET /templates-v2.
applyYourselfDesign in packages/merch/srv/src/seller-store.ts loads the template with getMerchTemplateV2ById, then:
templateImageUrl with replaceTalentPromptreplaceFanPrompt on the design config for per-fan generationPhase-1 consumer parity assumes castMode = "both"; talent-only / fan-only store behavior is a later refinement.
0192_merch_template_v1_to_v2.sql inserted every merch_template row into merch_template_v2 preserving id (so existing templateIds / configJson.templateId references keep working), set is_active = true and status = ready, then dropped merch_template.
Deleted code: v1 schema, access module, packages/merch-admin/srv/src/templates.ts, and /api/merch/admin/templates CRUD routes.
| Design Requests (ZLY-1420) | Template Studio (v2) | |
|---|---|---|
| Who starts it | Talent submits assets | Admin builds in studio |
| Deliverable | Store/design for that talent | Reusable template library |
| Table | merch_design_request | merch_template_v2 |
| Shared | Generation core + production UI components | Same |
| Not shared | Entity, APIs, “send to user” | Activate into picker |
| Area | Path |
|---|---|
| Schema | packages/db/src/schema/merchTemplateV2Table.ts |
| Access | packages/db/src/access/merch/merch-template-v2.ts |
| Migrations | packages/db/drizzle/0191_merch_template_v2.sql, 0192_merch_template_v1_to_v2.sql |
| Generation core | packages/merch/srv/src/generation-core.ts |
| Business logic | packages/merch/srv/src/template-v2.ts |
| Admin API | apps/zooly-app/app/api/merch/admin/templates-v2/ |
| Seller list | apps/zooly-app/app/api/merch/seller/templates/route.ts |
| Admin page | packages/merch-admin/client/src/app/pages/templates-page.tsx |
| Shared UI | packages/merch-admin/client/src/components/design-request/production-shared.tsx |
| Benchmark prefill UI | packages/merch-benchmark/client/src/pages/dashboard-page.tsx |
| Dummy asset (local) | assets/dummy.jpeg |
templateV2IdOn This Page
What is Template Studio?End-to-end pipelineCast modeThe four published outputsDummy conventionAssets (what the admin uploads)Stage detailsStage 1 — Display imageStage 2 — Dummy templateReplace promptsStage 3 — Bake talent (pre-benchmark)Benchmark + ActivateData model[object Object]Generation entry shape (,[object Object], jsonb)Benchmark session linkServer architectureShared generation coreTemplate v2 moduleAdmin APIAdmin UIPageShared production UIBenchmark wiringConsumers (after v1 removal)Talent / seller template listDesign RequestsYourself-store flowMigration from v1Relationship to Design RequestsKey file mapRelated docs