@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,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)
|
package/src/backdrop.ts
ADDED
|
@@ -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
|
+
}
|