@nodaro/prompts 1.7.2 → 1.8.0
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/dist/index.cjs +5453 -4271
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1221 -63
- package/dist/index.d.ts +1221 -63
- package/dist/index.js +5366 -4273
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/b7-person-pack-e2e.test.ts +37 -0
- package/src/__tests__/catalog-funnel-ratchet.test.ts +97 -0
- package/src/__tests__/catalog-packs.test.ts +130 -0
- package/src/__tests__/catalog-sidecar-coverage.test.ts +27 -0
- package/src/__tests__/catalog-terms.test.ts +162 -0
- package/src/__tests__/character-default-role.test.ts +3 -2
- package/src/__tests__/content-free-contract.test.ts +45 -0
- package/src/__tests__/dod-replace-pack-acceptance.test.ts +41 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +2959 -0
- package/src/__tests__/fixtures/person-sector-pack.ts +27 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +5 -2
- package/src/__tests__/parameter-hint-mode.test.ts +385 -0
- package/src/__tests__/parameter-prompt-hint-pack-fallback.test.ts +25 -0
- package/src/__tests__/person-packs.test.ts +166 -0
- package/src/__tests__/project-all-catalogs.test.ts +26 -0
- package/src/__tests__/prompt-builder.test.ts +53 -0
- package/src/__tests__/registered-catalogs-funnel.test.ts +30 -0
- package/src/__tests__/registered-catalogs-guard.test.ts +11 -0
- package/src/__tests__/term.test.ts +104 -0
- package/src/__tests__/transitions.test.ts +8 -5
- package/src/__tests__/upstream-immutability.test.ts +23 -0
- package/src/action-fx.ts +67 -17
- package/src/aesthetic.ts +54 -6
- package/src/atmosphere.ts +64 -23
- package/src/backdrop.ts +44 -30
- package/src/camera-format.ts +33 -11
- package/src/camera-motions.ts +89 -0
- package/src/catalog-packs.ts +125 -0
- package/src/catalog-sidecar-coverage.ts +36 -0
- package/src/character-fx.ts +112 -39
- package/src/color-look.ts +44 -27
- package/src/composition-effects.ts +21 -7
- package/src/era.ts +24 -0
- package/src/exposure-settings.ts +76 -18
- package/src/framing.ts +100 -0
- package/src/held-prop.ts +125 -63
- package/src/identity-lock.ts +12 -5
- package/src/image-reference-doctrine.ts +55 -0
- package/src/index.ts +6 -0
- package/src/instrumentation.ts +148 -57
- package/src/lens.ts +31 -15
- package/src/lighting.ts +120 -59
- package/src/loop-subject.ts +27 -1
- package/src/materials.ts +123 -69
- package/src/mood.ts +123 -51
- package/src/music-genre.ts +171 -67
- package/src/music-mood.ts +85 -17
- package/src/parameter-prompt-hint.ts +168 -56
- package/src/person-packs.ts +182 -0
- package/src/person.ts +506 -408
- package/src/photo-genre.ts +37 -22
- package/src/photographer.ts +138 -1
- package/src/picker-catalogs.ts +86 -40
- package/src/picker-wiring.ts +1 -1
- package/src/pose.ts +105 -40
- package/src/post-process-effects.ts +51 -8
- package/src/prompt-builder.ts +16 -1
- package/src/provider-prompt-doctrine.ts +1 -2
- package/src/render-quality.ts +25 -7
- package/src/setting.ts +30 -14
- package/src/style-presets.ts +1 -1
- package/src/style.ts +33 -15
- package/src/styling.ts +155 -94
- package/src/surround-fill.ts +67 -0
- package/src/temporal.ts +80 -18
- package/src/term.ts +155 -0
- package/src/transitions.ts +73 -26
- package/src/voice-character.ts +190 -90
- package/src/voice-delivery.ts +83 -12
|
@@ -21,14 +21,14 @@ export type { HintNodeLike, HintEdgeLike, HintGraphContext }
|
|
|
21
21
|
|
|
22
22
|
import { buildFramingHints } from "./framing.js"
|
|
23
23
|
import { buildLightingHints } from "./lighting.js"
|
|
24
|
-
import { getLensPromptHint } from "./lens.js"
|
|
25
|
-
import { getCameraFormatPromptHint } from "./camera-format.js"
|
|
26
|
-
import { getColorLookPromptHint } from "./color-look.js"
|
|
24
|
+
import { getLensPromptHint, getLensTerm } from "./lens.js"
|
|
25
|
+
import { getCameraFormatPromptHint, getCameraFormatTerm } from "./camera-format.js"
|
|
26
|
+
import { getColorLookPromptHint, getColorLookTerm } from "./color-look.js"
|
|
27
27
|
import { buildAtmosphereHints } from "./atmosphere.js"
|
|
28
28
|
import { buildActionFxHints } from "./action-fx.js"
|
|
29
|
-
import { getStylePromptHint } from "./style.js"
|
|
30
|
-
import { getSettingPromptHint } from "./setting.js"
|
|
31
|
-
import { getLoopSubjectPromptHint } from "./loop-subject.js"
|
|
29
|
+
import { getStylePromptHint, getStyleTerm } from "./style.js"
|
|
30
|
+
import { getSettingPromptHint, getSettingTerm } from "./setting.js"
|
|
31
|
+
import { getLoopSubjectPromptHint, getLoopSubjectTerm } from "./loop-subject.js"
|
|
32
32
|
import { buildPersonHints } from "./person.js"
|
|
33
33
|
import { buildMoodHints } from "./mood.js"
|
|
34
34
|
import { buildPoseHints } from "./pose.js"
|
|
@@ -42,21 +42,23 @@ import { getAnimal } from "@nodaro/shared"
|
|
|
42
42
|
import { getVehicle } from "@nodaro/shared"
|
|
43
43
|
import { getWeapon } from "@nodaro/shared"
|
|
44
44
|
import { getFurniture } from "@nodaro/shared"
|
|
45
|
-
import { getPhotoGenrePromptHint } from "./photo-genre.js"
|
|
46
|
-
import { getBackdropPromptHint } from "./backdrop.js"
|
|
45
|
+
import { getPhotoGenrePromptHint, getPhotoGenreTerm } from "./photo-genre.js"
|
|
46
|
+
import { getBackdropPromptHint, getBackdropTerm } from "./backdrop.js"
|
|
47
47
|
import { buildHeldPropHints } from "./held-prop.js"
|
|
48
48
|
import { buildPhotographerHints } from "./photographer.js"
|
|
49
49
|
import { buildAestheticHints } from "./aesthetic.js"
|
|
50
|
-
import { getEraPromptHint } from "./era.js"
|
|
50
|
+
import { getEraPromptHint, getEraTerm } from "./era.js"
|
|
51
51
|
import { buildExposureHints } from "./exposure-settings.js"
|
|
52
|
-
import { getRenderQualityPromptHint } from "./render-quality.js"
|
|
53
|
-
import { getCompositionEffectPromptHint } from "./composition-effects.js"
|
|
52
|
+
import { getRenderQualityPromptHint, getRenderQualityTerm } from "./render-quality.js"
|
|
53
|
+
import { getCompositionEffectPromptHint, getCompositionEffectTerm } from "./composition-effects.js"
|
|
54
54
|
import { buildPostProcessHints } from "./post-process-effects.js"
|
|
55
55
|
import { buildMusicGenreHints } from "./music-genre.js"
|
|
56
56
|
import { buildMusicMoodHints } from "./music-mood.js"
|
|
57
57
|
import { buildInstrumentationHints } from "./instrumentation.js"
|
|
58
58
|
import { buildVoiceCharacterHints } from "./voice-character.js"
|
|
59
59
|
import { buildVoiceDeliveryHints } from "./voice-delivery.js"
|
|
60
|
+
import { getPickerCatalog } from "./picker-catalogs.js"
|
|
61
|
+
import { deriveTerm, resolveTerm, type PickerHintMode } from "./term.js"
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
function asStr(v: unknown): string {
|
|
@@ -88,21 +90,87 @@ function withCustomText(data: Record<string, unknown>, mainHint: string): string
|
|
|
88
90
|
return fragments.join(", ")
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
/**
|
|
94
|
+
* The node-level hint mode a picker declares via `data.hintMode`.
|
|
95
|
+
*
|
|
96
|
+
* Only the two documented values count; anything else (a stale value, a typo,
|
|
97
|
+
* a non-string) is treated as UNDECLARED and falls back — to the inherited
|
|
98
|
+
* mode when the node is resolved as an upstream input, and to `"full"`
|
|
99
|
+
* otherwise. Compact is opt-in, so an unrecognized value can never silently
|
|
100
|
+
* shorten a prompt.
|
|
101
|
+
*/
|
|
102
|
+
function readHintMode(data: Record<string, unknown>): PickerHintMode | undefined {
|
|
103
|
+
const raw = data.hintMode
|
|
104
|
+
return raw === "compact" || raw === "full" ? raw : undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Pick the full-hint or the compact-term getter for the active mode. */
|
|
108
|
+
function byMode<T>(mode: PickerHintMode, full: T, compact: T): T {
|
|
109
|
+
return mode === "compact" ? compact : full
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The compact fragment for an OBJECT-entity entry (animal / vehicle / weapon /
|
|
114
|
+
* furniture). Those catalogs carry no `promptHint` of their own — the full
|
|
115
|
+
* fragment is synthesized as "featuring a {label}, {description}" — so the
|
|
116
|
+
* compact form is the authored `term` when there is one and the derived label
|
|
117
|
+
* otherwise (a concrete object's label IS its trade term). The framing verb
|
|
118
|
+
* ("featuring a", "with a") belongs to the HINT; a term drops bare into
|
|
119
|
+
* whatever sentence the consumer is building, and "the object is in the scene"
|
|
120
|
+
* is precisely what these four nodes mean.
|
|
121
|
+
*
|
|
122
|
+
* The fallback MUST be `deriveTerm` and not a bare `toLowerCase()`: it is the
|
|
123
|
+
* same fallback `objectOptions` uses to build the `/v1/catalogs` projection,
|
|
124
|
+
* so a parenthetical label ("Rifle (bolt-action)") must strip identically here
|
|
125
|
+
* or the injected fragment and the projected term would disagree.
|
|
126
|
+
*
|
|
127
|
+
* `term` is read structurally because it is being added to the shared entity
|
|
128
|
+
* interfaces separately; this stays correct before and after that lands.
|
|
129
|
+
*/
|
|
130
|
+
function objectEntityTerm(entry: { readonly label: string }): string {
|
|
131
|
+
return (entry as { term?: string }).term ?? deriveTerm(entry.label)
|
|
132
|
+
}
|
|
133
|
+
|
|
91
134
|
/**
|
|
92
135
|
* Dispatch by parameter-node type to its prompt-hint string. For camera-motion,
|
|
93
136
|
* pass `ctx` to include the composed start/end clauses; otherwise only the
|
|
94
137
|
* bare motion description is returned.
|
|
138
|
+
*
|
|
139
|
+
* VERBOSITY — a picker node may set `data.hintMode` to `"compact"` to inject
|
|
140
|
+
* its short professional `term` ("whip pan left", "hard cut") instead of the
|
|
141
|
+
* long `promptHint`. Absent, or any unrecognized value, means `"full"`, whose
|
|
142
|
+
* output is byte-identical to what this function returned before hint modes
|
|
143
|
+
* existed. ONLY the base catalog fragment swaps: `preText`/`postText`, the
|
|
144
|
+
* transition / camera-motion / character-fx timing and start-state/end-state
|
|
145
|
+
* clauses, multi-pick joining and multi-dimension composition are emitted the
|
|
146
|
+
* same way in both modes.
|
|
95
147
|
*/
|
|
96
148
|
export function getParameterPromptHint(
|
|
97
149
|
node: HintNodeLike | undefined,
|
|
98
150
|
ctx?: HintGraphContext,
|
|
151
|
+
): string {
|
|
152
|
+
return resolveParameterHint(node, ctx)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The dispatch body. `inherited` carries the mode DOWN into the nodes wired to
|
|
157
|
+
* a composer's `startState` / `endState` handles, so a compact transition
|
|
158
|
+
* composes compact start/end clauses rather than mixing a term with two
|
|
159
|
+
* paragraphs. A node that declares its own `hintMode` still wins over what it
|
|
160
|
+
* inherits.
|
|
161
|
+
*/
|
|
162
|
+
function resolveParameterHint(
|
|
163
|
+
node: HintNodeLike | undefined,
|
|
164
|
+
ctx?: HintGraphContext,
|
|
165
|
+
inherited?: PickerHintMode,
|
|
99
166
|
): string {
|
|
100
167
|
if (!node?.type) return ""
|
|
101
168
|
const data = (node.data ?? {}) as Record<string, unknown>
|
|
169
|
+
const mode: PickerHintMode = readHintMode(data) ?? inherited ?? "full"
|
|
102
170
|
|
|
103
171
|
if (node.type === "camera-motion") {
|
|
104
172
|
const motionId = asStr(data.cameraMotion) || undefined
|
|
105
|
-
if (!ctx) return withCustomText(data, composeCameraMotionHintFromConnections(motionId, [], []))
|
|
173
|
+
if (!ctx) return withCustomText(data, composeCameraMotionHintFromConnections(motionId, [], [], mode))
|
|
106
174
|
const startHints: string[] = []
|
|
107
175
|
const endHints: string[] = []
|
|
108
176
|
for (const edge of ctx.edges) {
|
|
@@ -111,13 +179,14 @@ export function getParameterPromptHint(
|
|
|
111
179
|
if (!src) continue
|
|
112
180
|
// Pass no ctx for nested resolution: startState/endState inputs are
|
|
113
181
|
// themselves parameter nodes (framing/tone/etc.) that don't need graph
|
|
114
|
-
// context, and avoiding recursion keeps the walk cycle-safe.
|
|
115
|
-
|
|
182
|
+
// context, and avoiding recursion keeps the walk cycle-safe. The mode
|
|
183
|
+
// rides down so the composed clause stays at one level of detail.
|
|
184
|
+
const hint = resolveParameterHint(src, undefined, mode)
|
|
116
185
|
if (!hint) continue
|
|
117
186
|
if (edge.targetHandle === "startState") startHints.push(hint)
|
|
118
187
|
else if (edge.targetHandle === "endState") endHints.push(hint)
|
|
119
188
|
}
|
|
120
|
-
return withCustomText(data, composeCameraMotionHintFromConnections(motionId, startHints, endHints))
|
|
189
|
+
return withCustomText(data, composeCameraMotionHintFromConnections(motionId, startHints, endHints, mode))
|
|
121
190
|
}
|
|
122
191
|
|
|
123
192
|
if (node.type === "transition") {
|
|
@@ -132,7 +201,7 @@ export function getParameterPromptHint(
|
|
|
132
201
|
intensity: asStr(data.intensity) as TransitionIntensity | undefined,
|
|
133
202
|
}
|
|
134
203
|
if (!ctx) {
|
|
135
|
-
return withCustomText(data, composeTransitionHintFromConnections(transitionId, [], [], timing))
|
|
204
|
+
return withCustomText(data, composeTransitionHintFromConnections(transitionId, [], [], timing, mode))
|
|
136
205
|
}
|
|
137
206
|
const startHints: string[] = []
|
|
138
207
|
const endHints: string[] = []
|
|
@@ -140,12 +209,12 @@ export function getParameterPromptHint(
|
|
|
140
209
|
if (edge.target !== node.id) continue
|
|
141
210
|
const src = ctx.nodes.find((n) => n.id === edge.source)
|
|
142
211
|
if (!src) continue
|
|
143
|
-
const hint =
|
|
212
|
+
const hint = resolveParameterHint(src, undefined, mode) // no ctx — cycle-safe
|
|
144
213
|
if (!hint) continue
|
|
145
214
|
if (edge.targetHandle === "startState") startHints.push(hint)
|
|
146
215
|
else if (edge.targetHandle === "endState") endHints.push(hint)
|
|
147
216
|
}
|
|
148
|
-
return withCustomText(data, composeTransitionHintFromConnections(transitionId, startHints, endHints, timing))
|
|
217
|
+
return withCustomText(data, composeTransitionHintFromConnections(transitionId, startHints, endHints, timing, mode))
|
|
149
218
|
}
|
|
150
219
|
|
|
151
220
|
if (node.type === "character-fx") {
|
|
@@ -160,7 +229,7 @@ export function getParameterPromptHint(
|
|
|
160
229
|
intensity: asStr(data.intensity) as CharacterFxIntensity | undefined,
|
|
161
230
|
}
|
|
162
231
|
if (!ctx) {
|
|
163
|
-
return withCustomText(data, composeCharacterFxHintFromConnections(effectId, [], timing))
|
|
232
|
+
return withCustomText(data, composeCharacterFxHintFromConnections(effectId, [], timing, mode))
|
|
164
233
|
}
|
|
165
234
|
const targetNames: string[] = []
|
|
166
235
|
for (const edge of ctx.edges) {
|
|
@@ -171,106 +240,149 @@ export function getParameterPromptHint(
|
|
|
171
240
|
const name = extractCharacterRefName(src)
|
|
172
241
|
if (name) targetNames.push(name)
|
|
173
242
|
}
|
|
174
|
-
return withCustomText(data, composeCharacterFxHintFromConnections(effectId, targetNames, timing))
|
|
243
|
+
return withCustomText(data, composeCharacterFxHintFromConnections(effectId, targetNames, timing, mode))
|
|
175
244
|
}
|
|
176
245
|
|
|
177
|
-
|
|
246
|
+
const base = resolveBaseHint(node.type, data, mode)
|
|
247
|
+
if (base) return base
|
|
248
|
+
// Pack-extend fallback: a single-dim pack entry the per-catalog getter (which
|
|
249
|
+
// reads the frozen base array) can't resolve. Resolve it against the
|
|
250
|
+
// registered (pack-composed) catalog's options. Composition already resolved
|
|
251
|
+
// `PickerOption.term`; `resolveTerm` is idempotent over a resolved option and
|
|
252
|
+
// is called anyway so a pack option that reached this array by some other
|
|
253
|
+
// route still injects SOMETHING in compact mode rather than `undefined`.
|
|
254
|
+
const cat = getPickerCatalog(node.type)
|
|
255
|
+
if (cat?.kind === "single" && cat.valueField) {
|
|
256
|
+
const id = typeof data[cat.valueField] === "string" ? (data[cat.valueField] as string) : ""
|
|
257
|
+
const opt = cat.options?.find((o) => o.id === id)
|
|
258
|
+
if (opt) return byMode(mode, opt.promptHint, resolveTerm(opt))
|
|
259
|
+
}
|
|
260
|
+
return base
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Base (upstream) hint dispatch by node type — the per-catalog getters read the
|
|
265
|
+
* frozen base arrays. Pack-added single-dim ids are resolved by the caller
|
|
266
|
+
* against the registered (pack-composed) catalog.
|
|
267
|
+
*
|
|
268
|
+
* `mode` selects the base fragment only: the compact `get<Name>Term` getter /
|
|
269
|
+
* `build<Name>Terms` builder instead of the verbose one. The `withCustomText`
|
|
270
|
+
* wrapper, the builders' own pre/post composition, and the free-text node
|
|
271
|
+
* types (tone / style-guide / text-prompt — user prose, not catalog copy) are
|
|
272
|
+
* identical in both modes.
|
|
273
|
+
*/
|
|
274
|
+
function resolveBaseHint(
|
|
275
|
+
type: string,
|
|
276
|
+
data: Record<string, unknown>,
|
|
277
|
+
mode: PickerHintMode = "full",
|
|
278
|
+
): string {
|
|
279
|
+
switch (type) {
|
|
178
280
|
case "framing":
|
|
179
|
-
return withCustomText(data, buildFramingHints(data).join(", "))
|
|
281
|
+
return withCustomText(data, buildFramingHints(data, false, mode).join(", "))
|
|
180
282
|
case "lighting":
|
|
181
|
-
return withCustomText(data, buildLightingHints(data).join(", "))
|
|
283
|
+
return withCustomText(data, buildLightingHints(data, mode).join(", "))
|
|
182
284
|
case "lens":
|
|
183
|
-
return withCustomText(data, getLensPromptHint(asStr(data.lens)))
|
|
285
|
+
return withCustomText(data, byMode(mode, getLensPromptHint, getLensTerm)(asStr(data.lens)))
|
|
184
286
|
case "camera-format":
|
|
185
|
-
return withCustomText(data, getCameraFormatPromptHint(asStr(data.cameraFormat)))
|
|
287
|
+
return withCustomText(data, byMode(mode, getCameraFormatPromptHint, getCameraFormatTerm)(asStr(data.cameraFormat)))
|
|
186
288
|
case "color-look":
|
|
187
|
-
return withCustomText(data, getColorLookPromptHint(asStr(data.colorLook)))
|
|
289
|
+
return withCustomText(data, byMode(mode, getColorLookPromptHint, getColorLookTerm)(asStr(data.colorLook)))
|
|
188
290
|
|
|
189
291
|
// build*Hints in music-* / voice-* / mood / person / etc. compose
|
|
190
292
|
// preText/postText internally — bypass the wrapper to avoid double-
|
|
191
293
|
// composition.
|
|
192
294
|
case "music-genre":
|
|
193
|
-
return buildMusicGenreHints((data ?? {}) as Parameters<typeof buildMusicGenreHints>[0])
|
|
295
|
+
return buildMusicGenreHints((data ?? {}) as Parameters<typeof buildMusicGenreHints>[0], mode)
|
|
194
296
|
case "music-mood":
|
|
195
|
-
return buildMusicMoodHints((data ?? {}) as Parameters<typeof buildMusicMoodHints>[0])
|
|
297
|
+
return buildMusicMoodHints((data ?? {}) as Parameters<typeof buildMusicMoodHints>[0], mode)
|
|
196
298
|
case "instrumentation":
|
|
197
|
-
return buildInstrumentationHints((data ?? {}) as Parameters<typeof buildInstrumentationHints>[0])
|
|
299
|
+
return buildInstrumentationHints((data ?? {}) as Parameters<typeof buildInstrumentationHints>[0], mode)
|
|
198
300
|
case "voice-character":
|
|
199
|
-
return buildVoiceCharacterHints((data ?? {}) as Parameters<typeof buildVoiceCharacterHints>[0])
|
|
301
|
+
return buildVoiceCharacterHints((data ?? {}) as Parameters<typeof buildVoiceCharacterHints>[0], mode)
|
|
200
302
|
case "voice-delivery":
|
|
201
|
-
return buildVoiceDeliveryHints((data ?? {}) as Parameters<typeof buildVoiceDeliveryHints>[0])
|
|
303
|
+
return buildVoiceDeliveryHints((data ?? {}) as Parameters<typeof buildVoiceDeliveryHints>[0], mode)
|
|
202
304
|
case "person":
|
|
203
|
-
return buildPersonHints(data).join(", ")
|
|
305
|
+
return buildPersonHints(data, mode).join(", ")
|
|
204
306
|
case "mood":
|
|
205
|
-
return buildMoodHints(data).join(", ")
|
|
307
|
+
return buildMoodHints(data, mode).join(", ")
|
|
206
308
|
case "pose":
|
|
207
|
-
return buildPoseHints(data).join(", ")
|
|
309
|
+
return buildPoseHints(data, mode).join(", ")
|
|
208
310
|
case "styling":
|
|
209
|
-
return buildStylingHints(data).join(", ")
|
|
311
|
+
return buildStylingHints(data, mode).join(", ")
|
|
210
312
|
|
|
211
313
|
case "atmosphere":
|
|
212
|
-
return withCustomText(data, buildAtmosphereHints(data.atmosphere).join(", "))
|
|
314
|
+
return withCustomText(data, buildAtmosphereHints(data.atmosphere, mode).join(", "))
|
|
213
315
|
case "action-fx":
|
|
214
|
-
return withCustomText(data, buildActionFxHints(data.actionFx).join(", "))
|
|
316
|
+
return withCustomText(data, buildActionFxHints(data.actionFx, mode).join(", "))
|
|
215
317
|
case "style":
|
|
216
|
-
return withCustomText(data, getStylePromptHint(asStr(data.style)))
|
|
318
|
+
return withCustomText(data, byMode(mode, getStylePromptHint, getStyleTerm)(asStr(data.style)))
|
|
217
319
|
case "setting":
|
|
218
|
-
return withCustomText(data, getSettingPromptHint(asStr(data.setting)))
|
|
320
|
+
return withCustomText(data, byMode(mode, getSettingPromptHint, getSettingTerm)(asStr(data.setting)))
|
|
219
321
|
case "loop-subject":
|
|
220
|
-
return withCustomText(data, getLoopSubjectPromptHint(asStr(data.loopSubject)))
|
|
322
|
+
return withCustomText(data, byMode(mode, getLoopSubjectPromptHint, getLoopSubjectTerm)(asStr(data.loopSubject)))
|
|
221
323
|
case "material":
|
|
222
|
-
return withCustomText(data, buildMaterialHints(data.material))
|
|
324
|
+
return withCustomText(data, buildMaterialHints(data.material, mode))
|
|
223
325
|
case "animal": {
|
|
224
326
|
const animal = getAnimal(asStr(data.animal))
|
|
225
327
|
return withCustomText(
|
|
226
328
|
data,
|
|
227
|
-
animal
|
|
329
|
+
animal
|
|
330
|
+
? byMode(mode, `featuring a ${animal.label.toLowerCase()}, ${animal.description}`, objectEntityTerm(animal))
|
|
331
|
+
: "",
|
|
228
332
|
)
|
|
229
333
|
}
|
|
230
334
|
case "vehicle": {
|
|
231
335
|
const vehicle = getVehicle(asStr(data.vehicle))
|
|
232
336
|
return withCustomText(
|
|
233
337
|
data,
|
|
234
|
-
vehicle
|
|
338
|
+
vehicle
|
|
339
|
+
? byMode(mode, `featuring a ${vehicle.label.toLowerCase()}, ${vehicle.description}`, objectEntityTerm(vehicle))
|
|
340
|
+
: "",
|
|
235
341
|
)
|
|
236
342
|
}
|
|
237
343
|
case "weapon": {
|
|
238
344
|
const weapon = getWeapon(asStr(data.weapon))
|
|
239
345
|
return withCustomText(
|
|
240
346
|
data,
|
|
241
|
-
weapon
|
|
347
|
+
weapon
|
|
348
|
+
? byMode(mode, `with a ${weapon.label.toLowerCase()}, ${weapon.description}`, objectEntityTerm(weapon))
|
|
349
|
+
: "",
|
|
242
350
|
)
|
|
243
351
|
}
|
|
244
352
|
case "furniture": {
|
|
245
353
|
const furniture = getFurniture(asStr(data.furniture))
|
|
246
354
|
return withCustomText(
|
|
247
355
|
data,
|
|
248
|
-
furniture
|
|
356
|
+
furniture
|
|
357
|
+
? byMode(mode, `including a ${furniture.label.toLowerCase()}, ${furniture.description}`, objectEntityTerm(furniture))
|
|
358
|
+
: "",
|
|
249
359
|
)
|
|
250
360
|
}
|
|
251
361
|
case "photo-genre":
|
|
252
|
-
return withCustomText(data, getPhotoGenrePromptHint(asStr(data.photoGenre)))
|
|
362
|
+
return withCustomText(data, byMode(mode, getPhotoGenrePromptHint, getPhotoGenreTerm)(asStr(data.photoGenre)))
|
|
253
363
|
case "backdrop":
|
|
254
|
-
return withCustomText(data, getBackdropPromptHint(asStr(data.backdrop)))
|
|
364
|
+
return withCustomText(data, byMode(mode, getBackdropPromptHint, getBackdropTerm)(asStr(data.backdrop)))
|
|
255
365
|
case "held-prop":
|
|
256
|
-
return withCustomText(data, buildHeldPropHints(data.heldProp).join(", "))
|
|
366
|
+
return withCustomText(data, buildHeldPropHints(data.heldProp, mode).join(", "))
|
|
257
367
|
case "photographer":
|
|
258
|
-
return withCustomText(data, buildPhotographerHints(data.photographer))
|
|
368
|
+
return withCustomText(data, buildPhotographerHints(data.photographer, mode))
|
|
259
369
|
case "aesthetic":
|
|
260
|
-
return withCustomText(data, buildAestheticHints(data.aesthetic))
|
|
370
|
+
return withCustomText(data, buildAestheticHints(data.aesthetic, mode))
|
|
261
371
|
case "era":
|
|
262
|
-
return withCustomText(data, getEraPromptHint(asStr(data.era)))
|
|
372
|
+
return withCustomText(data, byMode(mode, getEraPromptHint, getEraTerm)(asStr(data.era)))
|
|
263
373
|
case "temporal":
|
|
264
|
-
return withCustomText(data, buildTemporalHints(data).join(", "))
|
|
374
|
+
return withCustomText(data, buildTemporalHints(data, mode).join(", "))
|
|
265
375
|
case "exposure-settings":
|
|
266
|
-
return withCustomText(data, buildExposureHints(data).join(", "))
|
|
376
|
+
return withCustomText(data, buildExposureHints(data, mode).join(", "))
|
|
267
377
|
case "render-quality":
|
|
268
|
-
return withCustomText(data, getRenderQualityPromptHint(asStr(data.renderQuality)))
|
|
378
|
+
return withCustomText(data, byMode(mode, getRenderQualityPromptHint, getRenderQualityTerm)(asStr(data.renderQuality)))
|
|
269
379
|
case "composition-effects":
|
|
270
|
-
return withCustomText(data, getCompositionEffectPromptHint(asStr(data.compositionEffect)))
|
|
380
|
+
return withCustomText(data, byMode(mode, getCompositionEffectPromptHint, getCompositionEffectTerm)(asStr(data.compositionEffect)))
|
|
271
381
|
case "post-process-effects":
|
|
272
|
-
return withCustomText(data, buildPostProcessHints(data.postProcess).join(", "))
|
|
382
|
+
return withCustomText(data, buildPostProcessHints(data.postProcess, mode).join(", "))
|
|
273
383
|
|
|
384
|
+
// Free text authored by the user, not catalog copy — there is no shorter
|
|
385
|
+
// professional form to swap in, so these are identical in both modes.
|
|
274
386
|
case "tone":
|
|
275
387
|
return asStr(data.tone).trim()
|
|
276
388
|
case "style-guide":
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import {
|
|
2
|
+
PEOPLE,
|
|
3
|
+
PERSON_DIMENSION_ORDER,
|
|
4
|
+
PERSON_DIMENSION_LABELS,
|
|
5
|
+
PERSON_FIELD_BY_DIMENSION,
|
|
6
|
+
type Person,
|
|
7
|
+
} from "./person.js"
|
|
8
|
+
import { registerCatalogPack, getRegisteredCatalogPacks } from "./catalog-packs.js"
|
|
9
|
+
import type { PickerDimension, PickerOption } from "./picker-catalogs.js"
|
|
10
|
+
import { resolveTerm } from "./term.js"
|
|
11
|
+
import { setRegisteredPersonPackFields } from "@nodaro/shared"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A person-pack entry. Pack dimensions are new keys OUTSIDE the closed
|
|
15
|
+
* `PersonDimension` union, so `dimension` is widened to `string`.
|
|
16
|
+
*/
|
|
17
|
+
export type RegisteredPersonEntry = Omit<Person, "dimension"> & { dimension: string }
|
|
18
|
+
|
|
19
|
+
export interface PersonPack {
|
|
20
|
+
readonly id: string
|
|
21
|
+
readonly entries: readonly RegisteredPersonEntry[]
|
|
22
|
+
// Pack dimensions are single-select in Phase 0: the selection limit still
|
|
23
|
+
// comes from `getPersonDimensionLimit`, which only knows the base
|
|
24
|
+
// `PersonDimension` union (a pack key resolves to 1), so a per-dimension
|
|
25
|
+
// `maxSelected` would be silently ignored. Omitted rather than accepted-and-
|
|
26
|
+
// dropped; add it here AND in the limit lookup together when multi-select
|
|
27
|
+
// pack dimensions are wired.
|
|
28
|
+
readonly dimensions?: readonly { dimension: string; field: string; label: string }[]
|
|
29
|
+
readonly sidecars?: Parameters<typeof registerCatalogPack>[0]["sidecars"]
|
|
30
|
+
readonly exemptSidecarLocales?: Parameters<typeof registerCatalogPack>[0]["exemptSidecarLocales"]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let personPacks: PersonPack[] = []
|
|
34
|
+
let version = 0
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Push the aggregate of every registered person pack's dimension data-field
|
|
38
|
+
* names into `@nodaro/shared`'s content-free registry, so
|
|
39
|
+
* `getParameterValue(data, "person")` resolves a pack dimension in the
|
|
40
|
+
* `{PersonLabel}` field-mapping fallback (G4). shared never imports prompts —
|
|
41
|
+
* the field list is pushed in here. Empty aggregate ⇒ mainline identity.
|
|
42
|
+
*/
|
|
43
|
+
function syncPersonPackFields(): void {
|
|
44
|
+
setRegisteredPersonPackFields(
|
|
45
|
+
personPacks.flatMap((p) => (p.dimensions ?? []).map((d) => d.field)),
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function registerPersonPack(pack: PersonPack): void {
|
|
50
|
+
if (personPacks.some((p) => p.id === pack.id)) throw new Error(`duplicate person pack id "${pack.id}"`)
|
|
51
|
+
personPacks = [...personPacks, pack]
|
|
52
|
+
version++
|
|
53
|
+
// Fan out to the generic catalog seam so enumeration/projection/localization
|
|
54
|
+
// reflect the person pack too. Build one PickerDimension per new field.
|
|
55
|
+
const dimByKey = new Map((pack.dimensions ?? []).map((d) => [d.dimension, d]))
|
|
56
|
+
const byField = new Map<string, { field: string; label: string; options: PickerOption[] }>()
|
|
57
|
+
for (const e of pack.entries) {
|
|
58
|
+
const dim = dimByKey.get(e.dimension)
|
|
59
|
+
if (!dim) throw new Error(`person pack "${pack.id}" entry "${e.id}" references undeclared dimension "${e.dimension}"`)
|
|
60
|
+
const opt: PickerOption = {
|
|
61
|
+
id: e.id,
|
|
62
|
+
label: e.label,
|
|
63
|
+
description: e.description,
|
|
64
|
+
category: e.group,
|
|
65
|
+
promptHint: e.promptHint,
|
|
66
|
+
term: resolveTerm(e),
|
|
67
|
+
}
|
|
68
|
+
const existing = byField.get(dim.field)
|
|
69
|
+
if (existing) existing.options.push(opt)
|
|
70
|
+
else byField.set(dim.field, { field: dim.field, label: dim.label, options: [opt] })
|
|
71
|
+
}
|
|
72
|
+
registerCatalogPack({
|
|
73
|
+
id: pack.id,
|
|
74
|
+
catalogId: "person",
|
|
75
|
+
mode: "extend",
|
|
76
|
+
dimensions: [...byField.values()] as PickerDimension[],
|
|
77
|
+
sidecars: pack.sidecars,
|
|
78
|
+
exemptSidecarLocales: pack.exemptSidecarLocales,
|
|
79
|
+
})
|
|
80
|
+
syncPersonPackFields()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function resetPersonPacks(): void {
|
|
84
|
+
personPacks = []
|
|
85
|
+
version++
|
|
86
|
+
syncPersonPackFields()
|
|
87
|
+
}
|
|
88
|
+
export function personPacksVersion(): number {
|
|
89
|
+
return version
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Reverse of the registered dimension→field map, for reconstructing entries
|
|
93
|
+
* from a projected (field-keyed) catalog pack back into Person shape. */
|
|
94
|
+
function dimensionByField(): Readonly<Record<string, string>> {
|
|
95
|
+
const out: Record<string, string> = {}
|
|
96
|
+
const map = getRegisteredPersonFieldByDimension()
|
|
97
|
+
for (const dim of Object.keys(map)) out[map[dim]] = dim
|
|
98
|
+
return out
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Best-effort reconstruction of Person entries from a pack's projected
|
|
102
|
+
* (PickerDimension) list. `shortLabel`/swatches are absent in the projection
|
|
103
|
+
* and fall back at render time; the safety-critical id-set is exact. */
|
|
104
|
+
function personEntriesFromDims(
|
|
105
|
+
dims: ReadonlyArray<{
|
|
106
|
+
field: string
|
|
107
|
+
options: ReadonlyArray<{
|
|
108
|
+
id: string
|
|
109
|
+
label: string
|
|
110
|
+
description?: string
|
|
111
|
+
category?: string
|
|
112
|
+
promptHint: string
|
|
113
|
+
}>
|
|
114
|
+
}>,
|
|
115
|
+
): RegisteredPersonEntry[] {
|
|
116
|
+
const byField = dimensionByField()
|
|
117
|
+
const out: RegisteredPersonEntry[] = []
|
|
118
|
+
for (const d of dims) {
|
|
119
|
+
const dimension = byField[d.field] ?? d.field
|
|
120
|
+
for (const o of d.options) {
|
|
121
|
+
out.push({
|
|
122
|
+
id: o.id,
|
|
123
|
+
label: o.label,
|
|
124
|
+
description: o.description ?? "",
|
|
125
|
+
group: o.category,
|
|
126
|
+
promptHint: o.promptHint,
|
|
127
|
+
dimension,
|
|
128
|
+
} as RegisteredPersonEntry)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The registered/composed person taxonomy: base `PEOPLE` folded with every
|
|
136
|
+
* `catalogId:"person"` CatalogPack in registration order (the SAME registry
|
|
137
|
+
* `composePickerCatalogs` folds for `/v1/catalogs` + MCP). `extend` appends the
|
|
138
|
+
* pack's full Person entries, `deny` removes `denyIds`, `replace` swaps to a
|
|
139
|
+
* reconstruction of the vendored catalog. This is the single funnel the
|
|
140
|
+
* picker-ui grids read, so a deploy's deny/replace curation hides base entries
|
|
141
|
+
* in the picker exactly as it hides them in the catalogs projection.
|
|
142
|
+
*/
|
|
143
|
+
export function getRegisteredPeople(): readonly RegisteredPersonEntry[] {
|
|
144
|
+
const personCatalogPacks = getRegisteredCatalogPacks().filter((p) => p.catalogId === "person")
|
|
145
|
+
// Mainline identity on the empty path, matching every sibling getter below
|
|
146
|
+
// (`extra.length === 0 ? BASE : [...]`): with no person packs registered this
|
|
147
|
+
// returns the base PEOPLE reference ITSELF, not a copy — the overlay
|
|
148
|
+
// boot-smoke pins "inert boot" on exactly that identity, and an
|
|
149
|
+
// unconditional copy here is what broke the combined tree after two
|
|
150
|
+
// separately-green merges.
|
|
151
|
+
if (personCatalogPacks.length === 0) return PEOPLE
|
|
152
|
+
let people: RegisteredPersonEntry[] = [...PEOPLE]
|
|
153
|
+
const packsById = new Map(personPacks.map((p) => [p.id, p]))
|
|
154
|
+
for (const pack of personCatalogPacks) {
|
|
155
|
+
if (pack.mode === "extend") {
|
|
156
|
+
// Prefer full Person entries from the source person-pack; fall back to
|
|
157
|
+
// reconstructing from the projected dimensions for a direct extend pack.
|
|
158
|
+
const src = packsById.get(pack.id)
|
|
159
|
+
people = src
|
|
160
|
+
? [...people, ...src.entries]
|
|
161
|
+
: [...people, ...personEntriesFromDims(pack.dimensions ?? [])]
|
|
162
|
+
} else if (pack.mode === "deny") {
|
|
163
|
+
const deny = new Set(pack.denyIds ?? [])
|
|
164
|
+
people = people.filter((e) => !deny.has(e.id))
|
|
165
|
+
} else if (pack.mode === "replace" && pack.catalog) {
|
|
166
|
+
people = personEntriesFromDims(pack.catalog.dimensions ?? [])
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return people
|
|
170
|
+
}
|
|
171
|
+
export function getRegisteredPersonDimensionOrder(): readonly string[] {
|
|
172
|
+
const extra = personPacks.flatMap((p) => (p.dimensions ?? []).map((d) => d.dimension))
|
|
173
|
+
return extra.length === 0 ? PERSON_DIMENSION_ORDER : [...PERSON_DIMENSION_ORDER, ...extra]
|
|
174
|
+
}
|
|
175
|
+
export function getRegisteredPersonFieldByDimension(): Readonly<Record<string, string>> {
|
|
176
|
+
const extra = personPacks.flatMap((p) => (p.dimensions ?? []).map((d) => [d.dimension, d.field] as const))
|
|
177
|
+
return extra.length === 0 ? PERSON_FIELD_BY_DIMENSION : { ...PERSON_FIELD_BY_DIMENSION, ...Object.fromEntries(extra) }
|
|
178
|
+
}
|
|
179
|
+
export function getRegisteredPersonDimensionLabels(): Readonly<Record<string, string>> {
|
|
180
|
+
const extra = personPacks.flatMap((p) => (p.dimensions ?? []).map((d) => [d.dimension, d.label] as const))
|
|
181
|
+
return extra.length === 0 ? PERSON_DIMENSION_LABELS : { ...PERSON_DIMENSION_LABELS, ...Object.fromEntries(extra) }
|
|
182
|
+
}
|