@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.
Files changed (150) hide show
  1. package/LICENSE +105 -0
  2. package/README.md +27 -0
  3. package/dist/index.cjs +21240 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +3599 -0
  6. package/dist/index.d.ts +3599 -0
  7. package/dist/index.js +20854 -0
  8. package/dist/index.js.map +1 -0
  9. package/package.json +49 -0
  10. package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
  11. package/src/__tests__/action-fx.test.ts +155 -0
  12. package/src/__tests__/apply-picker-json.test.ts +50 -0
  13. package/src/__tests__/assemble-image-input.test.ts +248 -0
  14. package/src/__tests__/assemble-suno-input.test.ts +283 -0
  15. package/src/__tests__/brand-tokens.test.ts +81 -0
  16. package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
  17. package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
  18. package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
  19. package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
  20. package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
  21. package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
  22. package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
  23. package/src/__tests__/catalog-gapfill.test.ts +53 -0
  24. package/src/__tests__/character-convergence-image.test.ts +217 -0
  25. package/src/__tests__/character-default-role-image.test.ts +167 -0
  26. package/src/__tests__/character-default-role-video.test.ts +168 -0
  27. package/src/__tests__/character-default-role.test.ts +70 -0
  28. package/src/__tests__/character-fx.test.ts +200 -0
  29. package/src/__tests__/entity-prompts-location.test.ts +97 -0
  30. package/src/__tests__/entity-prompts.test.ts +240 -0
  31. package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
  32. package/src/__tests__/factory-presets.test.ts +1045 -0
  33. package/src/__tests__/factory-snippets.test.ts +68 -0
  34. package/src/__tests__/framing-multi.test.ts +71 -0
  35. package/src/__tests__/framing-vantage.test.ts +39 -0
  36. package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
  37. package/src/__tests__/identity-lock.test.ts +46 -0
  38. package/src/__tests__/instrumentation.test.ts +68 -0
  39. package/src/__tests__/lighting-multi.test.ts +68 -0
  40. package/src/__tests__/location-convergence-image.test.ts +124 -0
  41. package/src/__tests__/mention-lock-flag.test.ts +499 -0
  42. package/src/__tests__/multi-picker-spec.test.ts +71 -0
  43. package/src/__tests__/music-genre.test.ts +103 -0
  44. package/src/__tests__/music-mood.test.ts +87 -0
  45. package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
  46. package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
  47. package/src/__tests__/parameter-registry-sync.test.ts +243 -0
  48. package/src/__tests__/person-age.test.ts +80 -0
  49. package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
  50. package/src/__tests__/person-body-axes.test.ts +67 -0
  51. package/src/__tests__/person-facial-geometry.test.ts +156 -0
  52. package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
  53. package/src/__tests__/person-sections.test.ts +18 -0
  54. package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
  55. package/src/__tests__/picker-catalogs-project.test.ts +85 -0
  56. package/src/__tests__/picker-catalogs.test.ts +53 -0
  57. package/src/__tests__/picker-limits.test.ts +20 -0
  58. package/src/__tests__/prompt-builder-segments.test.ts +183 -0
  59. package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
  60. package/src/__tests__/prompt-builder.test.ts +1773 -0
  61. package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
  62. package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
  63. package/src/__tests__/resolve-prompt-append.test.ts +58 -0
  64. package/src/__tests__/resolve-prompt.test.ts +44 -0
  65. package/src/__tests__/role-picker-shared.test.ts +208 -0
  66. package/src/__tests__/seedance-2-inputs.test.ts +173 -0
  67. package/src/__tests__/seedance-extend.test.ts +44 -0
  68. package/src/__tests__/sound-aggregator.test.ts +350 -0
  69. package/src/__tests__/style-presets.test.ts +34 -0
  70. package/src/__tests__/temporal-multi.test.ts +74 -0
  71. package/src/__tests__/transitions.test.ts +213 -0
  72. package/src/__tests__/video-reference-features.test.ts +41 -0
  73. package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
  74. package/src/__tests__/video-reference-resolver.test.ts +233 -0
  75. package/src/__tests__/video-reference-roles.test.ts +96 -0
  76. package/src/__tests__/voice-character.test.ts +48 -0
  77. package/src/__tests__/voice-delivery.test.ts +41 -0
  78. package/src/__tests__/wardrobe.test.ts +25 -0
  79. package/src/action-fx.ts +255 -0
  80. package/src/aesthetic.ts +435 -0
  81. package/src/assemble-image-input.ts +236 -0
  82. package/src/assemble-suno-input.ts +147 -0
  83. package/src/atmosphere.ts +104 -0
  84. package/src/backdrop.ts +131 -0
  85. package/src/brand-tokens.ts +154 -0
  86. package/src/camera-format.ts +76 -0
  87. package/src/camera-motions.ts +615 -0
  88. package/src/character-fx.ts +274 -0
  89. package/src/color-look.ts +103 -0
  90. package/src/composition-effects.ts +67 -0
  91. package/src/entity-prompts.ts +231 -0
  92. package/src/era.ts +292 -0
  93. package/src/exposure-settings.ts +142 -0
  94. package/src/factory-presets/generate-image.ts +1644 -0
  95. package/src/factory-presets/generate-video.ts +1116 -0
  96. package/src/factory-presets/index.ts +46 -0
  97. package/src/factory-presets/lottie-overlay.ts +166 -0
  98. package/src/factory-presets/motion-graphics.ts +350 -0
  99. package/src/factory-presets/music.ts +734 -0
  100. package/src/factory-presets/sfx.ts +136 -0
  101. package/src/factory-presets/shared-image.ts +207 -0
  102. package/src/factory-presets/switchx.ts +65 -0
  103. package/src/factory-presets/text.ts +292 -0
  104. package/src/factory-presets/types.ts +51 -0
  105. package/src/factory-presets/video-edit.ts +136 -0
  106. package/src/factory-presets/voice.ts +172 -0
  107. package/src/factory-presets.ts +2 -0
  108. package/src/factory-snippets/catalog.ts +105 -0
  109. package/src/factory-snippets/index.ts +16 -0
  110. package/src/factory-snippets/types.ts +33 -0
  111. package/src/framing.ts +634 -0
  112. package/src/held-prop.ts +187 -0
  113. package/src/identity-lock.ts +213 -0
  114. package/src/index.ts +66 -0
  115. package/src/instrumentation.ts +337 -0
  116. package/src/lens.ts +59 -0
  117. package/src/lighting.ts +229 -0
  118. package/src/loop-subject.ts +276 -0
  119. package/src/materials.ts +184 -0
  120. package/src/mood.ts +186 -0
  121. package/src/music-genre.ts +662 -0
  122. package/src/music-mood.ts +137 -0
  123. package/src/object-asset-presets.ts +81 -0
  124. package/src/parameter-prompt-hint.ts +281 -0
  125. package/src/person.ts +1368 -0
  126. package/src/photo-genre.ts +151 -0
  127. package/src/photographer.ts +612 -0
  128. package/src/picker-analyzer-registry.ts +374 -0
  129. package/src/picker-catalogs.ts +858 -0
  130. package/src/pose.ts +246 -0
  131. package/src/post-process-effects.ts +94 -0
  132. package/src/prompt-builder-structured-fields.ts +116 -0
  133. package/src/prompt-builder.ts +2964 -0
  134. package/src/prompt-templates.ts +50 -0
  135. package/src/prompt-wizard-categories.ts +334 -0
  136. package/src/provider-prompt-doctrine.ts +85 -0
  137. package/src/render-quality.ts +89 -0
  138. package/src/resolve-prompt.ts +120 -0
  139. package/src/seedance-2-inputs.ts +100 -0
  140. package/src/setting.ts +130 -0
  141. package/src/sound-aggregator.ts +241 -0
  142. package/src/style-presets.ts +162 -0
  143. package/src/style.ts +99 -0
  144. package/src/styling.ts +585 -0
  145. package/src/temporal.ts +151 -0
  146. package/src/transitions.ts +333 -0
  147. package/src/video-reference-resolver.ts +823 -0
  148. package/src/voice-character.ts +237 -0
  149. package/src/voice-delivery.ts +142 -0
  150. package/src/wardrobe.ts +179 -0
@@ -0,0 +1,147 @@
1
+ /**
2
+ * `assembleSunoInput` — the single source of truth for turning a
3
+ * `suno-generate` node's inputs (a prompt + connected Sound pickers + the
4
+ * per-field levers) into the flat field set the Suno provider receives.
5
+ *
6
+ * WHY THIS EXISTS: the same custom-mode / audio-style fold —
7
+ * 1. resolve effective custom mode (`getEffectiveSunoCustomMode`),
8
+ * 2. compose the connected-picker hint (`composeSoundHintFromConnections`),
9
+ * 3. fold it into `style` (custom, 500-char budget) OR `prompt` (non-custom,
10
+ * 3000-char budget), skipping `truncateForField` when the user field is
11
+ * empty,
12
+ * 4. normalize the secondary fields,
13
+ * — was duplicated in THREE places kept in lockstep by hand: the frontend
14
+ * `execute-node.ts` `suno-generate` branch (the DESIGNATED SOURCE OF TRUTH),
15
+ * the backend `payload-builder.ts` `suno-generate` case, and the editor Final
16
+ * preview. This wrapper collapses them into one so preview == run.
17
+ *
18
+ * THE 5 FE↔BE DIVERGENCES, all reconciled toward the FE:
19
+ * (A) `throwOnEmpty` — the FE run rejects an empty prompt+hint; the BE run and
20
+ * the preview do not. Opt-in via the flag (default `false` = BE/preview).
21
+ * (B) skip-when-empty — when the user's `style`/`prompt` field is empty the
22
+ * bare hint is returned (NOT run through `truncateForField`), matching the
23
+ * FE / `foldStyle` behavior. The BE always-truncated.
24
+ * (C) `lyrics` is CALLER-PRE-RESOLVED (`input.lyrics`) — the fn never reads
25
+ * `data.lyrics` for the value (the caller resolves `{}`-refs first; this
26
+ * is the FE bugfix of adopting the BE's ref-resolution). `data.lyrics` is
27
+ * still consulted by `getEffectiveSunoCustomMode` for mode auto-detection.
28
+ * (D) `vocalGender` precedence: `data.vocalGender || fields.vocalGender ||
29
+ * undefined` (manual wins, else the connected voice-character gender).
30
+ * (E) `|| undefined` normalization on `model` / `style` / `title` /
31
+ * `negativeStyle` (and `lyrics`), so empty strings drop out of the payload.
32
+ *
33
+ * CALLER-ONLY fields (deliberately NOT in the result, mirroring how
34
+ * `assembleImageInput` leaves `userId`/`jobId` out): the FE adds `userId`; the
35
+ * BE adds `jobId` / `usageLogId` / the credit id. The caller spreads `...result`
36
+ * and appends those itself.
37
+ */
38
+ import {
39
+ getEffectiveSunoCustomMode,
40
+ composeSoundHintFromConnections,
41
+ appendField,
42
+ truncateForField,
43
+ } from "./sound-aggregator.js"
44
+ import type { HintGraphContext, HintNodeLike } from "./parameter-prompt-hint.js"
45
+
46
+ /** Suno's server-side caps the fold budgets mirror. */
47
+ const STYLE_BUDGET = 500
48
+ const PROMPT_BUDGET = 3000
49
+
50
+ export interface AssembleSunoInput {
51
+ /** The `suno-generate` node; `data` is ALREADY field-mapped by the caller. */
52
+ node: HintNodeLike
53
+ /** `{ nodes, edges }` for the connected-picker audio-style fold. */
54
+ graph: HintGraphContext
55
+ /**
56
+ * Caller-resolved user prompt (FE: `overridePrompt ?? inputs.prompt ??
57
+ * resolveTextRefs(data.prompt)`). Folded into `prompt` in non-custom mode.
58
+ */
59
+ userPrompt: string
60
+ /**
61
+ * Caller-resolved lyrics (divergence C — refMap-resolved by the caller, NOT
62
+ * read from `data.lyrics` here).
63
+ */
64
+ lyrics?: string
65
+ /** Persona fields (FE/BE `resolvePersona(...)`), spread onto the result. */
66
+ persona?: { personaId?: string; personaModel?: "voice_persona" | "style_persona" }
67
+ /**
68
+ * Reject an empty prompt+hint with `throw` (divergence A). FE run passes
69
+ * `true`; the BE run + the preview leave it `false`.
70
+ */
71
+ throwOnEmpty?: boolean
72
+ }
73
+
74
+ export interface AssembleSunoResult {
75
+ prompt: string
76
+ style?: string
77
+ lyrics?: string
78
+ title?: string
79
+ negativeStyle?: string
80
+ vocalGender?: string
81
+ styleWeight?: number
82
+ weirdnessConstraint?: number
83
+ audioWeight?: number
84
+ customMode: boolean
85
+ instrumental: boolean
86
+ model?: string
87
+ personaId?: string
88
+ personaModel?: "voice_persona" | "style_persona"
89
+ }
90
+
91
+ /**
92
+ * Assemble a `suno-generate` node's inputs into the flat field set the Suno
93
+ * provider receives. Byte-faithful to the FE `execute-node.ts` inline block.
94
+ */
95
+ export function assembleSunoInput(input: AssembleSunoInput): AssembleSunoResult {
96
+ const data = (input.node.data ?? {}) as { readonly [k: string]: unknown }
97
+
98
+ const customMode = getEffectiveSunoCustomMode(data)
99
+ // Fold connected Sound pickers (music-genre / music-mood / instrumentation /
100
+ // voice-character / voice-delivery) into style (custom) or prompt (non-custom).
101
+ const audioStyle = composeSoundHintFromConnections(input.node, "suno-generate", input.graph)
102
+
103
+ // Divergence A — opt-in empty-prompt guard. Checked BEFORE the fold so an
104
+ // upstream picker that supplies the whole prompt still runs (matches the FE
105
+ // `if (!typedPrompt && !audioStyle.text)` gate).
106
+ if (input.throwOnEmpty && !input.userPrompt && !audioStyle.text) {
107
+ throw new Error("Suno: no prompt found")
108
+ }
109
+
110
+ const userStyle = (data.style as string | undefined) ?? ""
111
+ // `?? ""` mirrors the FE source-of-truth (`finalPrompt = typedPrompt ?? ""`) and
112
+ // makes the `prompt: string` postcondition self-enforcing — in custom mode the
113
+ // prompt branch is never rewritten, so a stray `undefined` would otherwise leak.
114
+ const typedPrompt = input.userPrompt ?? ""
115
+
116
+ // Divergence B — skip `truncateForField` when the user field is empty (return
117
+ // the bare hint). Identical to the FE inline fold / `foldStyle`.
118
+ let finalStyle = userStyle
119
+ let finalPrompt = typedPrompt
120
+ if (customMode) {
121
+ finalStyle = userStyle
122
+ ? appendField(userStyle, truncateForField(audioStyle.text, userStyle, STYLE_BUDGET))
123
+ : audioStyle.text
124
+ } else {
125
+ finalPrompt = typedPrompt
126
+ ? appendField(typedPrompt, truncateForField(audioStyle.text, typedPrompt, PROMPT_BUDGET))
127
+ : audioStyle.text
128
+ }
129
+
130
+ return {
131
+ prompt: finalPrompt,
132
+ model: (data.model as string | undefined) || undefined,
133
+ // Divergence C — lyrics is caller-pre-resolved; Divergence E — `|| undefined`.
134
+ lyrics: input.lyrics || undefined,
135
+ style: finalStyle || undefined,
136
+ title: (data.title as string | undefined) || undefined,
137
+ negativeStyle: (data.negativeStyle as string | undefined) || undefined,
138
+ // Divergence D — manual vocalGender wins; else the connected voice-character.
139
+ vocalGender: (data.vocalGender as string | undefined) || audioStyle.fields.vocalGender || undefined,
140
+ styleWeight: data.styleWeight as number | undefined,
141
+ weirdnessConstraint: data.weirdnessConstraint as number | undefined,
142
+ audioWeight: data.audioWeight as number | undefined,
143
+ customMode,
144
+ instrumental: (data.instrumental as boolean | undefined) ?? false,
145
+ ...(input.persona ?? {}),
146
+ }
147
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Canonical catalog of atmosphere / environmental effects.
3
+ *
4
+ * Atmosphere dimension of a shot — environmental elements in the air that
5
+ * affect visibility and mood (rain, fog, god rays, particles). Independent
6
+ * of lighting (color/direction of light) and color/look (post-processing
7
+ * tone). A sunny scene with fog has different atmosphere than a clear sunny
8
+ * scene, even with identical subject, lighting and color grade.
9
+ *
10
+ * Shared between the picker UI and the prompt-hint injection on both the
11
+ * frontend DAG executor and the backend orchestrator.
12
+ */
13
+
14
+ export interface Atmosphere {
15
+ readonly id: string
16
+ readonly label: string
17
+ readonly description: string
18
+ readonly promptHint: string
19
+ }
20
+
21
+ export const ATMOSPHERES: ReadonlyArray<Atmosphere> = [
22
+ { id: "clear", label: "Clear", description: "Clean, no atmospheric effect", promptHint: "clear atmosphere, clean visibility with no fog or particles" },
23
+ { id: "cloudy", label: "Cloudy", description: "Partial cloud cover, mixed light", promptHint: "cloudy atmosphere, partial cloud cover with mixed direct and diffused light, soft contrast and intermittent shadows" },
24
+ { id: "overcast", label: "Overcast", description: "Uniform gray cloud cover", promptHint: "overcast atmosphere, uniform gray cloud cover with diffused flat light" },
25
+ { id: "fog", label: "Fog", description: "Dense low-visibility fog", promptHint: "dense fog atmosphere, thick low-visibility moisture obscuring all but the nearest subject, muted desaturated tones" },
26
+ { id: "mist", label: "Mist", description: "Thin diffusing mist", promptHint: "light mist atmosphere, thin atmospheric haze gently diffusing distant elements while keeping the subject crisp" },
27
+ { id: "fog-mist", label: "Fog / Mist", description: "Soft diffusing fog", promptHint: "foggy atmosphere, soft mist reducing visibility and diffusing distant elements" },
28
+ { id: "light-rain", label: "Light Rain", description: "Soft falling rain", promptHint: "light rain falling softly, subtle droplets visible in the air with wet surfaces" },
29
+ { id: "heavy-rain", label: "Heavy Rain", description: "Heavy storm with sheets of rain", promptHint: "heavy rainstorm, dense sheets of rain with visible droplets and splashing surfaces" },
30
+ { id: "storm", label: "Storm", description: "Violent thunderstorm with rain + lightning", promptHint: "stormy atmosphere, violent thunderstorm with sheeting rain, dramatic dark clouds, flickering lightning illuminating the scene, and wind-driven debris" },
31
+ { id: "snow", label: "Snow", description: "Falling snowflakes", promptHint: "falling snow, snowflakes drifting through the air and accumulating on surfaces" },
32
+ { id: "blizzard", label: "Blizzard", description: "Violent snowstorm, near whiteout", promptHint: "blizzard atmosphere, violent snowstorm with horizontal wind-driven snow, near-whiteout visibility, deep accumulation on every surface" },
33
+ { id: "dust", label: "Dust", description: "Dust particles in air", promptHint: "dusty atmosphere, visible airborne dust particles catching light and softening distant elements" },
34
+ { id: "god-rays", label: "God Rays", description: "Sun shafts through haze", promptHint: "visible volumetric god rays, sun shafts cutting through hazy atmosphere" },
35
+ { id: "smoke", label: "Smoke", description: "Drifting smoke", promptHint: "drifting smoke in the scene, hazy volumetric smoke partially obscuring the subject" },
36
+ { id: "bokeh-particles", label: "Bokeh Particles", description: "Floating out-of-focus specks", promptHint: "floating bokeh particles in the air, out-of-focus glowing specks adding ambient depth" },
37
+ { id: "chalk-dust", label: "Chalk Dust", description: "Soft chalk dust hanging in the air", promptHint: "soft chalk dust haze suspended in the air, fine pale powder catching light with a classroom-like quality" },
38
+ { id: "falling-petals", label: "Falling Petals", description: "Drifting flower petals", promptHint: "delicate flower petals drifting through the frame, cherry-blossom or rose petals tumbling slowly on a soft breeze" },
39
+ { id: "confetti", label: "Confetti", description: "Colorful confetti falling", promptHint: "colorful confetti raining through the frame, bright paper flecks tumbling and twirling against the background" },
40
+ { id: "sparks-embers", label: "Sparks / Embers", description: "Glowing embers drifting upward", promptHint: "glowing sparks and embers drifting upward through the air, hot orange specks trailing faint motion against darker surroundings" },
41
+ { id: "lens-flare", label: "Lens Flare", description: "Anamorphic flare streak across frame", promptHint: "anamorphic horizontal lens flare streaking across the frame, cinematic light artifact with subtle ghosting and bloom" },
42
+ { id: "heat-haze", label: "Heat Haze", description: "Visible heat shimmer warping background", promptHint: "visible heat shimmer rising in the scene, warping and rippling the background with refracted air distortion" },
43
+ { id: "steam", label: "Steam", description: "Rising white steam", promptHint: "soft white steam rising through the frame, warm vapor curling upward from a hot surface and catching ambient light" },
44
+ { id: "bubbles-underwater", label: "Underwater Bubbles", description: "Rising bubbles in water", promptHint: "streams of bubbles rising through water, clear underwater spheres drifting upward with shifting refractive light" },
45
+ { id: "rain-on-glass", label: "Rain on Glass", description: "Droplets streaking foreground glass", promptHint: "raindrops beading and streaking down a foreground window pane, distorting the view behind with soft refracted detail" },
46
+ { id: "pollen-light", label: "Pollen in Light", description: "Warm particles in a sunbeam", promptHint: "warm pollen and floating particles caught in a beam of sunlight, glowing motes drifting lazily through a shaft of light" },
47
+ { id: "water-droplets", label: "Water Droplets", description: "Droplets clinging to skin or surface", promptHint: "fine water droplets clinging to skin and surfaces, fresh post-shower wetness with small beads catching highlights" },
48
+ { id: "falling-ash", label: "Falling Ash", description: "Fine grey ash drifting through the air", promptHint: "fine grey ash drifting slowly through the air, weightless flakes settling across the scene with a quiet post-fire mood" },
49
+ { id: "fireflies", label: "Fireflies", description: "Drifting bioluminescent specks", promptHint: "fireflies drifting in soft warm pulses through the scene, summer-night magic glinting in the air with bioluminescent specks" },
50
+ { id: "incense-smoke", label: "Incense Smoke", description: "Slow rising thick incense smoke", promptHint: "thick incense smoke rising slowly through the frame, spiritual temple atmosphere with dust-lit shafts curling upward" },
51
+ { id: "cigarette-smoke", label: "Cigarette Smoke", description: "Exhaled smoke curling upward", promptHint: "exhaled cigarette smoke curling upward through the frame, café noir mood with thin pale plumes drifting against ambient light" },
52
+ { id: "candle-glow", label: "Candle-Glow", description: "Warm flame light with soft halo", promptHint: "warm candle flame casting a soft glowing halo across the scene, intimate vigil-like atmosphere with flickering golden light pooling around the source" },
53
+ { id: "glitter-sparkle", label: "Glitter / Sparkle", description: "Sparkly particles in air", promptHint: "sparkly glitter particles suspended in the air, party-disco atmosphere with tiny points of light catching and reflecting across the scene" },
54
+ { id: "starfield", label: "Starfield / Visible Stars", description: "Visible night sky with stars", promptHint: "visible starfield filling the night sky, milky way and countless points of light spread across a vast cosmic backdrop" },
55
+ { id: "dandelion-seeds", label: "Dandelion Seeds", description: "Drifting dandelion fluff", promptHint: "dandelion seed fluff drifting on a soft summer breeze, ethereal whitish parachutes floating weightlessly through the frame with whimsical magic" },
56
+ { id: "pollen-drift", label: "Pollen Drift", description: "Fine yellow-gold pollen in golden light", promptHint: "fine yellow-gold pollen drifting through golden hour light, warm dust motes suspended in slanted sunbeams across the scene" },
57
+ { id: "snowflakes-heavy", label: "Heavy Snowfall", description: "Heavy thick snowflakes filling the air, blizzard", promptHint: "heavy thick snowflakes filling the air in a near-blizzard, dense flurries blowing across the frame and accumulating rapidly on every surface" },
58
+ { id: "snowflakes-light", label: "Light Snow Drift", description: "Sparse drifting snowflakes, calm winter scene", promptHint: "sparse snowflakes drifting lazily through a calm winter scene, occasional flakes catching ambient light against a quiet hushed atmosphere" },
59
+ { id: "raindrops-on-skin", label: "Raindrops on Skin", description: "Visible water droplets beading on skin and hair", promptHint: "visible water droplets beading on skin and hair, fresh rain catching highlights with rivulets tracing down cheeks and damp strands clinging to the face" },
60
+ { id: "bioluminescent-cloud", label: "Bioluminescent Particle Cloud", description: "Glowing blue-green bioluminescent particles drifting like sea sparkle", promptHint: "glowing blue-green bioluminescent particles drifting through the air like sea sparkle, ethereal cyan motes pulsing softly with their own light against a darkened backdrop" },
61
+ { id: "motion-streaks", label: "Motion Streaks", description: "Speed-line motion-blur streaks suggesting fast movement", promptHint: "speed-line motion-blur streaks slicing across the frame, smeared light trails and directional blur suggesting fast movement through the scene" },
62
+ ] as const
63
+
64
+ const atmosphereById = new Map<string, Atmosphere>(ATMOSPHERES.map((a) => [a.id, a]))
65
+
66
+ export function getAtmosphere(id: string | undefined | null): Atmosphere | undefined {
67
+ if (!id) return undefined
68
+ return atmosphereById.get(id)
69
+ }
70
+
71
+ export function getAtmosphereLabel(id: string | undefined | null, fallback?: string): string {
72
+ const a = getAtmosphere(id)
73
+ if (a) return a.label
74
+ if (fallback !== undefined) return fallback
75
+ return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
76
+ }
77
+
78
+ export function getAtmospherePromptHint(id: string | undefined | null): string {
79
+ return getAtmosphere(id)?.promptHint ?? ""
80
+ }
81
+
82
+ /**
83
+ * Multi-pick: 1-2 atmosphere ids → composite atmospheric clause. Single →
84
+ * entry's own promptHint. Two → emit independently and join — atmospheres
85
+ * are particle-effect descriptions that compose naturally
86
+ * ("fog drifting in soft cool clouds, with golden god-rays cutting through").
87
+ */
88
+ export function buildAtmosphereHints(value: unknown): string[] {
89
+ const ids: string[] = []
90
+ if (typeof value === "string" && value) ids.push(value)
91
+ else if (Array.isArray(value)) {
92
+ for (const v of value) {
93
+ if (typeof v === "string" && v && !ids.includes(v)) ids.push(v)
94
+ }
95
+ }
96
+ const out: string[] = []
97
+ for (const id of ids) {
98
+ const hint = getAtmospherePromptHint(id)
99
+ if (hint) out.push(hint)
100
+ }
101
+ return out
102
+ }
103
+
104
+ export const ATMOSPHERE_IDS: ReadonlyArray<string> = ATMOSPHERES.map((a) => a.id)
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Canonical catalog of Backdrop / studio-background presets.
3
+ *
4
+ * Single-pick parameter node — user picks ONE backdrop describing the
5
+ * controlled wall / surface / effect *immediately behind the subject*.
6
+ * Distinct from Setting (which describes the location or environment a
7
+ * shot takes place in). Backdrop is the studio convention behind the
8
+ * subject during a portrait, fashion, beauty, e-commerce, or product
9
+ * shoot — the seamless paper, painted wall, gradient sweep, or lit
10
+ * effect that frames the subject.
11
+ *
12
+ * For full environments (cafe, forest, alley) use the Setting node.
13
+ * For artistic medium use Style. For "in the air" particles use
14
+ * Atmosphere. Backdrop occupies the narrow, well-defined slot of
15
+ * "what wall is this person standing in front of."
16
+ *
17
+ * Applies to both image and video consumers (a studio backdrop carries
18
+ * over to motion shoots). Not in STILL_IMAGE_EXCLUDE_TYPES.
19
+ *
20
+ * Shared between the picker UI, the standalone Backdrop parameter
21
+ * node, and the prompt-hint injection on both the frontend DAG executor
22
+ * and the backend orchestrator.
23
+ */
24
+
25
+ export type BackdropCategory =
26
+ | "solid"
27
+ | "gradient"
28
+ | "textured"
29
+ | "fabric"
30
+ | "effect"
31
+ | "reflective"
32
+
33
+ export interface Backdrop {
34
+ readonly id: string
35
+ readonly label: string
36
+ readonly category: BackdropCategory
37
+ readonly description: string
38
+ readonly promptHint: string
39
+ }
40
+
41
+ export const BACKDROPS: ReadonlyArray<Backdrop> = [
42
+ // -------------------- Solid / Seamless --------------------
43
+ { id: "white-seamless", label: "White Seamless", category: "solid", description: "Clean white studio paper", promptHint: "against a clean white seamless studio backdrop, evenly lit with soft diffused fill and no environmental detail beyond the subject" },
44
+ { id: "black-seamless", label: "Black Seamless", category: "solid", description: "Pure black studio backdrop", promptHint: "against a deep black seamless studio backdrop falling off into shadow, isolating the subject in a pool of contained light" },
45
+ { id: "grey-seamless", label: "Grey Seamless", category: "solid", description: "Neutral mid-grey studio paper", promptHint: "against a neutral mid-grey seamless studio backdrop, soft even lighting and a clean editorial portrait feel" },
46
+ { id: "ivory-seamless", label: "Ivory Seamless", category: "solid", description: "Warm ivory off-white backdrop", promptHint: "against a warm ivory seamless backdrop, soft golden-toned fill and an elegant editorial cleanliness" },
47
+ { id: "deep-red", label: "Deep Red", category: "solid", description: "Saturated deep red wall", promptHint: "against a saturated deep crimson studio backdrop, the rich red color reflecting subtly into the subject's skin and wardrobe" },
48
+ { id: "royal-blue", label: "Royal Blue", category: "solid", description: "Saturated royal blue backdrop", promptHint: "against a vivid royal-blue studio backdrop, the bold color framing the subject with regal editorial weight" },
49
+ { id: "emerald-green", label: "Emerald Green", category: "solid", description: "Saturated emerald wall", promptHint: "against a saturated emerald-green studio backdrop, the jewel tone giving the portrait a lush editorial intensity" },
50
+ { id: "dusty-pink", label: "Dusty Pink", category: "solid", description: "Soft muted pink backdrop", promptHint: "against a soft dusty-pink studio backdrop, gentle warm reflected light and a romantic muted editorial palette" },
51
+ { id: "mustard-yellow", label: "Mustard Yellow", category: "solid", description: "Warm mustard backdrop", promptHint: "against a warm mustard-yellow studio backdrop, the saturated tone radiating retro editorial warmth onto the subject" },
52
+ { id: "teal-textured-wall", label: "Textured Teal Wall", category: "solid", description: "Painted teal textured wall", promptHint: "against a painted teal wall with subtle texture and uneven coverage, mid-tone shadows pooling into the surface and an editorial-portrait color cast" },
53
+ { id: "chroma-green", label: "Chroma Green", category: "solid", description: "Flat saturated green-screen", promptHint: "against a flat saturated chroma-green screen backdrop, evenly lit with no shadow falloff and the uniform key-ready surface used for compositing" },
54
+ { id: "chroma-blue", label: "Chroma Blue", category: "solid", description: "Flat saturated blue-screen", promptHint: "against a flat saturated chroma-blue screen backdrop, evenly lit with no shadow falloff and the uniform key-ready surface used for compositing" },
55
+ { id: "paper-roll-seamless", label: "Paper Roll Seamless", category: "solid", description: "Generic neutral pastel paper roll", promptHint: "against a generic seamless paper-roll backdrop in a soft neutral pastel tone, evenly lit with the gentle curve of a sweep rolling onto the floor and a clean studio-portrait feel" },
56
+
57
+ // -------------------- Gradient --------------------
58
+ { id: "red-orange-gradient", label: "Red-Orange Gradient", category: "gradient", description: "Warm red-to-orange sweep", promptHint: "against a smoothly graded red-to-orange studio sweep, the warm fiery transition wrapping behind the subject for a dynamic editorial backdrop" },
59
+ { id: "pink-orange-gradient", label: "Pink-Orange Gradient", category: "gradient", description: "Sunset pink-to-orange sweep", promptHint: "against a sunset-style gradient backdrop fading from soft pink at the top to warm orange below, a dreamy color sweep behind the subject" },
60
+ { id: "blue-emerald-gradient", label: "Blue-Emerald Gradient", category: "gradient", description: "Cool blue-to-emerald sweep", promptHint: "against a cool gradient backdrop fading from royal blue to emerald, the saturated transition giving the portrait a sleek editorial poster feel" },
61
+ { id: "sunset-gradient", label: "Sunset Gradient", category: "gradient", description: "Multi-tone sunset sweep", promptHint: "against a multi-tone sunset gradient backdrop sweeping from violet through pink to gold, the colors blending behind the subject like a painted sky" },
62
+ { id: "two-tone-split", label: "Two-Tone Split", category: "gradient", description: "Split-color half-and-half wall", promptHint: "against a two-tone studio backdrop split vertically into a saturated color on one side and a contrasting tone on the other, the hard color line slicing behind the subject" },
63
+
64
+ // -------------------- Textured --------------------
65
+ { id: "brick-wall", label: "Brick Wall", category: "textured", description: "Exposed red-brick wall", promptHint: "against an exposed red-brick wall with weathered mortar, soft ambient light raking across the rough surface for a gritty street-portrait feel" },
66
+ { id: "concrete-wall", label: "Concrete Wall", category: "textured", description: "Raw concrete surface", promptHint: "against a raw cast-concrete wall with subtle imperfections and form-tie marks, a cool industrial backdrop with neutral grey shadow" },
67
+ { id: "plastered-wall", label: "Plastered Wall", category: "textured", description: "Hand-troweled plaster", promptHint: "against a hand-troweled plaster wall with soft directional texture and warm cream tones, an artisanal editorial backdrop" },
68
+ { id: "peeling-paint", label: "Peeling Paint", category: "textured", description: "Vintage peeling-paint wall", promptHint: "against a vintage wall with chipped and peeling layers of old paint, weathered texture and a moody worn-in editorial feel" },
69
+ { id: "wood-paneling", label: "Wood Paneling", category: "textured", description: "Warm wood-paneled wall", promptHint: "against warm wood-paneled wall with horizontal plank lines, soft light catching the natural grain and a vintage interior portrait feel" },
70
+ { id: "tile-wall", label: "Tile Wall", category: "textured", description: "Bathroom or kitchen square tile", promptHint: "against a square-tiled bathroom or kitchen wall with crisp grout lines, slight ceramic gloss catching the studio key and a clean utilitarian backdrop" },
71
+ { id: "marble-wall", label: "Marble Wall", category: "textured", description: "Luxury veined marble wall", promptHint: "against a luxury veined marble wall, soft directional light revealing the natural stone striations and a polished high-end editorial backdrop" },
72
+ { id: "graffiti-wall", label: "Graffiti Wall", category: "textured", description: "Vibrant urban graffiti tagged wall, street art", promptHint: "against a vibrant urban wall covered in layered graffiti tags and street-art murals, saturated spray-paint colors and the gritty energy of an alley shoot" },
73
+ { id: "exposed-stone", label: "Exposed Stone Wall", category: "textured", description: "Raw exposed stone or ashlar masonry wall", promptHint: "against an exposed stone wall of raw ashlar masonry, irregular blocks with weathered mortar joints catching directional light for a rugged architectural backdrop" },
74
+
75
+ // -------------------- Fabric / Drape --------------------
76
+ { id: "muslin-drape", label: "Muslin", category: "fabric", description: "Mottled hand-painted muslin", promptHint: "against a mottled hand-painted muslin backdrop, soft cloudy color variation and the classic painterly studio-portrait feel" },
77
+ { id: "velvet-drape", label: "Velvet Drape", category: "fabric", description: "Heavy velvet drape backdrop", promptHint: "against a richly draped velvet backdrop with deep saturated color, soft folds catching directional light and a luxurious old-Hollywood feel" },
78
+ { id: "satin-drape", label: "Satin Drape", category: "fabric", description: "Glossy satin drape", promptHint: "against a glossy satin drape with fluid folds catching specular highlights, a glamorous editorial backdrop with a soft sheen" },
79
+ { id: "canvas-painted", label: "Painted Canvas", category: "fabric", description: "Painterly canvas backdrop", promptHint: "against a painterly hand-painted canvas backdrop with soft brushstroke variation and a classic portrait-studio atmosphere" },
80
+
81
+ // -------------------- Effect / Lighting --------------------
82
+ { id: "bokeh-blur", label: "Bokeh Blur", category: "effect", description: "Out-of-focus bokeh field", promptHint: "against an out-of-focus bokeh field of soft warm light circles, a dreamy shallow-depth backdrop that throws all attention onto the subject" },
83
+ { id: "neon-bokeh", label: "Neon Bokeh", category: "effect", description: "Saturated neon bokeh blur", promptHint: "against a saturated neon-bokeh blur of pink, cyan and magenta lights, a nightlife-style shallow-depth backdrop that frames the subject in glowing color" },
84
+ { id: "halo-glow", label: "Halo Glow", category: "effect", description: "Glowing circular halo behind head", promptHint: "against a darkened backdrop with a single glowing circular halo of light directly behind the subject's head, sun-like and centered, framing them in a saintly aura" },
85
+ { id: "light-leak", label: "Light Leak", category: "effect", description: "Lens-flare light-leak streak", promptHint: "against a backdrop washed by a warm orange light leak streaking across one side of the frame, lens-flare flares and a dreamy analog feel" },
86
+ { id: "vignette-dark", label: "Dark Vignette", category: "effect", description: "Heavy dark-vignette surround", promptHint: "against a deep darkened backdrop with a heavy circular vignette pulling the edges into shadow, the subject illuminated in the center under a single soft key" },
87
+ { id: "window-with-light", label: "Window with Light", category: "effect", description: "Large window backdrop with light pouring through", promptHint: "against a large window backdrop with bright daylight pouring through the panes, soft silhouetted frame lines and glowing rim light haloing the subject from behind" },
88
+ { id: "rooftop-skyline", label: "Rooftop Skyline", category: "effect", description: "Outdoor rooftop with city skyline as backdrop", promptHint: "against an outdoor rooftop view with a sweeping city skyline as the backdrop, distant buildings softened by atmospheric haze and an open-air editorial feel" },
89
+
90
+ // -------------------- Reflective --------------------
91
+ { id: "mirror-floor", label: "Mirror Floor", category: "reflective", description: "Reflective mirrored surface", promptHint: "against a backdrop with a polished mirrored floor reflecting the subject upside-down beneath them, the doubled composition adding a surreal editorial touch" },
92
+ { id: "polished-floor", label: "Polished Floor", category: "reflective", description: "Glossy polished floor reflection", promptHint: "against a glossy polished floor with a soft hint of reflection beneath the subject, a clean architectural editorial backdrop" },
93
+ ] as const
94
+
95
+ const backdropById = new Map<string, Backdrop>(BACKDROPS.map((b) => [b.id, b]))
96
+
97
+ export function getBackdrop(id: string | undefined | null): Backdrop | undefined {
98
+ if (!id) return undefined
99
+ return backdropById.get(id)
100
+ }
101
+
102
+ export function getBackdropLabel(id: string | undefined | null, fallback?: string): string {
103
+ const b = getBackdrop(id)
104
+ if (b) return b.label
105
+ if (fallback !== undefined) return fallback
106
+ return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
107
+ }
108
+
109
+ export function getBackdropPromptHint(id: string | undefined | null): string {
110
+ return getBackdrop(id)?.promptHint ?? ""
111
+ }
112
+
113
+ export const BACKDROP_IDS: ReadonlyArray<string> = BACKDROPS.map((b) => b.id)
114
+
115
+ export const BACKDROP_CATEGORY_LABELS: Readonly<Record<BackdropCategory, string>> = {
116
+ solid: "Solid / Seamless",
117
+ gradient: "Gradient",
118
+ textured: "Textured",
119
+ fabric: "Fabric / Drape",
120
+ effect: "Effect / Lighting",
121
+ reflective: "Reflective",
122
+ }
123
+
124
+ export const BACKDROP_CATEGORY_ORDER: ReadonlyArray<BackdropCategory> = [
125
+ "solid",
126
+ "gradient",
127
+ "textured",
128
+ "fabric",
129
+ "effect",
130
+ "reflective",
131
+ ]
@@ -0,0 +1,154 @@
1
+ import type { SupportedFontName } from "@nodaro/shared"
2
+
3
+ /**
4
+ * Brand layer (Phase 3a). A pragmatic brand-token set — palette + fonts + logo —
5
+ * authored/selected ONCE and threaded through the shot-sequence pipeline as
6
+ * defaults. Preset palettes are adapted from HyperFrames' Apache-2.0
7
+ * `hyperframes-creative/frame-presets`, re-grounded on SUPPORTED_FONT_NAMES.
8
+ */
9
+ export interface BrandPalette {
10
+ /** Canvas background. */
11
+ bg: string
12
+ /** Secondary surface (cards/panels). Carried for forward-compat; unconsumed in 3a. */
13
+ bgAlt?: string
14
+ /** Primary on-bg text. */
15
+ text: string
16
+ /** Secondary text. Carried for forward-compat; unconsumed in 3a. */
17
+ textMuted?: string
18
+ /** Primary accent — the brand color. */
19
+ accent: string
20
+ /** Secondary accent. Carried for forward-compat; unconsumed in 3a. */
21
+ accent2?: string
22
+ /** Hairline/divider. Carried for forward-compat; unconsumed in 3a. */
23
+ line?: string
24
+ }
25
+
26
+ export type BrandCasing = "uppercase" | "lowercase" | "none"
27
+
28
+ export interface BrandTypeSpec {
29
+ /** CSS font-weight. MUST be a weight loaded for the role's font (guarded in remotion). */
30
+ weight?: number
31
+ /** absent => inherit call-site; "none" => force no transform; else force that transform. */
32
+ casing?: BrandCasing
33
+ /** letter-spacing in em; suppressed for Arabic at render time. */
34
+ tracking?: number
35
+ }
36
+
37
+ export interface BrandFonts {
38
+ heading: SupportedFontName
39
+ body: SupportedFontName
40
+ headingType?: BrandTypeSpec
41
+ bodyType?: BrandTypeSpec
42
+ }
43
+
44
+ export interface BrandLogo {
45
+ name: string
46
+ tagline?: string
47
+ /** Optional logo image — an https URL on an allowlisted Nodaro-CDN host (v1: URL only; obtain from the upload tools). */
48
+ image?: string
49
+ /** Optional hex color for a rounded panel rendered behind the logo. */
50
+ imageBackdrop?: string
51
+ }
52
+
53
+ export interface BrandTokens {
54
+ palette: BrandPalette
55
+ fonts: BrandFonts
56
+ logo?: BrandLogo
57
+ }
58
+
59
+ export interface BrandPresetMeta {
60
+ id: BrandPresetId
61
+ label: string
62
+ mood: string
63
+ description: string
64
+ }
65
+
66
+ export type BrandPresetId =
67
+ | "midnight-violet"
68
+ | "editorial-cream"
69
+ | "cobalt-corporate"
70
+ | "sandstone-warm"
71
+ | "poster-contrast"
72
+ | "mono-slate"
73
+ | "vibrant-pulse"
74
+ | "pastel-calm"
75
+
76
+ export const BRAND_PRESET_IDS: readonly BrandPresetId[] = [
77
+ "midnight-violet",
78
+ "editorial-cream",
79
+ "cobalt-corporate",
80
+ "sandstone-warm",
81
+ "poster-contrast",
82
+ "mono-slate",
83
+ "vibrant-pulse",
84
+ "pastel-calm",
85
+ ]
86
+
87
+ export const BRAND_PRESETS: Record<BrandPresetId, BrandTokens> = {
88
+ "midnight-violet": {
89
+ palette: { bg: "#0B0B12", bgAlt: "#16161F", text: "#FFFFFF", textMuted: "#A1A1AA", accent: "#8B5CF6", accent2: "#22D3EE", line: "#2A2A38" },
90
+ fonts: { heading: "Montserrat", body: "Inter", headingType: { weight: 700 }, bodyType: { weight: 400 } },
91
+ },
92
+ "editorial-cream": {
93
+ palette: { bg: "#F7F4EF", bgAlt: "#EDE8E0", text: "#1A1A1A", textMuted: "#5A5A5A", accent: "#C2410C", line: "#D6CEC1" },
94
+ fonts: { heading: "Playfair Display", body: "Lora", headingType: { weight: 400 }, bodyType: { weight: 400 } },
95
+ },
96
+ "cobalt-corporate": {
97
+ palette: { bg: "#0A1929", bgAlt: "#12263A", text: "#E8EEF5", textMuted: "#9FB3C8", accent: "#3B82F6", line: "#1E3A52" },
98
+ fonts: { heading: "Inter", body: "Inter", headingType: { weight: 700 }, bodyType: { weight: 400 } },
99
+ },
100
+ "sandstone-warm": {
101
+ palette: { bg: "#EDE8E0", bgAlt: "#E2DBD1", text: "#1A1A1A", textMuted: "#5A5A5A", accent: "#B45309", line: "#C9BEAE" },
102
+ fonts: { heading: "Poppins", body: "Nunito", headingType: { weight: 700 }, bodyType: { weight: 400 } },
103
+ },
104
+ "poster-contrast": {
105
+ palette: { bg: "#111111", bgAlt: "#1C1C1C", text: "#FFFFFF", textMuted: "#A3A3A3", accent: "#FACC15", line: "#333333" },
106
+ fonts: {
107
+ heading: "Anton",
108
+ body: "Oswald",
109
+ headingType: { weight: 400, casing: "uppercase" },
110
+ bodyType: { weight: 700, casing: "uppercase", tracking: 0.06 },
111
+ },
112
+ },
113
+ "mono-slate": {
114
+ palette: { bg: "#1C1C1E", bgAlt: "#2C2C2E", text: "#F5F5F7", textMuted: "#98989D", accent: "#A1A1AA", line: "#3A3A3C" },
115
+ fonts: {
116
+ heading: "Roboto",
117
+ body: "Roboto Mono",
118
+ headingType: { weight: 400, casing: "uppercase", tracking: 0.12 },
119
+ bodyType: { weight: 400 },
120
+ },
121
+ },
122
+ "vibrant-pulse": {
123
+ palette: { bg: "#0F172A", bgAlt: "#1E293B", text: "#F8FAFC", textMuted: "#94A3B8", accent: "#EC4899", accent2: "#22D3EE", line: "#334155" },
124
+ fonts: { heading: "Raleway", body: "Open Sans", headingType: { weight: 900 }, bodyType: { weight: 400 } },
125
+ },
126
+ "pastel-calm": {
127
+ palette: { bg: "#FDF2F8", bgAlt: "#FCE7F3", text: "#4C1D95", textMuted: "#7C3AED", accent: "#A855F7", line: "#F0D9EA" },
128
+ fonts: { heading: "Nunito", body: "Lato", headingType: { weight: 700 }, bodyType: { weight: 400 } },
129
+ },
130
+ }
131
+
132
+ export const BRAND_PRESET_META: Record<BrandPresetId, BrandPresetMeta> = {
133
+ "midnight-violet": { id: "midnight-violet", label: "Midnight Violet", mood: "bold / dark tech", description: "Near-black canvas, violet accent, cyan secondary — SaaS/tech launches." },
134
+ "editorial-cream": { id: "editorial-cream", label: "Editorial Cream", mood: "editorial / light", description: "Warm paper background, burnt-orange accent, serif type — magazine/editorial." },
135
+ "cobalt-corporate": { id: "cobalt-corporate", label: "Cobalt Corporate", mood: "corporate blue", description: "Deep navy, clean blue accent, Inter throughout — B2B/enterprise." },
136
+ "sandstone-warm": { id: "sandstone-warm", label: "Sandstone Warm", mood: "warm neutral", description: "Sandstone canvas, amber accent, rounded sans — approachable/lifestyle." },
137
+ "poster-contrast": { id: "poster-contrast", label: "Poster Contrast", mood: "high-contrast poster", description: "Ink-black, yellow accent, condensed display type — punchy/bold statements." },
138
+ "mono-slate": { id: "mono-slate", label: "Mono Slate", mood: "muted monochrome", description: "Slate greys, monospace body — technical/developer tone." },
139
+ "vibrant-pulse": { id: "vibrant-pulse", label: "Vibrant Pulse", mood: "vibrant", description: "Deep slate, magenta + cyan accents — energetic consumer/creator." },
140
+ "pastel-calm": { id: "pastel-calm", label: "Pastel Calm", mood: "calm pastel", description: "Soft pink canvas, purple accent — gentle/wellness/education." },
141
+ }
142
+
143
+ /** Resolve a preset-name string OR an inline BrandTokens object to BrandTokens. */
144
+ export function resolveBrandInput(brand: string | BrandTokens): BrandTokens {
145
+ if (typeof brand === "string") {
146
+ if (!Object.prototype.hasOwnProperty.call(BRAND_PRESETS, brand)) {
147
+ throw new Error(
148
+ `Unknown brand preset "${brand}". Valid presets: ${BRAND_PRESET_IDS.join(", ")}.`,
149
+ )
150
+ }
151
+ return BRAND_PRESETS[brand as BrandPresetId]
152
+ }
153
+ return brand
154
+ }