@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,435 @@
1
+ /**
2
+ * Canonical catalog of Aesthetic / Microtrend choices.
3
+ *
4
+ * Single-pick parameter node — user picks ONE microtrend bundle that
5
+ * captures wardrobe + setting + grade + mood in a single descriptor.
6
+ * Microtrends are dense, model-recognised tokens (Y2K, dark academia,
7
+ * cottagecore, gorpcore...) that pull a coherent visual world along with
8
+ * the name. Each promptHint expands the slang into the specific wardrobe
9
+ * cues, environment cues and color treatment that define the look.
10
+ *
11
+ * Categories:
12
+ * - mainstream: high-recognition microtrends with a stable visual signature
13
+ * - niche: smaller -core / -kei / -punk movements
14
+ * - era: fashion eras / sensibilities (minimalism, maximalism, glam)
15
+ * - mood: cultural mood-bundles (effortless cool, main-character energy)
16
+ *
17
+ * Shared between the picker UI, the standalone Aesthetic parameter node,
18
+ * and the prompt-hint injection on both the frontend DAG executor and the
19
+ * backend orchestrator.
20
+ */
21
+
22
+ export type AestheticCategory = "mainstream" | "niche" | "era" | "mood"
23
+
24
+ export interface Aesthetic {
25
+ readonly id: string
26
+ readonly label: string
27
+ readonly category: AestheticCategory
28
+ readonly description: string
29
+ readonly promptHint: string
30
+ }
31
+
32
+ export const AESTHETICS: ReadonlyArray<Aesthetic> = [
33
+ // -------------------- Mainstream microtrends --------------------
34
+ {
35
+ id: "y2k",
36
+ label: "Y2K",
37
+ category: "mainstream",
38
+ description: "Late-90s / early-2000s pop tech",
39
+ promptHint:
40
+ "in the Y2K aesthetic — low-rise denim, baby tees, butterfly clips and chrome accessories, lit with sharp digicam flash and a faint chromatic gloss",
41
+ },
42
+ {
43
+ id: "cottagecore",
44
+ label: "Cottagecore",
45
+ category: "mainstream",
46
+ description: "Pastoral, hand-baked countryside",
47
+ promptHint:
48
+ "in the cottagecore aesthetic — linen dresses, gingham aprons and woven baskets in a sun-dappled cottage garden, with warm honeyed grading and gentle film grain",
49
+ },
50
+ {
51
+ id: "dark-academia",
52
+ label: "Dark Academia",
53
+ category: "mainstream",
54
+ description: "Old-world scholarship and tweed",
55
+ promptHint:
56
+ "in the dark academia aesthetic — tweed blazers, oxford shirts and stacks of leather-bound books in candlelit libraries, with a desaturated golden-shadow grade",
57
+ },
58
+ {
59
+ id: "light-academia",
60
+ label: "Light Academia",
61
+ category: "mainstream",
62
+ description: "Sunlit ivy-league scholarship",
63
+ promptHint:
64
+ "in the light academia aesthetic — cream cardigans, pleated skirts and open journals on sunlit terraces, with warm cream-and-honey color and softly washed highlights",
65
+ },
66
+ {
67
+ id: "techwear",
68
+ label: "Techwear / Gorpcore",
69
+ category: "mainstream",
70
+ description: "Tactical urban performance gear",
71
+ promptHint:
72
+ "in the techwear-gorpcore aesthetic — black tactical shells, MOLLE webbing, cargo pants and chunky trail runners in a rain-slick neon city, with cold cyan-magenta color",
73
+ },
74
+ {
75
+ id: "old-money",
76
+ label: "Old Money / Preppy",
77
+ category: "mainstream",
78
+ description: "Quiet-luxury heritage prep",
79
+ promptHint:
80
+ "in the old-money preppy aesthetic — cashmere sweaters, polo shirts, pearl strands and tailored linen on yacht decks and clay courts, with crisp, sun-bleached Mediterranean color",
81
+ },
82
+ {
83
+ id: "streetwear",
84
+ label: "Streetwear",
85
+ category: "mainstream",
86
+ description: "Sneaker-and-hoodie urban casual",
87
+ promptHint:
88
+ "in the streetwear aesthetic — graphic hoodies, oversized tees, premium sneakers and beanies on graffitied urban backdrops, with punchy contemporary color",
89
+ },
90
+ {
91
+ id: "coquette",
92
+ label: "Coquette",
93
+ category: "mainstream",
94
+ description: "Bows, lace and balletcore softness",
95
+ promptHint:
96
+ "in the coquette aesthetic — pink ribbons, lace trims, pearls and ballet flats in soft vanilla bedrooms, with rosy-pink grading and warm soft-focus glow",
97
+ },
98
+ {
99
+ id: "fairycore",
100
+ label: "Fairycore",
101
+ category: "mainstream",
102
+ description: "Mossy, mushroom-strewn enchantment",
103
+ promptHint:
104
+ "in the fairycore aesthetic — flowing tulle, flower crowns and embroidered shawls in mossy enchanted forests, with iridescent dappled light and pastel-green color",
105
+ },
106
+ {
107
+ id: "goblincore",
108
+ label: "Goblincore",
109
+ category: "mainstream",
110
+ description: "Mossy, mud-loving wilderness joy",
111
+ promptHint:
112
+ "in the goblincore aesthetic — earth-toned knits, muddy boots and pockets full of mushrooms and beetles in damp forest undergrowth, with rich green-brown grading",
113
+ },
114
+ {
115
+ id: "normcore",
116
+ label: "Normcore",
117
+ category: "mainstream",
118
+ description: "Anti-fashion plain basics",
119
+ promptHint:
120
+ "in the normcore aesthetic — plain jeans, white sneakers, gray sweatshirts and bucket hats against banal everyday backdrops, with flat neutral color and even daylight",
121
+ },
122
+ {
123
+ id: "balletcore",
124
+ label: "Balletcore",
125
+ category: "mainstream",
126
+ description: "Soft pink ballet-rehearsal grace",
127
+ promptHint:
128
+ "in the balletcore aesthetic — leg warmers, wrap cardigans, ribboned flats and tulle skirts in dust-floating studios, with soft pink-cream color and warm window light",
129
+ },
130
+ {
131
+ id: "e-girl",
132
+ label: "E-Girl",
133
+ category: "mainstream",
134
+ description: "Webcore liner-and-blush internet",
135
+ promptHint:
136
+ "in the e-girl aesthetic — heart blush, winged liner, dyed split hair, plaid skirts and chain necklaces in bedroom LED light, with cool magenta-cyan color",
137
+ },
138
+ {
139
+ id: "soft-girl",
140
+ label: "Soft Girl",
141
+ category: "mainstream",
142
+ description: "Pastel, sticker-bright sweetness",
143
+ promptHint:
144
+ "in the soft-girl aesthetic — pastel hoodies, butterfly clips, glossy lip and dainty heart stickers in cotton-candy-toned rooms, with warm peachy grading",
145
+ },
146
+ {
147
+ id: "indie-sleaze",
148
+ label: "Indie Sleaze",
149
+ category: "mainstream",
150
+ description: "Tumblr-era flash-blown party",
151
+ promptHint:
152
+ "in the indie sleaze aesthetic — smudged eyeliner, leather jackets and skinny jeans at smoky house parties, lit by harsh on-camera flash with deep blacks and noisy grain",
153
+ },
154
+ {
155
+ id: "baddie",
156
+ label: "Baddie",
157
+ category: "mainstream",
158
+ description: "Confident, styled, trend-savvy",
159
+ promptHint:
160
+ "in the baddie aesthetic — sharp-arched eyebrows, glossy lip, hoop earrings, bodycon dresses and bouncy curls in mirror-selfie poses, with high-contrast color and a polished glamorous finish",
161
+ },
162
+
163
+ // -------------------- Niche -core / -kei / -punk --------------------
164
+ {
165
+ id: "vacation-dadcore",
166
+ label: "Vacation Dadcore",
167
+ category: "niche",
168
+ description: "Hawaiian-shirt suburban-dad ease",
169
+ promptHint:
170
+ "in the vacation-dadcore aesthetic — Hawaiian shirts, cargo shorts, white socks and visor caps on cul-de-sac driveways, with bright tourist-snapshot color",
171
+ },
172
+ {
173
+ id: "womancore",
174
+ label: "Womancore",
175
+ category: "niche",
176
+ description: "Confident, unfussy adult femininity",
177
+ promptHint:
178
+ "in the womancore aesthetic — tailored slacks, silk blouses and minimal jewelry in airy modern apartments, with soft natural light and a subdued elegant palette",
179
+ },
180
+ {
181
+ id: "angelcore",
182
+ label: "Angelcore",
183
+ category: "niche",
184
+ description: "Cherubic, lace-and-feather softness",
185
+ promptHint:
186
+ "in the angelcore aesthetic — flowing white lace, lace gloves, halo headbands and feathered wings against pale heaven-soft backdrops, with creamy pastel grading",
187
+ },
188
+ {
189
+ id: "hikecore",
190
+ label: "Hikecore",
191
+ category: "niche",
192
+ description: "Trail-day mountaineer chic",
193
+ promptHint:
194
+ "in the hikecore aesthetic — fleece pullovers, hiking boots, retro daypacks and topo-print baselayers on alpine trails, with crisp mountain color and high-altitude clarity",
195
+ },
196
+ {
197
+ id: "mote-kei",
198
+ label: "Mote-Kei",
199
+ category: "niche",
200
+ description: "Conservative-cute Japanese feminine",
201
+ promptHint:
202
+ "in the mote-kei aesthetic — pleated A-line skirts, soft knits, neat ribbons and pastel cardigans in sun-flooded Tokyo cafes, with delicate creamy color",
203
+ },
204
+ {
205
+ id: "kombuchapunk",
206
+ label: "Kombuchapunk",
207
+ category: "niche",
208
+ description: "Fermenting wellness counter-culture",
209
+ promptHint:
210
+ "in the kombuchapunk aesthetic — thrifted graphic tees, layered hemp, tarot decks and home-fermented jars in plant-cluttered shared kitchens, with warm earthy color",
211
+ },
212
+ {
213
+ id: "chinapunk",
214
+ label: "Chinapunk",
215
+ category: "niche",
216
+ description: "Y2K Hong Kong cyber glam",
217
+ promptHint:
218
+ "in the chinapunk aesthetic — qipao-cut crop tops, mesh sleeves, futuristic shades and chrome charms on neon Hong Kong streets, with electric magenta-cyan grading",
219
+ },
220
+ {
221
+ id: "karencore",
222
+ label: "Karencore",
223
+ category: "niche",
224
+ description: "Suburban-Karen blunt-bob power",
225
+ promptHint:
226
+ "in the karencore aesthetic — bobbed blonde hair, oversized sunglasses, cropped capris and white SUVs in sun-baked suburban parking lots, with bleached overexposed color",
227
+ },
228
+ {
229
+ id: "candycore",
230
+ label: "Candycore",
231
+ category: "niche",
232
+ description: "Sugar-rush rainbow saturation",
233
+ promptHint:
234
+ "in the candycore aesthetic — rainbow stripes, lollipop accessories, glittery eye shadow and platform sneakers in candy-bright sets, with maximum saturation",
235
+ },
236
+ {
237
+ id: "babycore",
238
+ label: "Babycore",
239
+ category: "niche",
240
+ description: "Pacifier-soft infantile pastel",
241
+ promptHint:
242
+ "in the babycore aesthetic — bonnets, ruffled rompers, oversized bows and stuffed animals against nursery-pastel backdrops, with milky soft-pink color",
243
+ },
244
+ {
245
+ id: "catcore",
246
+ label: "Catcore",
247
+ category: "niche",
248
+ description: "Cat-eared cozy domestic",
249
+ promptHint:
250
+ "in the catcore aesthetic — cat-ear hoodies, paw mittens, knitted cardigans and tabby-print scarves curled up by sun-warmed windows, with soft golden color",
251
+ },
252
+ {
253
+ id: "weirdcore",
254
+ label: "Weirdcore",
255
+ category: "niche",
256
+ description: "Surreal liminal-space unease",
257
+ promptHint:
258
+ "in the weirdcore aesthetic — surreal liminal-space anxiety blending glitch and nostalgia, low-fi muted color and JPEG artefacts, often empty hallways, fluorescent basements or backrooms-style pools rendered with quiet dread",
259
+ },
260
+ {
261
+ id: "dreamcore",
262
+ label: "Dreamcore",
263
+ category: "niche",
264
+ description: "Soft surreal dreamlike serenity",
265
+ promptHint:
266
+ "in the dreamcore aesthetic — dreamlike soft-focus surrealism with candy pastel palettes and uncanny serenity, often empty rooms, drifting clouds or abandoned playgrounds bathed in hazy diffuse light",
267
+ },
268
+ {
269
+ id: "traumacore",
270
+ label: "Traumacore",
271
+ category: "niche",
272
+ description: "Distorted nostalgic childhood collage",
273
+ promptHint:
274
+ "in the traumacore aesthetic — distorted nostalgic childhood imagery layered with melancholic captions and unsettling edits, Hello-Kitty stickers cut against scratched found-photo collage, washed-out pinks and faded sadness",
275
+ },
276
+ {
277
+ id: "alt-girl",
278
+ label: "Alt / Alt-Girl",
279
+ category: "niche",
280
+ description: "Grunge-meets-emo internet alt",
281
+ promptHint:
282
+ "in the alt-girl aesthetic — alternative subculture mix of grunge, scene, emo and e-girl with dyed split hair, layered chains, fishnets, oversized band tees and smudged dark eyeliner, lit with cool moody color",
283
+ },
284
+ {
285
+ id: "blokecore",
286
+ label: "Blokecore",
287
+ category: "niche",
288
+ description: "Football-jersey dadcore menswear",
289
+ promptHint:
290
+ "in the blokecore aesthetic — vintage football jerseys, Adidas Sambas, straight-leg denim, bucket hats and casual sportswear elevated to a fashion statement, on terraced streets with naturalistic everyday color",
291
+ },
292
+ {
293
+ id: "y2k-rave",
294
+ label: "Y2K Rave / Decora Club-Kid",
295
+ category: "niche",
296
+ description: "Neon Japanese decora rave kid",
297
+ promptHint:
298
+ "in the Y2K-rave decora club-kid aesthetic — fluorescent layered tops, plastic-charm bracelets, kawaii hair clips, leg warmers and platform sneakers under blacklight and neon strobes, with hyper-saturated electric color",
299
+ },
300
+
301
+ // -------------------- Fashion eras --------------------
302
+ {
303
+ id: "minimalism",
304
+ label: "Minimalism",
305
+ category: "era",
306
+ description: "Pared-down quiet-luxury restraint",
307
+ promptHint:
308
+ "in the minimalist aesthetic — clean monochrome tailoring, structured silhouettes and empty white-stone spaces, with restrained neutral color and even soft light",
309
+ },
310
+ {
311
+ id: "maximalism",
312
+ label: "Maximalism",
313
+ category: "era",
314
+ description: "More-is-more clashing pattern",
315
+ promptHint:
316
+ "in the maximalist aesthetic — clashing prints, layered jewelry, velvet, sequins and floral wallpaper in densely styled rooms, with rich saturated color",
317
+ },
318
+ {
319
+ id: "avant-garde",
320
+ label: "Avant-Garde",
321
+ category: "era",
322
+ description: "Conceptual sculptural fashion",
323
+ promptHint:
324
+ "in the avant-garde aesthetic — sculptural deconstructed silhouettes, exaggerated proportions and architectural draping in stark gallery-white spaces, with cold minimal color",
325
+ },
326
+ {
327
+ id: "old-hollywood-glam",
328
+ label: "Old Hollywood Glam",
329
+ category: "era",
330
+ description: "Silver-screen 1940s glamour",
331
+ promptHint:
332
+ "in the old-Hollywood-glam aesthetic — satin gowns, fur stoles, finger waves and red lipstick under sculpted studio key light, with silvery monochrome and high-contrast tonality",
333
+ },
334
+
335
+ // -------------------- Mood bundles --------------------
336
+ {
337
+ id: "main-character-energy",
338
+ label: "Main Character Energy",
339
+ category: "mood",
340
+ description: "Unbothered cinematic confidence",
341
+ promptHint:
342
+ "with main-character energy — unbothered confident posture, signature outfit and direct gaze, framed cinematically with shallow depth of field and warm filmic grading",
343
+ },
344
+ {
345
+ id: "soft-easygoing",
346
+ label: "Soft / Easygoing",
347
+ category: "mood",
348
+ description: "Gentle, low-effort warm calm",
349
+ promptHint:
350
+ "in a soft easygoing mood — relaxed knits, oversized layers and easy posture in softly lit rooms, with hazy natural light and creamy gentle color",
351
+ },
352
+ {
353
+ id: "casual-lived-in",
354
+ label: "Casual / Lived-In",
355
+ category: "mood",
356
+ description: "Worn-in, unstyled everyday",
357
+ promptHint:
358
+ "in a casual lived-in mood — worn jeans, faded tees and rumpled hair in everyday domestic settings, with flat naturalistic light and unpolished snapshot color",
359
+ },
360
+ {
361
+ id: "effortless-cool",
362
+ label: "Effortless Cool",
363
+ category: "mood",
364
+ description: "Unstudied off-duty French-girl ease",
365
+ promptHint:
366
+ "in an effortless-cool mood — slouchy knits, slip skirts, messy bangs and minimal makeup on cobbled European streets, with gentle desaturated film color",
367
+ },
368
+ {
369
+ id: "retro-90s",
370
+ label: "Retro 90s",
371
+ category: "mood",
372
+ description: "Throwback grunge-meets-mall 90s",
373
+ promptHint:
374
+ "in a retro 90s mood — flannel shirts, slip dresses, chokers and platform boots in mall food courts and bedroom posters, with slight VHS-grain and warm Kodachrome color",
375
+ },
376
+ ] as const
377
+
378
+ const aestheticById = new Map<string, Aesthetic>(
379
+ AESTHETICS.map((a) => [a.id, a]),
380
+ )
381
+
382
+ export function getAesthetic(id: string | undefined | null): Aesthetic | undefined {
383
+ if (!id) return undefined
384
+ return aestheticById.get(id)
385
+ }
386
+
387
+ export function getAestheticLabel(id: string | undefined | null, fallback?: string): string {
388
+ const a = getAesthetic(id)
389
+ if (a) return a.label
390
+ if (fallback !== undefined) return fallback
391
+ return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
392
+ }
393
+
394
+ export function getAestheticPromptHint(id: string | undefined | null): string {
395
+ return getAesthetic(id)?.promptHint ?? ""
396
+ }
397
+
398
+ /**
399
+ * Multi-pick variant: 1-2 aesthetic ids → blended hint. Single → entry's
400
+ * own promptHint. Two → "styled in a {A} + {B} aesthetic blend" with
401
+ * canonical entry labels (Y2K, dark academia, etc. stay as written).
402
+ */
403
+ export function buildAestheticHints(value: unknown): string {
404
+ const ids: string[] = []
405
+ if (typeof value === "string" && value) ids.push(value)
406
+ else if (Array.isArray(value)) {
407
+ for (const v of value) {
408
+ if (typeof v === "string" && v && !ids.includes(v)) ids.push(v)
409
+ }
410
+ }
411
+ if (ids.length === 0) return ""
412
+ if (ids.length === 1) return getAestheticPromptHint(ids[0])
413
+ const labels = ids
414
+ .slice(0, 2)
415
+ .map((id) => getAesthetic(id)?.label ?? "")
416
+ .filter((s): s is string => Boolean(s))
417
+ if (labels.length < 2) return getAestheticPromptHint(ids[0])
418
+ return `styled in a ${labels[0]} + ${labels[1]} aesthetic blend`
419
+ }
420
+
421
+ export const AESTHETIC_IDS: ReadonlyArray<string> = AESTHETICS.map((a) => a.id)
422
+
423
+ export const AESTHETIC_CATEGORY_LABELS: Readonly<Record<AestheticCategory, string>> = {
424
+ mainstream: "Mainstream",
425
+ niche: "Niche",
426
+ era: "Fashion Era",
427
+ mood: "Mood",
428
+ }
429
+
430
+ export const AESTHETIC_CATEGORY_ORDER: ReadonlyArray<AestheticCategory> = [
431
+ "mainstream",
432
+ "niche",
433
+ "era",
434
+ "mood",
435
+ ]
@@ -0,0 +1,236 @@
1
+ /**
2
+ * `assembleImageInput` — the single source of truth for turning a node's
3
+ * image-generation inputs (a prompt + cinematic direction + connected
4
+ * references + the per-provider levers) into the flat `{ prompt,
5
+ * nativeNegativePrompt, referenceImageUrls }` the `generate-image` route
6
+ * expects.
7
+ *
8
+ * WHY THIS EXISTS (WI-1a): the same three-step assembly —
9
+ * 1. compose the prompt text (fold cinematic id-hints + structured fields),
10
+ * 2. call `buildImagePrompt(...)` (the pure core; the per-provider reference
11
+ * gate lives INSIDE it),
12
+ * 3. (optionally) reject a truly-empty FINAL prompt,
13
+ * — was duplicated in THREE places kept in lockstep by hand: the frontend
14
+ * `execute-node.ts` `generate-image` branch, the backend
15
+ * `payload-builder.ts` `generate-image` case, and Studio's `assembly.ts`.
16
+ * This wrapper collapses them into one.
17
+ *
18
+ * THE NO-OP CONTRACT (load-bearing — the platform-caller parity relies on it):
19
+ * the two platform callers (`execute-node` / `payload-builder`) compose their
20
+ * prompt from the canvas graph themselves and pass NO cinematic `direction`
21
+ * ids and NO `structured` fields. In that case `composePromptText` MUST return
22
+ * the caller's `userPrompt` byte-for-byte unchanged, so the wrapper degenerates
23
+ * to exactly the `buildImagePrompt(...)` call those sites make today. Studio
24
+ * (and the MCP route) supply `direction` / `structured` and get the id-hint
25
+ * composition on top.
26
+ *
27
+ * THE EMPTY-CHECK FLAG (also load-bearing for parity): `execute-node` rejects a
28
+ * truly-empty assembled prompt (its "type one, mention a character, or connect
29
+ * a cinematography source" guard); `payload-builder` does NOT (it never threw
30
+ * there). So the post-assembly throw is OPT-IN via `throwOnEmpty` — defaulting
31
+ * to `false` preserves the backend's no-throw behavior. Callers that want the
32
+ * guard (frontend, Studio, route) pass `throwOnEmpty: true`.
33
+ */
34
+ import {
35
+ buildImagePrompt,
36
+ type BuildImagePromptResult,
37
+ } from "./prompt-builder.js"
38
+ import { getFramingPromptHint } from "./framing.js"
39
+ import { getLightingPromptHint } from "./lighting.js"
40
+ import { getLensPromptHint } from "./lens.js"
41
+ import { getCameraFormatPromptHint } from "./camera-format.js"
42
+ import {
43
+ renderStructuredFields,
44
+ type StructuredPromptFields,
45
+ } from "./prompt-builder-structured-fields.js"
46
+ import type { CharacterDef, ConnectedReference, IdentityMeta } from "@nodaro/shared"
47
+
48
+ /**
49
+ * Flat cinematic-direction ids the Studio framing UI (and the MCP route)
50
+ * expose — all optional. Promoted here from Studio's `assembly.ts` so the
51
+ * id → hint composition lives in one place. The platform callers pass none of
52
+ * these (they fold their hints from the graph into `userPrompt` themselves).
53
+ */
54
+ export interface DirectionFields {
55
+ /** Shot Type — the FRAMINGS shot-size/coverage/composition/vantage dimensions. */
56
+ framingId?: string
57
+ /** Angle — the FRAMINGS angle dimension (separate pill, so it can coexist with Shot Type). */
58
+ framingAngleId?: string
59
+ lightingId?: string
60
+ lensId?: string
61
+ cameraFormatId?: string
62
+ }
63
+
64
+ /**
65
+ * Input to `assembleImageInput`. A faithful SUPERSET of what the two platform
66
+ * callers pass to `buildImagePrompt` today (so they can route through this
67
+ * wrapper with byte-identical output), plus the id-based `direction` /
68
+ * `structured` composition levers that Studio + the MCP route use.
69
+ */
70
+ export interface AssembleImageInput {
71
+ /** Pre-composed (caller's graph) or raw user prompt text. */
72
+ userPrompt: string
73
+ /** Image model id / provider key (the catalog enum value, e.g. "flux-2-max"). */
74
+ provider: string
75
+ /**
76
+ * Connected references with URLs ALREADY resolved by the caller. Become
77
+ * `referenceImageUrls` + identity directives inside `buildImagePrompt`
78
+ * (gated per provider there). Omit when the caller wires only raw URLs.
79
+ */
80
+ connectedReferences?: ConnectedReference[]
81
+ /**
82
+ * Flat cinematic-direction ids → folded into the prompt as hints. Studio /
83
+ * MCP-route use; the platform callers pass none (so `composePromptText` is a
84
+ * no-op for them and the result is byte-identical to today).
85
+ */
86
+ direction?: DirectionFields
87
+ /** Path-1 structured fields → composed fragment appended to the prompt. */
88
+ structured?: StructuredPromptFields
89
+ /**
90
+ * Reference image URLs from direct connections / manual uploads — ride
91
+ * `buildImagePrompt`'s reference-URL channel so they pass through the SAME
92
+ * per-provider reference-image gate + ordering as `connectedReferences`.
93
+ * (This is `buildImagePrompt`'s `referenceImageUrls` config field, named
94
+ * `extra…` here to reflect that bound entities already carry their URLs.)
95
+ */
96
+ extraReferenceImageUrls?: string[]
97
+ /** Negative prompt text (routed to native vs. "Avoid:" by `buildImagePrompt`). */
98
+ negativePrompt?: string
99
+ /** Style text to append (e.g. "cinematic"). */
100
+ style?: string
101
+ /** User-defined reorder of the injected reference list (stable tile ids). */
102
+ referenceOrder?: readonly string[]
103
+ /** Per-identity (imageIndex+label) user overrides for fidelity / custom text. */
104
+ identityMeta?: readonly IdentityMeta[]
105
+ /** Character slugs whose canonical-fallback the user explicitly hid. */
106
+ suppressedCanonicalCharacterIds?: readonly string[]
107
+ /** Location slugs whose canonical-fallback the user explicitly hid. */
108
+ suppressedCanonicalLocationIds?: readonly string[]
109
+ /**
110
+ * Reference-prompt assembly format for the `{image:N:label}` path. Forwarded
111
+ * verbatim to `buildImagePrompt` (default legacy). "hybrid" = images-only
112
+ * reference-lock snippet + lettered inline scene.
113
+ */
114
+ referenceFormat?: "legacy" | "hybrid"
115
+ /** Override the hybrid reference-lock snippet (forwarded to buildImagePrompt). */
116
+ referenceLockSnippet?: string
117
+ /** Character definitions selected for this node (legacy `buildImagePrompt` path). */
118
+ characterDefs?: CharacterDef[]
119
+ /** User-level prompt template overrides. */
120
+ userTemplates?: Record<string, string>
121
+ /** Flow-level prompt template overrides. */
122
+ flowTemplates?: Record<string, string>
123
+ /** Ancestor reference image URLs (fallback when no direct refs exist). */
124
+ ancestorRefs?: string[]
125
+ /** Map of `connectedReferences[i].id → sourceNodeId` for wired-raw tile ids. */
126
+ sourceNodeIdById?: ReadonlyMap<string, string>
127
+ /**
128
+ * LoRA inference path: strip `@`-mention tokens + skip the connected-reference
129
+ * machinery (the trigger word + LoRA carry identity).
130
+ */
131
+ skipCharacterMentions?: boolean
132
+ /**
133
+ * Reject a truly-empty FINAL (post-assembly) prompt with `throw`. OFF by
134
+ * default to match the backend `payload-builder`, which never threw. The
135
+ * frontend / Studio / route pass `true` to keep their "type one, bind a
136
+ * character, or pick a cinematography direction" guard. Checked POST-assembly
137
+ * so a bound entity / `@`-mention / direction chip that filled an otherwise-
138
+ * empty prompt still runs.
139
+ */
140
+ throwOnEmpty?: boolean
141
+ }
142
+
143
+ /**
144
+ * Compose the cinematic-direction hints + structured-field fragment with the
145
+ * user's prompt. Each `get*PromptHint` returns "" on a miss, and
146
+ * `renderStructuredFields` returns "" when nothing is populated.
147
+ *
148
+ * EXACT NO-OP CONTRACT: when there are no cinematic/structured hint pieces (the
149
+ * platform-caller case — execute-node / payload-builder never pass `direction`/
150
+ * `structured`), the user's prompt is returned **verbatim, untrimmed**. This is
151
+ * load-bearing for parity: the old platform path passed the prompt straight to
152
+ * `buildImagePrompt`, which never trims, so trimming here would change the
153
+ * assembled prompt (and the recorded `jobs.input_data`) byte-for-byte. We only
154
+ * trim the user prompt when joining it WITH hints, so it reads cleanly
155
+ * ("prompt. hint", not "prompt . hint"). Never mutates inputs.
156
+ */
157
+ function composePromptText(
158
+ userPrompt: string,
159
+ direction: DirectionFields | undefined,
160
+ structured: StructuredPromptFields | undefined,
161
+ ): string {
162
+ const hints = [
163
+ getFramingPromptHint(direction?.framingId),
164
+ getFramingPromptHint(direction?.framingAngleId),
165
+ getLightingPromptHint(direction?.lightingId),
166
+ getLensPromptHint(direction?.lensId),
167
+ getCameraFormatPromptHint(direction?.cameraFormatId),
168
+ structured ? renderStructuredFields(structured) : "",
169
+ ].filter((p) => p.length > 0)
170
+ // No hints → verbatim (exact no-op = platform parity). With hints → trim the
171
+ // user prompt so the ". " join is clean. The trailing filter drops a blank
172
+ // user prompt so the join never starts with ". " (parity-critical — don't
173
+ // remove it as "redundant": `hints` is pre-filtered but `userPrompt` is not).
174
+ if (hints.length === 0) return userPrompt
175
+ return [userPrompt.trim(), ...hints].filter((p) => p.length > 0).join(". ")
176
+ }
177
+
178
+ /**
179
+ * Assemble a node's image-generation inputs into a `BuildImagePromptResult`
180
+ * (`{ prompt, nativeNegativePrompt, referenceImageUrls }`).
181
+ *
182
+ * Order: (1) compose the prompt text (no-op when no direction/structured),
183
+ * (2) `buildImagePrompt(...)` — exactly the call the three sites make today,
184
+ * (3) optional post-assembly empty-prompt throw (gated by `throwOnEmpty`).
185
+ */
186
+ export function assembleImageInput(
187
+ input: AssembleImageInput,
188
+ ): BuildImagePromptResult {
189
+ const prompt = composePromptText(input.userPrompt, input.direction, input.structured)
190
+
191
+ const result = buildImagePrompt({
192
+ prompt,
193
+ provider: input.provider,
194
+ ...(input.connectedReferences !== undefined
195
+ ? { connectedReferences: input.connectedReferences }
196
+ : {}),
197
+ // Manual uploads / direct refs ride the builder's reference-URL channel so
198
+ // the per-provider reference gate filters them alongside bound entities.
199
+ // Omit the field entirely when absent so the builder's default ([]) kicks
200
+ // in — byte-identical to a caller that didn't set it.
201
+ ...(input.extraReferenceImageUrls !== undefined
202
+ ? { referenceImageUrls: input.extraReferenceImageUrls }
203
+ : {}),
204
+ ...(input.negativePrompt !== undefined ? { negativePrompt: input.negativePrompt } : {}),
205
+ ...(input.style !== undefined ? { style: input.style } : {}),
206
+ ...(input.referenceOrder !== undefined ? { referenceOrder: input.referenceOrder } : {}),
207
+ ...(input.identityMeta !== undefined ? { identityMeta: input.identityMeta } : {}),
208
+ ...(input.suppressedCanonicalCharacterIds !== undefined
209
+ ? { suppressedCanonicalCharacterIds: input.suppressedCanonicalCharacterIds }
210
+ : {}),
211
+ ...(input.suppressedCanonicalLocationIds !== undefined
212
+ ? { suppressedCanonicalLocationIds: input.suppressedCanonicalLocationIds }
213
+ : {}),
214
+ ...(input.referenceFormat !== undefined ? { referenceFormat: input.referenceFormat } : {}),
215
+ ...(input.referenceLockSnippet !== undefined ? { referenceLockSnippet: input.referenceLockSnippet } : {}),
216
+ ...(input.characterDefs !== undefined ? { characterDefs: input.characterDefs } : {}),
217
+ ...(input.userTemplates !== undefined ? { userTemplates: input.userTemplates } : {}),
218
+ ...(input.flowTemplates !== undefined ? { flowTemplates: input.flowTemplates } : {}),
219
+ ...(input.ancestorRefs !== undefined ? { ancestorRefs: input.ancestorRefs } : {}),
220
+ ...(input.sourceNodeIdById !== undefined ? { sourceNodeIdById: input.sourceNodeIdById } : {}),
221
+ ...(input.skipCharacterMentions !== undefined
222
+ ? { skipCharacterMentions: input.skipCharacterMentions }
223
+ : {}),
224
+ })
225
+
226
+ // Post-assembly empty-prompt check (opt-in): a bound entity / `@`-mention /
227
+ // direction chip could have filled the assembled prompt even if the user
228
+ // typed nothing — so only reject when the FINAL prompt is truly empty.
229
+ if (input.throwOnEmpty && !result.prompt.trim()) {
230
+ throw new Error(
231
+ "No prompt — type one, bind a character, or pick a cinematography direction",
232
+ )
233
+ }
234
+
235
+ return result
236
+ }