Template Studio (v2)

Admin-only staged pipeline for building reusable personalized-merch templates — display image, blue/green dummy template, replace prompts, talent bake, benchmark, and activation

What is Template Studio?

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.

SideWhoWherePurpose
Template StudioAdminsMerch Admin → Templates (/templates)Build, verify, and activate reusable templates
Talent pickerTalents / sellersDashboard → Create your design / Yourself storeChoose an active template (public fields only)
RuntimeSystemYourself-store bake + per-fan generationSwap blue dummy → talent photo, green dummy → fan selfie

End-to-end pipeline

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.


Cast mode

castMode controls which figures the dummy template and replace prompts cover:

ModeDummiesReplace promptsUse case
both (default)Blue + greenTalent + fanFull personalization
talent_onlyBlue onlyTalent onlyBranded template (no per-fan swap)
fan_onlyGreen onlyFan onlyPersonalized 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.


The four published outputs

When activated, a template exposes the same public shape the old v1 table had (and what the talent picker / yourself-store still consume):

FieldMeaning
displayImageUrlMarketing / “final look” image shown when picking a template
templateImageUrlDummy composition the model works from at runtime
replaceTalentPromptSwap the blue dummy for the talent’s photo (once, when adopting the template)
replaceFanPromptSwap 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).


Dummy convention

Stage 2 recreates the chosen display image and replaces human figures with mannequins:

  • Blue dummy = talent stand-in
  • Green dummy = fan stand-in

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:

  1. Recreate the display image exactly (composition, background, lighting, text, props).
  2. Apply the mannequin look from the dummy example (featureless smooth body/head).
  3. Keep each figure’s original clothing, outfit, pose, and position — only visible person (skin, face, hair) becomes the dummy.
  4. Keep the same number of people as the display image — anyone who is not becoming a dummy stays untouched, with their own face, hair and skin.

Prompts are best-effort defaults and remain admin-editable.


Assets (what the admin uploads)

Template Studio assets are for a generic template. The UI collects:

AssetRequired?Notes
Reference imagesStrongly recommendedExact vs inspiration mode
Design guides / colorsOptionalBrand palettes / guidelines
Styles (merch_filter ids)OptionalStyle prompts appended to generation
Notes + figure placement textOptional but usefulPlacement / mood for the prompt writer
Template info (name, description, tags, cast mode)Name required to activate

Not collected in the Assets section (by design):

  • Talent photos — talent is chosen at bake time or when a talent adopts the template
  • Transparent PNG overlay — shop/design-specific, not part of a reusable template

(DB columns talent_image_urls / overlay_image_url may still exist for compatibility; the studio UI does not edit them for this flow.)


Stage details

Stage 1 — Display image

  1. Admin writes or generates a display prompt (“Generate prompt from assets” / Prompt Generator dialog).
  2. Server writes the prompt via LLM from references + guides + styles + notes (generateTemplateV2DisplayPrompt).
  3. Generate 3 images (generateTemplateV2DisplayCandidates) using the shared generation core.
  4. Admin picks one → displayImageUrl.

Figures in the prompt are described as generic placeholders (talent and/or fan depending on cast mode), not real people.

Stage 2 — Dummy template

Requires a chosen display image.

  1. Editable dummy prompt (cast-mode default; “Reset the prompts to the defaults for this cast mode” restores the dummy and replace prompts).
  2. Generate 3 templates with high fidelity: display image + dummy example as references.
  3. Admin picks one → templateImageUrl.

Replace prompts

Textareas for replaceTalentPrompt / replaceFanPrompt (shown per cast mode), prefilled with blue/green-dummy defaults.

Stage 3 — Bake talent (pre-benchmark)

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:

OptionSource
Default female influencerFirst selfie in GET /benchmark/selfies?set=western
Default male influencerSecond selfie in that list
Upload your ownAdmin upload → bake
No imageKeep 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 + Activate

  • Run benchmark opens /benchmark?templateV2=<id>, prefilled from getTemplateV2BenchmarkPrefill.
    • Template image = bakedTemplateUrl if set, else raw templateImageUrl.
    • Prompt focuses on the green-dummy → fan selfie swap when cast mode includes a fan; if a talent was baked, the model is told to keep that figure.
  • Linked sessions list: GET /benchmark/sessions?templateV2Id=….
  • Sessions store template_v2_id (parallel to design-request linking).
  • Activate validates name, display image, template image, and the replace prompts required by cast mode; sets isActive and status = ready.

Data model

merch_template_v2

Schema: 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).

AreaColumns
Metadataname, description, tags, cast_mode
Assetsreference_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)
Productiondisplay_prompt, dummy_template_prompt, generations (jsonb)
Stagingbaked_template_url
Outputdisplay_image_url, template_image_url, replace_talent_prompt, replace_fan_prompt
Lifecyclestatus (in_progress | ready), is_active, created_by, timestamps

Indexes: merch_template_v2_created_idx, merch_template_v2_active_idx.

Generation entry shape (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=.


Server architecture

Shared generation core

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 upload
  • generatePromptFromImages — multimodal Gemini prompt writer
  • aspectRatioPromptSuffix — square (1:1) by default

Design-request entity logic stays in design-request.ts / design-request-template-prompt.ts; they call the core instead of duplicating mechanics.

Template v2 module

packages/merch/srv/src/template-v2.ts (@zooly/merch-srv exports):

FunctionRole
createTemplateV2 / listTemplatesV2 / getTemplateV2 / updateTemplateV2 / deleteTemplateV2CRUD
generateTemplateV2DisplayPromptStage 1 LLM prompt from assets
generateTemplateV2DisplayCandidatesStage 1 image batch
generateTemplateV2DummyCandidatesStage 2 dummy batch
bakeTemplateV2TalentStage 3 talent bake
getTemplateV2BenchmarkPrefillBenchmark form payload
setTemplateV2ActiveActivate / deactivate with validation
getTemplateV2PromptDefaultsCast-mode default prompts
DUMMY_REFERENCE_IMAGE_URLPublic dummy example

Validation errors throw TemplateV2ValidationError → HTTP 400.


Admin API

All under /api/merch/admin/templates-v2, admin-only (requireAdmin), CORS-enabled. Response shape { status: "success", data: { ... } }.

EndpointMethodPurpose
/templates-v2GETList all (?active=true for published only)
/templates-v2POSTCreate empty studio row (default prompts for both)
/templates-v2/:idGETOne template + promptDefaults for current cast mode
/templates-v2/:idPATCHUpdate assets / prompts / chosen images / cast mode; returns the template + promptDefaults for the resulting cast mode
/templates-v2/:idDELETEDelete
/templates-v2/:id/display-promptPOSTLLM display prompt from assets (maxDuration 120)
/templates-v2/:id/generate-displayPOST{ prompt, count? } → display candidates (maxDuration 300)
/templates-v2/:id/generate-dummy-templatePOST{ prompt, count? } → dummy candidates (maxDuration 300)
/templates-v2/:id/bake-talentPOST{ talentImageUrl } → talent bake (maxDuration 300)
/templates-v2/:id/benchmark-prefillGETPrefill payload for Benchmark dashboard
/templates-v2/:id/activatePOST{ active: boolean }

Benchmark sessions:

EndpointChange
GET /benchmark/sessions?templateV2Id=Filter sessions for a template
POST /benchmark/sessionsAccepts 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.


Admin UI

Page

packages/merch-admin/client/src/app/pages/templates-page.tsx — route /templates (Merch Admin sidebar → Templates).

Layout:

  1. Sidebar — template list (thumbnail = display → template → first reference), status / Active badge, + create, deep link ?template=<id>.
  2. Editor — single scrollable page: info → assets → production stages → bake → benchmark → activate / delete.

Text fields auto-save with a debounced PATCH; image/mode/style changes PATCH immediately. Uploads use uploadProcessedImage → S3 under merch/templates-v2/<id>/….

Shared production UI

Extracted from Design Requests into
packages/merch-admin/client/src/components/design-request/production-shared.tsx:

  • PromptCard, TemplateCandidateCard, ImageGroup, SectionLabel
  • CandidateHistoryModal, CandidateDetailsModal
  • PROD_TEAL accent

Design Requests and Template Studio both import these; behavior of Design Requests is unchanged.

Benchmark wiring

packages/merch-benchmark/client/src/pages/dashboard-page.tsx:

  • ?templateV2=<id> loads /templates-v2/:id/benchmark-prefill
  • Sets templateV2Id on session create
  • Banner links back to /templates?template=<id>

Consumers (after v1 removal)

Talent / seller template list

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).

Design Requests

Template-sourced requests resolve chosen template ids via getMerchTemplateV2ById in:

  • getDesignRequestBenchmarkPrefill
  • generateDesignRequestArt (reference images)

Admin Design Requests dashboard template lookup uses GET /templates-v2.

Yourself-store flow

applyYourselfDesign in packages/merch/srv/src/seller-store.ts loads the template with getMerchTemplateV2ById, then:

  1. Bakes talent into templateImageUrl with replaceTalentPrompt
  2. Stores replaceFanPrompt on the design config for per-fan generation

Phase-1 consumer parity assumes castMode = "both"; talent-only / fan-only store behavior is a later refinement.

Migration from v1

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.


Relationship to Design Requests

Design Requests (ZLY-1420)Template Studio (v2)
Who starts itTalent submits assetsAdmin builds in studio
DeliverableStore/design for that talentReusable template library
Tablemerch_design_requestmerch_template_v2
SharedGeneration core + production UI componentsSame
Not sharedEntity, APIs, “send to user”Activate into picker

Key file map

AreaPath
Schemapackages/db/src/schema/merchTemplateV2Table.ts
Accesspackages/db/src/access/merch/merch-template-v2.ts
Migrationspackages/db/drizzle/0191_merch_template_v2.sql, 0192_merch_template_v1_to_v2.sql
Generation corepackages/merch/srv/src/generation-core.ts
Business logicpackages/merch/srv/src/template-v2.ts
Admin APIapps/zooly-app/app/api/merch/admin/templates-v2/
Seller listapps/zooly-app/app/api/merch/seller/templates/route.ts
Admin pagepackages/merch-admin/client/src/app/pages/templates-page.tsx
Shared UIpackages/merch-admin/client/src/components/design-request/production-shared.tsx
Benchmark prefill UIpackages/merch-benchmark/client/src/pages/dashboard-page.tsx
Dummy asset (local)assets/dummy.jpeg