@nodaro/prompts 1.38.0 → 1.39.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.
- package/dist/{chunk-LVSH7K7R.cjs → chunk-22KZOAGL.cjs} +81 -42
- package/dist/chunk-22KZOAGL.cjs.map +1 -0
- package/dist/chunk-2P26SZX5.cjs +414 -0
- package/dist/chunk-2P26SZX5.cjs.map +1 -0
- package/dist/{chunk-CVPB2N3Z.cjs → chunk-3INXNDV7.cjs} +102 -4
- package/dist/chunk-3INXNDV7.cjs.map +1 -0
- package/dist/{chunk-7XKHDXCL.cjs → chunk-3U7X6JZN.cjs} +13 -12
- package/dist/chunk-3U7X6JZN.cjs.map +1 -0
- package/dist/chunk-4GKTOVFM.cjs +8 -0
- package/dist/chunk-4GKTOVFM.cjs.map +1 -0
- package/dist/{chunk-HRBBNODV.js → chunk-6GVFJLDL.js} +4 -3
- package/dist/chunk-6GVFJLDL.js.map +1 -0
- package/dist/{chunk-NO6XHX4R.js → chunk-6MPZHFIS.js} +99 -6
- package/dist/chunk-6MPZHFIS.js.map +1 -0
- package/dist/{chunk-4FDNVNJE.js → chunk-7XMTVYS5.js} +3 -76
- package/dist/chunk-7XMTVYS5.js.map +1 -0
- package/dist/{chunk-QDEWBBUR.js → chunk-DUXX6CBX.js} +12 -143
- package/dist/chunk-DUXX6CBX.js.map +1 -0
- package/dist/chunk-EAYEZXWT.js +388 -0
- package/dist/chunk-EAYEZXWT.js.map +1 -0
- package/dist/chunk-GILUVQQJ.js +6 -0
- package/dist/chunk-GILUVQQJ.js.map +1 -0
- package/dist/{chunk-KIYNOOPJ.js → chunk-GTDKDD7Y.js} +12 -6
- package/dist/chunk-GTDKDD7Y.js.map +1 -0
- package/dist/{chunk-TLVX5PXD.cjs → chunk-IUCNJAHW.cjs} +13 -7
- package/dist/chunk-IUCNJAHW.cjs.map +1 -0
- package/dist/{chunk-ZFIXQRBZ.cjs → chunk-KUBZVWME.cjs} +2 -80
- package/dist/chunk-KUBZVWME.cjs.map +1 -0
- package/dist/{chunk-JKCR6WRZ.cjs → chunk-NMWKXAYA.cjs} +2 -2
- package/dist/{chunk-JKCR6WRZ.cjs.map → chunk-NMWKXAYA.cjs.map} +1 -1
- package/dist/{chunk-LK2CZK5U.cjs → chunk-PWWM7Q3T.cjs} +7 -7
- package/dist/{chunk-LK2CZK5U.cjs.map → chunk-PWWM7Q3T.cjs.map} +1 -1
- package/dist/{chunk-N7TOWXDJ.js → chunk-QDUTMOF4.js} +3 -3
- package/dist/{chunk-N7TOWXDJ.js.map → chunk-QDUTMOF4.js.map} +1 -1
- package/dist/{chunk-MENBHEAY.cjs → chunk-WRM52ZBN.cjs} +19 -161
- package/dist/chunk-WRM52ZBN.cjs.map +1 -0
- package/dist/chunk-XIWWDGT5.cjs +86 -0
- package/dist/chunk-XIWWDGT5.cjs.map +1 -0
- package/dist/{chunk-CEPVQQVO.js → chunk-XRLOMTBZ.js} +2 -2
- package/dist/{chunk-CEPVQQVO.js.map → chunk-XRLOMTBZ.js.map} +1 -1
- package/dist/{chunk-QBEZMR2F.js → chunk-XU54YJXQ.js} +54 -15
- package/dist/chunk-XU54YJXQ.js.map +1 -0
- package/dist/chunk-Z3SZXWRU.js +80 -0
- package/dist/chunk-Z3SZXWRU.js.map +1 -0
- package/dist/entries/assembly.cjs +26 -25
- package/dist/entries/assembly.d.cts +32 -4
- package/dist/entries/assembly.d.ts +32 -4
- package/dist/entries/assembly.js +5 -4
- package/dist/entries/doctrine.cjs +46 -46
- package/dist/entries/doctrine.d.cts +3 -3
- package/dist/entries/doctrine.d.ts +3 -3
- package/dist/entries/doctrine.js +1 -1
- package/dist/entries/picker-catalogs.cjs +42 -42
- package/dist/entries/picker-catalogs.js +3 -3
- package/dist/entries/presets.cjs +49 -26
- package/dist/entries/presets.d.cts +51 -1
- package/dist/entries/presets.d.ts +51 -1
- package/dist/entries/presets.js +4 -1
- package/dist/entries/prompt-builder.cjs +135 -98
- package/dist/entries/prompt-builder.d.cts +22 -11
- package/dist/entries/prompt-builder.d.ts +22 -11
- package/dist/entries/prompt-builder.js +4 -3
- package/dist/entries/prompt-fields.cjs +41 -40
- package/dist/entries/prompt-fields.js +2 -1
- package/dist/index.cjs +331 -273
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +11 -9
- package/dist/{subject-registry-BUswuEVD.d.cts → subject-registry-CQi_CZfJ.d.cts} +78 -9
- package/dist/{subject-registry-D7V8PEG-.d.ts → subject-registry-DFiGx49v.d.ts} +78 -9
- package/package.json +2 -2
- package/src/__tests__/assemble-image-input-style-references.test.ts +173 -0
- package/src/__tests__/catalog-id-guard.test.ts +30 -0
- package/src/__tests__/factory-preset-previews.test.ts +123 -0
- package/src/__tests__/factory-preset-references.test.ts +222 -0
- package/src/__tests__/user-direction-refs.test.ts +180 -0
- package/src/assemble-image-input.ts +114 -13
- package/src/catalog-id-guard.ts +18 -2
- package/src/color-names.ts +192 -0
- package/src/direction-registry.ts +66 -5
- package/src/entries/prompt-builder.ts +2 -0
- package/src/factory-presets/generate-image.ts +29 -0
- package/src/factory-presets/index.ts +2 -0
- package/src/factory-presets/preview-files.generated.ts +4 -0
- package/src/factory-presets/previews.ts +40 -0
- package/src/factory-presets/references.ts +80 -0
- package/src/factory-presets/shared-image.ts +4 -0
- package/src/factory-presets/types.ts +8 -0
- package/src/image-reference-doctrine.ts +3 -3
- package/src/image-token-pattern.ts +13 -0
- package/src/prompt-builder.ts +4 -5
- package/src/prompt-style-section.ts +3 -3
- package/src/user-direction-refs.ts +110 -0
- package/dist/chunk-4FDNVNJE.js.map +0 -1
- package/dist/chunk-7XKHDXCL.cjs.map +0 -1
- package/dist/chunk-CVPB2N3Z.cjs.map +0 -1
- package/dist/chunk-HRBBNODV.js.map +0 -1
- package/dist/chunk-KIYNOOPJ.js.map +0 -1
- package/dist/chunk-LVSH7K7R.cjs.map +0 -1
- package/dist/chunk-MENBHEAY.cjs.map +0 -1
- package/dist/chunk-NO6XHX4R.js.map +0 -1
- package/dist/chunk-QBEZMR2F.js.map +0 -1
- package/dist/chunk-QDEWBBUR.js.map +0 -1
- package/dist/chunk-TLVX5PXD.cjs.map +0 -1
- package/dist/chunk-UJKFHPZ7.js +0 -37
- package/dist/chunk-UJKFHPZ7.js.map +0 -1
- package/dist/chunk-W5SCZIOB.cjs +0 -43
- package/dist/chunk-W5SCZIOB.cjs.map +0 -1
- package/dist/chunk-ZFIXQRBZ.cjs.map +0 -1
|
@@ -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:
|
|
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
|
|
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:
|
|
500
|
+
opts: DirectionRenderOptions,
|
|
440
501
|
): string[] {
|
|
441
502
|
return renderDirectionHintClauses(direction, opts).map((c) => c.text)
|
|
442
503
|
}
|
|
@@ -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",
|
|
@@ -14,6 +14,8 @@ import { LOTTIE_OVERLAY_PRESETS } from "./lottie-overlay.js"
|
|
|
14
14
|
|
|
15
15
|
export type { FactoryPreset, FactoryPresetGroup } from "./types.js"
|
|
16
16
|
export { groupFactoryPresets } from "./types.js"
|
|
17
|
+
export { MAX_PRESET_REFERENCES, presetMinReferences } from "./references.js"
|
|
18
|
+
export { FACTORY_PRESET_PREVIEW_FILES, PRESET_PREVIEW_PATH, factoryPresetPreviewUrl } from "./previews.js"
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* A shared preset as Modify Image takes it. Modify Image always works from a source photo, so its
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { IMAGE_TOKEN_PATTERN } from "../image-token-pattern.js"
|
|
2
|
+
import { getPromptFields, nodeSupportsPromptAffixes, promptAffixCoreField } from "../node-prompt-fields.js"
|
|
3
|
+
import { NODE_PROMPT_CANDIDATE_FIELDS } from "../resolve-prompt.js"
|
|
4
|
+
import { MODELS_WITH_REFERENCE_IMAGE_SUPPORT, imageReferenceLimit } from "@nodaro/shared/models"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The most reference images any image model takes. A preset can never need
|
|
8
|
+
* more than some model could be sent, so a count is clamped to it — a typo
|
|
9
|
+
* like `{image:99999}` must not tell a client to ask for 99,999 photos.
|
|
10
|
+
*/
|
|
11
|
+
export const MAX_PRESET_REFERENCES: number = Math.max(
|
|
12
|
+
1,
|
|
13
|
+
...[...MODELS_WITH_REFERENCE_IMAGE_SUPPORT].map((provider) => imageReferenceLimit(provider)),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The data fields of `nodeType` a reference token can sit in — every field the
|
|
18
|
+
* run reads its prompt from: the candidate fields `computeNodePrompt` joins
|
|
19
|
+
* (`NODE_PROMPT_CANDIDATE_FIELDS` — how a preset's prompt is resolved when it
|
|
20
|
+
* runs, on the canvas and through `presetId` alike), the prompt-field
|
|
21
|
+
* registry's own prompt and negative fields, the field the run wraps with
|
|
22
|
+
* pre/post text, and the pre/post text itself where the node takes it. A node
|
|
23
|
+
* whose prompt lives elsewhere (`llm-chat` → `userInput`) is read where its
|
|
24
|
+
* prompt really is; a type with no prompt (an app namespace) has none.
|
|
25
|
+
*/
|
|
26
|
+
function promptFieldsOf(nodeType: string): string[] {
|
|
27
|
+
const spec = getPromptFields(nodeType)
|
|
28
|
+
const candidates = NODE_PROMPT_CANDIDATE_FIELDS[nodeType] ?? []
|
|
29
|
+
if (!spec && candidates.length === 0) return []
|
|
30
|
+
const fields = new Set<string>(candidates)
|
|
31
|
+
if (spec) {
|
|
32
|
+
fields.add(spec.prompt)
|
|
33
|
+
if (spec.negative) fields.add(spec.negative)
|
|
34
|
+
}
|
|
35
|
+
const core = promptAffixCoreField(nodeType)
|
|
36
|
+
if (core) fields.add(core)
|
|
37
|
+
if (nodeSupportsPromptAffixes(nodeType)) {
|
|
38
|
+
fields.add("promptPrefix")
|
|
39
|
+
fields.add("promptSuffix")
|
|
40
|
+
}
|
|
41
|
+
return [...fields]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The highest `{image:N}` / `{image:N:label}` position in `text`, or 0. */
|
|
45
|
+
function highestImageToken(text: string): number {
|
|
46
|
+
let highest = 0
|
|
47
|
+
for (const match of text.matchAll(IMAGE_TOKEN_PATTERN)) {
|
|
48
|
+
const n = Number.parseInt(match[1] ?? "", 10)
|
|
49
|
+
if (Number.isInteger(n) && n > highest) highest = n
|
|
50
|
+
}
|
|
51
|
+
return highest
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* How many connected reference images a preset works from — 0 for a preset
|
|
56
|
+
* that needs none. A client shows a preset with a non-zero count as "needs a
|
|
57
|
+
* photo" (`needsImage`) and asks for that many before running it.
|
|
58
|
+
*
|
|
59
|
+
* Two sources, and the larger wins:
|
|
60
|
+
* - the `{image:N}` / `{image:N:label}` tokens in the preset's prompt fields
|
|
61
|
+
* (the highest N): a template that names its references always reports them;
|
|
62
|
+
* - `declared` — the factory preset's own `minReferences`, for presets whose
|
|
63
|
+
* prompt refers to "the attached photo" in words, which no data field marks.
|
|
64
|
+
* A user preset has no declaration, so its count comes from its tokens.
|
|
65
|
+
*
|
|
66
|
+
* Pure; never throws. A declaration that is not a positive whole number is
|
|
67
|
+
* ignored, and the result is at most {@link MAX_PRESET_REFERENCES}.
|
|
68
|
+
*/
|
|
69
|
+
export function presetMinReferences(
|
|
70
|
+
nodeType: string,
|
|
71
|
+
data: Readonly<Record<string, unknown>>,
|
|
72
|
+
declared?: number,
|
|
73
|
+
): number {
|
|
74
|
+
let count = typeof declared === "number" && Number.isInteger(declared) && declared > 0 ? declared : 0
|
|
75
|
+
for (const field of promptFieldsOf(nodeType)) {
|
|
76
|
+
const value = data[field]
|
|
77
|
+
if (typeof value === "string") count = Math.max(count, highestImageToken(value))
|
|
78
|
+
}
|
|
79
|
+
return Math.min(count, MAX_PRESET_REFERENCES)
|
|
80
|
+
}
|
|
@@ -89,6 +89,8 @@ export function stylizedSubjectFor(nodeType: string): FactoryPreset[] {
|
|
|
89
89
|
description: p.description,
|
|
90
90
|
group: "Stylized Subject",
|
|
91
91
|
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
92
|
+
// Restyles the person in the connected photo ("the reference image").
|
|
93
|
+
minReferences: 1,
|
|
92
94
|
}))
|
|
93
95
|
}
|
|
94
96
|
|
|
@@ -242,5 +244,7 @@ export function editsFor(nodeType: string): FactoryPreset[] {
|
|
|
242
244
|
description: p.description,
|
|
243
245
|
group: "Edits",
|
|
244
246
|
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
247
|
+
// Every edit operates on the connected photo.
|
|
248
|
+
minReferences: 1,
|
|
245
249
|
}))
|
|
246
250
|
}
|
|
@@ -12,6 +12,14 @@ export interface FactoryPreset {
|
|
|
12
12
|
readonly groupKind?: "folder" | "section"
|
|
13
13
|
/** Capture-shaped config (no label / fieldMappings / runtime keys). */
|
|
14
14
|
readonly data: Readonly<Record<string, unknown>>
|
|
15
|
+
/**
|
|
16
|
+
* How many connected reference images the preset works from, stated for a
|
|
17
|
+
* preset whose prompt refers to "the attached photo" in words (Reference
|
|
18
|
+
* Sheet, Portrait Transformations, Stylized Subject, Edits, …). Leave it out
|
|
19
|
+
* otherwise: `{image:N}` tokens in the prompt are counted on their own, and
|
|
20
|
+
* the count a client sees is the larger of the two (`presetMinReferences`).
|
|
21
|
+
*/
|
|
22
|
+
readonly minReferences?: number
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
/** A render-ready bucket of factory presets sharing one `group` (or the leading
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* the video node skills.
|
|
7
7
|
*
|
|
8
8
|
* Unlike the per-provider video doctrines, this is a PLATFORM mechanism: the
|
|
9
|
-
* token grammar and expansion are Nodaro's own (`
|
|
10
|
-
* `IMAGE_TOKEN_PATTERN
|
|
11
|
-
* not one per model family.
|
|
9
|
+
* token grammar and expansion are Nodaro's own (`image-token-pattern.ts` —
|
|
10
|
+
* `IMAGE_TOKEN_PATTERN`; `prompt-builder.ts` — `expandImageRefTokens`), so
|
|
11
|
+
* there is one doctrine, not one per model family.
|
|
12
12
|
*
|
|
13
13
|
* Source of truth for the semantics documented here (verified 2026-08-25):
|
|
14
14
|
* - `IMAGE_TOKEN_PATTERN` = `/\{image:(\d+)(?::([^}\n]+))?\}/gi`
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `{image:N}` / `{image:N:label}` reference-token grammar — one pattern for
|
|
3
|
+
* the image prompt assembly (`prompt-builder.ts`) and for the preset metadata
|
|
4
|
+
* that reads which references a template names (`factory-presets/references.ts`).
|
|
5
|
+
*
|
|
6
|
+
* Group 1 = position (1-based), group 2 = optional label. The label allows
|
|
7
|
+
* spaces (multi-word labels like "clothes and shoes") but never a newline or
|
|
8
|
+
* `}`, so a match can't run away across lines or token boundaries.
|
|
9
|
+
*
|
|
10
|
+
* Global on purpose (callers use `.replace` / `.matchAll`, which never leave a
|
|
11
|
+
* `lastIndex` behind); never call `.test` / `.exec` on it.
|
|
12
|
+
*/
|
|
13
|
+
export const IMAGE_TOKEN_PATTERN = /\{image:(\d+)(?::([^}\n]+))?\}/gi
|
package/src/prompt-builder.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { joinSentences } from "./hint-join.js"
|
|
8
|
+
import { IMAGE_TOKEN_PATTERN } from "./image-token-pattern.js"
|
|
8
9
|
import { resolveTemplate, applyTemplate } from "./prompt-templates.js"
|
|
9
10
|
import { NATIVE_NEGATIVE_PROMPT_MODELS, MODELS_WITH_REFERENCE_IMAGE_SUPPORT, imageReferenceLimit, getMaxImagePromptChars, getMaxNegativePromptChars } from "@nodaro/shared/models"
|
|
10
11
|
import { getStylePromptHint, isDeniedStyleId } from "./style.js"
|
|
@@ -3276,11 +3277,9 @@ const STRICT_DEFAULT_SOURCES: ReadonlySet<ReferenceSource> = new Set([
|
|
|
3276
3277
|
"wired-location",
|
|
3277
3278
|
])
|
|
3278
3279
|
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
//
|
|
3282
|
-
// newline or `}` so the match can't run away across lines / token boundaries.
|
|
3283
|
-
const IMAGE_TOKEN_PATTERN = /\{image:(\d+)(?::([^}\n]+))?\}/gi
|
|
3280
|
+
// `IMAGE_TOKEN_PATTERN` (`{image:N}` / `{image:N:label}`) lives in
|
|
3281
|
+
// image-token-pattern.ts, shared with the preset metadata that reads which
|
|
3282
|
+
// references a template names.
|
|
3284
3283
|
|
|
3285
3284
|
/**
|
|
3286
3285
|
* The final 1-based slot an `{image:N}` token binds to on the connected-reference
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
renderDirectionHintClauses,
|
|
52
52
|
type DirectionFamily,
|
|
53
53
|
type DirectionFields,
|
|
54
|
-
type
|
|
54
|
+
type DirectionRenderOptions,
|
|
55
55
|
type DirectionStyleGroup,
|
|
56
56
|
} from "./direction-registry.js"
|
|
57
57
|
|
|
@@ -159,7 +159,7 @@ export function asBodyClauses(texts: readonly string[]): SlottedPromptClause[] {
|
|
|
159
159
|
*/
|
|
160
160
|
export function partitionStyleClauses(
|
|
161
161
|
direction: DirectionFields | undefined,
|
|
162
|
-
opts:
|
|
162
|
+
opts: DirectionRenderOptions,
|
|
163
163
|
): SlottedPromptClause[] {
|
|
164
164
|
return renderDirectionHintClauses(direction, opts).map((clause) => ({
|
|
165
165
|
text: clause.text,
|
|
@@ -185,7 +185,7 @@ export function styleSectionFromClauses(clauses: readonly SlottedPromptClause[])
|
|
|
185
185
|
*/
|
|
186
186
|
export function renderStyleSection(
|
|
187
187
|
direction: DirectionFields | undefined,
|
|
188
|
-
opts:
|
|
188
|
+
opts: DirectionRenderOptions,
|
|
189
189
|
): string {
|
|
190
190
|
return styleSectionFromClauses(partitionStyleClauses(direction, opts))
|
|
191
191
|
}
|