@nodaro/prompts 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +105 -0
- package/README.md +27 -0
- package/dist/index.cjs +21240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3599 -0
- package/dist/index.d.ts +3599 -0
- package/dist/index.js +20854 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/action-fx.test.ts +155 -0
- package/src/__tests__/apply-picker-json.test.ts +50 -0
- package/src/__tests__/assemble-image-input.test.ts +248 -0
- package/src/__tests__/assemble-suno-input.test.ts +283 -0
- package/src/__tests__/brand-tokens.test.ts +81 -0
- package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
- package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
- package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
- package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
- package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
- package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
- package/src/__tests__/catalog-gapfill.test.ts +53 -0
- package/src/__tests__/character-convergence-image.test.ts +217 -0
- package/src/__tests__/character-default-role-image.test.ts +167 -0
- package/src/__tests__/character-default-role-video.test.ts +168 -0
- package/src/__tests__/character-default-role.test.ts +70 -0
- package/src/__tests__/character-fx.test.ts +200 -0
- package/src/__tests__/entity-prompts-location.test.ts +97 -0
- package/src/__tests__/entity-prompts.test.ts +240 -0
- package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
- package/src/__tests__/factory-presets.test.ts +1045 -0
- package/src/__tests__/factory-snippets.test.ts +68 -0
- package/src/__tests__/framing-multi.test.ts +71 -0
- package/src/__tests__/framing-vantage.test.ts +39 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
- package/src/__tests__/identity-lock.test.ts +46 -0
- package/src/__tests__/instrumentation.test.ts +68 -0
- package/src/__tests__/lighting-multi.test.ts +68 -0
- package/src/__tests__/location-convergence-image.test.ts +124 -0
- package/src/__tests__/mention-lock-flag.test.ts +499 -0
- package/src/__tests__/multi-picker-spec.test.ts +71 -0
- package/src/__tests__/music-genre.test.ts +103 -0
- package/src/__tests__/music-mood.test.ts +87 -0
- package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
- package/src/__tests__/parameter-registry-sync.test.ts +243 -0
- package/src/__tests__/person-age.test.ts +80 -0
- package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
- package/src/__tests__/person-body-axes.test.ts +67 -0
- package/src/__tests__/person-facial-geometry.test.ts +156 -0
- package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
- package/src/__tests__/person-sections.test.ts +18 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
- package/src/__tests__/picker-catalogs-project.test.ts +85 -0
- package/src/__tests__/picker-catalogs.test.ts +53 -0
- package/src/__tests__/picker-limits.test.ts +20 -0
- package/src/__tests__/prompt-builder-segments.test.ts +183 -0
- package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
- package/src/__tests__/prompt-builder.test.ts +1773 -0
- package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
- package/src/__tests__/resolve-prompt-append.test.ts +58 -0
- package/src/__tests__/resolve-prompt.test.ts +44 -0
- package/src/__tests__/role-picker-shared.test.ts +208 -0
- package/src/__tests__/seedance-2-inputs.test.ts +173 -0
- package/src/__tests__/seedance-extend.test.ts +44 -0
- package/src/__tests__/sound-aggregator.test.ts +350 -0
- package/src/__tests__/style-presets.test.ts +34 -0
- package/src/__tests__/temporal-multi.test.ts +74 -0
- package/src/__tests__/transitions.test.ts +213 -0
- package/src/__tests__/video-reference-features.test.ts +41 -0
- package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
- package/src/__tests__/video-reference-resolver.test.ts +233 -0
- package/src/__tests__/video-reference-roles.test.ts +96 -0
- package/src/__tests__/voice-character.test.ts +48 -0
- package/src/__tests__/voice-delivery.test.ts +41 -0
- package/src/__tests__/wardrobe.test.ts +25 -0
- package/src/action-fx.ts +255 -0
- package/src/aesthetic.ts +435 -0
- package/src/assemble-image-input.ts +236 -0
- package/src/assemble-suno-input.ts +147 -0
- package/src/atmosphere.ts +104 -0
- package/src/backdrop.ts +131 -0
- package/src/brand-tokens.ts +154 -0
- package/src/camera-format.ts +76 -0
- package/src/camera-motions.ts +615 -0
- package/src/character-fx.ts +274 -0
- package/src/color-look.ts +103 -0
- package/src/composition-effects.ts +67 -0
- package/src/entity-prompts.ts +231 -0
- package/src/era.ts +292 -0
- package/src/exposure-settings.ts +142 -0
- package/src/factory-presets/generate-image.ts +1644 -0
- package/src/factory-presets/generate-video.ts +1116 -0
- package/src/factory-presets/index.ts +46 -0
- package/src/factory-presets/lottie-overlay.ts +166 -0
- package/src/factory-presets/motion-graphics.ts +350 -0
- package/src/factory-presets/music.ts +734 -0
- package/src/factory-presets/sfx.ts +136 -0
- package/src/factory-presets/shared-image.ts +207 -0
- package/src/factory-presets/switchx.ts +65 -0
- package/src/factory-presets/text.ts +292 -0
- package/src/factory-presets/types.ts +51 -0
- package/src/factory-presets/video-edit.ts +136 -0
- package/src/factory-presets/voice.ts +172 -0
- package/src/factory-presets.ts +2 -0
- package/src/factory-snippets/catalog.ts +105 -0
- package/src/factory-snippets/index.ts +16 -0
- package/src/factory-snippets/types.ts +33 -0
- package/src/framing.ts +634 -0
- package/src/held-prop.ts +187 -0
- package/src/identity-lock.ts +213 -0
- package/src/index.ts +66 -0
- package/src/instrumentation.ts +337 -0
- package/src/lens.ts +59 -0
- package/src/lighting.ts +229 -0
- package/src/loop-subject.ts +276 -0
- package/src/materials.ts +184 -0
- package/src/mood.ts +186 -0
- package/src/music-genre.ts +662 -0
- package/src/music-mood.ts +137 -0
- package/src/object-asset-presets.ts +81 -0
- package/src/parameter-prompt-hint.ts +281 -0
- package/src/person.ts +1368 -0
- package/src/photo-genre.ts +151 -0
- package/src/photographer.ts +612 -0
- package/src/picker-analyzer-registry.ts +374 -0
- package/src/picker-catalogs.ts +858 -0
- package/src/pose.ts +246 -0
- package/src/post-process-effects.ts +94 -0
- package/src/prompt-builder-structured-fields.ts +116 -0
- package/src/prompt-builder.ts +2964 -0
- package/src/prompt-templates.ts +50 -0
- package/src/prompt-wizard-categories.ts +334 -0
- package/src/provider-prompt-doctrine.ts +85 -0
- package/src/render-quality.ts +89 -0
- package/src/resolve-prompt.ts +120 -0
- package/src/seedance-2-inputs.ts +100 -0
- package/src/setting.ts +130 -0
- package/src/sound-aggregator.ts +241 -0
- package/src/style-presets.ts +162 -0
- package/src/style.ts +99 -0
- package/src/styling.ts +585 -0
- package/src/temporal.ts +151 -0
- package/src/transitions.ts +333 -0
- package/src/video-reference-resolver.ts +823 -0
- package/src/voice-character.ts +237 -0
- package/src/voice-delivery.ts +142 -0
- package/src/wardrobe.ts +179 -0
|
@@ -0,0 +1,858 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public, discoverable registry of the parameter-picker catalogs.
|
|
3
|
+
*
|
|
4
|
+
* This is the pure-data mirror of the frontend picker registry
|
|
5
|
+
* (`frontend/src/lib/parameter-picker-registry.tsx`). It contains NO React —
|
|
6
|
+
* just the catalog metadata + flattened options — so it can be consumed by
|
|
7
|
+
* the backend, the public SDK, docs tooling, and the editor alike from one
|
|
8
|
+
* source of truth.
|
|
9
|
+
*
|
|
10
|
+
* Two kinds of entry (mirroring the frontend):
|
|
11
|
+
* - "single": one value field whose value is a string id chosen from a
|
|
12
|
+
* catalog. `options` carries the full flattened catalog (id/label/
|
|
13
|
+
* description/category/promptHint), so a consumer can render a picker or
|
|
14
|
+
* resolve an id → prompt fragment without importing the heavy frontend
|
|
15
|
+
* registry.
|
|
16
|
+
* - "multi": several value fields (e.g. Framing.shotSize + .angle + …).
|
|
17
|
+
* Structured / multi-dimensional. There is no single catalog to flatten,
|
|
18
|
+
* so instead of top-level `options` each multi entry carries a
|
|
19
|
+
* `dimensions` array — one self-describing entry per field
|
|
20
|
+
* ({ field, label, options }) in `fields` order — so a consumer can render
|
|
21
|
+
* a per-field picker or resolve any field's id → prompt fragment without
|
|
22
|
+
* importing the heavy frontend registry.
|
|
23
|
+
*
|
|
24
|
+
* A drift-guard test (`frontend/src/lib/__tests__/picker-catalogs-sync.test.ts`)
|
|
25
|
+
* asserts parity between this registry and the frontend one so the two cannot
|
|
26
|
+
* silently diverge.
|
|
27
|
+
*
|
|
28
|
+
* NOTE on `promptHint` for the four Object-entity catalogs (animal / vehicle /
|
|
29
|
+
* weapon / furniture): those catalog entries do NOT carry a `promptHint`
|
|
30
|
+
* field — at runtime `getParameterPromptHint` synthesizes the fragment from
|
|
31
|
+
* `label` + `description` ("featuring a golden retriever, …"). We reproduce the
|
|
32
|
+
* exact same phrasing here so each option's `promptHint` is non-empty AND
|
|
33
|
+
* matches what actually gets injected downstream.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { SETTINGS, SETTING_CATEGORY_LABELS } from "./setting.js"
|
|
37
|
+
import { ATMOSPHERES } from "./atmosphere.js"
|
|
38
|
+
import { STYLES } from "./style.js"
|
|
39
|
+
import { COLOR_LOOKS, COLOR_LOOK_CATEGORY_LABELS, COLOR_LOOK_CATEGORY_ORDER } from "./color-look.js"
|
|
40
|
+
import { MOODS, MOOD_CATEGORY_LABELS, MOOD_CATEGORY_ORDER } from "./mood.js"
|
|
41
|
+
import { PHOTOGRAPHERS, PHOTOGRAPHER_CATEGORY_LABELS, PHOTOGRAPHER_CATEGORY_ORDER } from "./photographer.js"
|
|
42
|
+
import { AESTHETICS, AESTHETIC_CATEGORY_LABELS, AESTHETIC_CATEGORY_ORDER } from "./aesthetic.js"
|
|
43
|
+
import { ERAS, ERA_CATEGORY_LABELS, ERA_CATEGORY_ORDER } from "./era.js"
|
|
44
|
+
import { PHOTO_GENRES, PHOTO_GENRE_CATEGORY_LABELS, PHOTO_GENRE_CATEGORY_ORDER } from "./photo-genre.js"
|
|
45
|
+
import { BACKDROPS, BACKDROP_CATEGORY_LABELS, BACKDROP_CATEGORY_ORDER } from "./backdrop.js"
|
|
46
|
+
import { RENDER_QUALITIES } from "./render-quality.js"
|
|
47
|
+
import { COMPOSITION_EFFECTS } from "./composition-effects.js"
|
|
48
|
+
import { ACTION_FX, ACTION_FX_CATEGORY_LABELS, ACTION_FX_CATEGORY_ORDER } from "./action-fx.js"
|
|
49
|
+
import { LOOP_SUBJECTS, LOOP_SUBJECT_CATEGORY_LABELS, LOOP_SUBJECT_CATEGORY_ORDER } from "./loop-subject.js"
|
|
50
|
+
import { POST_PROCESS_EFFECTS } from "./post-process-effects.js"
|
|
51
|
+
import { CAMERA_MOTIONS, CAMERA_MOTION_CATEGORY_LABELS, CAMERA_MOTION_CATEGORY_ORDER } from "./camera-motions.js"
|
|
52
|
+
import { LENSES } from "./lens.js"
|
|
53
|
+
import { CAMERA_FORMATS } from "./camera-format.js"
|
|
54
|
+
import { TRANSITIONS, TRANSITION_CATEGORY_LABELS, TRANSITION_CATEGORY_ORDER } from "./transitions.js"
|
|
55
|
+
import { CHARACTER_FX, CHARACTER_FX_CATEGORY_LABELS, CHARACTER_FX_CATEGORY_ORDER } from "./character-fx.js"
|
|
56
|
+
import { POSES, POSE_CATEGORY_LABELS, POSE_CATEGORY_ORDER } from "./pose.js"
|
|
57
|
+
import { MATERIALS, MATERIAL_CATEGORY_LABELS, MATERIAL_CATEGORY_ORDER } from "./materials.js"
|
|
58
|
+
import { ANIMALS, ANIMAL_SUBCATEGORY_LABELS, ANIMAL_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
59
|
+
import { VEHICLES, VEHICLE_SUBCATEGORY_LABELS, VEHICLE_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
60
|
+
import { WEAPONS, WEAPON_SUBCATEGORY_LABELS, WEAPON_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
61
|
+
import { FURNITURE, FURNITURE_SUBCATEGORY_LABELS, FURNITURE_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
62
|
+
import { HELD_PROPS, HELD_PROP_CATEGORY_LABELS, HELD_PROP_CATEGORY_ORDER } from "./held-prop.js"
|
|
63
|
+
import { FRAMINGS, FRAMING_FIELD_BY_CATEGORY, FRAMING_CATEGORY_LABELS } from "./framing.js"
|
|
64
|
+
import { LIGHTINGS, LIGHTING_FIELD_BY_CATEGORY, LIGHTING_CATEGORY_ORDER, LIGHTING_CATEGORY_LABELS } from "./lighting.js"
|
|
65
|
+
import { TEMPORALS, TEMPORAL_FIELD_BY_CATEGORY, TEMPORAL_CATEGORY_LABELS } from "./temporal.js"
|
|
66
|
+
import { EXPOSURE_SETTINGS, EXPOSURE_FIELD_BY_CATEGORY, EXPOSURE_CATEGORY_LABELS } from "./exposure-settings.js"
|
|
67
|
+
import { PEOPLE, PERSON_FIELD_BY_DIMENSION, PERSON_DIMENSION_ORDER, PERSON_DIMENSION_LABELS } from "./person.js"
|
|
68
|
+
import { STYLINGS, STYLING_FIELD_BY_DIMENSION, STYLING_DIMENSION_ORDER, STYLING_DIMENSION_LABELS } from "./styling.js"
|
|
69
|
+
import { MUSIC_GENRES, MUSIC_ERAS } from "./music-genre.js"
|
|
70
|
+
import { MUSIC_ENERGIES, MUSIC_EMOTIONS, MUSIC_VIBES } from "./music-mood.js"
|
|
71
|
+
import { INSTRUMENTS, PRODUCTION_STYLES, VOCAL_PRESENCE, SINGING_STYLES } from "./instrumentation.js"
|
|
72
|
+
import { VOICE_AGES, VOICE_GENDERS, VOICE_LANGUAGES, VOICE_ACCENTS, VOICE_TIMBRES } from "./voice-character.js"
|
|
73
|
+
import { VOICE_PACES, VOICE_EMOTIONS, VOICE_ARCHETYPES } from "./voice-delivery.js"
|
|
74
|
+
|
|
75
|
+
export interface PickerOption {
|
|
76
|
+
readonly id: string
|
|
77
|
+
readonly label: string
|
|
78
|
+
readonly description?: string
|
|
79
|
+
/** The group id (matches `categoryOrder` / `categoryLabels`). */
|
|
80
|
+
readonly category?: string
|
|
81
|
+
readonly promptHint: string
|
|
82
|
+
/** Only present if the source catalog entry already carries a data icon/emoji/thumbnail field. */
|
|
83
|
+
readonly icon?: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A self-describing dimension of a multi-dim picker: one value field plus the
|
|
88
|
+
* full option list valid for that field. `dimensions[i].field` mirrors the
|
|
89
|
+
* frontend picker's `fields[i]` exactly (same order), so a consumer can render
|
|
90
|
+
* a per-field picker or resolve an id → prompt fragment for any dimension
|
|
91
|
+
* without importing the heavy frontend registry.
|
|
92
|
+
*/
|
|
93
|
+
export interface PickerDimension {
|
|
94
|
+
/** The node-data field this dimension writes to (e.g. "shotSize"). */
|
|
95
|
+
readonly field: string
|
|
96
|
+
/** Human-readable label for the dimension (e.g. "Shot Size"). */
|
|
97
|
+
readonly label: string
|
|
98
|
+
/** Flattened options valid for this field. */
|
|
99
|
+
readonly options: readonly PickerOption[]
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface PickerCatalog {
|
|
103
|
+
readonly nodeType: string
|
|
104
|
+
readonly label: string
|
|
105
|
+
/** The i18n catalog id (mirrors the frontend entry's `catalogId`). */
|
|
106
|
+
readonly catalogId: string
|
|
107
|
+
readonly kind: "single" | "multi"
|
|
108
|
+
/** single only — the node-data field the chosen id is written to. */
|
|
109
|
+
readonly valueField?: string
|
|
110
|
+
/** single only — the catalog id selected by default. */
|
|
111
|
+
readonly defaultValue?: string
|
|
112
|
+
readonly categoryOrder?: readonly string[]
|
|
113
|
+
readonly categoryLabels?: Readonly<Record<string, string>>
|
|
114
|
+
/** single-dim: flattened catalog options. */
|
|
115
|
+
readonly options?: readonly PickerOption[]
|
|
116
|
+
/** multi-dim: the dimension keys (no single catalog to flatten). */
|
|
117
|
+
readonly fields?: readonly string[]
|
|
118
|
+
/** multi-dim: one self-describing entry per dimension field, in `fields` order. */
|
|
119
|
+
readonly dimensions?: readonly PickerDimension[]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Minimal shape every single-dim catalog entry satisfies. */
|
|
123
|
+
interface BaseCatalogEntry {
|
|
124
|
+
readonly id: string
|
|
125
|
+
readonly label: string
|
|
126
|
+
readonly description: string
|
|
127
|
+
readonly promptHint: string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Flatten a single-dim catalog array into `PickerOption`s.
|
|
132
|
+
*
|
|
133
|
+
* `categoryField` mirrors the field the frontend's `mapCat(ARR, "<field>")`
|
|
134
|
+
* used — `"category"`, `"subcategory"`, or omitted (no grouping). None of the
|
|
135
|
+
* catalogs carry an entry-level icon/emoji/thumbnail field, so `icon` is never
|
|
136
|
+
* emitted (icons are derived from the id via render helpers, which are React
|
|
137
|
+
* and intentionally excluded).
|
|
138
|
+
*/
|
|
139
|
+
function toOptions<T extends BaseCatalogEntry>(
|
|
140
|
+
arr: ReadonlyArray<T>,
|
|
141
|
+
categoryField?: keyof T,
|
|
142
|
+
): ReadonlyArray<PickerOption> {
|
|
143
|
+
return arr.map((e) => {
|
|
144
|
+
const opt: { -readonly [K in keyof PickerOption]?: PickerOption[K] } = {
|
|
145
|
+
id: e.id,
|
|
146
|
+
label: e.label,
|
|
147
|
+
promptHint: e.promptHint,
|
|
148
|
+
}
|
|
149
|
+
if (e.description) opt.description = e.description
|
|
150
|
+
if (categoryField) opt.category = e[categoryField] as unknown as string
|
|
151
|
+
return opt as PickerOption
|
|
152
|
+
})
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The four Object-entity catalogs (animal / vehicle / weapon / furniture) lack
|
|
157
|
+
* a `promptHint` field; their fragment is synthesized at runtime from
|
|
158
|
+
* label + description. Reproduce the exact phrasing from
|
|
159
|
+
* `getParameterPromptHint` so the registry stays faithful + every option's
|
|
160
|
+
* `promptHint` is non-empty.
|
|
161
|
+
*/
|
|
162
|
+
interface ObjectCatalogEntry {
|
|
163
|
+
readonly id: string
|
|
164
|
+
readonly label: string
|
|
165
|
+
readonly subcategory: string
|
|
166
|
+
readonly description: string
|
|
167
|
+
}
|
|
168
|
+
function objectOptions(
|
|
169
|
+
arr: ReadonlyArray<ObjectCatalogEntry>,
|
|
170
|
+
phrase: (label: string, description: string) => string,
|
|
171
|
+
): ReadonlyArray<PickerOption> {
|
|
172
|
+
return arr.map((e) => ({
|
|
173
|
+
id: e.id,
|
|
174
|
+
label: e.label,
|
|
175
|
+
description: e.description,
|
|
176
|
+
category: e.subcategory,
|
|
177
|
+
promptHint: phrase(e.label.toLowerCase(), e.description),
|
|
178
|
+
}))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Multi-dim `dimensions` builders
|
|
183
|
+
//
|
|
184
|
+
// Each builder is driven by the picker's `fields` array (the canonical order
|
|
185
|
+
// mirrored from `MULTI_PICKERS` in the frontend registry), so a dimension is
|
|
186
|
+
// emitted for exactly those fields, in that order. Catalog dimensions that
|
|
187
|
+
// have no corresponding picker field (e.g. lighting's lighting-ratio /
|
|
188
|
+
// color-temperature, person's regional-aesthetic, styling's outfit/top/…) are
|
|
189
|
+
// intentionally omitted — they aren't exposed by the multi-dim picker UI.
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
|
|
192
|
+
/** "shotSize" → "Shot Size", "vocalPresence" → "Vocal Presence". */
|
|
193
|
+
function humanize(field: string): string {
|
|
194
|
+
return field
|
|
195
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
196
|
+
.replace(/^./, (c) => c.toUpperCase())
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Pattern A — category-discriminated. ONE catalog whose entries each carry a
|
|
201
|
+
* `category`; a `fieldByCategory` map translates category → consumer field.
|
|
202
|
+
* For each picker `field`, options = catalog entries whose mapped field equals
|
|
203
|
+
* it; the dimension label comes from `categoryLabels[category]`.
|
|
204
|
+
*/
|
|
205
|
+
function categoryDiscriminatedDims<C extends string, T extends BaseCatalogEntry & { category: C }>(
|
|
206
|
+
fields: ReadonlyArray<string>,
|
|
207
|
+
arr: ReadonlyArray<T>,
|
|
208
|
+
fieldByCategory: Readonly<Record<C, string>>,
|
|
209
|
+
categoryLabels: Readonly<Record<C, string>>,
|
|
210
|
+
): ReadonlyArray<PickerDimension> {
|
|
211
|
+
// Resolve, per field, the category whose mapped field equals it.
|
|
212
|
+
const categoryByField = new Map<string, C>()
|
|
213
|
+
for (const cat of Object.keys(fieldByCategory) as C[]) {
|
|
214
|
+
categoryByField.set(fieldByCategory[cat], cat)
|
|
215
|
+
}
|
|
216
|
+
return fields.map((field) => {
|
|
217
|
+
const category = categoryByField.get(field)
|
|
218
|
+
if (!category) {
|
|
219
|
+
throw new Error(`picker-catalogs: no category maps to field "${field}"`)
|
|
220
|
+
}
|
|
221
|
+
const options = toOptions(arr.filter((e) => e.category === category))
|
|
222
|
+
return { field, label: categoryLabels[category], options }
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Pattern A' — dimension-discriminated. ONE catalog whose entries each carry a
|
|
228
|
+
* `dimension` (kebab-case); a `fieldByDimension` map translates dimension →
|
|
229
|
+
* consumer field (camelCase). For each picker `field`, options = catalog
|
|
230
|
+
* entries whose mapped field equals it; the dimension label comes from
|
|
231
|
+
* `dimensionLabels[dimension]`.
|
|
232
|
+
*/
|
|
233
|
+
function dimensionDiscriminatedDims<D extends string, T extends BaseCatalogEntry & { dimension: D }>(
|
|
234
|
+
fields: ReadonlyArray<string>,
|
|
235
|
+
arr: ReadonlyArray<T>,
|
|
236
|
+
fieldByDimension: Readonly<Record<D, string>>,
|
|
237
|
+
dimensionLabels: Readonly<Record<D, string>>,
|
|
238
|
+
): ReadonlyArray<PickerDimension> {
|
|
239
|
+
const dimensionByField = new Map<string, D>()
|
|
240
|
+
for (const dim of Object.keys(fieldByDimension) as D[]) {
|
|
241
|
+
dimensionByField.set(fieldByDimension[dim], dim)
|
|
242
|
+
}
|
|
243
|
+
return fields.map((field) => {
|
|
244
|
+
const dimension = dimensionByField.get(field)
|
|
245
|
+
if (!dimension) {
|
|
246
|
+
throw new Error(`picker-catalogs: no dimension maps to field "${field}"`)
|
|
247
|
+
}
|
|
248
|
+
const options = toOptions(arr.filter((e) => e.dimension === dimension))
|
|
249
|
+
return { field, label: dimensionLabels[dimension], options }
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Pattern B — one independent catalog array per field, in `fields` order.
|
|
255
|
+
* The dimension label is humanized from the field name.
|
|
256
|
+
*/
|
|
257
|
+
function perFieldDims(
|
|
258
|
+
fieldArrays: ReadonlyArray<readonly [field: string, arr: ReadonlyArray<BaseCatalogEntry>]>,
|
|
259
|
+
): ReadonlyArray<PickerDimension> {
|
|
260
|
+
return fieldArrays.map(([field, arr]) => ({
|
|
261
|
+
field,
|
|
262
|
+
label: humanize(field),
|
|
263
|
+
options: toOptions(arr),
|
|
264
|
+
}))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Pattern C — hierarchical music-genre. Three fields:
|
|
269
|
+
* - genre → MUSIC_GENRES (top-level)
|
|
270
|
+
* - subgenre → every MUSIC_GENRES[].subgenres flattened
|
|
271
|
+
* - era → MUSIC_ERAS
|
|
272
|
+
*
|
|
273
|
+
* Genre / subgenre / era entries each carry their own `promptHint` (the exact
|
|
274
|
+
* fragment `buildMusicGenreHints` injects for the genre and era cases). We
|
|
275
|
+
* pass those through directly rather than re-deriving via
|
|
276
|
+
* `getParameterPromptHint`, because that helper resolves a subgenre only in
|
|
277
|
+
* the context of its parent genre — a lone `{ subgenre }` yields an empty
|
|
278
|
+
* string. Pass-through keeps every option's hint non-empty and faithful.
|
|
279
|
+
*/
|
|
280
|
+
function musicGenreDims(fields: ReadonlyArray<string>): ReadonlyArray<PickerDimension> {
|
|
281
|
+
const optionsByField: Record<string, ReadonlyArray<PickerOption>> = {
|
|
282
|
+
genre: MUSIC_GENRES.map((g) => ({ id: g.id, label: g.label, description: g.description, category: g.category, promptHint: g.promptHint })),
|
|
283
|
+
subgenre: MUSIC_GENRES.flatMap((g) =>
|
|
284
|
+
g.subgenres.map((s) => ({ id: s.id, label: s.label, promptHint: s.promptHint })),
|
|
285
|
+
),
|
|
286
|
+
era: MUSIC_ERAS.map((e) => ({ id: e.id, label: e.label, description: e.description, promptHint: e.promptHint })),
|
|
287
|
+
}
|
|
288
|
+
return fields.map((field) => {
|
|
289
|
+
const options = optionsByField[field]
|
|
290
|
+
if (!options) throw new Error(`picker-catalogs: music-genre has no options for field "${field}"`)
|
|
291
|
+
return { field, label: humanize(field), options }
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const SINGLE_CATALOGS: readonly PickerCatalog[] = [
|
|
296
|
+
// -------- "Look" family --------
|
|
297
|
+
{
|
|
298
|
+
nodeType: "setting",
|
|
299
|
+
label: "Setting",
|
|
300
|
+
catalogId: "setting",
|
|
301
|
+
kind: "single",
|
|
302
|
+
valueField: "setting",
|
|
303
|
+
defaultValue: "forest",
|
|
304
|
+
// Frontend hardcodes this order (there is no SETTING_CATEGORY_ORDER export).
|
|
305
|
+
categoryOrder: ["indoor", "urban", "nature", "fantastical"],
|
|
306
|
+
categoryLabels: SETTING_CATEGORY_LABELS,
|
|
307
|
+
options: toOptions(SETTINGS, "category"),
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
nodeType: "atmosphere",
|
|
311
|
+
label: "Atmosphere",
|
|
312
|
+
catalogId: "atmosphere",
|
|
313
|
+
kind: "single",
|
|
314
|
+
valueField: "atmosphere",
|
|
315
|
+
defaultValue: "clear",
|
|
316
|
+
options: toOptions(ATMOSPHERES),
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
nodeType: "style",
|
|
320
|
+
label: "Style",
|
|
321
|
+
catalogId: "style",
|
|
322
|
+
kind: "single",
|
|
323
|
+
valueField: "style",
|
|
324
|
+
defaultValue: "cinematic",
|
|
325
|
+
options: toOptions(STYLES),
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
nodeType: "color-look",
|
|
329
|
+
label: "Color / Look",
|
|
330
|
+
catalogId: "color-look",
|
|
331
|
+
kind: "single",
|
|
332
|
+
valueField: "colorLook",
|
|
333
|
+
defaultValue: "warm",
|
|
334
|
+
categoryOrder: COLOR_LOOK_CATEGORY_ORDER,
|
|
335
|
+
categoryLabels: COLOR_LOOK_CATEGORY_LABELS,
|
|
336
|
+
options: toOptions(COLOR_LOOKS, "category"),
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
nodeType: "mood",
|
|
340
|
+
label: "Mood",
|
|
341
|
+
catalogId: "mood",
|
|
342
|
+
kind: "single",
|
|
343
|
+
valueField: "mood",
|
|
344
|
+
defaultValue: "calm",
|
|
345
|
+
categoryOrder: MOOD_CATEGORY_ORDER,
|
|
346
|
+
categoryLabels: MOOD_CATEGORY_LABELS,
|
|
347
|
+
options: toOptions(MOODS, "category"),
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
nodeType: "photographer",
|
|
351
|
+
label: "Photographer / Artist",
|
|
352
|
+
catalogId: "photographer",
|
|
353
|
+
kind: "single",
|
|
354
|
+
valueField: "photographer",
|
|
355
|
+
defaultValue: "tim-walker",
|
|
356
|
+
categoryOrder: PHOTOGRAPHER_CATEGORY_ORDER,
|
|
357
|
+
categoryLabels: PHOTOGRAPHER_CATEGORY_LABELS,
|
|
358
|
+
options: toOptions(PHOTOGRAPHERS, "category"),
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
nodeType: "aesthetic",
|
|
362
|
+
label: "Aesthetic / Microtrend",
|
|
363
|
+
catalogId: "aesthetic",
|
|
364
|
+
kind: "single",
|
|
365
|
+
valueField: "aesthetic",
|
|
366
|
+
defaultValue: "y2k",
|
|
367
|
+
categoryOrder: AESTHETIC_CATEGORY_ORDER,
|
|
368
|
+
categoryLabels: AESTHETIC_CATEGORY_LABELS,
|
|
369
|
+
options: toOptions(AESTHETICS, "category"),
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
nodeType: "era",
|
|
373
|
+
label: "Era / Period",
|
|
374
|
+
catalogId: "era",
|
|
375
|
+
kind: "single",
|
|
376
|
+
valueField: "era",
|
|
377
|
+
defaultValue: "1990s-mall",
|
|
378
|
+
categoryOrder: ERA_CATEGORY_ORDER,
|
|
379
|
+
categoryLabels: ERA_CATEGORY_LABELS,
|
|
380
|
+
options: toOptions(ERAS, "category"),
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
nodeType: "photo-genre",
|
|
384
|
+
label: "Photo Genre",
|
|
385
|
+
catalogId: "photo-genre",
|
|
386
|
+
kind: "single",
|
|
387
|
+
valueField: "photoGenre",
|
|
388
|
+
defaultValue: "fashion-editorial",
|
|
389
|
+
categoryOrder: PHOTO_GENRE_CATEGORY_ORDER,
|
|
390
|
+
categoryLabels: PHOTO_GENRE_CATEGORY_LABELS,
|
|
391
|
+
options: toOptions(PHOTO_GENRES, "category"),
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
nodeType: "backdrop",
|
|
395
|
+
label: "Backdrop",
|
|
396
|
+
catalogId: "backdrop",
|
|
397
|
+
kind: "single",
|
|
398
|
+
valueField: "backdrop",
|
|
399
|
+
defaultValue: "white-seamless",
|
|
400
|
+
categoryOrder: BACKDROP_CATEGORY_ORDER,
|
|
401
|
+
categoryLabels: BACKDROP_CATEGORY_LABELS,
|
|
402
|
+
options: toOptions(BACKDROPS, "category"),
|
|
403
|
+
},
|
|
404
|
+
{
|
|
405
|
+
nodeType: "render-quality",
|
|
406
|
+
label: "Render Quality",
|
|
407
|
+
catalogId: "render-quality",
|
|
408
|
+
kind: "single",
|
|
409
|
+
valueField: "renderQuality",
|
|
410
|
+
defaultValue: "raytracing",
|
|
411
|
+
options: toOptions(RENDER_QUALITIES),
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
nodeType: "composition-effects",
|
|
415
|
+
label: "Composition Effect",
|
|
416
|
+
catalogId: "composition-effects",
|
|
417
|
+
kind: "single",
|
|
418
|
+
valueField: "compositionEffect",
|
|
419
|
+
defaultValue: "bursting-through-frame",
|
|
420
|
+
options: toOptions(COMPOSITION_EFFECTS),
|
|
421
|
+
},
|
|
422
|
+
{
|
|
423
|
+
nodeType: "action-fx",
|
|
424
|
+
label: "Action FX",
|
|
425
|
+
catalogId: "action-fx",
|
|
426
|
+
kind: "single",
|
|
427
|
+
valueField: "actionFx",
|
|
428
|
+
defaultValue: "earthquake-tremor",
|
|
429
|
+
categoryOrder: ACTION_FX_CATEGORY_ORDER,
|
|
430
|
+
categoryLabels: ACTION_FX_CATEGORY_LABELS,
|
|
431
|
+
options: toOptions(ACTION_FX, "category"),
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
nodeType: "loop-subject",
|
|
435
|
+
label: "Loop Subject",
|
|
436
|
+
catalogId: "loop-subject",
|
|
437
|
+
kind: "single",
|
|
438
|
+
valueField: "loopSubject",
|
|
439
|
+
defaultValue: "tunnel",
|
|
440
|
+
categoryOrder: LOOP_SUBJECT_CATEGORY_ORDER,
|
|
441
|
+
categoryLabels: LOOP_SUBJECT_CATEGORY_LABELS,
|
|
442
|
+
options: toOptions(LOOP_SUBJECTS, "category"),
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
nodeType: "post-process-effects",
|
|
446
|
+
label: "Post-Process Effect",
|
|
447
|
+
catalogId: "post-process-effects",
|
|
448
|
+
kind: "single",
|
|
449
|
+
valueField: "postProcess",
|
|
450
|
+
defaultValue: "vignette-soft",
|
|
451
|
+
options: toOptions(POST_PROCESS_EFFECTS),
|
|
452
|
+
},
|
|
453
|
+
|
|
454
|
+
// -------- "Camera" family --------
|
|
455
|
+
{
|
|
456
|
+
nodeType: "camera-motion",
|
|
457
|
+
label: "Camera Motion",
|
|
458
|
+
catalogId: "camera-motions",
|
|
459
|
+
kind: "single",
|
|
460
|
+
valueField: "cameraMotion",
|
|
461
|
+
defaultValue: "static",
|
|
462
|
+
categoryOrder: CAMERA_MOTION_CATEGORY_ORDER,
|
|
463
|
+
categoryLabels: CAMERA_MOTION_CATEGORY_LABELS,
|
|
464
|
+
options: toOptions(CAMERA_MOTIONS, "category"),
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
nodeType: "lens",
|
|
468
|
+
label: "Lens",
|
|
469
|
+
catalogId: "lens",
|
|
470
|
+
kind: "single",
|
|
471
|
+
valueField: "lens",
|
|
472
|
+
defaultValue: "normal-50mm",
|
|
473
|
+
options: toOptions(LENSES),
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
nodeType: "camera-format",
|
|
477
|
+
label: "Camera / Film",
|
|
478
|
+
catalogId: "camera-format",
|
|
479
|
+
kind: "single",
|
|
480
|
+
valueField: "cameraFormat",
|
|
481
|
+
defaultValue: "35mm-film",
|
|
482
|
+
options: toOptions(CAMERA_FORMATS),
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
nodeType: "transition",
|
|
486
|
+
label: "Transition",
|
|
487
|
+
catalogId: "transitions",
|
|
488
|
+
kind: "single",
|
|
489
|
+
valueField: "transition",
|
|
490
|
+
defaultValue: "auto",
|
|
491
|
+
categoryOrder: TRANSITION_CATEGORY_ORDER,
|
|
492
|
+
categoryLabels: TRANSITION_CATEGORY_LABELS,
|
|
493
|
+
options: toOptions(TRANSITIONS, "category"),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
nodeType: "character-fx",
|
|
497
|
+
label: "Character FX",
|
|
498
|
+
catalogId: "character-fx",
|
|
499
|
+
kind: "single",
|
|
500
|
+
valueField: "characterFx",
|
|
501
|
+
defaultValue: "auto",
|
|
502
|
+
categoryOrder: CHARACTER_FX_CATEGORY_ORDER,
|
|
503
|
+
categoryLabels: CHARACTER_FX_CATEGORY_LABELS,
|
|
504
|
+
options: toOptions(CHARACTER_FX, "category"),
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
// -------- "Subject / Object" family --------
|
|
508
|
+
{
|
|
509
|
+
nodeType: "pose",
|
|
510
|
+
label: "Pose",
|
|
511
|
+
catalogId: "pose",
|
|
512
|
+
kind: "single",
|
|
513
|
+
valueField: "pose",
|
|
514
|
+
defaultValue: "standing-upright",
|
|
515
|
+
categoryOrder: POSE_CATEGORY_ORDER,
|
|
516
|
+
categoryLabels: POSE_CATEGORY_LABELS,
|
|
517
|
+
options: toOptions(POSES, "category"),
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
nodeType: "material",
|
|
521
|
+
label: "Material",
|
|
522
|
+
catalogId: "materials",
|
|
523
|
+
kind: "single",
|
|
524
|
+
valueField: "material",
|
|
525
|
+
defaultValue: "silk",
|
|
526
|
+
categoryOrder: MATERIAL_CATEGORY_ORDER,
|
|
527
|
+
categoryLabels: MATERIAL_CATEGORY_LABELS,
|
|
528
|
+
options: toOptions(MATERIALS, "category"),
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
nodeType: "animal",
|
|
532
|
+
label: "Animal",
|
|
533
|
+
catalogId: "animals",
|
|
534
|
+
kind: "single",
|
|
535
|
+
valueField: "animal",
|
|
536
|
+
defaultValue: "dog-golden-retriever",
|
|
537
|
+
categoryOrder: ANIMAL_SUBCATEGORY_ORDER,
|
|
538
|
+
categoryLabels: ANIMAL_SUBCATEGORY_LABELS,
|
|
539
|
+
options: objectOptions(ANIMALS, (label, description) => `featuring a ${label}, ${description}`),
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
nodeType: "vehicle",
|
|
543
|
+
label: "Vehicle",
|
|
544
|
+
catalogId: "vehicles",
|
|
545
|
+
kind: "single",
|
|
546
|
+
valueField: "vehicle",
|
|
547
|
+
defaultValue: "sedan",
|
|
548
|
+
categoryOrder: VEHICLE_SUBCATEGORY_ORDER,
|
|
549
|
+
categoryLabels: VEHICLE_SUBCATEGORY_LABELS,
|
|
550
|
+
options: objectOptions(VEHICLES, (label, description) => `featuring a ${label}, ${description}`),
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
nodeType: "weapon",
|
|
554
|
+
label: "Weapon",
|
|
555
|
+
catalogId: "weapons",
|
|
556
|
+
kind: "single",
|
|
557
|
+
valueField: "weapon",
|
|
558
|
+
defaultValue: "katana",
|
|
559
|
+
categoryOrder: WEAPON_SUBCATEGORY_ORDER,
|
|
560
|
+
categoryLabels: WEAPON_SUBCATEGORY_LABELS,
|
|
561
|
+
options: objectOptions(WEAPONS, (label, description) => `with a ${label}, ${description}`),
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
nodeType: "furniture",
|
|
565
|
+
label: "Furniture",
|
|
566
|
+
catalogId: "furniture",
|
|
567
|
+
kind: "single",
|
|
568
|
+
valueField: "furniture",
|
|
569
|
+
defaultValue: "sofa",
|
|
570
|
+
categoryOrder: FURNITURE_SUBCATEGORY_ORDER,
|
|
571
|
+
categoryLabels: FURNITURE_SUBCATEGORY_LABELS,
|
|
572
|
+
options: objectOptions(FURNITURE, (label, description) => `including a ${label}, ${description}`),
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
nodeType: "held-prop",
|
|
576
|
+
label: "Held Prop",
|
|
577
|
+
catalogId: "held-prop",
|
|
578
|
+
kind: "single",
|
|
579
|
+
valueField: "heldProp",
|
|
580
|
+
defaultValue: "smartphone",
|
|
581
|
+
categoryOrder: HELD_PROP_CATEGORY_ORDER,
|
|
582
|
+
categoryLabels: HELD_PROP_CATEGORY_LABELS,
|
|
583
|
+
options: toOptions(HELD_PROPS, "category"),
|
|
584
|
+
},
|
|
585
|
+
]
|
|
586
|
+
|
|
587
|
+
// Canonical per-picker `fields` order (mirrors `MULTI_PICKERS` in
|
|
588
|
+
// `frontend/src/lib/parameter-picker-registry.tsx`). Declared once and reused
|
|
589
|
+
// for both `fields` and the `dimensions` builder so the two can never drift.
|
|
590
|
+
const FRAMING_FIELDS = ["shotSize", "angle", "coverage", "composition", "vantage"] as const
|
|
591
|
+
// LIGHTING / PERSON / STYLING fields are DERIVED from the canonical
|
|
592
|
+
// dimension/category order + field map — the SAME source the picker components
|
|
593
|
+
// and the describe-to-picker analyzer already use. Deriving (not hand-listing)
|
|
594
|
+
// means the app-input `fields`, the `dimensions` builder, and the mirrored
|
|
595
|
+
// frontend registry can't drift from the actual node-data shape: a new
|
|
596
|
+
// dimension in the catalog flows through automatically. (As hand-lists these
|
|
597
|
+
// HAD drifted — styling omitted outfit/top/bottom/outerwear/legwear/footwear/
|
|
598
|
+
// hairState/wardrobeState; lighting omitted lightingRatio/colorTemperature;
|
|
599
|
+
// person omitted regionalAesthetic — so app-input cards silently dropped them.)
|
|
600
|
+
const LIGHTING_FIELDS: ReadonlyArray<string> = LIGHTING_CATEGORY_ORDER.map((c) => LIGHTING_FIELD_BY_CATEGORY[c])
|
|
601
|
+
const PERSON_FIELDS: ReadonlyArray<string> = PERSON_DIMENSION_ORDER.map((d) => PERSON_FIELD_BY_DIMENSION[d])
|
|
602
|
+
const STYLING_FIELDS: ReadonlyArray<string> = STYLING_DIMENSION_ORDER.map((d) => STYLING_FIELD_BY_DIMENSION[d])
|
|
603
|
+
const TEMPORAL_FIELDS = ["temporalSpeed", "temporalFreeze", "temporalDirection", "temporalShutter"] as const
|
|
604
|
+
const EXPOSURE_FIELDS = ["aperture", "shutterSpeed", "isoValue"] as const
|
|
605
|
+
const MUSIC_GENRE_FIELDS = ["genre", "subgenre", "era"] as const
|
|
606
|
+
const MUSIC_MOOD_FIELDS = ["energy", "emotion", "vibe"] as const
|
|
607
|
+
const INSTRUMENTATION_FIELDS = ["instruments", "production", "vocalPresence", "singingStyle"] as const
|
|
608
|
+
const VOICE_CHARACTER_FIELDS = ["age", "gender", "language", "accent", "timbre"] as const
|
|
609
|
+
const VOICE_DELIVERY_FIELDS = ["pace", "emotion", "archetype"] as const
|
|
610
|
+
|
|
611
|
+
const MULTI_CATALOGS: readonly PickerCatalog[] = [
|
|
612
|
+
{
|
|
613
|
+
nodeType: "framing",
|
|
614
|
+
label: "Framing",
|
|
615
|
+
catalogId: "framing",
|
|
616
|
+
kind: "multi",
|
|
617
|
+
fields: FRAMING_FIELDS,
|
|
618
|
+
dimensions: categoryDiscriminatedDims(FRAMING_FIELDS, FRAMINGS, FRAMING_FIELD_BY_CATEGORY, FRAMING_CATEGORY_LABELS),
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
nodeType: "lighting",
|
|
622
|
+
label: "Lighting",
|
|
623
|
+
catalogId: "lighting",
|
|
624
|
+
kind: "multi",
|
|
625
|
+
fields: LIGHTING_FIELDS,
|
|
626
|
+
dimensions: categoryDiscriminatedDims(LIGHTING_FIELDS, LIGHTINGS, LIGHTING_FIELD_BY_CATEGORY, LIGHTING_CATEGORY_LABELS),
|
|
627
|
+
},
|
|
628
|
+
{
|
|
629
|
+
nodeType: "person",
|
|
630
|
+
label: "Person",
|
|
631
|
+
catalogId: "person",
|
|
632
|
+
kind: "multi",
|
|
633
|
+
fields: PERSON_FIELDS,
|
|
634
|
+
dimensions: dimensionDiscriminatedDims(PERSON_FIELDS, PEOPLE, PERSON_FIELD_BY_DIMENSION, PERSON_DIMENSION_LABELS),
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
nodeType: "styling",
|
|
638
|
+
label: "Styling",
|
|
639
|
+
catalogId: "styling",
|
|
640
|
+
kind: "multi",
|
|
641
|
+
fields: STYLING_FIELDS,
|
|
642
|
+
dimensions: dimensionDiscriminatedDims(STYLING_FIELDS, STYLINGS, STYLING_FIELD_BY_DIMENSION, STYLING_DIMENSION_LABELS),
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
nodeType: "temporal",
|
|
646
|
+
label: "Temporal",
|
|
647
|
+
catalogId: "temporal",
|
|
648
|
+
kind: "multi",
|
|
649
|
+
fields: TEMPORAL_FIELDS,
|
|
650
|
+
dimensions: categoryDiscriminatedDims(TEMPORAL_FIELDS, TEMPORALS, TEMPORAL_FIELD_BY_CATEGORY, TEMPORAL_CATEGORY_LABELS),
|
|
651
|
+
},
|
|
652
|
+
{
|
|
653
|
+
nodeType: "exposure-settings",
|
|
654
|
+
label: "Exposure Settings",
|
|
655
|
+
catalogId: "exposure-settings",
|
|
656
|
+
kind: "multi",
|
|
657
|
+
fields: EXPOSURE_FIELDS,
|
|
658
|
+
dimensions: categoryDiscriminatedDims(EXPOSURE_FIELDS, EXPOSURE_SETTINGS, EXPOSURE_FIELD_BY_CATEGORY, EXPOSURE_CATEGORY_LABELS),
|
|
659
|
+
},
|
|
660
|
+
// -------- "Sound" family --------
|
|
661
|
+
{
|
|
662
|
+
nodeType: "music-genre",
|
|
663
|
+
label: "Music Genre",
|
|
664
|
+
catalogId: "music-genre",
|
|
665
|
+
kind: "multi",
|
|
666
|
+
fields: MUSIC_GENRE_FIELDS,
|
|
667
|
+
dimensions: musicGenreDims(MUSIC_GENRE_FIELDS),
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
nodeType: "music-mood",
|
|
671
|
+
label: "Music Mood",
|
|
672
|
+
catalogId: "music-mood",
|
|
673
|
+
kind: "multi",
|
|
674
|
+
fields: MUSIC_MOOD_FIELDS,
|
|
675
|
+
dimensions: perFieldDims([
|
|
676
|
+
["energy", MUSIC_ENERGIES],
|
|
677
|
+
["emotion", MUSIC_EMOTIONS],
|
|
678
|
+
["vibe", MUSIC_VIBES],
|
|
679
|
+
]),
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
nodeType: "instrumentation",
|
|
683
|
+
label: "Instrumentation",
|
|
684
|
+
catalogId: "instrumentation",
|
|
685
|
+
kind: "multi",
|
|
686
|
+
fields: INSTRUMENTATION_FIELDS,
|
|
687
|
+
dimensions: perFieldDims([
|
|
688
|
+
["instruments", INSTRUMENTS],
|
|
689
|
+
["production", PRODUCTION_STYLES],
|
|
690
|
+
["vocalPresence", VOCAL_PRESENCE],
|
|
691
|
+
["singingStyle", SINGING_STYLES],
|
|
692
|
+
]),
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
nodeType: "voice-character",
|
|
696
|
+
label: "Voice Character",
|
|
697
|
+
catalogId: "voice-character",
|
|
698
|
+
kind: "multi",
|
|
699
|
+
fields: VOICE_CHARACTER_FIELDS,
|
|
700
|
+
dimensions: perFieldDims([
|
|
701
|
+
["age", VOICE_AGES],
|
|
702
|
+
["gender", VOICE_GENDERS],
|
|
703
|
+
["language", VOICE_LANGUAGES],
|
|
704
|
+
["accent", VOICE_ACCENTS],
|
|
705
|
+
["timbre", VOICE_TIMBRES],
|
|
706
|
+
]),
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
nodeType: "voice-delivery",
|
|
710
|
+
label: "Voice Delivery",
|
|
711
|
+
catalogId: "voice-delivery",
|
|
712
|
+
kind: "multi",
|
|
713
|
+
fields: VOICE_DELIVERY_FIELDS,
|
|
714
|
+
dimensions: perFieldDims([
|
|
715
|
+
["pace", VOICE_PACES],
|
|
716
|
+
["emotion", VOICE_EMOTIONS],
|
|
717
|
+
["archetype", VOICE_ARCHETYPES],
|
|
718
|
+
]),
|
|
719
|
+
},
|
|
720
|
+
]
|
|
721
|
+
|
|
722
|
+
export const PICKER_CATALOGS: readonly PickerCatalog[] = [
|
|
723
|
+
...SINGLE_CATALOGS,
|
|
724
|
+
...MULTI_CATALOGS,
|
|
725
|
+
]
|
|
726
|
+
|
|
727
|
+
/** Resolve a catalog by `nodeType` first, then by `catalogId`. */
|
|
728
|
+
export function getPickerCatalog(nodeTypeOrCatalogId: string): PickerCatalog | undefined {
|
|
729
|
+
return (
|
|
730
|
+
PICKER_CATALOGS.find((c) => c.nodeType === nodeTypeOrCatalogId) ??
|
|
731
|
+
PICKER_CATALOGS.find((c) => c.catalogId === nodeTypeOrCatalogId)
|
|
732
|
+
)
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export function listPickerCatalogs(): readonly PickerCatalog[] {
|
|
736
|
+
return PICKER_CATALOGS
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// ─── Summary + projection (for MCP / REST exposure) ──────────────────────────
|
|
740
|
+
|
|
741
|
+
export interface PickerCatalogSummary {
|
|
742
|
+
readonly nodeType: string
|
|
743
|
+
readonly label: string
|
|
744
|
+
readonly catalogId: string
|
|
745
|
+
readonly kind: "single" | "multi"
|
|
746
|
+
/** single only. */
|
|
747
|
+
readonly valueField?: string
|
|
748
|
+
/** multi only. */
|
|
749
|
+
readonly fields?: readonly string[]
|
|
750
|
+
/** single: options.length; multi: sum of every dimension's options. */
|
|
751
|
+
readonly optionCount: number
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/** Lightweight directory of every picker catalog — no option payloads. */
|
|
755
|
+
export function summarizePickerCatalogs(): readonly PickerCatalogSummary[] {
|
|
756
|
+
return PICKER_CATALOGS.map((c) => ({
|
|
757
|
+
nodeType: c.nodeType,
|
|
758
|
+
label: c.label,
|
|
759
|
+
catalogId: c.catalogId,
|
|
760
|
+
kind: c.kind,
|
|
761
|
+
valueField: c.valueField,
|
|
762
|
+
fields: c.fields,
|
|
763
|
+
optionCount:
|
|
764
|
+
c.kind === "single"
|
|
765
|
+
? (c.options?.length ?? 0)
|
|
766
|
+
: (c.dimensions?.reduce((n, d) => n + d.options.length, 0) ?? 0),
|
|
767
|
+
}))
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export type PickerCatalogDetail = "compact" | "full"
|
|
771
|
+
|
|
772
|
+
export interface ProjectPickerCatalogOptions {
|
|
773
|
+
/** Default "compact" (id/label/category/icon). "full" adds description + promptHint. */
|
|
774
|
+
readonly detail?: PickerCatalogDetail
|
|
775
|
+
/** single-dim: keep only options in this category. */
|
|
776
|
+
readonly category?: string
|
|
777
|
+
/** multi-dim: keep only this dimension field. */
|
|
778
|
+
readonly field?: string
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** An option after projection — description/promptHint present only when detail="full". */
|
|
782
|
+
export interface ProjectedPickerOption {
|
|
783
|
+
readonly id: string
|
|
784
|
+
readonly label: string
|
|
785
|
+
readonly description?: string
|
|
786
|
+
readonly category?: string
|
|
787
|
+
readonly promptHint?: string
|
|
788
|
+
readonly icon?: string
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
export interface ProjectedPickerDimension {
|
|
792
|
+
readonly field: string
|
|
793
|
+
readonly label: string
|
|
794
|
+
readonly options: readonly ProjectedPickerOption[]
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export interface ProjectedPickerCatalog {
|
|
798
|
+
readonly nodeType: string
|
|
799
|
+
readonly label: string
|
|
800
|
+
readonly catalogId: string
|
|
801
|
+
readonly kind: "single" | "multi"
|
|
802
|
+
readonly valueField?: string
|
|
803
|
+
readonly defaultValue?: string
|
|
804
|
+
readonly categoryOrder?: readonly string[]
|
|
805
|
+
readonly categoryLabels?: Readonly<Record<string, string>>
|
|
806
|
+
readonly options?: readonly ProjectedPickerOption[]
|
|
807
|
+
readonly fields?: readonly string[]
|
|
808
|
+
readonly dimensions?: readonly ProjectedPickerDimension[]
|
|
809
|
+
readonly detail: PickerCatalogDetail
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
function projectOption(o: PickerOption, detail: PickerCatalogDetail): ProjectedPickerOption {
|
|
813
|
+
return detail === "full"
|
|
814
|
+
? {
|
|
815
|
+
id: o.id,
|
|
816
|
+
label: o.label,
|
|
817
|
+
description: o.description,
|
|
818
|
+
category: o.category,
|
|
819
|
+
promptHint: o.promptHint,
|
|
820
|
+
icon: o.icon,
|
|
821
|
+
}
|
|
822
|
+
: { id: o.id, label: o.label, category: o.category, icon: o.icon }
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/** Project a catalog to the wire shape: compact by default, optional category/field filter. */
|
|
826
|
+
export function projectPickerCatalog(
|
|
827
|
+
c: PickerCatalog,
|
|
828
|
+
opts: ProjectPickerCatalogOptions = {},
|
|
829
|
+
): ProjectedPickerCatalog {
|
|
830
|
+
const detail: PickerCatalogDetail = opts.detail ?? "compact"
|
|
831
|
+
const base = {
|
|
832
|
+
nodeType: c.nodeType,
|
|
833
|
+
label: c.label,
|
|
834
|
+
catalogId: c.catalogId,
|
|
835
|
+
kind: c.kind,
|
|
836
|
+
valueField: c.valueField,
|
|
837
|
+
defaultValue: c.defaultValue,
|
|
838
|
+
categoryOrder: c.categoryOrder,
|
|
839
|
+
categoryLabels: c.categoryLabels,
|
|
840
|
+
detail,
|
|
841
|
+
}
|
|
842
|
+
if (c.kind === "single") {
|
|
843
|
+
let options = c.options ?? []
|
|
844
|
+
if (opts.category) options = options.filter((o) => o.category === opts.category)
|
|
845
|
+
return { ...base, options: options.map((o) => projectOption(o, detail)) }
|
|
846
|
+
}
|
|
847
|
+
let dims = c.dimensions ?? []
|
|
848
|
+
if (opts.field) dims = dims.filter((d) => d.field === opts.field)
|
|
849
|
+
return {
|
|
850
|
+
...base,
|
|
851
|
+
fields: c.fields,
|
|
852
|
+
dimensions: dims.map((d) => ({
|
|
853
|
+
field: d.field,
|
|
854
|
+
label: d.label,
|
|
855
|
+
options: d.options.map((o) => projectOption(o, detail)),
|
|
856
|
+
})),
|
|
857
|
+
}
|
|
858
|
+
}
|