---
path: /studio/generation/curated-cards
surface: studio
domain: generation
auth: signed-in + creator (proxy-gated; not anonymous, not search-indexed)
source: apps/oshun/web/src/app/studio/generation/curated-cards/page.tsx
status: walked
last_walked: '2026-06-03 automated runtime probe (Playwright headless) — prod build + mock BFF; render, HTTP 200, server-rendered <h1>, 0 page errors verified. Evidence: WALKTHROUGH/results/walkthrough-runtime-probe-2026-06-03.md'
---

# Studio · Generation · Curated Cards

## Purpose

The approved curated creator-card generation surface (§24.6). Renders the six
server-authoritative curated cards (illustration, narration, ambient-audio,
explainer, caption-dub, accessibility-pass) from
`GET /v1/isis/curated-cards/catalog` and submits each through
`POST /v1/isis/curated-cards/generate`. The backend runs entitlement +
raw-graph-leak audit + per-card field validation, then enqueues generation; the
surface never fabricates a success or a cost — it reports entitlement-denied,
invalid-inputs, provider-unconfigured (503), and accepted outcomes honestly.

## Entry points

- **Direct URL / bookmark** — `/studio/generation/curated-cards` (proxy gates to
  authed creators; the page sets `robots: noindex/nofollow/noarchive/nosnippet`
  so a crawler that discovers it via an inbound link does not index it)
- **Studio shell nav** — `active="studio"` sidebar entry (shortcut `6`), via the
  generation sub-area
- **Sibling generation surfaces** — cross-linked from
  [`studio-generation-music.md`](./studio-generation-music.md),
  [`studio-generation-nyx-3d.md`](./studio-generation-nyx-3d.md),
  [`studio-generation-living-scene.md`](./studio-generation-living-scene.md)

## Layout regions

- **Header**: shell header (`ShellLayout active="studio"`)
- **Main**: `<section data-curated-cards-studio>` containing
  - **`<h1>Curated Cards</h1>`** — page heading, rendered in all three states
    (error → inside `data-curated-cards-error`; loading → inside
    `data-curated-cards-loading`; populated → top of the studio section), plus a
    lede `<p>` "Generate from the six approved curated creator cards. Every
    submission is entitlement-checked and provenance-tracked server-side."
  - **Consent panel** (`.panel`, `data-curated-cards-consent`) — a single
    rights/terms confirmation checkbox at the top, shared by every card
  - **Card list** — one wrapper (`data-curated-card-wrapper="<cardId>"`) per
    catalog entry, each rendering the kind-specific card (via `CardShell`) and,
    below it, a per-card status line (`data-curated-card-status="<cardId>"`)
    once a submission has been attempted
- **Footer / sticky bottom**: _None._ (shell footer only)

## States

- [ ] **Loading** — `data-curated-cards-loading` panel "Loading approved cards…"
      while `GET /v1/isis/curated-cards/catalog` is in flight; no card list yet
- [ ] **Empty** — N/A: the catalog is server-authoritative; an empty `cards[]`
      simply renders the consent panel with no cards (the approved set is
      expected to be non-empty in a configured deployment)
- [ ] **Populated** — consent panel + up to six cards, each in its
      `data-card-status="available"` shell with the kind-specific inputs panel,
      preflight cost/latency meta, provenance line, and Generate control
- [ ] **Per-card entitlement-denied** — a card whose `checkCardEntitlement`
      verdict is `deny` renders `data-card-status="entitlement-denied"` with
      "This card is not available for your tier or tenant." + the reason; other
      cards stay interactive
- [ ] **Per-card feature-unavailable** — kill-switch / frozen workflow class
      renders `data-card-status="feature-unavailable"` ("temporarily
      unavailable. Underlying workflow is frozen.")
- [ ] **Submit accepted** — status line "Accepted — queued for generation."
- [ ] **Submit rejected (provider unconfigured)** — 503 → "Generation provider
      is not configured for this deployment yet."
- [ ] **Submit rejected (entitlement)** — 403 → "Not available: <message>"
- [ ] **Submit rejected (validation)** — 422 → "Please fix the highlighted
      fields: <message>"
- [ ] **Submit rejected (not in catalog)** — 404 → "This card is not in the
      approved catalog."
- [ ] **Error (catalog load failed)** — `data-curated-cards-error` panel
      "Curated cards are unavailable right now: <message>"
- [ ] **Offline** — catalog GET fails → error panel; submit POSTs surface the
      generic "Submission failed." when the network is unreachable
- [ ] **Gated** — non-creator tiers are blocked by the proxy before the page
      renders (route-level), and per-card by `checkCardEntitlement` at the
      component layer
- [ ] **Standalone PWA** — renders within the shell; no URL-bar dependency

## Interactions

- [ ] **Rights/terms consent** (checkbox, `data-curated-cards-consent-checkbox`)
  - Function: toggles `consentAccepted`; builds a `ConsentConfirmation`
    (`disclosureVersion: 1`, `acknowledgedAtUnixSeconds` stamped on accept) that
    every card's preflight consumes — Generate stays blocked until accepted
  - Keyboard: Space toggles; in tab order before the cards
  - Screen reader: announces the label "I confirm I have rights to the inputs
    and accept the curated-generation terms."
  - Telemetry: none direct (state feeds preflight `canProceed`)

Each card embeds the shared `CardShell`, which owns these controls (one per
card; nested under the card wrapper):

- [ ] **Kind-specific inputs panel** (per card)
  - **illustration** — `subject` (input), `stylePreset` (select), `aspect`
    (select), `locale` (select)
  - **narration** — `text` (textarea), `voiceProfileId` (select), `locale`
    (select), `prosody` (select)
  - **ambient-audio** — `mood`, `tempoBpm`, `durationSeconds`, `loopable`
  - **explainer** — `subject`, `claimId`, `sophiaEvidencePinId`, `stylePreset`,
    `aspect` (Sophia evidence pin carries grounding)
  - **caption-dub** — `sourceAssetId`, `targetLocales` (operates on an existing
    asset, not generate-from-scratch)
  - **accessibility-pass** — `sourceAssetId`, `passes` (checkbox group)
- [ ] **Direction** (textarea, `direction-<cardKind>`) — free-text steer for
      regenerate; placeholder "e.g. softer palette, slower pace"
- [ ] **Generate** (button, `data-card-action="generate"`)
  - Function: calls `submit(spec, inputs)` →
    `POST /v1/isis/curated-cards/generate` with
    `{ cardId, inputs, supportedStylePresets, tenantSupportedLocales }`
  - Disabled when: `!preflight.canProceed || !inputsValid || !canSubmit || busy`
    → blocking reasons listed above the buttons; label switches to "Generating…"
    while `busy`
  - Telemetry: none client-side beyond the status line
- [ ] **Regenerate with direction** (button, `data-card-action="regenerate"`) —
      disabled until at least one variant exists
- [ ] **Variant compare** (checkbox per variant,
      `data-variant-action="toggle-compare"`) — up to 4; renders a side-by-side
      compare grid (`data-testid="card-variant-compare"`) once ≥2 selected
- [ ] **Send to editorial** (button, `data-variant-action="send-to-editorial"`)
      — per non-taken-down variant; wired to `onSendToEditorial` (no-op here —
      this surface passes `() => undefined`, so the studio editorial routing is
      not yet connected from this page)
- [ ] **Takedown** (button, `data-variant-action="takedown"`) — per variant;
      `onTakedown` is `() => undefined` here (same caveat)

## Data & contracts

- **Reads**: `GET /v1/isis/curated-cards/catalog` →
  `{ cards: CatalogEntry[], creatorTier }` where each entry is
  `{ spec: CardSpec, provenance: ProvenancePreview }`
- **Writes**: `POST /v1/isis/curated-cards/generate` —
  `{ cardId, inputs: CuratedCardInputs, supportedStylePresets, tenantSupportedLocales }`;
  fail-closed 503 until a provider is configured
- **Realtime**: _None._
- **Caching**: client fetch on mount (no SWR); SSR shell only
- **Auth/role check**: proxy route gate (creator) + `checkCardEntitlement`
  (component layer) + server-side entitlement/leak-audit on generate
- **Contracts**: `@oshun-web/isis-curated-cards` — `CardSpec`,
  `CuratedCardInputs`, `ConsentConfirmation`, `ProvenancePreview`,
  `PENDING_COST_COEFFICIENTS`, `computePreflight`, `checkCardEntitlement`,
  `gracefulStatus`

## Cross-references

- Parent: [`../studio-overview.md`](../studio-overview.md)
- Siblings: [`studio-generation-music.md`](./studio-generation-music.md),
  [`studio-generation-nyx-3d.md`](./studio-generation-nyx-3d.md),
  [`studio-generation-living-scene.md`](./studio-generation-living-scene.md),
  [`studio-generation-gallery.md`](./studio-generation-gallery.md)
- Component sources:
  `apps/oshun/web/src/components/studio/generation/CuratedCardsStudio.tsx`,
  `CardShell.tsx`, `IllustrationCard.tsx`, `NarrationCard.tsx`,
  `AmbientAudioCard.tsx`, `ExplainerCard.tsx`, `CaptionDubCard.tsx`,
  `AccessibilityPassCard.tsx`
- Journeys: `journeys/atelier-image-to-library.md` (illustration path),
  `journeys/sophia-grounded-answer.md` (explainer evidence pin)

## Open questions / known gaps

- [ ] `onSendToEditorial` / `onTakedown` are wired to `() => undefined` on this
      page — the editorial-routing and takedown call sites exist in `CardShell`
      but are not connected from `CuratedCardsStudio`. Confirm whether these are
      intended to route to `/operator/studio` once the BFF endpoints land.
- [ ] `tenantId` is hard-coded to `''` in `renderCard` — confirm the tenant is
      meant to be resolved from the session/proxy header rather than left blank.
- [ ] Caption-dub and accessibility-pass operate on a pre-existing
      `sourceAssetId`; the backend providers are now REAL (2026-06-10):
      caption-dub resolves ElevenLabs Dubbing from
      `OSHUN_DUBBING_ELEVENLABS_API_KEY`/`OSHUN_ELEVENLABS_API_KEY` + the
      `OSHUN_ASSET_STORE_*` object store (dub per target language → persist to
      the store → signed URL; every dub carries the `cloned-voice-used`
      human-review trigger at the Isis gate), and accessibility-pass runs the
      in-repo WCAG contrast pass + optional `OSHUN_A11Y_ML_*` vision
      alt-text/Whisper transcript service (see `.env.example` +
      `V1_DEPLOYMENT_REQUIREMENTS.md` §8/§9). Still open on THIS surface: an
      asset-picker so the creator can choose a `sourceAssetId` from their
      library instead of typing an object key.
