@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,231 @@
|
|
|
1
|
+
import { EntityStyle, PLACEHOLDER_CHARACTER_NAME } from "@nodaro/shared"
|
|
2
|
+
|
|
3
|
+
function effectiveName(name: string): string | undefined {
|
|
4
|
+
const trimmed = name.trim()
|
|
5
|
+
if (!trimmed || trimmed === PLACEHOLDER_CHARACTER_NAME) return undefined
|
|
6
|
+
return trimmed
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CharacterPromptInput {
|
|
10
|
+
name: string
|
|
11
|
+
description?: string
|
|
12
|
+
gender?: string
|
|
13
|
+
style?: EntityStyle | string
|
|
14
|
+
baseOutfit?: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildCharacterPrompt(input: CharacterPromptInput): string {
|
|
18
|
+
const charDesc = [effectiveName(input.name), input.gender, input.description].filter(Boolean).join(", ")
|
|
19
|
+
const outfitDesc = input.baseOutfit ? `, wearing ${input.baseOutfit}` : ""
|
|
20
|
+
const styleDesc = input.style ?? "realistic"
|
|
21
|
+
return [
|
|
22
|
+
`${charDesc}${outfitDesc},`,
|
|
23
|
+
`${styleDesc} style, front view, looking at camera,`,
|
|
24
|
+
"full body portrait, 4k, highly detailed, clean background.",
|
|
25
|
+
].join(" ")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ObjectPromptInput {
|
|
29
|
+
name: string
|
|
30
|
+
description?: string
|
|
31
|
+
category?: string
|
|
32
|
+
style?: EntityStyle | string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function buildObjectPrompt(input: ObjectPromptInput): string {
|
|
36
|
+
const categoryDesc = input.category ?? "object"
|
|
37
|
+
const descPart = input.description ? `, ${input.description}` : ""
|
|
38
|
+
const styleDesc = input.style ?? "realistic"
|
|
39
|
+
return [
|
|
40
|
+
`Single ${categoryDesc} ${input.name}${descPart},`,
|
|
41
|
+
`${styleDesc} art style, front view,`,
|
|
42
|
+
"4k, highly detailed, white/plain background, no text, no labels, no watermarks, product photography style.",
|
|
43
|
+
].join(" ")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface CreaturePromptInput {
|
|
47
|
+
name: string
|
|
48
|
+
description?: string
|
|
49
|
+
/** Free-text species/type (e.g. "dragon", "wolf") — the creature delta vs object. */
|
|
50
|
+
species?: string
|
|
51
|
+
category?: string
|
|
52
|
+
style?: EntityStyle | string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creature establishing-shot prompt. Mirrors {@link buildObjectPrompt} but leads
|
|
57
|
+
* with the free-text `species` (a dragon/wolf/etc. IS the subject) and frames the
|
|
58
|
+
* subject as a living creature rather than a product. Falls back to "creature"
|
|
59
|
+
* when no species is given so the prompt always names a subject.
|
|
60
|
+
*/
|
|
61
|
+
export function buildCreaturePrompt(input: CreaturePromptInput): string {
|
|
62
|
+
const subject = input.species?.trim() || "creature"
|
|
63
|
+
const descPart = input.description ? `, ${input.description}` : ""
|
|
64
|
+
const styleDesc = input.style ?? "realistic"
|
|
65
|
+
return [
|
|
66
|
+
`Single ${subject}, ${input.name}${descPart},`,
|
|
67
|
+
`${styleDesc} art style, full body, front view,`,
|
|
68
|
+
"4k, highly detailed, plain background, no text, no labels, no watermarks.",
|
|
69
|
+
].join(" ")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface LocationPromptInput {
|
|
73
|
+
name: string
|
|
74
|
+
description?: string
|
|
75
|
+
category?: string
|
|
76
|
+
style?: EntityStyle | string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function buildLocationPrompt(input: LocationPromptInput): string {
|
|
80
|
+
const categoryDesc = input.category ?? "location"
|
|
81
|
+
const descPart = input.description ? `, ${input.description}` : ""
|
|
82
|
+
const styleDesc = input.style ?? "realistic"
|
|
83
|
+
return [
|
|
84
|
+
`${categoryDesc} scene, ${input.name}${descPart},`,
|
|
85
|
+
`${styleDesc} art style,`,
|
|
86
|
+
"wide establishing shot, 4k, highly detailed, cinematic lighting, no people, no text, no labels, no watermarks.",
|
|
87
|
+
].join(" ")
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface LocationRefinePromptInput {
|
|
91
|
+
/**
|
|
92
|
+
* The user's TRANSIENT edit/refine instruction (e.g. "make it night, add
|
|
93
|
+
* drifting fog"). Used only to build this one generation's prompt - it is
|
|
94
|
+
* never written back to the location row, so the stored name / description /
|
|
95
|
+
* canonical description stay untouched. This is the deliberate contrast with
|
|
96
|
+
* `LocationPromptInput.description`, which IS the persisted scene description
|
|
97
|
+
* that `buildLocationPrompt` weaves in.
|
|
98
|
+
*/
|
|
99
|
+
editPrompt: string
|
|
100
|
+
style?: EntityStyle | string
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Build a TRANSIENT prompt from a user-supplied edit/refine instruction.
|
|
105
|
+
*
|
|
106
|
+
* Used by `POST /v1/generate-location` when the caller passes `userPrompt`
|
|
107
|
+
* (the studio's "describe changes" / sparkle Edit flow). Paired with an i2i
|
|
108
|
+
* `sourceImageUrl` the provider edits the source establishing shot toward the
|
|
109
|
+
* instruction; with no source image it reads as a from-scratch custom scene
|
|
110
|
+
* prompt. Mirrors `characters.generate`'s seed/edit-prompt path, but locations
|
|
111
|
+
* have no stored seed prompt - this output is purely transient and the route
|
|
112
|
+
* persists nothing derived from it back to the row.
|
|
113
|
+
*/
|
|
114
|
+
export function buildLocationRefinePrompt(input: LocationRefinePromptInput): string {
|
|
115
|
+
const instruction = input.editPrompt.trim()
|
|
116
|
+
const styleDesc = input.style ?? "realistic"
|
|
117
|
+
return [
|
|
118
|
+
`${instruction},`,
|
|
119
|
+
`${styleDesc} art style, wide establishing shot,`,
|
|
120
|
+
"4k, highly detailed, cinematic lighting, no text, no labels, no watermarks.",
|
|
121
|
+
].join(" ")
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface FacePromptInput {
|
|
125
|
+
name: string
|
|
126
|
+
description?: string
|
|
127
|
+
style?: EntityStyle | string
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Face prompt uses the "face-generation" template (resolved via prompt-templates.ts).
|
|
132
|
+
* Returns the template inputs so callers can call resolveTemplate + applyTemplate.
|
|
133
|
+
*/
|
|
134
|
+
export function buildFaceTemplateInputs(input: FacePromptInput): {
|
|
135
|
+
description: string
|
|
136
|
+
style: string
|
|
137
|
+
} {
|
|
138
|
+
const descParts = [input.name, input.description].filter(Boolean).join(", ")
|
|
139
|
+
return { description: descParts, style: input.style ?? "realistic" }
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface CharacterMotionPromptInput {
|
|
143
|
+
name: string
|
|
144
|
+
description?: string
|
|
145
|
+
gender?: string
|
|
146
|
+
style?: EntityStyle | string
|
|
147
|
+
baseOutfit?: string
|
|
148
|
+
motionPrompt: string
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function buildMotionPrompt(input: CharacterMotionPromptInput): string {
|
|
152
|
+
const charDesc = [effectiveName(input.name), input.gender, input.description].filter(Boolean).join(", ")
|
|
153
|
+
const outfitDesc = input.baseOutfit ? `, wearing ${input.baseOutfit}` : ""
|
|
154
|
+
const styleDesc = input.style ?? "realistic"
|
|
155
|
+
const charPart = charDesc ? `${charDesc}${outfitDesc}, ` : ""
|
|
156
|
+
return `${charPart}${input.motionPrompt}. ${styleDesc} style.`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Input shape for buildLocationMotionPrompt.
|
|
161
|
+
*
|
|
162
|
+
* Mirrors CharacterMotionPromptInput's role. `canonicalDescription` is preferred
|
|
163
|
+
* (LLM-authored from the approved main image) but the helper falls back to
|
|
164
|
+
* category+name if not yet set, and to a generic placeholder if both are absent.
|
|
165
|
+
*/
|
|
166
|
+
export interface LocationMotionPromptInput {
|
|
167
|
+
name: string
|
|
168
|
+
category?: string
|
|
169
|
+
style?: EntityStyle | string
|
|
170
|
+
motionPrompt: string
|
|
171
|
+
canonicalDescription?: string
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build the prompt sent to the i2v provider for a location atmosphere clip.
|
|
176
|
+
*
|
|
177
|
+
* Note: character's analog is named `buildMotionPrompt` (historical); location
|
|
178
|
+
* uses the more specific `buildLocationMotionPrompt`.
|
|
179
|
+
*/
|
|
180
|
+
export function buildLocationMotionPrompt(input: LocationMotionPromptInput): string {
|
|
181
|
+
const sceneDesc =
|
|
182
|
+
input.canonicalDescription?.trim() ||
|
|
183
|
+
[input.category, input.name].filter(Boolean).join(", ").trim() ||
|
|
184
|
+
"A generic location"
|
|
185
|
+
return `${sceneDesc}. Camera move: ${input.motionPrompt}. ${input.style ?? "realistic"} style. Slow, ambient, cinematic.`
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Object asset-type enum — the kinds of variant a user can generate off an
|
|
190
|
+
* object's anchor main image. Mirrors the literal accepted by
|
|
191
|
+
* `POST /v1/generate-object-asset` (`backend/src/routes/generate-object-asset.ts`)
|
|
192
|
+
* and consumed by the MCP `generate_object` verb (kind="asset").
|
|
193
|
+
*
|
|
194
|
+
* The `motion` value is reserved for type-system exhaustiveness on the
|
|
195
|
+
* frontend; the route rejects it because motion variants flow through the
|
|
196
|
+
* dedicated `/v1/generate-object-motion` endpoint (worker-side it's a different
|
|
197
|
+
* BullMQ job type). `custom` is the free-form bucket — callers must supply
|
|
198
|
+
* `attachToColumn` explicitly since the worker can't infer it.
|
|
199
|
+
*/
|
|
200
|
+
export interface ObjectMotionPromptInput {
|
|
201
|
+
name: string
|
|
202
|
+
category?: string
|
|
203
|
+
style?: EntityStyle | string
|
|
204
|
+
motionPrompt: string
|
|
205
|
+
canonicalDescription?: string
|
|
206
|
+
seedPromptHint?: string
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build the prompt sent to the i2v provider for an object atmosphere/motion clip.
|
|
211
|
+
*
|
|
212
|
+
* Naming note: character's analog is `buildMotionPrompt`; location uses
|
|
213
|
+
* `buildLocationMotionPrompt`; object uses `buildObjectMotionPrompt` to make
|
|
214
|
+
* the entity type explicit at call sites.
|
|
215
|
+
*
|
|
216
|
+
* The `seedPromptHint` is appended verbatim — Phase C's route layer composes
|
|
217
|
+
* wired-picker hints (Material/Animal/Vehicle/Weapon/Furniture) into this
|
|
218
|
+
* field before passing the input. Empty hint = no-op (the trailing dot still
|
|
219
|
+
* reads cleanly: "...motion. " not "...motion. .").
|
|
220
|
+
*/
|
|
221
|
+
export function buildObjectMotionPrompt(input: ObjectMotionPromptInput): string {
|
|
222
|
+
const objDesc =
|
|
223
|
+
input.canonicalDescription?.trim() ||
|
|
224
|
+
[input.category, input.name].filter(Boolean).join(", ").trim() ||
|
|
225
|
+
"A generic object"
|
|
226
|
+
const baseStyle = input.style ?? "realistic"
|
|
227
|
+
const seedSuffix = input.seedPromptHint?.trim()
|
|
228
|
+
? `. ${input.seedPromptHint.trim()}`
|
|
229
|
+
: ""
|
|
230
|
+
return `${objDesc}. Motion: ${input.motionPrompt}. ${baseStyle} style. Smooth, controlled, product-showcase quality${seedSuffix}.`
|
|
231
|
+
}
|
package/src/era.ts
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical catalog of Era / Period choices.
|
|
3
|
+
*
|
|
4
|
+
* Single-pick parameter node — user picks ONE historical era or speculative
|
|
5
|
+
* period. Each promptHint bundles wardrobe + environment + photographic
|
|
6
|
+
* treatment so the model gets a coherent world rather than a stray decade
|
|
7
|
+
* label. Color, lighting, grain and signature props are all part of the
|
|
8
|
+
* descriptor because period-piece looks live or die on those cues.
|
|
9
|
+
*
|
|
10
|
+
* Categories:
|
|
11
|
+
* - decade-20c: 20th-century decades from 1920s flapper to 2000s tabloid
|
|
12
|
+
* - pre-modern: pre-20th-century historical eras (medieval -> Edwardian)
|
|
13
|
+
* - speculative: future, retrofuture, post-apocalyptic, dieselpunk etc.
|
|
14
|
+
*
|
|
15
|
+
* Shared between the picker UI, the standalone Era parameter node, and the
|
|
16
|
+
* prompt-hint injection on both the frontend DAG executor and the backend
|
|
17
|
+
* orchestrator.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export type EraCategory = "decade-20c" | "pre-modern" | "speculative"
|
|
21
|
+
|
|
22
|
+
export interface Era {
|
|
23
|
+
readonly id: string
|
|
24
|
+
readonly label: string
|
|
25
|
+
readonly category: EraCategory
|
|
26
|
+
readonly description: string
|
|
27
|
+
readonly promptHint: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const ERAS: ReadonlyArray<Era> = [
|
|
31
|
+
// -------------------- 20th-century decades --------------------
|
|
32
|
+
{
|
|
33
|
+
id: "1920s-flapper",
|
|
34
|
+
label: "1920s Flapper",
|
|
35
|
+
category: "decade-20c",
|
|
36
|
+
description: "Jazz-age speakeasy glamour",
|
|
37
|
+
promptHint:
|
|
38
|
+
"set in the 1920s flapper era — beaded drop-waist dresses, finger waves, feathered headbands and long pearl ropes in smoky speakeasies, with sepia-toned silver-rich monochrome and soft halation",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "1930s-art-deco",
|
|
42
|
+
label: "1930s Art Deco",
|
|
43
|
+
category: "decade-20c",
|
|
44
|
+
description: "Streamlined deco glamour",
|
|
45
|
+
promptHint:
|
|
46
|
+
"set in the 1930s art-deco era — bias-cut satin gowns, wide pinstripe suits and chrome geometric interiors, with dramatic theatrical key light and a polished black-and-white tonality",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "1940s-wartime",
|
|
50
|
+
label: "1940s Wartime",
|
|
51
|
+
category: "decade-20c",
|
|
52
|
+
description: "Wartime utility and victory rolls",
|
|
53
|
+
promptHint:
|
|
54
|
+
"set in the 1940s wartime era — utility dresses, victory rolls, seamed stockings and military uniforms in train stations and home-front kitchens, with warm sepia tones and gentle film grain",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "1950s-diner",
|
|
58
|
+
label: "1950s Diner / Pin-up",
|
|
59
|
+
category: "decade-20c",
|
|
60
|
+
description: "Chrome diners and bouffant pin-ups",
|
|
61
|
+
promptHint:
|
|
62
|
+
"set in the 1950s diner pin-up era — full circle skirts, cropped denim, leather jackets and bouffant hair in chrome diners and drive-ins, with saturated Kodachrome reds and turquoise",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
id: "atomic-age-50s",
|
|
66
|
+
label: "Atomic Age 50s",
|
|
67
|
+
category: "decade-20c",
|
|
68
|
+
description: "1950s sci-fi futurism and Cold War anxiety",
|
|
69
|
+
promptHint:
|
|
70
|
+
"set in the 1950s atomic-age era — ranch-style suburban homes, atomic starburst ornaments, finned appliances, B-movie sci-fi posters and Cold War civil-defense propaganda, with bright pulpy Technicolor saturation and hard mid-century optimism shot through with quiet nuclear dread",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "1960s-mod",
|
|
74
|
+
label: "1960s Mod",
|
|
75
|
+
category: "decade-20c",
|
|
76
|
+
description: "Swinging-London mod graphic",
|
|
77
|
+
promptHint:
|
|
78
|
+
"set in the 1960s mod era — geometric A-line dresses, op-art prints, white go-go boots and graphic eyeliner on mini-skirted Carnaby Street, with crisp graphic color and bright daylight",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "1970s-disco",
|
|
82
|
+
label: "1970s Disco",
|
|
83
|
+
category: "decade-20c",
|
|
84
|
+
description: "Studio-54 mirror-ball glitter",
|
|
85
|
+
promptHint:
|
|
86
|
+
"set in the 1970s disco era — sequined jumpsuits, wide-collar shirts, flares and feathered hair in mirror-ball nightclubs, with hot strobe-saturated color and lens-flared neon",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "1980s-neon",
|
|
90
|
+
label: "1980s Neon",
|
|
91
|
+
category: "decade-20c",
|
|
92
|
+
description: "Power-suit MTV neon excess",
|
|
93
|
+
promptHint:
|
|
94
|
+
"set in the 1980s neon era — power-shoulder blazers, leg warmers, perms and bright neon eye shadow under hard pink-and-blue gels, with chromatic-aberration neon color and grain",
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "1990s-mall",
|
|
98
|
+
label: "1990s Mall",
|
|
99
|
+
category: "decade-20c",
|
|
100
|
+
description: "Mall-rat grunge-and-pop nineties",
|
|
101
|
+
promptHint:
|
|
102
|
+
"set in the 1990s mall era — denim and crop tops, slip dresses, chokers and platform sneakers under fluorescent food-court lighting, with slight VHS-grain and a warm disposable-camera flash cast",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "2000s-y2k",
|
|
106
|
+
label: "2000s Tabloid / Y2K",
|
|
107
|
+
category: "decade-20c",
|
|
108
|
+
description: "Paparazzi-flash low-rise tabloid",
|
|
109
|
+
promptHint:
|
|
110
|
+
"set in the 2000s tabloid Y2K era — low-rise jeans, butterfly tops, trucker hats and chrome flip phones outside celebrity nightclubs, with harsh paparazzi flash, deep blacks and faint chromatic gloss",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: "gen-z-2020s",
|
|
114
|
+
label: "Gen Z 2020s",
|
|
115
|
+
category: "decade-20c",
|
|
116
|
+
description: "Phone-first TikTok-coded current era",
|
|
117
|
+
promptHint:
|
|
118
|
+
"set in the Gen Z 2020s era — phone-first vertical composition, TikTok-coded styling with baggy denim, Y2K nostalgia revivals, claw clips and oversized headphones, micro-trend churn and the saturated front-camera glow of contemporary social-media aesthetics",
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// -------------------- Pre-modern --------------------
|
|
122
|
+
{
|
|
123
|
+
id: "medieval",
|
|
124
|
+
label: "Medieval",
|
|
125
|
+
category: "pre-modern",
|
|
126
|
+
description: "Stone-castle European Middle Ages",
|
|
127
|
+
promptHint:
|
|
128
|
+
"set in the medieval era — heavy linen tunics, woolen cloaks, leather belts and chain-mail in candlelit stone halls and muddy villages, with smoky firelight color and earthen pigment",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: "renaissance",
|
|
132
|
+
label: "Renaissance",
|
|
133
|
+
category: "pre-modern",
|
|
134
|
+
description: "Florentine velvet-and-fresco grandeur",
|
|
135
|
+
promptHint:
|
|
136
|
+
"set in the Renaissance era — velvet doublets, brocade gowns, pearl headdresses and fresco-painted halls, with chiaroscuro candle light and rich oil-painting color",
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: "victorian",
|
|
140
|
+
label: "Victorian",
|
|
141
|
+
category: "pre-modern",
|
|
142
|
+
description: "Gaslit corseted-and-lace 19th-century",
|
|
143
|
+
promptHint:
|
|
144
|
+
"set in the Victorian era — corseted bustled gowns, top hats, lace gloves and pocket watches on cobbled gaslit London streets, with sepia-toned monochrome and dense atmospheric fog",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: "edwardian",
|
|
148
|
+
label: "Edwardian",
|
|
149
|
+
category: "pre-modern",
|
|
150
|
+
description: "Belle-epoque tea-garden refinement",
|
|
151
|
+
promptHint:
|
|
152
|
+
"set in the Edwardian era — high-collared tea dresses, parasols, three-piece suits and pocket squares in manicured tea gardens, with soft warm afternoon light and gentle pastel color",
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: "wild-west",
|
|
156
|
+
label: "Wild West",
|
|
157
|
+
category: "pre-modern",
|
|
158
|
+
description: "Sun-baked frontier-cowboy Americana",
|
|
159
|
+
promptHint:
|
|
160
|
+
"set in the Wild West era — leather dusters, wide-brim hats, prairie dresses and tin-stars on dusty saloon porches and red-rock canyons, with hot orange-and-teal grading and sun-bleached grain",
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: "ancient-rome",
|
|
164
|
+
label: "Ancient Rome",
|
|
165
|
+
category: "pre-modern",
|
|
166
|
+
description: "Marble-columned imperial Rome",
|
|
167
|
+
promptHint:
|
|
168
|
+
"set in ancient Rome — draped togas, leather sandals, gilded laurel wreaths and bronze armor among marble columns and frescoed villas, with warm Mediterranean sun and umber-and-ochre color",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "ancient-egypt",
|
|
172
|
+
label: "Ancient Egypt",
|
|
173
|
+
category: "pre-modern",
|
|
174
|
+
description: "Pharaonic gold-and-linen Nile",
|
|
175
|
+
promptHint:
|
|
176
|
+
"set in ancient Egypt — pleated linen kalasiris, gold collars, kohl eyeliner and uraeus crowns beside sandstone temples and Nile reeds, with hot sand-yellow color and crisp midday shadows",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "feudal-japan",
|
|
180
|
+
label: "Feudal Japan",
|
|
181
|
+
category: "pre-modern",
|
|
182
|
+
description: "Edo-period samurai-and-geisha",
|
|
183
|
+
promptHint:
|
|
184
|
+
"set in feudal Japan — layered kimono, wooden geta, samurai armor and paper lanterns in shoji-screened tea houses and snowy castle courtyards, with cool ink-wash color and quiet fog",
|
|
185
|
+
},
|
|
186
|
+
{
|
|
187
|
+
id: "fin-de-siecle",
|
|
188
|
+
label: "Fin-de-siècle (1895-1905)",
|
|
189
|
+
category: "pre-modern",
|
|
190
|
+
description: "Turn-of-century Belle Epoque elegance",
|
|
191
|
+
promptHint:
|
|
192
|
+
"set in the fin-de-siècle turn-of-century era — art-nouveau curves, gaslight glow, corseted high-collared gowns, top hats and cane-and-cloak in proto-modern Belle Epoque Paris and Vienna, with warm gas-flame amber color and soft sepia halation",
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
id: "roaring-prewar",
|
|
196
|
+
label: "Pre-War Roaring",
|
|
197
|
+
category: "pre-modern",
|
|
198
|
+
description: "Late-1910s art-nouveau cusp",
|
|
199
|
+
promptHint:
|
|
200
|
+
"set in the late-1910s pre-war era — art-nouveau drapery, chignon hair, lace collars and cane-and-cloak in gas-lit Paris cafes, with sepia-bronze monochrome and soft halation",
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
// -------------------- Speculative --------------------
|
|
204
|
+
{
|
|
205
|
+
id: "near-future",
|
|
206
|
+
label: "Near-Future",
|
|
207
|
+
category: "speculative",
|
|
208
|
+
description: "Plausible 5-to-15-years-ahead",
|
|
209
|
+
promptHint:
|
|
210
|
+
"set in a plausible near-future — minimalist technical fabrics, slim AR earpieces and clean modular interiors with subtle ambient-screen glow, with cool desaturated color and crisp digital clarity",
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: "far-future",
|
|
214
|
+
label: "Far-Future",
|
|
215
|
+
category: "speculative",
|
|
216
|
+
description: "Centuries-ahead spacefaring",
|
|
217
|
+
promptHint:
|
|
218
|
+
"set in a far-future spacefaring era — sleek bodysuits, exo-skeletons and holographic visors in arcology corridors and starship bridges, with luminous bioplastic color and volumetric LED light",
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
id: "dieselpunk",
|
|
222
|
+
label: "Dieselpunk",
|
|
223
|
+
category: "speculative",
|
|
224
|
+
description: "1930s-40s industrial alt-history",
|
|
225
|
+
promptHint:
|
|
226
|
+
"set in a dieselpunk alt-history — riveted leather jackets, brass goggles, military trench coats and oil-stained mechanic boots in soot-blackened industrial cities, with smoky amber-grey color",
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
id: "atompunk",
|
|
230
|
+
label: "Atompunk",
|
|
231
|
+
category: "speculative",
|
|
232
|
+
description: "1950s-future space-age optimism",
|
|
233
|
+
promptHint:
|
|
234
|
+
"set in an atompunk 1950s-future — chrome-finned dresses, ray-gun props, mid-century furniture and tail-finned cars under candy-pastel skies, with clean Technicolor saturation",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "cyberpunk-future",
|
|
238
|
+
label: "Cyberpunk Future",
|
|
239
|
+
category: "speculative",
|
|
240
|
+
description: "Neon-megacity high-tech low-life",
|
|
241
|
+
promptHint:
|
|
242
|
+
"set in a cyberpunk future — chrome-trimmed jackets, holographic eyewear, mesh tops and cybernetic implants in neon mega-city alleys, with electric magenta-cyan color and rain-soaked reflections",
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
id: "post-apocalyptic",
|
|
246
|
+
label: "Post-Apocalyptic",
|
|
247
|
+
category: "speculative",
|
|
248
|
+
description: "Scavenger wasteland survival",
|
|
249
|
+
promptHint:
|
|
250
|
+
"set in a post-apocalyptic wasteland — patched leather, gas masks, scavenged armor and rust-stained scarves in ruined highways and broken cityscapes, with desaturated dust-orange color and gritty grain",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
id: "retrofuturism",
|
|
254
|
+
label: "Retrofuturism",
|
|
255
|
+
category: "speculative",
|
|
256
|
+
description: "Yesterday's tomorrow nostalgia",
|
|
257
|
+
promptHint:
|
|
258
|
+
"set in a retrofuturist tomorrow — silver lame jumpsuits, bubble helmets, aerodynamic furniture and analog spaceport interiors, with optimistic Technicolor saturation and chrome-bright highlights",
|
|
259
|
+
},
|
|
260
|
+
] as const
|
|
261
|
+
|
|
262
|
+
const eraById = new Map<string, Era>(ERAS.map((e) => [e.id, e]))
|
|
263
|
+
|
|
264
|
+
export function getEra(id: string | undefined | null): Era | undefined {
|
|
265
|
+
if (!id) return undefined
|
|
266
|
+
return eraById.get(id)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function getEraLabel(id: string | undefined | null, fallback?: string): string {
|
|
270
|
+
const e = getEra(id)
|
|
271
|
+
if (e) return e.label
|
|
272
|
+
if (fallback !== undefined) return fallback
|
|
273
|
+
return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function getEraPromptHint(id: string | undefined | null): string {
|
|
277
|
+
return getEra(id)?.promptHint ?? ""
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export const ERA_IDS: ReadonlyArray<string> = ERAS.map((e) => e.id)
|
|
281
|
+
|
|
282
|
+
export const ERA_CATEGORY_LABELS: Readonly<Record<EraCategory, string>> = {
|
|
283
|
+
"decade-20c": "20th-Century Decade",
|
|
284
|
+
"pre-modern": "Pre-Modern",
|
|
285
|
+
speculative: "Speculative",
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export const ERA_CATEGORY_ORDER: ReadonlyArray<EraCategory> = [
|
|
289
|
+
"decade-20c",
|
|
290
|
+
"pre-modern",
|
|
291
|
+
"speculative",
|
|
292
|
+
]
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical catalog of Exposure Settings choices.
|
|
3
|
+
*
|
|
4
|
+
* Exposure Settings is a multi-category dimension covering the core photographic
|
|
5
|
+
* triangle that controls exposure and visual character: aperture (depth of field
|
|
6
|
+
* and subject isolation), shutter speed (motion treatment), and ISO sensitivity
|
|
7
|
+
* (grain quality and noise). Independent of optics (lens), lighting, framing,
|
|
8
|
+
* camera motion, and capture medium (camera-format).
|
|
9
|
+
*
|
|
10
|
+
* Each picked entry contributes a focused descriptive clause — the whole point
|
|
11
|
+
* is to give the model a strong technical hint about what the photographer
|
|
12
|
+
* "set the dial to", and the visual consequence of that choice.
|
|
13
|
+
*
|
|
14
|
+
* Typical applications: portraiture wide-aperture isolation, action freeze with
|
|
15
|
+
* fast shutter, gritty high-ISO grain, light-trail long exposure, etc.
|
|
16
|
+
*
|
|
17
|
+
* Shared between the picker UI, the standalone Exposure Settings parameter
|
|
18
|
+
* node, and the prompt-hint injection on both the frontend DAG executor and
|
|
19
|
+
* the backend orchestrator.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type ExposureCategory = "aperture" | "shutter-speed" | "iso"
|
|
23
|
+
|
|
24
|
+
export interface ExposureSettings {
|
|
25
|
+
readonly id: string
|
|
26
|
+
readonly label: string
|
|
27
|
+
readonly category: ExposureCategory
|
|
28
|
+
readonly description: string
|
|
29
|
+
readonly promptHint: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const EXPOSURE_SETTINGS: ReadonlyArray<ExposureSettings> = [
|
|
33
|
+
// ---------------------------- Aperture ----------------------------
|
|
34
|
+
// Wide-open primes through medium tele, then deep landscape stops.
|
|
35
|
+
{ id: "aperture-f1-2", label: "f/1.2", category: "aperture", description: "Razor-thin DOF, dreamy bokeh", promptHint: "shot wide open at f/1.2 — paper-thin depth of field with the subject's eyes in razor focus and everything else dissolving into creamy bokeh" },
|
|
36
|
+
{ id: "aperture-f1-4", label: "f/1.4", category: "aperture", description: "Aggressive subject isolation", promptHint: "shot at f/1.4 — extremely shallow depth of field, aggressive subject isolation against a smoothly melted background" },
|
|
37
|
+
{ id: "aperture-f1-8", label: "f/1.8", category: "aperture", description: "Classic portrait separation", promptHint: "shot at f/1.8 — shallow depth of field with the classic portrait separation between subject and softly defocused background" },
|
|
38
|
+
{ id: "aperture-f2-8", label: "f/2.8", category: "aperture", description: "Subject sharp, BG soft", promptHint: "shot at f/2.8 — subject crisply sharp with a gently defocused background, working aperture for low-light portraiture" },
|
|
39
|
+
{ id: "aperture-f4", label: "f/4", category: "aperture", description: "Balanced everyday DOF", promptHint: "shot at f/4 — balanced depth of field where the subject is fully in focus and the background is suggested rather than detailed" },
|
|
40
|
+
{ id: "aperture-f5-6", label: "f/5.6", category: "aperture", description: "Sharp across the subject", promptHint: "shot at f/5.6 — comfortable working aperture with edge-to-edge subject sharpness and a moderately rendered background" },
|
|
41
|
+
{ id: "aperture-f8", label: "f/8", category: "aperture", description: "Sweet-spot sharpness", promptHint: "shot at f/8 — peak optical sharpness across the frame, the photographer's sweet spot for storytelling and editorial work" },
|
|
42
|
+
{ id: "aperture-f11", label: "f/11", category: "aperture", description: "Deep landscape DOF", promptHint: "shot at f/11 — deep depth of field with foreground and distant elements both rendered crisply, classic landscape stop" },
|
|
43
|
+
{ id: "aperture-f16", label: "f/16", category: "aperture", description: "Hyperfocal, sun-stars", promptHint: "shot stopped down to f/16 — hyperfocal depth of field with everything sharp, sunlight rendered as crisp diffraction stars on bright highlights" },
|
|
44
|
+
|
|
45
|
+
// ---------------------------- Shutter Speed ----------------------------
|
|
46
|
+
{ id: "shutter-1-30", label: "1/30 (handheld blur)", category: "shutter-speed", description: "Hint of handheld motion", promptHint: "captured at 1/30s — a hint of handheld motion blur on moving subjects, slight camera shake suggesting an in-the-moment documentary feel" },
|
|
47
|
+
{ id: "shutter-1-60", label: "1/60", category: "shutter-speed", description: "Standard everyday shutter", promptHint: "captured at 1/60s — standard everyday shutter speed, sharp on still subjects with subtle blur on fast motion" },
|
|
48
|
+
{ id: "shutter-1-200", label: "1/200", category: "shutter-speed", description: "Crisp on most subjects", promptHint: "captured at 1/200s — crisp on most subjects, the working shutter speed for portraits and general photography" },
|
|
49
|
+
{ id: "shutter-1-500", label: "1/500", category: "shutter-speed", description: "Sharp on quick action", promptHint: "captured at 1/500s — sharp rendering of quick human action, hair and fabric mid-motion frozen with clean edges" },
|
|
50
|
+
{ id: "shutter-1-1000", label: "1/1000 (action freeze)", category: "shutter-speed", description: "Frozen sports/wildlife", promptHint: "captured at 1/1000s — frozen mid-air action, water droplets suspended in space, every fast motion crystallized with clinical sharpness" },
|
|
51
|
+
{ id: "shutter-long-1s", label: "Long exposure (1s)", category: "shutter-speed", description: "Streaks and motion trails", promptHint: "captured with a one-second long exposure — flowing motion rendered as smooth light trails and streaks, static subjects sharp while moving elements paint across the frame" },
|
|
52
|
+
|
|
53
|
+
// ---------------------------- ISO ----------------------------
|
|
54
|
+
{ id: "iso-100", label: "ISO 100 (clean)", category: "iso", description: "Minimal noise, fine grain", promptHint: "ISO 100 — pristinely clean image with virtually no noise, ultra-fine grain structure and rich tonal latitude" },
|
|
55
|
+
{ id: "iso-400", label: "ISO 400", category: "iso", description: "Slight texture, daily-driver ISO", promptHint: "ISO 400 — subtle film-like grain texture, the daily-driver sensitivity that retains detail with a hint of organic noise" },
|
|
56
|
+
{ id: "iso-800", label: "ISO 800", category: "iso", description: "Visible but pleasant grain", promptHint: "ISO 800 — visible but pleasant grain pattern, evening-indoor sensitivity with a touch of analog character" },
|
|
57
|
+
{ id: "iso-1600", label: "ISO 1600 (visible grain)", category: "iso", description: "Editorial low-light texture", promptHint: "ISO 1600 — clearly visible grain, editorial low-light feel with rich texture and slightly muted shadows" },
|
|
58
|
+
{ id: "iso-3200", label: "ISO 3200 (heavy grain)", category: "iso", description: "Pushed, gritty documentary feel", promptHint: "ISO 3200 — heavy push-processed grain, gritty documentary character with elevated shadow noise and a raw, journalistic texture" },
|
|
59
|
+
] as const
|
|
60
|
+
|
|
61
|
+
export const EXPOSURE_CATEGORY_ORDER: ReadonlyArray<ExposureCategory> = [
|
|
62
|
+
"aperture",
|
|
63
|
+
"shutter-speed",
|
|
64
|
+
"iso",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
export const EXPOSURE_CATEGORY_LABELS: Record<ExposureCategory, string> = {
|
|
68
|
+
aperture: "Aperture",
|
|
69
|
+
"shutter-speed": "Shutter Speed",
|
|
70
|
+
iso: "ISO",
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const exposureById = new Map<string, ExposureSettings>(
|
|
74
|
+
EXPOSURE_SETTINGS.map((e) => [e.id, e]),
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
export function getExposure(id: string | undefined | null): ExposureSettings | undefined {
|
|
78
|
+
if (!id) return undefined
|
|
79
|
+
return exposureById.get(id)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function getExposureLabel(id: string | undefined | null, fallback?: string): string {
|
|
83
|
+
const e = getExposure(id)
|
|
84
|
+
if (e) return e.label
|
|
85
|
+
if (fallback !== undefined) return fallback
|
|
86
|
+
return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function getExposurePromptHint(id: string | undefined | null): string {
|
|
90
|
+
return getExposure(id)?.promptHint ?? ""
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export const EXPOSURE_IDS: ReadonlyArray<string> = EXPOSURE_SETTINGS.map((e) => e.id)
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Maps each ExposureCategory to the consumer data field name that holds the
|
|
97
|
+
* selected entry id for that category. Multi-category exposure: a consumer
|
|
98
|
+
* (image/video) can independently set a value in each of the 3 dimensions.
|
|
99
|
+
*/
|
|
100
|
+
export const EXPOSURE_FIELD_BY_CATEGORY: Record<
|
|
101
|
+
ExposureCategory,
|
|
102
|
+
"aperture" | "shutterSpeed" | "isoValue"
|
|
103
|
+
> = {
|
|
104
|
+
aperture: "aperture",
|
|
105
|
+
"shutter-speed": "shutterSpeed",
|
|
106
|
+
iso: "isoValue",
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Shape of the per-category exposure fields on ExposureSettingsData and any
|
|
111
|
+
* consumer that opts in. All fields optional — user may set zero, one, or
|
|
112
|
+
* all categories.
|
|
113
|
+
*/
|
|
114
|
+
export interface ExposureValue {
|
|
115
|
+
aperture?: string
|
|
116
|
+
shutterSpeed?: string
|
|
117
|
+
isoValue?: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const EXPOSURE_FIELDS_IN_ORDER: ReadonlyArray<readonly [keyof ExposureValue, ExposureCategory]> =
|
|
121
|
+
EXPOSURE_CATEGORY_ORDER.map((cat) => [EXPOSURE_FIELD_BY_CATEGORY[cat], cat] as const)
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Aggregate all enabled per-category exposure prompt hints from a consumer's
|
|
125
|
+
* data, in canonical category order (aperture, shutter-speed, iso).
|
|
126
|
+
*/
|
|
127
|
+
export function buildExposureHints(
|
|
128
|
+
data: Record<string, unknown> & {
|
|
129
|
+
aperture?: unknown
|
|
130
|
+
shutterSpeed?: unknown
|
|
131
|
+
isoValue?: unknown
|
|
132
|
+
},
|
|
133
|
+
): string[] {
|
|
134
|
+
const hints: string[] = []
|
|
135
|
+
for (const [field] of EXPOSURE_FIELDS_IN_ORDER) {
|
|
136
|
+
const id = data[field]
|
|
137
|
+
if (typeof id !== "string" || id.length === 0) continue
|
|
138
|
+
const hint = getExposurePromptHint(id)
|
|
139
|
+
if (hint) hints.push(hint)
|
|
140
|
+
}
|
|
141
|
+
return hints
|
|
142
|
+
}
|