# Picker catalogs

> Build Nodaro's pickers in your own app with @nodaro/prompts, turn selections into prompt clauses, translate labels, show pictures or read catalogs over the API.

Source: https://nodaro.ai/docs/developers/picker-catalogs

A **picker catalog** is the data behind one of Nodaro's pickers, the Creative Controls nodes such as [Mood](https://nodaro.ai/docs/nodes/creative-controls/mood), [Lens](https://nodaro.ai/docs/nodes/creative-controls/lens), [Framing](https://nodaro.ai/docs/nodes/creative-controls/framing) and [Person](https://nodaro.ai/docs/nodes/creative-controls/person). It holds the picker's options, the prompt text each option adds, its categories and the keys of its translations. The catalogs ship as plain data in npm packages, so you can build the same pickers in your own app, in your own style, and assemble exactly the prompts Nodaro assembles.

A picker never calls a model. It adds a descriptive clause to the prompt of the node it feeds; see [Creative controls](https://nodaro.ai/docs/guides/creative-controls).

## Import the package or call the API

| Your app | Use |
| --- | --- |
| Can bundle an npm package | Import the catalogs from `@nodaro/prompts`. They are typed, work offline and need no API call. |
| Cannot bundle a package, such as an AI agent over MCP | Read the same data from the discovery surface: the REST endpoint `GET /v1/picker-catalogs`, `client.pickerCatalogs` in the [SDK](https://nodaro.ai/docs/developers/sdk), `nodaro pickers` in the [CLI](https://nodaro.ai/docs/developers/cli/commands#pickers), or the MCP tool `get_picker_catalog`. |

Prefer the package when you can: it saves a round trip for data that changes only with a release.

```bash
npm install @nodaro/prompts @nodaro/shared @nodaro/sdk
```

`@nodaro/prompts` holds the catalogs and the prompt functions, `@nodaro/shared` holds the translations, and `@nodaro/sdk` runs the generation.

## The registry

```ts

```

| Export | What it returns |
| --- | --- |
| `PICKER_CATALOGS` | Every picker catalog: 28 single-dimension and 11 multi-dimension pickers. |
| `getPickerCatalog(nodeTypeOrCatalogId)` | One catalog, by node type (such as `"mood"`) or by catalog id. |
| `listPickerCatalogs()` | All of them. |

The catalog types:

```ts
interface PickerOption {
id: string
label: string            // English display text; translate it with the catalogId (see below)
description?: string
category?: string        // group id, matching categoryOrder and categoryLabels
promptHint: string       // the clause this option adds ("" for a no-op option such as "auto")
term: string             // the short professional term Compact mode adds ("" for a no-op option)
icon?: string            // reserved; pictures are served separately (see "Pictures")
}

interface PickerDimension {  // multi-dimension pickers, and the secondary fields of a single-dimension picker
field: string              // for example "shotSize"
label: string
options: readonly PickerOption[]
}

interface PickerCatalog {
nodeType: string           // "mood", "framing", ...
label: string
catalogId: string          // the key of the translations
kind: "single" | "multi"
valueField?: string        // single: the field a selection writes
defaultValue?: string
categoryOrder?: readonly string[]
categoryLabels?: Readonly<Record<string, string>>
options?: readonly PickerOption[]        // single-dimension pickers
fields?: readonly string[]               // multi-dimension pickers: the dimension fields
dimensions?: readonly PickerDimension[]  // multi-dimension pickers, and secondary fields
}
```

Render `label` and send `term` or `promptHint` to the model. Never derive one from another.

## Build a single-dimension picker

A single-dimension picker, such as Mood, is one choice from a flat list, optionally grouped. `options` carries everything you need to draw the grid.

```tsx

const client = createClient({
baseUrl: "https://app.nodaro.ai",
auth: new StaticTokenAuth(process.env.NODARO_ACCESS_TOKEN!),
})
const mood = getPickerCatalog("mood")! // { nodeType: "mood", valueField: "mood", options, categoryOrder, categoryLabels }

// 1. Render your own tile grid, grouped by category
function MoodPicker({ value, onChange }: { value?: string; onChange: (id: string) => void }) {
return (mood.categoryOrder ?? [undefined]).map((cat) => (
<section key={cat ?? "all"}>
{cat && <h4>{mood.categoryLabels?.[cat]}</h4>}
{mood.options!
.filter((o) => !cat || o.category === cat)
.map((o) => (
<button key={o.id} aria-pressed={value === o.id} title={o.description} onClick={() => onChange(o.id)}>
{o.label}
</button>
))}
</section>
))
}

// 2. Selection, then prompt clause, then generation
const selected = "serene"
const clause = getParameterPromptHint({ type: "mood", data: { mood: selected } })
// or: mood.options!.find((o) => o.id === selected)!.promptHint

const prompt = ["a portrait of a woman", clause].filter(Boolean).join(", ")
const result = await client.nodes.runAndWait("generate-image", { prompt })
console.log(result.imageUrl)
```

### Secondary parameters of Transition, Character FX and Character Motion

Three single-dimension pickers carry extra fields beside the main choice. [Transition](https://nodaro.ai/docs/nodes/creative-controls/transition) and [Character FX](https://nodaro.ai/docs/nodes/creative-controls/character-fx) each have `position`, `duration` and `intensity`. [Character Motion](https://nodaro.ai/docs/nodes/creative-controls/character-motion) has `position` and `pace`.

Those fields are catalogs too, with the same rows as every other option, each list led by a no-op `auto`. The catalog exposes them as `dimensions`, **in addition to** `options`. Render the main picker from `options` and the dropdowns from `dimensions`; a client that sends only ids never writes the timing clause itself.

```ts
const fx = getPickerCatalog("character-fx")!
fx.options     // the effects of the main picker
fx.dimensions  // [{ field: "position", ... }, { field: "duration", ... }, { field: "intensity", ... }]

// Each dimension's rows are also exported directly:
//   TRANSITION_POSITIONS / TRANSITION_DURATIONS / TRANSITION_INTENSITIES
//   CHARACTER_FX_POSITIONS / CHARACTER_FX_DURATIONS / CHARACTER_FX_INTENSITIES
//   CHARACTER_MOTION_POSITIONS / CHARACTER_MOTION_PACES
```

Transition and Character FX share the same ids: `start`, `middle`, `end` and `full`; `instant`, `short`, `medium` and `long`; `subtle`, `natural`, `dynamic` and `crazy`. They do **not** share the wording: a transition occurs and spans the clip, while an effect appears and persists. Always read the rows from the node's own catalog. Character Motion shares the position ids and adds its own pace ids, `slow-motion`, `slow`, `natural`, `fast` and `explosive`, with its own wording.

## Build a multi-dimension picker

A multi-dimension picker sets several independent fields at once. Framing, for example, sets the shot size, the angle, the coverage, the composition and the vantage. Its catalog has one `{ field, label, options }` entry per field in `dimensions`.

```tsx

const framing = getPickerCatalog("framing")!
// framing.dimensions = [
//   { field: "shotSize",    label: "Shot Size",   options: [{ id: "close-up", ... }, ...] },
//   { field: "angle",       label: "Angle",       options: [{ id: "low-angle", ... }, ...] },
//   { field: "coverage",    label: "Coverage",    options: [...] },
//   { field: "composition", label: "Composition", options: [...] },
//   { field: "vantage",     label: "Vantage",     options: [...] },
// ]

function FramingPicker({ value, onChange }: {
value: Record<string, string>
onChange: (v: Record<string, string>) => void
}) {
return framing.dimensions!.map((dim) => (
<section key={dim.field}>
<h4>{dim.label}</h4>
{dim.options.map((o) => (
<button
key={o.id}
aria-pressed={value[dim.field] === o.id}
title={o.description}
onClick={() => onChange({ ...value, [dim.field]: o.id })}
>
{o.label}
</button>
))}
</section>
))
}

// Selection to clause: getParameterPromptHint composes every field that is set
const value = { shotSize: "close-up", angle: "low-angle", composition: "rule-of-thirds" }
const clause = getParameterPromptHint({ type: "framing", data: value })
```

## Turn selections into a prompt

`getParameterPromptHint({ type, data })` turns any selection, single or multi, into its clause. It is the same function Nodaro runs on its servers, so your prompts match the editor's. Per-catalog builders exist too, such as `buildFramingHints(value)` for a multi-dimension picker and `getMoodPromptHint(id)` for one option.

```ts
// Several pickers, one prompt, one generation
const clauses = [
getParameterPromptHint({ type: "mood",    data: { mood: "serene" } }),
getParameterPromptHint({ type: "lens",    data: { lens: "portrait-85mm" } }),
getParameterPromptHint({ type: "framing", data: { shotSize: "close-up", angle: "eye-level" } }),
]
const prompt = ["a portrait of a woman in a garden", ...clauses].filter(Boolean).join(", ")
const result = await client.nodes.runAndWait("generate-image", { prompt, provider: "nano-banana-2" })
```

Add `hintMode: "compact"` to a picker's `data` to get its short `term` instead of the full clause. This is the **Compact** mode of the editor's **Prompt hint** toggle; see [Full or Compact](https://nodaro.ai/docs/nodes/creative-controls/mood#full-or-compact).

| Function | What it does |
| --- | --- |
| `getParameterPromptHint({ type, data })` | Turns any selection into its composed clause. |
| `build<Name>Hints(value)` | Builds the clause of one catalog, such as `buildFramingHints`. |
| `get<Name>PromptHint(id)` | Returns the clause of one option, such as `getMoodPromptHint`. |
| `PICKER_CATALOGS`, `getPickerCatalog`, `listPickerCatalogs` | The registry. |

## Translate the labels

Catalog labels are English. Translations for 12 locales ship in `@nodaro/shared`, keyed by each catalog's `catalogId`: `en`, `es`, `fr`, `de`, `pt-BR`, `ru`, `hi`, `ja`, `ko`, `zh-CN`, `he` and `ar`. The `promptHint` and `term` values stay English, because the models read them.

English needs no setup. For the other locales, register the translation bundles once at startup, then resolve labels synchronously, with English as the fallback:

```ts

// 1. Once at startup, in a Vite app: wire the lazily loaded locale bundles
registerSidecarLoaders(import.meta.glob("/node_modules/@nodaro/shared/src/i18n/*.*.ts"))

// 2. Before rendering a locale, load that catalog's bundle
await ensureLocaleCatalogLoaded(mood.catalogId, "fr")

// 3. Resolve a label, synchronously; English when a translation is missing or not loaded yet
const label = resolveLabel(mood.catalogId, option.id, option.label, "fr")
```

The bundles load on demand, so `registerSidecarLoaders` takes a map of loaders, such as the one Vite's `import.meta.glob` returns. Backends and tests can skip translation and use the English `label`.

## Show pictures

Your app can show the same pictures the editor shows on these pickers:

- **Photos** on [Person](https://nodaro.ai/docs/nodes/creative-controls/person), [Styling](https://nodaro.ai/docs/nodes/creative-controls/styling), [Held Prop](https://nodaro.ai/docs/nodes/creative-controls/held-prop), [Material](https://nodaro.ai/docs/nodes/creative-controls/material) and [Animal](https://nodaro.ai/docs/nodes/creative-controls/animal).
- **Art** on the music and voice pickers: Music Genre, Music Mood, Instrumentation, Voice Character and Voice Delivery.
- **Preview stills** on the look pickers, on Nodaro Cloud only. These are Style, Color / Look, Era / Period, Lens, Mood, Atmosphere, Composition Effect, Camera / Film, Framing, Lighting and Camera Motion.

**Over the API,** every option with a picture carries an absolute `imageUrl` on the install you asked, and an option without one has none. Person and Styling also return `sections`: their topics in order, each with a round picture. The URL rules follow in [Read the catalogs over the API](#read-the-catalogs-over-the-api).

**In the library,** `@nodaro/prompts` builds the same URLs from the same data:

- `pickerOptionImageUrl(catalog, field, id, { baseUrl })` for an option;
- `pickerSectionImageUrl(topicLabel, { baseUrl })` for a Person or Styling topic.

`baseUrl` is the Nodaro install that serves the files: the pictures belong to the install, not to the npm package. Pass `lookPreviews: true` only against Nodaro Cloud. For every other picker, draw your own visuals from `label`, `description` and `category`.

## Read the catalogs over the API

Both endpoints are public, need no token, and are cached for 5 minutes.

| Method | Path | What it returns |
| --- | --- | --- |
| `GET` | `/v1/picker-catalogs` | A directory of every picker: `nodeType`, `label`, `catalogId`, `kind`, `valueField` or `fields`, `optionCount`, and `imageCount`, the number of options with a picture. |
| `GET` | `/v1/picker-catalogs/:nodeType` | One picker's catalog. An unknown type answers `404 not_found`. |

`GET /v1/picker-catalogs/:nodeType` takes three query parameters. A bad value answers `400 validation_error`.

| Parameter | Values | What it does |
| --- | --- | --- |
| `detail` | `compact` (default) or `full` | `compact` returns `id`, `label`, `category`, `term`, `icon` and `imageUrl`. `full` adds each option's `description` and `promptHint`. |
| `category` | A category id | Filters a single-dimension picker to one category. |
| `field` | A field name | Returns one dimension of a multi-dimension picker, or one secondary field of Transition, Character FX or Character Motion. |

```bash
curl -s https://app.nodaro.ai/v1/picker-catalogs/mood | jq '.data.options[0]'
```

```json
{ "id": "happy", "label": "Happy", "category": "positive", "term": "happy expression",
"imageUrl": "https://cdn.nodaro.ai/cdn-cgi/image/width=480,format=auto,quality=80/images/710df65a-3c1e-485b-b8b7-29f7b3baf479.png" }
```

### The picture URLs

| Pickers | Picture |
| --- | --- |
| `person`, `styling`, `held-prop`, `material`, `animal` | A photo, WebP, up to 480 px wide. |
| `music-genre`, `music-mood`, `instrumentation`, `voice-character`, `voice-delivery` | A 3D emoji picture (WebP, 128 px) or a flag (WebP, 120 px wide). |
| Look pickers, such as `style`, `color-look`, `lens`, `framing`, `lighting`, `mood`, `camera-format` and `camera-motion` | On Nodaro Cloud, a 480 px still of the rendered preview from the Nodaro CDN; for `camera-motion`, a frame of the clip. A self-hosted install returns none. |

- **Host.** An install serves its own pictures under `/picker-art/`, so `imageUrl` uses its public address: `https://app.nodaro.ai` on Nodaro Cloud, or the `PUBLIC_URL` of a self-hosted install. Use each URL as given; never build one from an option id.
- **Caching.** File names carry a content hash, so a changed picture gets a new URL. The files are served with `Cache-Control: public, max-age=31536000, immutable` and `Access-Control-Allow-Origin: *`, so any origin can load them in an `<img>`, with `fetch` or on a canvas.
- **Topics.** `person` and `styling` also return `sections`, each with a `label`, the `fields` it groups and an optional round `imageUrl`.

```bash
curl -s https://app.nodaro.ai/v1/picker-catalogs/person | jq '.data.sections[0], .data.dimensions[0].options[0]'
```

```json
{ "label": "Identity", "fields": ["type", "age", "ethnicity", "regionalAesthetic"],
"imageUrl": "https://app.nodaro.ai/picker-art/character/sections/identity.2d5ec1a4.webp" }
{ "id": "man", "label": "Man", "term": "man",
"imageUrl": "https://app.nodaro.ai/picker-art/character/person/man.441363db.webp" }
```

### A deployment's curated catalogs

`GET /v1/catalogs` returns every catalog in one call, as a single flat shape. A deployment can curate its catalogs with vendored packs that replace, extend or deny options, and this endpoint reflects that curation. When the deployment registered packs, the response carries `curated: true` and the catalogs in `data`. With none, it carries `curated: false`, and you read the bundled catalogs per picker from `/v1/picker-catalogs/:nodeType`. The SDK method is `client.catalogs.list()`.

### Fill pickers from a description

`POST /v1/text-to-picker` chooses picker values from a free-text description of a scene or a shot. This is the AI Fill feature. It needs a token and is billed as an LLM call. It returns `pickerJson`, keyed by picker type, then dimension, then the chosen ids, and `gaps`, the described details no catalog option represents. The SDK method is `client.pickerCatalogs.analyzeText()`, and the CLI command is `nodaro pickers analyze`.

## Character Motion metadata

The options of the Character Motion catalog can carry `motion` metadata, at both detail levels. The metadata describes the move:

- Its requirements, its start and end poses, and the visibility and hands after the move.
- Whether it needs free hands, its kind, a fixed pace and its counterpart.
- Search aliases, and whether the id is retired and which id replaces it.

A missing field means unknown.

Keep retired ids working when you load saved workflows, and hide them from new choices. The type is `CharacterMotionMetadata`, exported by `@nodaro/prompts`.

## Frequently asked questions

### Do I need the Nodaro API to use the picker catalogs?

No. The catalogs, and the function that turns a selection into a prompt clause, ship in the @nodaro/prompts npm package, so your app can render the pickers and build prompts offline. The API endpoints exist for clients that cannot bundle the package.

### How do I turn a picker selection into prompt text?

Call getParameterPromptHint with the picker's node type and its data, such as the chosen mood id. Nodaro runs the same function on its servers, so your prompt matches the one the editor builds.

### Which languages do the picker labels support?

12 locales, English, Spanish, French, German, Brazilian Portuguese, Russian, Hindi, Japanese, Korean, Simplified Chinese, Hebrew and Arabic. The prompt text and the terms stay in English, because the models read them.

### Why do the look pickers have no pictures on my self-hosted install?

The previews of the look pickers, such as Mood and Lighting, come from the Nodaro CDN, and only Nodaro Cloud returns them. Every install serves the photos of Person, Styling, Held Prop, Material and Animal, and the music and voice art.

### How many picker catalogs are there?

39. There are 28 single-dimension pickers, such as Mood and Lens, and 11 multi-dimension pickers, such as Framing, Lighting and Person.
