@nodaro/prompts 1.33.0 → 1.35.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/index.cjs +133 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -10
- package/dist/index.d.ts +19 -10
- package/dist/index.js +133 -109
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/image-bound-reference-tokens-golden.test.ts.snap +1273 -0
- package/src/__tests__/factory-presets.test.ts +20 -11
- package/src/__tests__/image-bound-reference-tokens-golden.test.ts +90 -0
- package/src/__tests__/image-unwired-reference-tokens.test.ts +283 -0
- package/src/__tests__/mention-splice-spacing.test.ts +3 -3
- package/src/__tests__/prompt-builder.test.ts +5 -2
- package/src/__tests__/unbound-reference-tokens.test.ts +32 -0
- package/src/factory-presets/generate-image.ts +79 -78
- package/src/factory-presets/shared-image.ts +14 -17
- package/src/image-reference-doctrine.ts +7 -3
- package/src/prompt-builder.ts +80 -13
- package/src/prompt-wizard-categories.ts +4 -0
- package/src/unbound-reference-tokens.ts +32 -0
- package/src/video-reference-resolver.ts +20 -20
|
@@ -4,8 +4,8 @@ import type { FactoryPreset } from "./types.js"
|
|
|
4
4
|
* Selective-stylization presets shared by `modify-image` (transform an input photo) and
|
|
5
5
|
* `generate-image` (use a connected reference image). `modify-image` is slated for deprecation in
|
|
6
6
|
* favor of `generate-image`, so the catalog lives in ONE place to prevent the two from drifting —
|
|
7
|
-
* when `modify-image` is removed, just drop its key from `FACTORY_PRESETS` below.
|
|
8
|
-
*
|
|
7
|
+
* when `modify-image` is removed, just drop its key from `FACTORY_PRESETS` below. The provider is
|
|
8
|
+
* set per node by {@link editProviderFor}.
|
|
9
9
|
*/
|
|
10
10
|
const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
11
11
|
readonly slug: string
|
|
@@ -18,7 +18,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
18
18
|
name: "Cartoon Person, Real World",
|
|
19
19
|
description: "Subject → 3D cartoon, rest stays photoreal.",
|
|
20
20
|
data: {
|
|
21
|
-
provider: "nano-banana-pro",
|
|
22
21
|
promptPrefix:
|
|
23
22
|
"Restyle ONLY the person in the reference image into a polished 3D animated character — Pixar/Disney sensibility: soft subsurface skin shading, expressive oversized eyes, smoothly stylized hair, clean rounded forms. Preserve their identity, exact pose, expression, and framing. Everything else stays untouched and fully photorealistic: keep the original background, props, clothing fabric and texture, and the scene's real lighting, shadows, and color grade. Composite the stylized figure into the real plate so it sits naturally — matched light direction, contact shadows, and depth.",
|
|
24
23
|
negativePrompt:
|
|
@@ -30,7 +29,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
30
29
|
name: "Caricature, Real Photo",
|
|
31
30
|
description: "Exaggerated cartoon head on a real scene.",
|
|
32
31
|
data: {
|
|
33
|
-
provider: "nano-banana-pro",
|
|
34
32
|
promptPrefix:
|
|
35
33
|
"Turn ONLY the person into a playful hand-illustrated caricature: enlarge the head, exaggerate their most recognizable features, and amplify expression with bold, confident linework and clean cel-style shading — while keeping them clearly identifiable. Leave everything else exactly as shot and photorealistic: the body proportions below the neck, the clothing, the background, props, and the scene's real lighting and shadows remain unchanged. Blend the caricature head onto the real photo seamlessly with matched lighting and a believable neckline.",
|
|
36
34
|
negativePrompt:
|
|
@@ -42,7 +40,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
42
40
|
name: "Anime Person, Real Background",
|
|
43
41
|
description: "Subject → 2D anime, real environment.",
|
|
44
42
|
data: {
|
|
45
|
-
provider: "nano-banana-pro",
|
|
46
43
|
promptPrefix:
|
|
47
44
|
"Restyle ONLY the person as a crisp 2D anime character — clean ink outlines, flat cel shading with sharp shadow shapes, glossy stylized hair, and expressive anime eyes — while preserving their identity, pose, expression, and framing. Keep the real-world background, environment, props, and the original photographic lighting, shadows, and color completely untouched and photorealistic. Integrate the 2D figure into the live-action plate so the light direction and contact shadows read naturally.",
|
|
48
45
|
negativePrompt:
|
|
@@ -54,7 +51,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
54
51
|
name: "Real Person, Cartoon World",
|
|
55
52
|
description: "Inverse — real subject, stylized world.",
|
|
56
53
|
data: {
|
|
57
|
-
provider: "nano-banana-pro",
|
|
58
54
|
promptPrefix:
|
|
59
55
|
"Inverse stylization: keep the PERSON completely photorealistic and unchanged — real skin, hair, clothing, identity, pose, and framing all preserved exactly. Restyle ONLY the surrounding world into a vibrant, hand-crafted cartoon environment: simplified painterly shapes, bold saturated colors, soft graphic shading, and whimsical stylized props and scenery. Light the real person to match the cartoon world's color and direction so they sit convincingly inside it, with believable contact shadows.",
|
|
60
56
|
negativePrompt:
|
|
@@ -66,7 +62,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
66
62
|
name: "Claymation Figure, Real Set",
|
|
67
63
|
description: "Subject → clay figure, real surroundings.",
|
|
68
64
|
data: {
|
|
69
|
-
provider: "nano-banana-pro",
|
|
70
65
|
promptPrefix:
|
|
71
66
|
"Restyle ONLY the person into a tactile stop-motion claymation figure — molded plasticine surface with visible fingerprints, tool marks, subtle seams, and a soft matte sheen, in the handmade spirit of Aardman puppets — while keeping their identity, pose, expression, and framing. Everything else stays exactly as shot and photorealistic: the real background, set, props, and the scene's natural lighting and shadows are unchanged. Seat the clay figure into the real plate with matched light direction and grounded contact shadows.",
|
|
72
67
|
negativePrompt:
|
|
@@ -76,13 +71,23 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
76
71
|
]
|
|
77
72
|
|
|
78
73
|
/** Build the Stylized Subject presets for a node type (id = `<nodeType>/<slug>`). */
|
|
74
|
+
/**
|
|
75
|
+
* Both preset families are edits of a connected photo, so they run on the platform's edit model,
|
|
76
|
+
* GPT Image 2.5 Flare (decided 2026-10-08 from the image model bake-off: it kept pose, framing and
|
|
77
|
+
* product design on every edit). Modify Image takes the i2i id; Generate Image takes the t2i id,
|
|
78
|
+
* which the platform swaps to the same i2i model whenever a reference image is connected.
|
|
79
|
+
*/
|
|
80
|
+
export function editProviderFor(nodeType: string): string {
|
|
81
|
+
return nodeType === "modify-image" ? "gpt-image-2-5-flare-i2i" : "gpt-image-2-5-flare"
|
|
82
|
+
}
|
|
83
|
+
|
|
79
84
|
export function stylizedSubjectFor(nodeType: string): FactoryPreset[] {
|
|
80
85
|
return STYLIZED_SUBJECT.map((p) => ({
|
|
81
86
|
id: `${nodeType}/${p.slug}`,
|
|
82
87
|
name: p.name,
|
|
83
88
|
description: p.description,
|
|
84
89
|
group: "Stylized Subject",
|
|
85
|
-
data: p.data,
|
|
90
|
+
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
86
91
|
}))
|
|
87
92
|
}
|
|
88
93
|
|
|
@@ -102,7 +107,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
102
107
|
name: "Remove Background",
|
|
103
108
|
description: "Cut out the subject on white.",
|
|
104
109
|
data: {
|
|
105
|
-
provider: "nano-banana-pro",
|
|
106
110
|
promptPrefix:
|
|
107
111
|
"Cleanly cut the main subject out of the reference image and place it on a pure, solid white background. Trace a crisp, accurate edge — preserve fine detail like hair strands, fur, and translucent or wispy edges — with no leftover background pixels and no color fringing. Keep the subject itself completely unchanged: same pose, lighting, color, and full silhouette, nothing cropped.",
|
|
108
112
|
negativePrompt:
|
|
@@ -114,7 +118,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
114
118
|
name: "Replace Background",
|
|
115
119
|
description: "Swap in a new scene.",
|
|
116
120
|
data: {
|
|
117
|
-
provider: "nano-banana-pro",
|
|
118
121
|
prompt:
|
|
119
122
|
"Replace the background behind the subject with {scene || a sunlit Mediterranean terrace}. Keep the subject perfectly unchanged — identity, pose, edges, and proportions intact — and composite it into the new scene so it looks truly photographed there: relight the subject's rim and ambient tones to match the new environment, cast believable contact shadows, and match perspective, depth of field, and color grade.",
|
|
120
123
|
negativePrompt:
|
|
@@ -126,7 +129,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
126
129
|
name: "Colorize B&W Photo",
|
|
127
130
|
description: "Natural color from grayscale.",
|
|
128
131
|
data: {
|
|
129
|
-
provider: "nano-banana-pro",
|
|
130
132
|
promptPrefix:
|
|
131
133
|
"Colorize this black-and-white photograph with natural, historically plausible color. Give skin realistic, even tones, and assign believable hues to clothing, foliage, sky, and materials. Preserve every original detail, grain, contrast, and the exact composition — add color only, never invent or remove content.",
|
|
132
134
|
negativePrompt:
|
|
@@ -138,7 +140,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
138
140
|
name: "Restore Old Photo",
|
|
139
141
|
description: "Repair scratches, denoise, sharpen.",
|
|
140
142
|
data: {
|
|
141
|
-
provider: "nano-banana-pro",
|
|
142
143
|
promptPrefix:
|
|
143
144
|
"Restore this old, damaged photograph: remove scratches, dust, creases, stains, and noise, repair torn or missing areas by reconstructing what is plainly there, and gently recover sharpness and tonal detail. Keep the result authentic and true to the original — preserve identity, era, natural film grain, and composition. Repair only; do not beautify, restyle, or invent new features.",
|
|
144
145
|
negativePrompt:
|
|
@@ -150,7 +151,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
150
151
|
name: "Relight Scene",
|
|
151
152
|
description: "New lighting, same subject.",
|
|
152
153
|
data: {
|
|
153
|
-
provider: "nano-banana-pro",
|
|
154
154
|
prompt:
|
|
155
155
|
"Relight the image with {lighting style || soft golden-hour key light from the left}. Re-derive highlights, shadows, ambient fill, and color temperature consistently across the whole frame for a believable, physically plausible result. Keep the subject, pose, expression, materials, and composition completely unchanged — only the lighting changes.",
|
|
156
156
|
negativePrompt:
|
|
@@ -162,7 +162,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
162
162
|
name: "Restyle (whole image)",
|
|
163
163
|
description: "Apply an art style to everything.",
|
|
164
164
|
data: {
|
|
165
|
-
provider: "nano-banana-pro",
|
|
166
165
|
prompt:
|
|
167
166
|
"Restyle the entire image in a {art style || soft watercolor illustration} look, applying the style consistently to the subject, background, and every element. Keep the composition, subject placement, and the subject clearly recognizable — reinterpret texture, color, and rendering, not the underlying content or layout.",
|
|
168
167
|
negativePrompt:
|
|
@@ -174,7 +173,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
174
173
|
name: "Doodle Overlay",
|
|
175
174
|
description: "Hand-drawn marker doodles on top — photo stays untouched.",
|
|
176
175
|
data: {
|
|
177
|
-
provider: "nano-banana-pro",
|
|
178
176
|
promptPrefix:
|
|
179
177
|
"Layer playful hand-drawn doodles ON TOP of the reference photo while keeping every pixel of the underlying photograph completely unchanged — same people, same colors, same lighting, same crop. Doodle style: wobbly hand-drawn felt-tip marker lines in white with a few yellow accents, like quick sketches on a printed photo. Add: loose outline traces hugging one or two key shapes, small sparkles and stars, a hand-drawn thought cloud or speech bubble with a short witty handwritten phrase that reacts to what is actually happening in the photo, and simple arrows or underlines pointing at fun details. Outlines only — never fill shapes with color. Never cover faces or the main focal point; place doodles in empty areas like sky, walls or margins. Keep it balanced: four to seven doodle elements total, charming and confident, not cluttered. The photo is the hero — the doodles are the seasoning.",
|
|
180
178
|
negativePrompt:
|
|
@@ -186,7 +184,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
186
184
|
name: "Doodle Overlay · Expressive",
|
|
187
185
|
description: "Looser free-form doodles — the model improvises.",
|
|
188
186
|
data: {
|
|
189
|
-
provider: "nano-banana-pro",
|
|
190
187
|
promptPrefix:
|
|
191
188
|
"Decorate the reference photo with expressive hand-drawn doodles layered on top, keeping the underlying photograph itself completely unchanged. Free, confident hand-drawn marker linework — wobbly outlines, sparkles, squiggles, little flames or hearts where they fit the mood, and a short handwritten phrase that responds cleverly to the content of the photo (never a generic cliché). Mix of white and one or two bright accent colors. Outline style only, no filled shapes. Compose the doodles around the subject — never on top of faces or the focal point — using empty areas and edges. Loose, playful, zine-like energy: it should feel like a bored genius doodled on a printed photo. Keep the original image pixels intact beneath the doodle layer.",
|
|
192
189
|
negativePrompt:
|
|
@@ -202,6 +199,6 @@ export function editsFor(nodeType: string): FactoryPreset[] {
|
|
|
202
199
|
name: p.name,
|
|
203
200
|
description: p.description,
|
|
204
201
|
group: "Edits",
|
|
205
|
-
data: p.data,
|
|
202
|
+
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
206
203
|
}))
|
|
207
204
|
}
|
|
@@ -13,8 +13,12 @@
|
|
|
13
13
|
* Source of truth for the semantics documented here (verified 2026-08-25):
|
|
14
14
|
* - `IMAGE_TOKEN_PATTERN` = `/\{image:(\d+)(?::([^}\n]+))?\}/gi`
|
|
15
15
|
* - `expandImageRefTokens`: `{image:N:label}` → `Image N (label)`,
|
|
16
|
-
* `{image:N}` → `Image N
|
|
17
|
-
*
|
|
16
|
+
* `{image:N}` → `Image N`.
|
|
17
|
+
* - A token with no reference at N follows the video resolver's rule
|
|
18
|
+
* (`dropUnwiredImageTokens` in `prompt-builder.ts`): `{image:N:label}` →
|
|
19
|
+
* `label`, a bare `{image:N}` is dropped. Two places keep a token as typed,
|
|
20
|
+
* on purpose: a wired token in a hybrid `Avoid:` line, and the native
|
|
21
|
+
* negative prompt of models with their own negative field (e.g. ideogram-v3).
|
|
18
22
|
* - N is the reference's 1-based position in the assembled reference list
|
|
19
23
|
* (connection order on the `references` handle).
|
|
20
24
|
*/
|
|
@@ -32,7 +36,7 @@ export const IMAGE_REFERENCE_PROMPT_DOCTRINE: ImageReferenceDoctrine = {
|
|
|
32
36
|
|
|
33
37
|
- \`{image:N:label}\` → expands server-side to \`Image N (label)\`, aligned with the numbered reference list sent to the provider.
|
|
34
38
|
- \`{image:N}\` → \`Image N\` (no role named).
|
|
35
|
-
- A token whose N has no wired reference
|
|
39
|
+
- A token whose N has no wired reference falls back to its label (\`{image:3:dog}\` → \`dog\`); a bare \`{image:N}\` is dropped. The prompt still runs but binds nothing at N, so check the numbering against what is wired.
|
|
36
40
|
|
|
37
41
|
**How to compose**
|
|
38
42
|
- The prompt should be little more than tokens plus glue words: \`{image:1:person} with {image:2:face}\`. The label tells the model what to TAKE from that image — \`person\`, \`face\`, \`background\`, \`settings\`, or a concrete garment/prop name (\`jacket\`).
|
package/src/prompt-builder.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { findEntityMentionTokens, entityMentionSlugForRef, knownEntitySlugsFromR
|
|
|
25
25
|
import type { CharacterDef, ConnectedReference, DescribedReference, IdentityFidelity, IdentityMeta, ReferenceSource, SceneData } from "@nodaro/shared"
|
|
26
26
|
import { appendReferenceLines, referenceDescriptionLine, renderDescribedReferenceLines } from "./described-references.js"
|
|
27
27
|
import { locationReferencePhotoKindLabel, type LocationReferencePhotoKind } from "@nodaro/shared"
|
|
28
|
+
import { tidyReferenceTokenGaps, unboundReferenceTokenText } from "./unbound-reference-tokens.js"
|
|
28
29
|
|
|
29
30
|
export interface ResolveCharacterMentionsResult {
|
|
30
31
|
/** Prompt with @-tokens replaced by display names + "Use these characters:" directive prepended. */
|
|
@@ -3068,7 +3069,16 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
3068
3069
|
|
|
3069
3070
|
const refsToSend = supportsRefs && finalUrls.length > 0 ? finalUrls : undefined
|
|
3070
3071
|
|
|
3071
|
-
|
|
3072
|
+
// LAST, after the reorder (as the video core runs `resolveReferenceTokens`
|
|
3073
|
+
// last): an `{image:N}` with no image at N becomes its label, or nothing.
|
|
3074
|
+
// The binding test is the one both expanders above use, so a token that
|
|
3075
|
+
// binds is never touched here and every wired output is unchanged.
|
|
3076
|
+
const assembledPrompt = dropUnwiredImageTokens(
|
|
3077
|
+
finalPrompt,
|
|
3078
|
+
(n) => Boolean(tokenFinalSlot(n, nonCharacterRefs, finalIndexByUrl)),
|
|
3079
|
+
)
|
|
3080
|
+
|
|
3081
|
+
return { prompt: assembledPrompt, nativeNegativePrompt, referenceImageUrls: refsToSend }
|
|
3072
3082
|
}
|
|
3073
3083
|
|
|
3074
3084
|
// -------------------------------------------------------------------------
|
|
@@ -3160,6 +3170,9 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
3160
3170
|
|
|
3161
3171
|
// Expand {image:N} position references in prompt (legacy: → "[reference image N]")
|
|
3162
3172
|
prompt = expandImagePositionRefs(prompt, allRefs.length)
|
|
3173
|
+
// Then the unwired rule for a token past the last image — same pass, same
|
|
3174
|
+
// binding test, as the connected-reference return above.
|
|
3175
|
+
prompt = dropUnwiredImageTokens(prompt, (n) => hasImageAt(n, allRefs.length))
|
|
3163
3176
|
|
|
3164
3177
|
return { prompt, nativeNegativePrompt, referenceImageUrls: refsToSend }
|
|
3165
3178
|
}
|
|
@@ -3221,6 +3234,61 @@ const STRICT_DEFAULT_SOURCES: ReadonlySet<ReferenceSource> = new Set([
|
|
|
3221
3234
|
// newline or `}` so the match can't run away across lines / token boundaries.
|
|
3222
3235
|
const IMAGE_TOKEN_PATTERN = /\{image:(\d+)(?::([^}\n]+))?\}/gi
|
|
3223
3236
|
|
|
3237
|
+
/**
|
|
3238
|
+
* The final 1-based slot an `{image:N}` token binds to on the connected-reference
|
|
3239
|
+
* path — the slot of `refs[N - 1]`'s URL — or `undefined` when no image sits at
|
|
3240
|
+
* N (no such reference, or one without a URL). The ONE binding test shared by
|
|
3241
|
+
* both token expanders and the unwired-token pass, so "this token is wired"
|
|
3242
|
+
* means the same thing to all three.
|
|
3243
|
+
*/
|
|
3244
|
+
function tokenFinalSlot(
|
|
3245
|
+
n: number,
|
|
3246
|
+
refs: readonly ConnectedReference[],
|
|
3247
|
+
finalIndexByUrl: ReadonlyMap<string, number>,
|
|
3248
|
+
): number | undefined {
|
|
3249
|
+
const ref = refs[n - 1]
|
|
3250
|
+
return ref?.url ? finalIndexByUrl.get(ref.url) : undefined
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
/** Whether position N has an image among `count` flat references (the
|
|
3254
|
+
* no-`connectedReferences` path). Shared by its expander and the unwired pass. */
|
|
3255
|
+
function hasImageAt(n: number, count: number): boolean {
|
|
3256
|
+
return n >= 1 && n <= count
|
|
3257
|
+
}
|
|
3258
|
+
|
|
3259
|
+
/**
|
|
3260
|
+
* The last step of image prompt assembly. Every `{image:N}` token still in the
|
|
3261
|
+
* prompt whose N has no image follows the unwired rule SHARED with the video
|
|
3262
|
+
* resolver (`resolveReferenceTokens`): a labelled token becomes its label, an
|
|
3263
|
+
* unlabelled one is dropped, and the horizontal gap left behind is collapsed
|
|
3264
|
+
* and the prompt trimmed.
|
|
3265
|
+
*
|
|
3266
|
+
* WIRED OUTPUT IS UNCHANGED BY CONSTRUCTION, on two counts:
|
|
3267
|
+
* - a token whose N binds (`isWired`) is returned verbatim — the expanders
|
|
3268
|
+
* already rendered every such token they own;
|
|
3269
|
+
* - the tidy runs only when at least one token was dropped, so a prompt whose
|
|
3270
|
+
* tokens all bind keeps every doubled space and indent its author typed.
|
|
3271
|
+
* (The video resolver tidies unconditionally; the image path never did, and
|
|
3272
|
+
* a wired image prompt stays byte-identical.)
|
|
3273
|
+
*
|
|
3274
|
+
* TWO PLACES KEEP A TOKEN AS TYPED, on purpose:
|
|
3275
|
+
* - a WIRED token the hybrid format never expands — its expansion covers the
|
|
3276
|
+
* scene only, so one typed into the negative prompt keeps its text in the
|
|
3277
|
+
* `Avoid:` line (rewriting it would move a wired output);
|
|
3278
|
+
* - the native negative prompt (`nativeNegativePrompt`, the models in
|
|
3279
|
+
* `NATIVE_NEGATIVE_PROMPT_MODELS` such as ideogram-v3) is never read by this
|
|
3280
|
+
* pass — it goes to the provider's own negative field as typed, wired or not.
|
|
3281
|
+
*/
|
|
3282
|
+
function dropUnwiredImageTokens(prompt: string, isWired: (n: number) => boolean): string {
|
|
3283
|
+
let dropped = false
|
|
3284
|
+
const out = prompt.replace(IMAGE_TOKEN_PATTERN, (match: string, num: string, label?: string) => {
|
|
3285
|
+
if (isWired(parseInt(num, 10))) return match
|
|
3286
|
+
dropped = true
|
|
3287
|
+
return unboundReferenceTokenText(label)
|
|
3288
|
+
})
|
|
3289
|
+
return dropped ? tidyReferenceTokenGaps(out) : prompt
|
|
3290
|
+
}
|
|
3291
|
+
|
|
3224
3292
|
interface ResolvedIdentity {
|
|
3225
3293
|
imageIndex: number
|
|
3226
3294
|
label: string // empty string = bare positional ref (no role)
|
|
@@ -3506,7 +3574,9 @@ function buildNonCharacterDirectives(
|
|
|
3506
3574
|
* consistent identifier in both the bulleted list and the scene description.
|
|
3507
3575
|
* Numeric indices match the user-typed slug format (`@kira:1:smile`).
|
|
3508
3576
|
*
|
|
3509
|
-
* Out-of-range indices are left untouched
|
|
3577
|
+
* Out-of-range indices are left untouched by this helper. `buildImagePrompt`
|
|
3578
|
+
* then renders any such token by the unwired rule (`dropUnwiredImageTokens`):
|
|
3579
|
+
* its label, or nothing — never the raw token.
|
|
3510
3580
|
*/
|
|
3511
3581
|
export function expandImageRefTokens(prompt: string, imageCount: number): string {
|
|
3512
3582
|
return prompt.replace(IMAGE_TOKEN_PATTERN, (match, num, label) => {
|
|
@@ -3525,8 +3595,9 @@ export function expandImageRefTokens(prompt: string, imageCount: number): string
|
|
|
3525
3595
|
*
|
|
3526
3596
|
* When the final slot equals the token index (no Phase-0 prefix shifting the
|
|
3527
3597
|
* positions), the output is byte-identical to `expandImageRefTokens`.
|
|
3528
|
-
* Out-of-range tokens (or refs without a URL) are left untouched
|
|
3529
|
-
*
|
|
3598
|
+
* Out-of-range tokens (or refs without a URL) are left untouched here; the
|
|
3599
|
+
* assembly's last step, `dropUnwiredImageTokens`, renders them by the unwired
|
|
3600
|
+
* rule with the same `tokenFinalSlot` binding test.
|
|
3530
3601
|
*/
|
|
3531
3602
|
function expandImageRefTokensForRefs(
|
|
3532
3603
|
prompt: string,
|
|
@@ -3534,9 +3605,7 @@ function expandImageRefTokensForRefs(
|
|
|
3534
3605
|
finalIndexByUrl: ReadonlyMap<string, number>,
|
|
3535
3606
|
): string {
|
|
3536
3607
|
return prompt.replace(IMAGE_TOKEN_PATTERN, (match, num, label) => {
|
|
3537
|
-
const
|
|
3538
|
-
const ref = refs[n - 1]
|
|
3539
|
-
const finalIdx = ref?.url ? finalIndexByUrl.get(ref.url) : undefined
|
|
3608
|
+
const finalIdx = tokenFinalSlot(parseInt(num, 10), refs, finalIndexByUrl)
|
|
3540
3609
|
if (!finalIdx) return match
|
|
3541
3610
|
return label ? `Image ${finalIdx} (${label})` : `Image ${finalIdx}`
|
|
3542
3611
|
})
|
|
@@ -3571,8 +3640,8 @@ function hybridRolePhrase(label: string, letter: string): string {
|
|
|
3571
3640
|
/**
|
|
3572
3641
|
* Like `expandImageRefTokensForRefs`, but emits the hybrid lettered phrase
|
|
3573
3642
|
* ("the subject from reference image A") instead of the legacy "Image N
|
|
3574
|
-
* (label)". Out-of-range tokens / URL-less refs are left untouched
|
|
3575
|
-
* the
|
|
3643
|
+
* (label)". Out-of-range tokens / URL-less refs are left untouched here and
|
|
3644
|
+
* rendered by the unwired rule at the end of assembly (`dropUnwiredImageTokens`).
|
|
3576
3645
|
*
|
|
3577
3646
|
* A `{image:N:label}` pill is a mention chip like any other: `buildRefPillNodes`
|
|
3578
3647
|
* appends its own trailing space to EVERY pill it builds, the positional
|
|
@@ -3591,9 +3660,7 @@ function expandImageRefTokensHybrid(
|
|
|
3591
3660
|
): string {
|
|
3592
3661
|
const splices: Array<{ offset: number; length: number; phrase: string }> = []
|
|
3593
3662
|
for (const m of prompt.matchAll(IMAGE_TOKEN_PATTERN)) {
|
|
3594
|
-
const
|
|
3595
|
-
const ref = refs[n - 1]
|
|
3596
|
-
const finalIdx = ref?.url ? finalIndexByUrl.get(ref.url) : undefined
|
|
3663
|
+
const finalIdx = tokenFinalSlot(parseInt(m[1], 10), refs, finalIndexByUrl)
|
|
3597
3664
|
if (!finalIdx) continue
|
|
3598
3665
|
splices.push({
|
|
3599
3666
|
offset: m.index ?? 0,
|
|
@@ -3669,7 +3736,7 @@ export function expandImagePositionRefs(
|
|
|
3669
3736
|
): string {
|
|
3670
3737
|
return prompt.replace(IMAGE_TOKEN_PATTERN, (match, num, label) => {
|
|
3671
3738
|
const n = parseInt(num, 10)
|
|
3672
|
-
if (n
|
|
3739
|
+
if (!hasImageAt(n, imageCount)) return match
|
|
3673
3740
|
if (label) return `Image ${n} (${label})`
|
|
3674
3741
|
if (names && names[n - 1]) return names[n - 1]
|
|
3675
3742
|
return `Image ${n}`
|
|
@@ -162,6 +162,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
162
162
|
"nano-banana": "Fast generation, style flexibility, reference image support",
|
|
163
163
|
"nano-banana-pro": "Higher quality Nano Banana with better detail",
|
|
164
164
|
"nano-banana-2": "Latest Nano Banana with resolution options (1K/2K/4K)",
|
|
165
|
+
"nano-banana-2-1": "Nano Banana 2.1 — sharper quality, prompt adherence, character consistency and text rendering, 1K/2K/4K",
|
|
165
166
|
"nano-banana-2-lite": "Fast, low-cost 1K generation for drafts and iteration",
|
|
166
167
|
"gpt-image": "Creative concepts, illustration, variable quality tiers",
|
|
167
168
|
"gpt-image-2": "Latest GPT Image — sharp text, photorealism, 1K/2K/4K resolution",
|
|
@@ -177,6 +178,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
177
178
|
"seedream": "Artistic, painterly styles, creative interpretation",
|
|
178
179
|
"seedream-5-lite": "Lighter Seedream, faster artistic generation",
|
|
179
180
|
"seedream-5-pro": "Flagship Seedream — strongest instruction following, 2K via high quality",
|
|
181
|
+
"seedream-5-flash": "Fast, low-cost Seedream 5 — quick drafts and iteration at 1K or 2K",
|
|
180
182
|
"z-image": "Experimental, novel generation approaches",
|
|
181
183
|
"wan-2.7": "Wan 2.7 T2I — 1K/2K/4K, up to 9 ref images",
|
|
182
184
|
"wan-2.7-pro": "Wan 2.7 Pro T2I — higher quality, 1K/2K/4K",
|
|
@@ -201,6 +203,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
201
203
|
"seedream-edit": "Artistic style editing",
|
|
202
204
|
"seedream-5-lite-i2i": "Light artistic transformation",
|
|
203
205
|
"seedream-5-pro-i2i": "Pro-grade instruction-based edits, multi-reference",
|
|
206
|
+
"seedream-5-flash-i2i": "Fast, low-cost Seedream edits — up to 10 input images",
|
|
204
207
|
"flux-kontext": "Character-consistent edits with reference awareness",
|
|
205
208
|
"flux-kontext-max": "Premium character-consistent editing",
|
|
206
209
|
"kontext-multi": "Multi-image Kontext via Replicate — up to 4 refs, no safety filter",
|
|
@@ -225,6 +228,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
225
228
|
"seedream-edit": "Artistic style editing",
|
|
226
229
|
"seedream-5-lite-i2i": "Light artistic transformation",
|
|
227
230
|
"seedream-5-pro-i2i": "Pro-grade instruction-based edits, multi-reference",
|
|
231
|
+
"seedream-5-flash-i2i": "Fast, low-cost Seedream edits — up to 10 input images",
|
|
228
232
|
"flux-kontext": "Character-consistent edits with reference awareness",
|
|
229
233
|
"flux-kontext-max": "Premium character-consistent editing",
|
|
230
234
|
"kontext-multi": "Multi-image Kontext via Replicate — up to 4 refs, no safety filter",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The unwired half of the editor reference-token contract — what a
|
|
3
|
+
* `{image:N:label}` / `{video:N:label}` / `{audio:N:label}` token becomes when
|
|
4
|
+
* no reference of its kind sits at its position. ONE rule for every surface:
|
|
5
|
+
* the video resolver (`resolveReferenceTokens`) and the image prompt builder
|
|
6
|
+
* (`buildImagePrompt`) both render an unwired token through these two
|
|
7
|
+
* functions, so the image and video paths cannot drift apart on it.
|
|
8
|
+
*
|
|
9
|
+
* Each caller keeps its own token grammar and its own WIRED rendering (video:
|
|
10
|
+
* `the person from @image_1`; image: `Image 1 (person)` /
|
|
11
|
+
* `the person from reference image A`); only the unwired outcome is shared.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The text an unwired token renders to: its label as written, or nothing for a
|
|
16
|
+
* label-less token. Text a resolver does not rewrite keeps its tokens as typed
|
|
17
|
+
* — on image prompts a wired token in a hybrid `Avoid:` line and the native
|
|
18
|
+
* negative prompt, both on purpose (see `dropUnwiredImageTokens`).
|
|
19
|
+
*/
|
|
20
|
+
export function unboundReferenceTokenText(label: string | undefined): string {
|
|
21
|
+
return label ?? ""
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Tidy the gap a dropped token leaves: collapse every run of 2+ HORIZONTAL
|
|
26
|
+
* whitespace to one space, then trim. The class is `[^\S\r\n]` (NOT `\s`) so
|
|
27
|
+
* the `\n` / `\n\n` separators between assembled blocks survive — a `\s{2,}`
|
|
28
|
+
* collapse would merge paragraphs.
|
|
29
|
+
*/
|
|
30
|
+
export function tidyReferenceTokenGaps(prompt: string): string {
|
|
31
|
+
return prompt.replace(/[^\S\r\n]{2,}/g, " ").trim()
|
|
32
|
+
}
|
|
@@ -42,6 +42,7 @@ import {
|
|
|
42
42
|
import { REF_BINDING } from "./ref-binding.js"
|
|
43
43
|
import { resolveRefIdTokens } from "./ref-id-tokens.js"
|
|
44
44
|
import { insertBeforeStyleSection } from "./prompt-style-section.js"
|
|
45
|
+
import { tidyReferenceTokenGaps, unboundReferenceTokenText } from "./unbound-reference-tokens.js"
|
|
45
46
|
|
|
46
47
|
// The binding surface string and the id-addressed token resolver live in their
|
|
47
48
|
// own modules (see them for the contracts); re-exported here so every existing
|
|
@@ -76,19 +77,22 @@ const REFERENCE_TOKEN_RE = /\{(image|video|audio):(\d+)(?::([a-zA-Z0-9_ -]+))?\}
|
|
|
76
77
|
* - `N < 1` or `N > counts[kind]` (out of range / no such reference) → drop to
|
|
77
78
|
* the bare `label` (or empty if label-less). This is the legacy
|
|
78
79
|
* `stripVideoImageTokens` strip behavior: drop the token, keep the label text.
|
|
80
|
+
* The rendering is `unboundReferenceTokenText` — shared with the image prompt
|
|
81
|
+
* builder, which applies the same rule to its own unwired `{image:N}` tokens.
|
|
79
82
|
* - in range, label present → `REF_BINDING[kind](label, N)`
|
|
80
83
|
* (e.g. `the person from @image_2`).
|
|
81
84
|
* - in range, no label → the bare `@${kind}_${N}` (the ref-only default): the
|
|
82
85
|
* binding lands with no descriptive wrapper.
|
|
83
86
|
*
|
|
84
87
|
* Runs of 2+ HORIZONTAL whitespace (left behind by a dropped label-less token)
|
|
85
|
-
* collapse to one space, the result is trimmed
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* a `\s{2,}` collapse
|
|
91
|
-
*
|
|
88
|
+
* collapse to one space, the result is trimmed (`tidyReferenceTokenGaps`, also
|
|
89
|
+
* shared with the image builder), and an empty result becomes `undefined` —
|
|
90
|
+
* matching the `stripVideoImageTokens` contract so this can replace it cleanly.
|
|
91
|
+
* The collapse class is `[^\S\r\n]` (NOT `\s`) on purpose: the core applies this
|
|
92
|
+
* to the FULLY-ASSEMBLED prompt, which carries `\n\n` block separators between
|
|
93
|
+
* the "Use these characters:" directive block and the body — a `\s{2,}` collapse
|
|
94
|
+
* would silently merge those paragraphs. Horizontal-only collapse preserves
|
|
95
|
+
* newline structure while still tidying the dropped-token gap.
|
|
92
96
|
* The `kind` is lowercased before indexing `counts`/`REF_BINDING`, so a
|
|
93
97
|
* case-variant token (`{Image:1}`) resolves to the same binding rather than
|
|
94
98
|
* mis-classifying as out-of-range.
|
|
@@ -98,19 +102,15 @@ export function resolveReferenceTokens(
|
|
|
98
102
|
counts: ReferenceCounts,
|
|
99
103
|
): string | undefined {
|
|
100
104
|
if (!prompt) return prompt
|
|
101
|
-
return (
|
|
102
|
-
prompt
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// Horizontal whitespace only — preserve `\n` / `\n\n` block separators.
|
|
111
|
-
.replace(/[^\S\r\n]{2,}/g, " ")
|
|
112
|
-
.trim() || undefined
|
|
113
|
-
)
|
|
105
|
+
return tidyReferenceTokenGaps(
|
|
106
|
+
prompt.replace(REFERENCE_TOKEN_RE, (_match, rawKind: string, nStr: string, label?: string) => {
|
|
107
|
+
const kind = rawKind.toLowerCase() as keyof ReferenceCounts
|
|
108
|
+
const n = parseInt(nStr, 10)
|
|
109
|
+
if (n < 1 || n > counts[kind]) return unboundReferenceTokenText(label)
|
|
110
|
+
if (label) return REF_BINDING[kind](label, n)
|
|
111
|
+
return `@${kind}_${n}`
|
|
112
|
+
}),
|
|
113
|
+
) || undefined
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
|