@nodaro/prompts 1.38.0 → 1.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/chunk-2EW5B7ZV.cjs +86 -0
  2. package/dist/chunk-2EW5B7ZV.cjs.map +1 -0
  3. package/dist/chunk-2P26SZX5.cjs +414 -0
  4. package/dist/chunk-2P26SZX5.cjs.map +1 -0
  5. package/dist/{chunk-7XKHDXCL.cjs → chunk-3U7X6JZN.cjs} +13 -12
  6. package/dist/chunk-3U7X6JZN.cjs.map +1 -0
  7. package/dist/chunk-4GKTOVFM.cjs +8 -0
  8. package/dist/chunk-4GKTOVFM.cjs.map +1 -0
  9. package/dist/{chunk-HRBBNODV.js → chunk-6GVFJLDL.js} +4 -3
  10. package/dist/chunk-6GVFJLDL.js.map +1 -0
  11. package/dist/{chunk-LVSH7K7R.cjs → chunk-C3NHNECH.cjs} +165 -88
  12. package/dist/chunk-C3NHNECH.cjs.map +1 -0
  13. package/dist/{chunk-CEPVQQVO.js → chunk-C46256GZ.js} +43 -5
  14. package/dist/chunk-C46256GZ.js.map +1 -0
  15. package/dist/{chunk-JKCR6WRZ.cjs → chunk-DQFQJECU.cjs} +43 -5
  16. package/dist/chunk-DQFQJECU.cjs.map +1 -0
  17. package/dist/{chunk-QDEWBBUR.js → chunk-DUXX6CBX.js} +12 -143
  18. package/dist/chunk-DUXX6CBX.js.map +1 -0
  19. package/dist/chunk-EAYEZXWT.js +388 -0
  20. package/dist/chunk-EAYEZXWT.js.map +1 -0
  21. package/dist/chunk-GILUVQQJ.js +6 -0
  22. package/dist/chunk-GILUVQQJ.js.map +1 -0
  23. package/dist/{chunk-KIYNOOPJ.js → chunk-GTDKDD7Y.js} +12 -6
  24. package/dist/chunk-GTDKDD7Y.js.map +1 -0
  25. package/dist/{chunk-QBEZMR2F.js → chunk-HQTHDTHN.js} +137 -62
  26. package/dist/chunk-HQTHDTHN.js.map +1 -0
  27. package/dist/{chunk-TLVX5PXD.cjs → chunk-IUCNJAHW.cjs} +13 -7
  28. package/dist/chunk-IUCNJAHW.cjs.map +1 -0
  29. package/dist/{chunk-ZFIXQRBZ.cjs → chunk-LXPKTI2K.cjs} +12 -83
  30. package/dist/chunk-LXPKTI2K.cjs.map +1 -0
  31. package/dist/{chunk-CVPB2N3Z.cjs → chunk-LZ53F5BN.cjs} +103 -5
  32. package/dist/chunk-LZ53F5BN.cjs.map +1 -0
  33. package/dist/{chunk-4FDNVNJE.js → chunk-N6VMPG5S.js} +12 -79
  34. package/dist/chunk-N6VMPG5S.js.map +1 -0
  35. package/dist/{chunk-LK2CZK5U.cjs → chunk-PWWM7Q3T.cjs} +7 -7
  36. package/dist/{chunk-LK2CZK5U.cjs.map → chunk-PWWM7Q3T.cjs.map} +1 -1
  37. package/dist/{chunk-N7TOWXDJ.js → chunk-QDUTMOF4.js} +3 -3
  38. package/dist/{chunk-N7TOWXDJ.js.map → chunk-QDUTMOF4.js.map} +1 -1
  39. package/dist/{chunk-NO6XHX4R.js → chunk-RB2F5JQF.js} +100 -7
  40. package/dist/chunk-RB2F5JQF.js.map +1 -0
  41. package/dist/{chunk-MENBHEAY.cjs → chunk-WRM52ZBN.cjs} +19 -161
  42. package/dist/chunk-WRM52ZBN.cjs.map +1 -0
  43. package/dist/chunk-YS6HOWMW.js +80 -0
  44. package/dist/chunk-YS6HOWMW.js.map +1 -0
  45. package/dist/entries/assembly.cjs +34 -25
  46. package/dist/entries/assembly.d.cts +121 -37
  47. package/dist/entries/assembly.d.ts +121 -37
  48. package/dist/entries/assembly.js +5 -4
  49. package/dist/entries/doctrine.cjs +46 -46
  50. package/dist/entries/doctrine.d.cts +3 -3
  51. package/dist/entries/doctrine.d.ts +3 -3
  52. package/dist/entries/doctrine.js +1 -1
  53. package/dist/entries/picker-catalogs.cjs +42 -42
  54. package/dist/entries/picker-catalogs.js +3 -3
  55. package/dist/entries/presets.cjs +49 -26
  56. package/dist/entries/presets.d.cts +53 -1
  57. package/dist/entries/presets.d.ts +53 -1
  58. package/dist/entries/presets.js +4 -1
  59. package/dist/entries/prompt-builder.cjs +135 -98
  60. package/dist/entries/prompt-builder.d.cts +22 -11
  61. package/dist/entries/prompt-builder.d.ts +22 -11
  62. package/dist/entries/prompt-builder.js +4 -3
  63. package/dist/entries/prompt-fields.cjs +41 -40
  64. package/dist/entries/prompt-fields.d.cts +2 -1
  65. package/dist/entries/prompt-fields.d.ts +2 -1
  66. package/dist/entries/prompt-fields.js +2 -1
  67. package/dist/index.cjs +339 -273
  68. package/dist/index.d.cts +4 -4
  69. package/dist/index.d.ts +4 -4
  70. package/dist/index.js +11 -9
  71. package/dist/{subject-registry-BUswuEVD.d.cts → subject-registry-CQi_CZfJ.d.cts} +78 -9
  72. package/dist/{subject-registry-D7V8PEG-.d.ts → subject-registry-DFiGx49v.d.ts} +78 -9
  73. package/package.json +2 -2
  74. package/src/__tests__/assemble-image-input-style-references.test.ts +173 -0
  75. package/src/__tests__/catalog-id-guard.test.ts +30 -0
  76. package/src/__tests__/factory-preset-previews.test.ts +123 -0
  77. package/src/__tests__/factory-preset-references.test.ts +222 -0
  78. package/src/__tests__/factory-presets-prototype-keys.test.ts +43 -0
  79. package/src/__tests__/frame-reference-slots.test.ts +43 -0
  80. package/src/__tests__/gemini-omni-inputs.test.ts +12 -10
  81. package/src/__tests__/provider-prompt-doctrine.test.ts +14 -0
  82. package/src/__tests__/user-direction-refs.test.ts +180 -0
  83. package/src/__tests__/veo-i2v-inputs.test.ts +16 -11
  84. package/src/assemble-image-input.ts +114 -13
  85. package/src/catalog-id-guard.ts +18 -2
  86. package/src/color-names.ts +192 -0
  87. package/src/direction-registry.ts +66 -5
  88. package/src/entries/assembly.ts +1 -0
  89. package/src/entries/prompt-builder.ts +2 -0
  90. package/src/factory-presets/generate-image.ts +29 -0
  91. package/src/factory-presets/index.ts +6 -1
  92. package/src/factory-presets/preview-files.generated.ts +4 -0
  93. package/src/factory-presets/previews.ts +40 -0
  94. package/src/factory-presets/references.ts +81 -0
  95. package/src/factory-presets/shared-image.ts +4 -0
  96. package/src/factory-presets/types.ts +8 -0
  97. package/src/frame-reference-slots.ts +92 -0
  98. package/src/gemini-omni-inputs.ts +23 -22
  99. package/src/image-reference-doctrine.ts +3 -3
  100. package/src/image-token-pattern.ts +13 -0
  101. package/src/node-prompt-fields.ts +5 -3
  102. package/src/own-entry.ts +11 -0
  103. package/src/prompt-builder.ts +4 -5
  104. package/src/prompt-style-section.ts +3 -3
  105. package/src/prompt-wizard-categories.ts +2 -0
  106. package/src/provider-prompt-doctrine.ts +45 -3
  107. package/src/ref-binding.ts +28 -3
  108. package/src/resolve-prompt.ts +2 -1
  109. package/src/seedance-2-inputs.ts +15 -40
  110. package/src/user-direction-refs.ts +110 -0
  111. package/src/veo-i2v-inputs.ts +24 -17
  112. package/src/video-reference-resolver.ts +1 -1
  113. package/dist/chunk-4FDNVNJE.js.map +0 -1
  114. package/dist/chunk-7XKHDXCL.cjs.map +0 -1
  115. package/dist/chunk-CEPVQQVO.js.map +0 -1
  116. package/dist/chunk-CVPB2N3Z.cjs.map +0 -1
  117. package/dist/chunk-HRBBNODV.js.map +0 -1
  118. package/dist/chunk-JKCR6WRZ.cjs.map +0 -1
  119. package/dist/chunk-KIYNOOPJ.js.map +0 -1
  120. package/dist/chunk-LVSH7K7R.cjs.map +0 -1
  121. package/dist/chunk-MENBHEAY.cjs.map +0 -1
  122. package/dist/chunk-NO6XHX4R.js.map +0 -1
  123. package/dist/chunk-QBEZMR2F.js.map +0 -1
  124. package/dist/chunk-QDEWBBUR.js.map +0 -1
  125. package/dist/chunk-TLVX5PXD.cjs.map +0 -1
  126. package/dist/chunk-UJKFHPZ7.js +0 -37
  127. package/dist/chunk-UJKFHPZ7.js.map +0 -1
  128. package/dist/chunk-W5SCZIOB.cjs +0 -43
  129. package/dist/chunk-W5SCZIOB.cjs.map +0 -1
  130. package/dist/chunk-ZFIXQRBZ.cjs.map +0 -1
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Plain colour names for a hex value — what a saved colour palette reads as
3
+ * in a prompt ("deep teal #0F4C5C"). A model follows a named colour more
4
+ * reliably than a bare hex code, and the hex keeps the exact value.
5
+ *
6
+ * NEAREST NAME, NOT A CLASSIFIER: a fixed table of everyday colour names, each
7
+ * with a representative value, and the one closest to the input in CIE Lab
8
+ * (ΔE 1976 — Euclidean distance in Lab, which tracks perceived difference far
9
+ * better than RGB). Deterministic, dependency-free, and a name is always one a
10
+ * person would say. Ties go to the earlier row.
11
+ */
12
+
13
+ /** Everyday colour names and a representative sRGB value for each. */
14
+ const NAMED: ReadonlyArray<readonly [name: string, hex: string]> = [
15
+ // Neutrals
16
+ ["black", "#000000"],
17
+ ["charcoal", "#36454F"],
18
+ ["dark gray", "#555555"],
19
+ ["slate gray", "#708090"],
20
+ ["gray", "#808080"],
21
+ ["warm gray", "#9A9088"],
22
+ ["silver", "#C0C0C0"],
23
+ ["light gray", "#D3D3D3"],
24
+ ["off-white", "#F2F0EB"],
25
+ ["white", "#FFFFFF"],
26
+ ["ivory", "#FFFFF0"],
27
+ ["cream", "#FFFDD0"],
28
+ ["eggshell", "#F0EAD6"],
29
+ ["linen", "#FAF0E6"],
30
+ ["champagne", "#F7E7CE"],
31
+ ["wheat", "#F5DEB3"],
32
+ // Earth tones and browns
33
+ ["sand", "#C2B280"],
34
+ ["tan", "#D2B48C"],
35
+ ["khaki", "#C3B091"],
36
+ ["taupe", "#8B8589"],
37
+ ["camel", "#C19A6B"],
38
+ ["caramel", "#C68E17"],
39
+ ["brown", "#964B00"],
40
+ ["chocolate", "#7B3F00"],
41
+ ["coffee", "#6F4E37"],
42
+ ["espresso", "#4E312D"],
43
+ ["walnut", "#773F1A"],
44
+ ["chestnut", "#954535"],
45
+ ["mahogany", "#C04000"],
46
+ ["rust", "#B7410E"],
47
+ ["terracotta", "#E2725B"],
48
+ ["copper", "#B87333"],
49
+ ["bronze", "#CD7F32"],
50
+ ["ochre", "#CC7722"],
51
+ // Oranges
52
+ ["burnt orange", "#CC5500"],
53
+ ["orange", "#FFA500"],
54
+ ["tangerine", "#F28500"],
55
+ ["apricot", "#FBCEB1"],
56
+ ["peach", "#FFCBA4"],
57
+ ["coral", "#FF7F50"],
58
+ ["salmon", "#FA8072"],
59
+ // Reds
60
+ ["red", "#FF0000"],
61
+ ["scarlet", "#FF2400"],
62
+ ["crimson", "#DC143C"],
63
+ ["cherry red", "#D2042D"],
64
+ ["brick red", "#CB4154"],
65
+ ["burgundy", "#800020"],
66
+ ["maroon", "#800000"],
67
+ ["wine red", "#722F37"],
68
+ ["oxblood", "#4A0000"],
69
+ // Pinks
70
+ ["raspberry", "#E30B5C"],
71
+ ["rose", "#FF007F"],
72
+ ["hot pink", "#FF69B4"],
73
+ ["pink", "#FFC0CB"],
74
+ ["blush pink", "#E8B4B8"],
75
+ ["dusty rose", "#C08081"],
76
+ ["mauve", "#B784A7"],
77
+ // Purples
78
+ ["magenta", "#FF00FF"],
79
+ ["orchid", "#DA70D6"],
80
+ ["plum", "#8E4585"],
81
+ ["eggplant", "#614051"],
82
+ ["dusty purple", "#6E5A7E"],
83
+ ["lilac", "#C8A2C8"],
84
+ ["lavender", "#B57EDC"],
85
+ ["pale lavender", "#E6E6FA"],
86
+ ["amethyst", "#9966CC"],
87
+ ["violet", "#8F00FF"],
88
+ ["purple", "#800080"],
89
+ ["indigo", "#4B0082"],
90
+ // Blues
91
+ ["navy blue", "#000080"],
92
+ ["midnight blue", "#191970"],
93
+ ["prussian blue", "#003153"],
94
+ ["dusky blue", "#4B5E7D"],
95
+ ["royal blue", "#4169E1"],
96
+ ["cobalt blue", "#0047AB"],
97
+ ["blue", "#0000FF"],
98
+ ["cornflower blue", "#6495ED"],
99
+ ["denim blue", "#1560BD"],
100
+ ["steel blue", "#4682B4"],
101
+ ["slate blue", "#6A5ACD"],
102
+ ["periwinkle", "#CCCCFF"],
103
+ ["sky blue", "#87CEEB"],
104
+ ["baby blue", "#89CFF0"],
105
+ ["powder blue", "#B0E0E6"],
106
+ ["ice blue", "#D6ECEF"],
107
+ ["azure", "#007FFF"],
108
+ ["cerulean", "#007BA7"],
109
+ ["petrol blue", "#005F6A"],
110
+ // Cyans and teals
111
+ ["cyan", "#00FFFF"],
112
+ ["turquoise", "#40E0D0"],
113
+ ["aquamarine", "#7FFFD4"],
114
+ ["teal", "#008080"],
115
+ ["dark teal", "#014D4E"],
116
+ ["dark slate gray", "#2F4F4F"],
117
+ // Greens
118
+ ["sea green", "#2E8B57"],
119
+ ["emerald green", "#50C878"],
120
+ ["jade green", "#00A86B"],
121
+ ["mint green", "#98FF98"],
122
+ ["seafoam green", "#93E9BE"],
123
+ ["sage green", "#9CAF88"],
124
+ ["olive green", "#708238"],
125
+ ["olive", "#808000"],
126
+ ["moss green", "#8A9A5B"],
127
+ ["forest green", "#228B22"],
128
+ ["hunter green", "#355E3B"],
129
+ ["bottle green", "#006A4E"],
130
+ ["dark green", "#013220"],
131
+ ["green", "#008000"],
132
+ ["kelly green", "#4CBB17"],
133
+ ["lime green", "#32CD32"],
134
+ ["chartreuse", "#7FFF00"],
135
+ ["pistachio", "#93C572"],
136
+ // Yellows and golds
137
+ ["yellow", "#FFFF00"],
138
+ ["lemon yellow", "#FFF44F"],
139
+ ["pale yellow", "#FDFD96"],
140
+ ["canary yellow", "#FFEF00"],
141
+ ["mustard yellow", "#E1AD01"],
142
+ ["gold", "#FFD700"],
143
+ ["amber", "#FFBF00"],
144
+ ["honey", "#F0A830"],
145
+ ["saffron", "#F4C430"],
146
+ ]
147
+
148
+ const HEX_RE = /^#[0-9a-f]{6}$/i
149
+
150
+ type Lab = readonly [l: number, a: number, b: number]
151
+
152
+ function channel(v: number): number {
153
+ const c = v / 255
154
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
155
+ }
156
+
157
+ function labPivot(t: number): number {
158
+ return t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116
159
+ }
160
+
161
+ /** sRGB hex → CIE Lab (D65). Only called with a value that passed `HEX_RE`. */
162
+ function toLab(hex: string): Lab {
163
+ const r = channel(parseInt(hex.slice(1, 3), 16))
164
+ const g = channel(parseInt(hex.slice(3, 5), 16))
165
+ const b = channel(parseInt(hex.slice(5, 7), 16))
166
+ const x = labPivot((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047)
167
+ const y = labPivot(0.2126729 * r + 0.7151522 * g + 0.072175 * b)
168
+ const z = labPivot((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / 1.08883)
169
+ return [116 * y - 16, 500 * (x - y), 200 * (y - z)]
170
+ }
171
+
172
+ let namedLab: ReadonlyArray<readonly [string, Lab]> | null = null
173
+
174
+ /**
175
+ * The everyday name nearest to `hex` (`#RRGGBB`, any case), or `""` when the
176
+ * value is not a `#RRGGBB` colour.
177
+ */
178
+ export function nearestColorName(hex: string): string {
179
+ if (!HEX_RE.test(hex)) return ""
180
+ namedLab ??= NAMED.map(([name, value]) => [name, toLab(value)] as const)
181
+ const [l, a, b] = toLab(hex)
182
+ let best = ""
183
+ let bestDistance = Infinity
184
+ for (const [name, [nl, na, nb]] of namedLab) {
185
+ const d = (l - nl) ** 2 + (a - na) ** 2 + (b - nb) ** 2
186
+ if (d < bestDistance) {
187
+ bestDistance = d
188
+ best = name
189
+ }
190
+ }
191
+ return best
192
+ }
@@ -69,6 +69,7 @@ import { buildActionFxHints } from "./action-fx.js"
69
69
  import { getTemporalPromptHint, getTemporalTerm } from "./temporal.js"
70
70
  import { renderTransitionBases } from "./transitions.js"
71
71
  import { getLoopSubjectPromptHint, getLoopSubjectTerm } from "./loop-subject.js"
72
+ import { parseUserDirectionRef, type UserDirectionRefKind } from "./user-direction-refs.js"
72
73
 
73
74
  /** Which generation stages fold a dimension. */
74
75
  export type DirectionSurface = "image" | "video" | "both"
@@ -112,6 +113,14 @@ export interface DirectionFieldSpec {
112
113
  readonly styleGroup?: DirectionStyleGroup
113
114
  /** Ids honored per dimension. Extras are SLICED at render, never a 400. */
114
115
  readonly maxPicks: number
116
+ /**
117
+ * The kind of the caller's OWN saved resource this row also takes, as
118
+ * `<kind>:<uuid>` (`user-direction-refs.ts`) — a palette on `colorLook`, a
119
+ * moodboard on `style`. Only single-pick rows carry one, so a pick budget can
120
+ * never slice a reference away. The platform resolves the reference before
121
+ * the fold and hands the rendered clause in as `refs`.
122
+ */
123
+ readonly userRef?: UserDirectionRefKind
115
124
  /**
116
125
  * Render selected ids in this catalog's own doctrine: per-id for independent
117
126
  * catalogs, ONE blended clause for Mood / Aesthetic / Photographer, the
@@ -228,12 +237,12 @@ export const DIRECTION_FIELDS = [
228
237
  { key: "lightingDirection", surface: "both", family: "look", maxPicks: 1, render: lighting },
229
238
  { key: "lightingRatio", surface: "both", family: "look", maxPicks: 1, render: lighting },
230
239
  { key: "colorTemperature", surface: "both", family: "look", maxPicks: 1, render: lighting },
231
- { key: "colorLook", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, render: perId(getColorLookPromptHint, getColorLookTerm) },
240
+ { key: "colorLook", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, userRef: "palette", render: perId(getColorLookPromptHint, getColorLookTerm) },
232
241
  { key: "atmosphere", surface: "both", family: "look", maxPicks: 2, render: viaListBuilder(buildAtmosphereHints) },
233
242
  { key: "postProcess", surface: "image", family: "look", maxPicks: 2, render: viaListBuilder(buildPostProcessHints) },
234
243
 
235
244
  // Style.
236
- { key: "style", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, render: perId(getStylePromptHint, getStyleTerm) },
245
+ { key: "style", surface: "both", family: "look", styleGroup: "film", maxPicks: 1, userRef: "moodboard", render: perId(getStylePromptHint, getStyleTerm) },
237
246
  { key: "mood", surface: "both", family: "look", maxPicks: 2, render: viaMood },
238
247
  { key: "aesthetic", surface: "both", family: "look", maxPicks: 2, render: viaStringBuilder(buildAestheticHints) },
239
248
  { key: "photoGenre", surface: "image", family: "look", maxPicks: 1, render: perId(getPhotoGenrePromptHint, getPhotoGenreTerm) },
@@ -303,11 +312,39 @@ export const FILM_STYLE_KEYS: ReadonlyArray<DirectionKey> = DIRECTION_SPECS.filt
303
312
  (f) => f.styleGroup === "film",
304
313
  ).map((f) => f.key as DirectionKey)
305
314
 
315
+ /**
316
+ * The direction key that takes a reference of this kind — `colorLook` for a
317
+ * palette, `style` for a moodboard. Derived from the table's `userRef` column.
318
+ */
319
+ export function directionKeyForUserRef(kind: UserDirectionRefKind): DirectionKey {
320
+ const row = DIRECTION_SPECS.find((f) => f.userRef === kind)
321
+ if (!row) throw new Error(`no direction row takes a ${kind} reference`)
322
+ return row.key as DirectionKey
323
+ }
324
+
325
+ /** The kind of saved resource a direction key takes, or undefined (most keys). */
326
+ export function userRefKindOfDirectionKey(key: string): UserDirectionRefKind | undefined {
327
+ return DIRECTION_SPECS.find((f) => f.key === key)?.userRef
328
+ }
329
+
306
330
  /** Verbosity for a whole fold, or split per family. */
307
331
  export type DirectionHintMode =
308
332
  | PickerHintMode
309
333
  | { readonly look: PickerHintMode; readonly motion: PickerHintMode }
310
334
 
335
+ /** How a direction bag is folded. */
336
+ export interface DirectionRenderOptions {
337
+ readonly surface: "image" | "video"
338
+ readonly mode?: DirectionHintMode
339
+ /**
340
+ * Rendered clauses for the caller's OWN saved resources, keyed by the
341
+ * direction value (`palette:<id>` → `color palette: …`), resolved by the
342
+ * platform before the fold. A reference absent from the map renders nothing,
343
+ * exactly like an unknown catalog id. Absent = no reference resolves.
344
+ */
345
+ readonly refs?: ReadonlyMap<string, string>
346
+ }
347
+
311
348
  /** Image policy: full clause for every dimension (today's behavior). */
312
349
  export const IMAGE_HINT_MODE_DEFAULT: DirectionHintMode = "full"
313
350
 
@@ -360,6 +397,30 @@ function normalizeDirectionIds(value: unknown, maxPicks: number): string[] {
360
397
  return ids.slice(0, maxPicks)
361
398
  }
362
399
 
400
+ /**
401
+ * One row's clauses. A row with no user-owned reference among its ids renders
402
+ * through its catalog doctrine exactly as before (byte parity). On a row that
403
+ * takes a reference (`userRef`), each id renders on its own, in order: a
404
+ * reference of the row's kind becomes its resolved clause from `refs` (nothing
405
+ * when unresolved), any other value goes through the catalog.
406
+ */
407
+ function renderRow(
408
+ spec: DirectionFieldSpec,
409
+ ids: readonly string[],
410
+ mode: PickerHintMode,
411
+ refs: ReadonlyMap<string, string> | undefined,
412
+ ): string[] {
413
+ if (spec.userRef === undefined || !ids.some((id) => parseUserDirectionRef(id) !== null)) {
414
+ return spec.render(ids, mode)
415
+ }
416
+ return ids.flatMap((id) => {
417
+ const ref = parseUserDirectionRef(id)
418
+ if (ref === null) return spec.render([id], mode)
419
+ const clause = ref.kind === spec.userRef ? refs?.get(id) : undefined
420
+ return clause ? [clause] : []
421
+ })
422
+ }
423
+
363
424
  /** The rows a given generation stage folds, in table order. */
364
425
  export function directionFieldsForSurface(
365
426
  surface: "image" | "video",
@@ -383,7 +444,7 @@ export interface DirectionHintClause {
383
444
  */
384
445
  export function renderDirectionHintClauses(
385
446
  direction: DirectionFields | undefined,
386
- opts: { surface: "image" | "video"; mode?: DirectionHintMode },
447
+ opts: DirectionRenderOptions,
387
448
  ): DirectionHintClause[] {
388
449
  if (!direction) return []
389
450
  const mode = opts.mode ?? "full"
@@ -396,7 +457,7 @@ export function renderDirectionHintClauses(
396
457
  spec.maxPicks,
397
458
  )
398
459
  if (ids.length === 0) continue
399
- for (const hint of spec.render(ids, modeForFamily(mode, spec.family))) {
460
+ for (const hint of renderRow(spec, ids, modeForFamily(mode, spec.family), opts.refs)) {
400
461
  if (hint.length > 0 && !seen.has(hint)) {
401
462
  seen.add(hint)
402
463
  out.push({
@@ -436,7 +497,7 @@ export function renderDirectionHintClauses(
436
497
  */
437
498
  export function renderDirectionHints(
438
499
  direction: DirectionFields | undefined,
439
- opts: { surface: "image" | "video"; mode?: DirectionHintMode },
500
+ opts: DirectionRenderOptions,
440
501
  ): string[] {
441
502
  return renderDirectionHintClauses(direction, opts).map((c) => c.text)
442
503
  }
@@ -20,6 +20,7 @@ export * from "../assemble-video-input.js"
20
20
  export * from "../seedance-2-inputs.js"
21
21
  export * from "../gemini-omni-inputs.js"
22
22
  export * from "../veo-i2v-inputs.js"
23
+ export * from "../frame-reference-slots.js"
23
24
  export * from "../surround-fill.js"
24
25
  // --- Start/end frame delivery: one plan for the dispatch and the editor ---
25
26
  export * from "../frame-delivery.js"
@@ -16,6 +16,8 @@ export * from "../described-references.js"
16
16
  export * from "../prompt-builder.js"
17
17
  export * from "../prompt-builder-structured-fields.js"
18
18
  export * from "../direction-registry.js"
19
+ export * from "../user-direction-refs.js"
20
+ export * from "../color-names.js"
19
21
  export * from "../subject-registry.js"
20
22
  export * from "../prompt-style-section.js"
21
23
  export * from "../read-node-direction.js"
@@ -10,12 +10,17 @@ const REALISM_MODEL = { provider: "gpt-image-2", resolution: "2K" } as const
10
10
  const SKIN_STUDY_MODEL = { provider: "seedream-5-pro", quality: "high" } as const
11
11
 
12
12
  export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
13
+ // `minReferences` marks a preset that works from connected photos its prompt
14
+ // describes in words ("the attached photo"); `{image:N}` tokens count on their
15
+ // own, so a tokenized preset declares nothing. The groups stay all-or-nothing
16
+ // (factory-preset-references.test.ts), so a new preset copies the line along.
13
17
  // ── Reference Sheet ──────────────────────────────────────────────────────
14
18
  {
15
19
  id: "generate-image/character-board",
16
20
  name: "Character Board",
17
21
  description: "Connect a sharp, well-lit photo → dense character sheet; reuse it as a reference for consistent shots.",
18
22
  group: "Reference Sheet",
23
+ minReferences: 1,
19
24
  data: {
20
25
  provider: "gpt-image-2",
21
26
  aspectRatio: "16:9",
@@ -31,6 +36,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
31
36
  name: "Pose Board",
32
37
  description: "Connect a sharp photo → animation-ready pose & expression sheet; reuse it as a reference for consistent action.",
33
38
  group: "Reference Sheet",
39
+ minReferences: 1,
34
40
  data: {
35
41
  provider: "gpt-image-2",
36
42
  aspectRatio: "16:9",
@@ -46,6 +52,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
46
52
  name: "Location Board",
47
53
  description: "Connect a photo → dense location sheet; reuse it as a reference for consistent scenes.",
48
54
  group: "Reference Sheet",
55
+ minReferences: 1,
49
56
  data: {
50
57
  provider: "gpt-image-2",
51
58
  aspectRatio: "16:9",
@@ -61,6 +68,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
61
68
  name: "Product Board",
62
69
  description: "Connect a photo → dense product sheet; reuse it as a reference for consistent shots.",
63
70
  group: "Reference Sheet",
71
+ minReferences: 1,
64
72
  data: {
65
73
  provider: "gpt-image-2",
66
74
  aspectRatio: "16:9",
@@ -76,6 +84,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
76
84
  name: "Outfit Board",
77
85
  description: "Connect a photo → dense wardrobe sheet; reuse it as a reference for consistent looks.",
78
86
  group: "Reference Sheet",
87
+ minReferences: 1,
79
88
  data: {
80
89
  provider: "gpt-image-2",
81
90
  aspectRatio: "16:9",
@@ -91,6 +100,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
91
100
  name: "Scene Board",
92
101
  description: "Connect a photo → set-dressing & props study; reuse it as a reference for consistent sets.",
93
102
  group: "Reference Sheet",
103
+ minReferences: 1,
94
104
  data: {
95
105
  provider: "gpt-image-2",
96
106
  aspectRatio: "16:9",
@@ -106,6 +116,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
106
116
  name: "Creature Board",
107
117
  description: "Connect a sharp photo → dense creature sheet; reuse it as a reference for consistent shots.",
108
118
  group: "Reference Sheet",
119
+ minReferences: 1,
109
120
  data: {
110
121
  provider: "gpt-image-2",
111
122
  aspectRatio: "16:9",
@@ -121,6 +132,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
121
132
  name: "Vehicle Board",
122
133
  description: "Connect a photo → dense vehicle sheet; reuse it as a reference for consistent shots.",
123
134
  group: "Reference Sheet",
135
+ minReferences: 1,
124
136
  data: {
125
137
  provider: "gpt-image-2",
126
138
  aspectRatio: "16:9",
@@ -136,6 +148,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
136
148
  name: "Food Board",
137
149
  description: "Connect a photo → dense dish sheet; reuse it as a reference for consistent food shots.",
138
150
  group: "Reference Sheet",
151
+ minReferences: 1,
139
152
  data: {
140
153
  provider: "gpt-image-2",
141
154
  aspectRatio: "16:9",
@@ -151,6 +164,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
151
164
  name: "Mascot Board",
152
165
  description: "Connect a mascot image → brand-character sheet with brand applications; reuse it for a consistent mascot.",
153
166
  group: "Reference Sheet",
167
+ minReferences: 1,
154
168
  data: {
155
169
  provider: "gpt-image-2",
156
170
  aspectRatio: "16:9",
@@ -166,6 +180,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
166
180
  name: "Pet Board",
167
181
  description: "Connect a sharp photo → dense pet sheet; reuse it as a reference for consistent shots.",
168
182
  group: "Reference Sheet",
183
+ minReferences: 1,
169
184
  data: {
170
185
  provider: "gpt-image-2",
171
186
  aspectRatio: "16:9",
@@ -181,6 +196,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
181
196
  name: "Skin Detail Sheet",
182
197
  description: "Connect a sharp face photo → a six-panel skin reference.",
183
198
  group: "Reference Sheet",
199
+ minReferences: 1,
184
200
  data: {
185
201
  ...SKIN_STUDY_MODEL,
186
202
  aspectRatio: "3:2",
@@ -203,6 +219,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
203
219
  name: "Character Reference Grid",
204
220
  description: "Connect a photo → clean 4-angle identity grid (no decorations) — the strongest consistency anchor.",
205
221
  group: "Cast & Consistency",
222
+ minReferences: 1,
206
223
  data: {
207
224
  provider: "gpt-image-2",
208
225
  aspectRatio: "3:4",
@@ -218,6 +235,8 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
218
235
  name: "Cast Mega Grid",
219
236
  description: "Connect 2–4 character refs → one labeled cast sheet; then reference cast members by name in scenes.",
220
237
  group: "Cast & Consistency",
238
+ // A cast sheet is two characters or more ("Connect 2–4 character refs").
239
+ minReferences: 2,
221
240
  data: {
222
241
  provider: "gpt-image-2",
223
242
  aspectRatio: "3:4",
@@ -232,6 +251,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
232
251
  name: "Cast Scene (by name)",
233
252
  description: "Connect a cast grid → stage a scene naming the characters; never re-describe their looks.",
234
253
  group: "Cast & Consistency",
254
+ minReferences: 1,
235
255
  data: {
236
256
  provider: "gpt-image-2",
237
257
  aspectRatio: "16:9",
@@ -247,6 +267,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
247
267
  name: "Label Elements",
248
268
  description: "Step 1 · connect a photo → the same image with a numbered label on each changeable element.",
249
269
  group: "Edit by Name",
270
+ minReferences: 1,
250
271
  data: {
251
272
  provider: "gpt-image-2",
252
273
  quality: "high",
@@ -260,6 +281,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
260
281
  name: "Apply Named Edit",
261
282
  description: "Step 2 · connect the labeled sheet → type your change in the prompt (e.g. change \"drink 1\" to blue); the labels are removed from the result.",
262
283
  group: "Edit by Name",
284
+ minReferences: 1,
263
285
  data: {
264
286
  provider: "gpt-image-2",
265
287
  quality: "high",
@@ -1628,6 +1650,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1628
1650
  name: "Five Ages (Timeless Soul)",
1629
1651
  description: "One person at 5 ages, studio line-up. Needs a reference photo.",
1630
1652
  group: "Portrait Transformations",
1653
+ minReferences: 1,
1631
1654
  data: {
1632
1655
  provider: "gpt-image-2",
1633
1656
  aspectRatio: "3:4",
@@ -1642,6 +1665,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1642
1665
  name: "Three-Age Triptych",
1643
1666
  description: "Same person at 3 ages (~10 / 40 / 75). Needs a reference photo.",
1644
1667
  group: "Portrait Transformations",
1668
+ minReferences: 1,
1645
1669
  data: {
1646
1670
  provider: "gpt-image-2",
1647
1671
  aspectRatio: "3:4",
@@ -1656,6 +1680,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1656
1680
  name: "Five Ages — B&W",
1657
1681
  description: "Monochrome age line-up. Needs a reference photo.",
1658
1682
  group: "Portrait Transformations",
1683
+ minReferences: 1,
1659
1684
  data: {
1660
1685
  provider: "gpt-image-2",
1661
1686
  aspectRatio: "3:4",
@@ -1670,6 +1695,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1670
1695
  name: "Decade Timeline",
1671
1696
  description: "Same person across the '80s–2020s. Needs a reference photo.",
1672
1697
  group: "Portrait Transformations",
1698
+ minReferences: 1,
1673
1699
  data: {
1674
1700
  provider: "gpt-image-2",
1675
1701
  aspectRatio: "3:4",
@@ -1684,6 +1710,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1684
1710
  name: "Four Seasons",
1685
1711
  description: "Same person in spring / summer / autumn / winter. Needs a reference photo.",
1686
1712
  group: "Portrait Transformations",
1713
+ minReferences: 1,
1687
1714
  data: {
1688
1715
  provider: "gpt-image-2",
1689
1716
  aspectRatio: "3:4",
@@ -1698,6 +1725,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1698
1725
  name: "Times of Day",
1699
1726
  description: "Same person at dawn / midday / golden hour / night. Needs a reference photo.",
1700
1727
  group: "Portrait Transformations",
1728
+ minReferences: 1,
1701
1729
  data: {
1702
1730
  provider: "gpt-image-2",
1703
1731
  aspectRatio: "3:4",
@@ -1712,6 +1740,7 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
1712
1740
  name: "Sports Jersey Portrait",
1713
1741
  description: "You in an oversized team jersey, studio editorial. Needs a reference photo.",
1714
1742
  group: "Portrait Transformations",
1743
+ minReferences: 1,
1715
1744
  data: {
1716
1745
  provider: "nano-banana-2-1",
1717
1746
  aspectRatio: "4:5",
@@ -1,4 +1,5 @@
1
1
  import { T2I_TO_I2I_VARIANT } from "@nodaro/shared/models"
2
+ import { ownEntry } from "../own-entry.js"
2
3
  import type { FactoryPreset } from "./types.js"
3
4
  import { stylizedSubjectFor, editsFor } from "./shared-image.js"
4
5
  import { GENERATE_IMAGE_PRESETS } from "./generate-image.js"
@@ -14,6 +15,8 @@ import { LOTTIE_OVERLAY_PRESETS } from "./lottie-overlay.js"
14
15
 
15
16
  export type { FactoryPreset, FactoryPresetGroup } from "./types.js"
16
17
  export { groupFactoryPresets } from "./types.js"
18
+ export { MAX_PRESET_REFERENCES, presetMinReferences } from "./references.js"
19
+ export { FACTORY_PRESET_PREVIEW_FILES, PRESET_PREVIEW_PATH, factoryPresetPreviewUrl } from "./previews.js"
17
20
 
18
21
  /**
19
22
  * A shared preset as Modify Image takes it. Modify Image always works from a source photo, so its
@@ -55,6 +58,8 @@ export const FACTORY_PRESETS: Readonly<Record<string, readonly FactoryPreset[]>>
55
58
  "lottie-overlay": LOTTIE_OVERLAY_PRESETS,
56
59
  }
57
60
 
61
+ /** The catalog for `nodeType`, or an empty one — also for a name that is an
62
+ * Object.prototype member (`constructor`), which is no catalog of ours. */
58
63
  export function getFactoryPresets(nodeType: string): readonly FactoryPreset[] {
59
- return FACTORY_PRESETS[nodeType] ?? []
64
+ return ownEntry(FACTORY_PRESETS, nodeType) ?? []
60
65
  }
@@ -0,0 +1,4 @@
1
+ // GENERATED by tools/preset-previews/build.mts — do not edit. Factory preset id →
2
+ // content-hashed file under frontend/public/preset-previews/ (`<nodeType>/<slug>.<hash8>.webp`).
3
+ export const FACTORY_PRESET_PREVIEW_FILES: Readonly<Record<string, string>> = {
4
+ }
@@ -0,0 +1,40 @@
1
+ import { FACTORY_PRESET_PREVIEW_FILES } from "./preview-files.generated.js"
2
+
3
+ export { FACTORY_PRESET_PREVIEW_FILES }
4
+
5
+ /**
6
+ * Where the factory preset previews live, relative to the app's origin:
7
+ * `frontend/public/preset-previews/` is served at `/preset-previews/` in every
8
+ * edition. Each name carries its content hash (`<slug>.<hash8>.webp`), so the
9
+ * server caches them as immutable and a regenerated picture is a new URL.
10
+ */
11
+ export const PRESET_PREVIEW_PATH = "/preset-previews/"
12
+
13
+ function originOf(baseUrl: string): string | undefined {
14
+ try {
15
+ const url = new URL(baseUrl)
16
+ if (url.protocol !== "https:" && url.protocol !== "http:") return undefined
17
+ return url.origin + url.pathname.replace(/\/+$/, "")
18
+ } catch {
19
+ return undefined
20
+ }
21
+ }
22
+
23
+ /**
24
+ * The absolute URL of a factory preset's example picture on the installation
25
+ * at `baseUrl` (its public origin, e.g. "https://app.nodaro.ai"), or null when
26
+ * the preset has none. Only a preset whose file is in the committed map gets a
27
+ * URL, so it never points at a file that does not exist.
28
+ *
29
+ * @param files the preset id → file map; defaults to the committed one
30
+ */
31
+ export function factoryPresetPreviewUrl(
32
+ presetId: string,
33
+ baseUrl: string,
34
+ files: Readonly<Record<string, string>> = FACTORY_PRESET_PREVIEW_FILES,
35
+ ): string | null {
36
+ const file = Object.prototype.hasOwnProperty.call(files, presetId) ? files[presetId] : undefined
37
+ if (!file) return null
38
+ const origin = originOf(baseUrl)
39
+ return origin ? `${origin}${PRESET_PREVIEW_PATH}${file}` : null
40
+ }