Skip to content

Generation Families

A family is one user-facing generation product (e.g. “Custom Image”) that groups several workflows behind one set of controls. The user sees one product and picks a few controls; the server uses the combination of those picks to resolve exactly one workflow to run.

Brisket Create pages and Explore/Gallery overlays use one shared faceted form without PostHog rollout switches. Workflow contracts determine the controls and supported generation paths; incompatible inputs show an error and report sanitized context to Sentry. See Create Form Routing and Recovery for the current routing and failure policy.

A family is one product the user sees. Behind it sit several workflows. The user picks a few controls (axes), and the server uses those picks to choose exactly one workflow to run — so adding another variant is data, not code.

sequenceDiagram
    participant Fennec as Fennec (admin)
    participant Brain
    participant Brisket as Brisket (user)
    participant Sirloin

    Fennec->>Brain: Create family + axes, assign members with a full tuple
    Note over Brain: Enforces the rules (full tuple, uniqueness, axis-in-use)
    Brisket->>Sirloin: List catalog, render axes as controls
    Brisket->>Sirloin: Generate(family, axis picks)
    Note over Sirloin: Resolve: exact tuple match -> workflow
    Sirloin->>Brain: Run the resolved workflow
  • Family — a single user-facing product that groups several workflows behind one set of controls. The user sees one thing; their choices route to the right workflow underneath.
  • Axis — one control the user picks. The combination of all axis picks selects the workflow, not any single axis alone. Two kinds today: toggle (e.g. 18+, on/off) and dropdown (e.g. Model: likeness / realism / …).
  • Option — one selectable value of an axis (the Model axis has options likeness and realism).
  • Tuple (called variant in code/API) — the set of axis values that tags a member, e.g. {Model: likeness, 18+: off}. It’s how a workflow says “I’m the one for this combination.”
  • Member — a workflow added to a family and tagged with a tuple. A family has many members, and a workflow can be a member of more than one family at the same time — each membership carries its own full tuple.

How a pick becomes a workflow (resolution)

Section titled “How a pick becomes a workflow (resolution)”

Every member sets a value for every axis, and no two members share a tuple. So when the user submits their picks, the server (sirloin, at generate time) finds the one member whose tuple equals the pick exactly — a direct lookup that always lands on a single workflow. (Workflow name is the defensive final tie-break, but with unique full tuples there’s nothing to break.)

(brisket also runs a preview-only version of this for pricing; the authoritative match is server-side.)

The design is centralised and contract-driven: there is one definition of a family and its controls, and every page reads from it instead of hand-coding its own form.

  1. The family — and the resolved variant’s contract — is the single source of truth for what controls exist.
  2. The app turns that contract plus the family’s axes into a list of small, typed descriptors, one per control. Each descriptor carries what kind of control it is (toggle, dropdown, aspect-ratio, number, text, character, media), its key, its options, its default, and its current value.
  3. A shared renderer reads those descriptors and, through a field-type → component registry, decides which component to render for each control and where to place it.

So a generation page assembles itself from the data — adding a new control type, or a whole new product, is mostly a matter of the descriptors, with no bespoke form code per page. The same engine (useWorkflowForm producing descriptors rendered by a shared controls layer) drives the dev demo and production forms today. FacetedProductPage and the dialog hosts render those controls through FacetedCreateForm.

Most controls render generically from their descriptor through the field-type → component registry. Three keys are the exception: brisket gives them bespoke controls, so an author gets the right UI just by naming the key.

  • is_nsfw → the 18+ toggle. Gated behind KYC, and the rating always comes from the resolved workflow, never the toggle.
  • input_model → the model picker (“Choose your flavour”), docked in the prompt composer.
  • aspect_ratio → the ratio-pill format selector. Model it as a plain contract input (an enum of ratio strings), not an axis — the ratio never picks a different workflow.

Every other axis or input renders through the generic control with no page change. Fennec recognises the same three keys: type one as an axis key and it prefills the option values brisket expects (likeness / realism, the ratio list, 18+ / SFW).

  • A tuple must set every axis — no blanks. Each member pins one value per axis.
  • Each member’s tuple is unique within the family. A duplicate tuple is rejected (it would be ambiguous about which workflow to run).
  • The 18+ rating comes from the resolved workflow itself, never the client toggle — so the toggle can’t make an SFW workflow serve NSFW or vice-versa.
  • You can’t delete an axis (or option) a member still uses — re-tuple or unassign that member first. Brain blocks it; fennec surfaces a toast.
  • Each axis has a default value; together they form the family’s starting selection (kept SFW, not 18+).
  • A workflow can belong to several families at once — membership is many-to-many, stored per (workflow, family) with that family’s own full tuple. Removing it from one family leaves the others untouched; a family holds a given workflow at most once.

Availability (why a workflow can show as unavailable)

Section titled “Availability (why a workflow can show as unavailable)”

A member is only selectable if it’s a published workflow that actually produces media — its graph must expose an image or video output. A subworkflow, or a draft with no output mapping, has no media output, so it’s marked unavailable (“does not expose image or video outputs”). A family stays usable as long as at least one of its members is.

There’s a second way a member silently drops out: adding a new axis to a family makes every existing member incomplete until each is re-tupled to set a value for it. An unset axis matches no pick, so those members resolve to nothing and show unavailable (and the controls disable the options that lead nowhere). Re-tuple every member whenever you add an axis.

  • fennec (admin) — author families, axes, options, members. Entry: WorkflowFamiliesPage (Configs → Workflow Families) + the axes/members editors.

    Reading the family list and a single family is CREATOR+, so any staff role can open the page; creating, updating, deleting, and member assignment stay ADMIN-only (workflow-family.http.controller.ts). Sirloin reads its own API-key-authed surface (api/workflow-family) and is unaffected by these roles.

  • brain — stores families and the membership of each workflow (its per-family full tuple); enforces all the rules above. The WorkflowFamily model + the WorkflowFamilyMember join table (workflow name → family id, carrying the tuple) live here.

  • sirloin — at generate time, resolves the picked tuple → one workflow (ResolveVariant), and aggregates the generation catalog. (Ships with the consumer work.)

  • brisket — renders the axes as controls from the family’s contract and sends the pick. (Ships with the consumer work.)

  1. Go to Configs → Workflow Families → Create.
  2. Set the family key and display name.
  3. Add axes: each has a key, a display name, a control type (toggle or dropdown), options (for dropdowns), and a default value.
  4. Assign members: for each workflow, pick a value for every axis via the dropdowns. The workflow must be published and produce media to be selectable.

The rules are enforced on save: an incomplete tuple, a duplicate tuple, or deleting an in-use axis are all rejected with a toast.