@nodaro/prompts 1.34.0 → 1.36.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 +339 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +39 -12
- package/dist/index.d.ts +39 -12
- package/dist/index.js +335 -136
- 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__/creator-presets.test.ts +214 -1
- package/src/__tests__/factory-presets.test.ts +38 -12
- 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__/realism-wording.test.ts +41 -0
- package/src/__tests__/unbound-reference-tokens.test.ts +32 -0
- package/src/factory-presets/generate-image.ts +193 -79
- package/src/factory-presets/index.ts +15 -1
- package/src/factory-presets/shared-image.ts +56 -17
- package/src/factory-presets/text.ts +11 -0
- package/src/image-reference-doctrine.ts +7 -3
- package/src/index.ts +1 -0
- package/src/prompt-builder.ts +80 -13
- package/src/prompt-wizard-categories.ts +5 -2
- package/src/provider-prompt-doctrine.ts +2 -2
- package/src/realism-wording.ts +39 -0
- package/src/unbound-reference-tokens.ts +32 -0
- package/src/video-reference-resolver.ts +20 -20
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { T2I_TO_I2I_VARIANT } from "@nodaro/shared"
|
|
1
2
|
import type { FactoryPreset } from "./types.js"
|
|
2
3
|
import { stylizedSubjectFor, editsFor } from "./shared-image.js"
|
|
3
4
|
import { GENERATE_IMAGE_PRESETS } from "./generate-image.js"
|
|
@@ -14,6 +15,19 @@ import { LOTTIE_OVERLAY_PRESETS } from "./lottie-overlay.js"
|
|
|
14
15
|
export type { FactoryPreset, FactoryPresetGroup } from "./types.js"
|
|
15
16
|
export { groupFactoryPresets } from "./types.js"
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* A shared preset as Modify Image takes it. Modify Image always works from a source photo, so its
|
|
20
|
+
* provider list names a model's image-to-image id where the model has a separate one
|
|
21
|
+
* (`seedream-5-pro` → `seedream-5-pro-i2i`). The shared catalogs may carry the text-to-image id
|
|
22
|
+
* Generate Image takes — its route switches to that same image-to-image model once a photo is
|
|
23
|
+
* connected. Providers with no separate image-to-image id are left as they are.
|
|
24
|
+
*/
|
|
25
|
+
function forModifyImage(p: FactoryPreset): FactoryPreset {
|
|
26
|
+
const provider = p.data.provider
|
|
27
|
+
const imageToImage = typeof provider === "string" ? T2I_TO_I2I_VARIANT[provider] : undefined
|
|
28
|
+
return imageToImage ? { ...p, data: { ...p.data, provider: imageToImage } } : p
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
/** System/factory presets shipped with the app. Assembled here in the original
|
|
18
32
|
* single-file key order; each value lives in its per-domain module. */
|
|
19
33
|
export const FACTORY_PRESETS: Readonly<Record<string, readonly FactoryPreset[]>> = {
|
|
@@ -22,7 +36,7 @@ export const FACTORY_PRESETS: Readonly<Record<string, readonly FactoryPreset[]>>
|
|
|
22
36
|
// modify-image shares the Stylized Subject + Edits catalogs with generate-image
|
|
23
37
|
// (single source of truth in STYLIZED_SUBJECT / IMAGE_EDITS). modify-image is
|
|
24
38
|
// slated for deprecation in favor of generate-image — when removed, drop this key.
|
|
25
|
-
"modify-image": [...stylizedSubjectFor("modify-image"), ...editsFor("modify-image")],
|
|
39
|
+
"modify-image": [...stylizedSubjectFor("modify-image"), ...editsFor("modify-image")].map(forModifyImage),
|
|
26
40
|
"generate-video": GENERATE_VIDEO_PRESETS,
|
|
27
41
|
"text-to-speech": TEXT_TO_SPEECH_PRESETS,
|
|
28
42
|
"text-to-audio": TEXT_TO_AUDIO_PRESETS,
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { FactoryPreset } from "./types.js"
|
|
2
|
+
import { REALISM_FINISH, REALISM_PASS_INSTRUCTION } from "../realism-wording.js"
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Selective-stylization presets shared by `modify-image` (transform an input photo) and
|
|
5
6
|
* `generate-image` (use a connected reference image). `modify-image` is slated for deprecation in
|
|
6
7
|
* 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
|
-
*
|
|
8
|
+
* when `modify-image` is removed, just drop its key from `FACTORY_PRESETS` below. The provider is
|
|
9
|
+
* set per node by {@link editProviderFor}.
|
|
9
10
|
*/
|
|
10
11
|
const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
11
12
|
readonly slug: string
|
|
@@ -18,7 +19,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
18
19
|
name: "Cartoon Person, Real World",
|
|
19
20
|
description: "Subject → 3D cartoon, rest stays photoreal.",
|
|
20
21
|
data: {
|
|
21
|
-
provider: "nano-banana-pro",
|
|
22
22
|
promptPrefix:
|
|
23
23
|
"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
24
|
negativePrompt:
|
|
@@ -30,7 +30,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
30
30
|
name: "Caricature, Real Photo",
|
|
31
31
|
description: "Exaggerated cartoon head on a real scene.",
|
|
32
32
|
data: {
|
|
33
|
-
provider: "nano-banana-pro",
|
|
34
33
|
promptPrefix:
|
|
35
34
|
"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
35
|
negativePrompt:
|
|
@@ -42,7 +41,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
42
41
|
name: "Anime Person, Real Background",
|
|
43
42
|
description: "Subject → 2D anime, real environment.",
|
|
44
43
|
data: {
|
|
45
|
-
provider: "nano-banana-pro",
|
|
46
44
|
promptPrefix:
|
|
47
45
|
"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
46
|
negativePrompt:
|
|
@@ -54,7 +52,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
54
52
|
name: "Real Person, Cartoon World",
|
|
55
53
|
description: "Inverse — real subject, stylized world.",
|
|
56
54
|
data: {
|
|
57
|
-
provider: "nano-banana-pro",
|
|
58
55
|
promptPrefix:
|
|
59
56
|
"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
57
|
negativePrompt:
|
|
@@ -66,7 +63,6 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
66
63
|
name: "Claymation Figure, Real Set",
|
|
67
64
|
description: "Subject → clay figure, real surroundings.",
|
|
68
65
|
data: {
|
|
69
|
-
provider: "nano-banana-pro",
|
|
70
66
|
promptPrefix:
|
|
71
67
|
"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
68
|
negativePrompt:
|
|
@@ -76,13 +72,23 @@ const STYLIZED_SUBJECT: ReadonlyArray<{
|
|
|
76
72
|
]
|
|
77
73
|
|
|
78
74
|
/** Build the Stylized Subject presets for a node type (id = `<nodeType>/<slug>`). */
|
|
75
|
+
/**
|
|
76
|
+
* Both preset families are edits of a connected photo, so they run on the platform's edit model,
|
|
77
|
+
* GPT Image 2.5 Flare (decided 2026-10-08 from the image model bake-off: it kept pose, framing and
|
|
78
|
+
* product design on every edit). Modify Image takes the i2i id; Generate Image takes the t2i id,
|
|
79
|
+
* which the platform swaps to the same i2i model whenever a reference image is connected.
|
|
80
|
+
*/
|
|
81
|
+
export function editProviderFor(nodeType: string): string {
|
|
82
|
+
return nodeType === "modify-image" ? "gpt-image-2-5-flare-i2i" : "gpt-image-2-5-flare"
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
export function stylizedSubjectFor(nodeType: string): FactoryPreset[] {
|
|
80
86
|
return STYLIZED_SUBJECT.map((p) => ({
|
|
81
87
|
id: `${nodeType}/${p.slug}`,
|
|
82
88
|
name: p.name,
|
|
83
89
|
description: p.description,
|
|
84
90
|
group: "Stylized Subject",
|
|
85
|
-
data: p.data,
|
|
91
|
+
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
86
92
|
}))
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -102,7 +108,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
102
108
|
name: "Remove Background",
|
|
103
109
|
description: "Cut out the subject on white.",
|
|
104
110
|
data: {
|
|
105
|
-
provider: "nano-banana-pro",
|
|
106
111
|
promptPrefix:
|
|
107
112
|
"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
113
|
negativePrompt:
|
|
@@ -114,7 +119,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
114
119
|
name: "Replace Background",
|
|
115
120
|
description: "Swap in a new scene.",
|
|
116
121
|
data: {
|
|
117
|
-
provider: "nano-banana-pro",
|
|
118
122
|
prompt:
|
|
119
123
|
"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
124
|
negativePrompt:
|
|
@@ -126,7 +130,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
126
130
|
name: "Colorize B&W Photo",
|
|
127
131
|
description: "Natural color from grayscale.",
|
|
128
132
|
data: {
|
|
129
|
-
provider: "nano-banana-pro",
|
|
130
133
|
promptPrefix:
|
|
131
134
|
"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
135
|
negativePrompt:
|
|
@@ -138,7 +141,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
138
141
|
name: "Restore Old Photo",
|
|
139
142
|
description: "Repair scratches, denoise, sharpen.",
|
|
140
143
|
data: {
|
|
141
|
-
provider: "nano-banana-pro",
|
|
142
144
|
promptPrefix:
|
|
143
145
|
"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
146
|
negativePrompt:
|
|
@@ -150,7 +152,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
150
152
|
name: "Relight Scene",
|
|
151
153
|
description: "New lighting, same subject.",
|
|
152
154
|
data: {
|
|
153
|
-
provider: "nano-banana-pro",
|
|
154
155
|
prompt:
|
|
155
156
|
"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
157
|
negativePrompt:
|
|
@@ -162,7 +163,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
162
163
|
name: "Restyle (whole image)",
|
|
163
164
|
description: "Apply an art style to everything.",
|
|
164
165
|
data: {
|
|
165
|
-
provider: "nano-banana-pro",
|
|
166
166
|
prompt:
|
|
167
167
|
"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
168
|
negativePrompt:
|
|
@@ -174,7 +174,6 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
174
174
|
name: "Doodle Overlay",
|
|
175
175
|
description: "Hand-drawn marker doodles on top — photo stays untouched.",
|
|
176
176
|
data: {
|
|
177
|
-
provider: "nano-banana-pro",
|
|
178
177
|
promptPrefix:
|
|
179
178
|
"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
179
|
negativePrompt:
|
|
@@ -186,13 +185,53 @@ const IMAGE_EDITS: ReadonlyArray<{
|
|
|
186
185
|
name: "Doodle Overlay · Expressive",
|
|
187
186
|
description: "Looser free-form doodles — the model improvises.",
|
|
188
187
|
data: {
|
|
189
|
-
provider: "nano-banana-pro",
|
|
190
188
|
promptPrefix:
|
|
191
189
|
"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
190
|
negativePrompt:
|
|
193
191
|
"altered photo, restyled or recolored scene, covered faces, filled color blocks, sticker or clipart look, cliché phrases, garbled or misspelled handwriting, cluttered composition, watermark, blurry",
|
|
194
192
|
},
|
|
195
193
|
},
|
|
194
|
+
{
|
|
195
|
+
slug: "add-skin-realism",
|
|
196
|
+
name: "Add Skin Realism",
|
|
197
|
+
description: "Brings back visible skin texture — same person, same photo.",
|
|
198
|
+
data: { provider: "nano-banana-2-1", aspectRatio: "auto", resolution: "2K", promptPrefix: REALISM_PASS_INSTRUCTION },
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
slug: "natural-photo-finish",
|
|
202
|
+
name: "Natural Photo Finish",
|
|
203
|
+
description: "Takes out a rendered-looking finish, keeps the photo.",
|
|
204
|
+
data: {
|
|
205
|
+
provider: "nano-banana-2-1",
|
|
206
|
+
aspectRatio: "auto",
|
|
207
|
+
resolution: "2K",
|
|
208
|
+
promptPrefix: `Keep this photo as it is and take out only what makes it look rendered: smoothed, waxy or airbrushed skin, a glow, over-sharpened edges, hair too perfect to be real. ${REALISM_FINISH} Otherwise the same people, framing, pose and background.`,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
slug: "phone-camera-look",
|
|
213
|
+
name: "Phone Camera Look",
|
|
214
|
+
description: "Re-finishes a photo as an everyday snapshot.",
|
|
215
|
+
data: {
|
|
216
|
+
provider: "nano-banana-2-1",
|
|
217
|
+
aspectRatio: "auto",
|
|
218
|
+
resolution: "2K",
|
|
219
|
+
promptPrefix: `Re-finish this photo, changing nothing in it: ${REALISM_FINISH} Same people, framing, pose and background.`,
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
slug: "undo-beauty-filter",
|
|
224
|
+
name: "Undo Beauty Filter",
|
|
225
|
+
description: "Reverses smoothing and reshaping on a real photo.",
|
|
226
|
+
data: {
|
|
227
|
+
provider: "seedream-5-pro",
|
|
228
|
+
// "auto" keeps the photo's shape: this model lists no native "auto", so on either node the
|
|
229
|
+
// run takes its listed ratio nearest the connected photo.
|
|
230
|
+
aspectRatio: "auto",
|
|
231
|
+
quality: "high",
|
|
232
|
+
promptPrefix: "Undo the beauty filter on this photo: bring back the person's real skin texture, the natural shape of the face, jaw, nose and eyes, and the real colour of skin and lips. Change nothing else: the same person, expression, hair, clothing, framing and background.",
|
|
233
|
+
},
|
|
234
|
+
},
|
|
196
235
|
]
|
|
197
236
|
|
|
198
237
|
/** Build the Edits presets for a node type (id = `<nodeType>/<slug>`). */
|
|
@@ -202,6 +241,6 @@ export function editsFor(nodeType: string): FactoryPreset[] {
|
|
|
202
241
|
name: p.name,
|
|
203
242
|
description: p.description,
|
|
204
243
|
group: "Edits",
|
|
205
|
-
data: p.data,
|
|
244
|
+
data: { provider: editProviderFor(nodeType), ...p.data },
|
|
206
245
|
}))
|
|
207
246
|
}
|
|
@@ -318,4 +318,15 @@ export const IMAGE_TO_TEXT_PRESETS: readonly FactoryPreset[] = [
|
|
|
318
318
|
group: "Creative",
|
|
319
319
|
data: { detailLevel: "detailed", customPrompt: "Describe this image as a detailed text-to-image generation prompt: subject, style, lighting, composition and lens. Return only the prompt." },
|
|
320
320
|
},
|
|
321
|
+
{
|
|
322
|
+
id: "image-to-text/realism-audit",
|
|
323
|
+
name: "Realism Audit",
|
|
324
|
+
description: "Lists the signs an image was generated, with a count.",
|
|
325
|
+
group: "Creative",
|
|
326
|
+
data: {
|
|
327
|
+
detailLevel: "structured",
|
|
328
|
+
customPrompt:
|
|
329
|
+
"Check this image for signs that it was generated rather than photographed. Answer each question yes or no, with a few words why. 1. Is the skin smooth, waxy or airbrushed, with no visible pores? 2. Is the face unusually symmetrical, or are the teeth unnaturally even and white? 3. Are any hands or fingers malformed, merged or the wrong count? 4. Is any text, logo or sign garbled? 5. Is the background blurred far more than an everyday snapshot would be? 6. Are the catchlights in the eyes perfect rings, or identical in both eyes? 7. Is the hair uniformly perfect, with no flyaways? 8. Do edges look over-sharpened or haloed? 9. Is there a glow or bloom on the skin? 10. Do shadows or reflections disagree with the light? Finish with one line: \"Tells found: N of 10\".",
|
|
330
|
+
},
|
|
331
|
+
},
|
|
321
332
|
]
|
|
@@ -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/index.ts
CHANGED
|
@@ -90,6 +90,7 @@ export { stripAudioTags } from "./audio-tags.js"
|
|
|
90
90
|
export * from "./node-prompt-fields.js"
|
|
91
91
|
export * from "./factory-presets.js"
|
|
92
92
|
export * from "./caption-treatments.js"
|
|
93
|
+
export * from "./realism-wording.js"
|
|
93
94
|
export * from "./style-presets.js"
|
|
94
95
|
export * from "./object-asset-presets.js"
|
|
95
96
|
export * from "./factory-snippets/index.js"
|
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}`
|
|
@@ -178,6 +178,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
178
178
|
"seedream": "Artistic, painterly styles, creative interpretation",
|
|
179
179
|
"seedream-5-lite": "Lighter Seedream, faster artistic generation",
|
|
180
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",
|
|
181
182
|
"z-image": "Experimental, novel generation approaches",
|
|
182
183
|
"wan-2.7": "Wan 2.7 T2I — 1K/2K/4K, up to 9 ref images",
|
|
183
184
|
"wan-2.7-pro": "Wan 2.7 Pro T2I — higher quality, 1K/2K/4K",
|
|
@@ -202,6 +203,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
202
203
|
"seedream-edit": "Artistic style editing",
|
|
203
204
|
"seedream-5-lite-i2i": "Light artistic transformation",
|
|
204
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",
|
|
205
207
|
"flux-kontext": "Character-consistent edits with reference awareness",
|
|
206
208
|
"flux-kontext-max": "Premium character-consistent editing",
|
|
207
209
|
"kontext-multi": "Multi-image Kontext via Replicate — up to 4 refs, no safety filter",
|
|
@@ -226,6 +228,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
226
228
|
"seedream-edit": "Artistic style editing",
|
|
227
229
|
"seedream-5-lite-i2i": "Light artistic transformation",
|
|
228
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",
|
|
229
232
|
"flux-kontext": "Character-consistent edits with reference awareness",
|
|
230
233
|
"flux-kontext-max": "Premium character-consistent editing",
|
|
231
234
|
"kontext-multi": "Multi-image Kontext via Replicate — up to 4 refs, no safety filter",
|
|
@@ -249,7 +252,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
249
252
|
"seedance-2-fast": "Seedance 2.0 Fast — same multimodal + audio capabilities, cheaper and quicker",
|
|
250
253
|
"seedance-2-mini": "Seedance 2.0 Mini — same multimodal + audio capabilities, budget tier, 480p/720p, 4-15s",
|
|
251
254
|
"seedance-2-5": "Seedance 2.5 — up to 30s in ONE shot (no stitching), wider multimodal refs (30 images / 10 videos / 10 audio), native audio, 480p/720p/1080p",
|
|
252
|
-
"minimax-h3": "MiniMax
|
|
255
|
+
"minimax-h3": "MiniMax H3 — premium multimodal refs (9 images / 3 videos / 3 audio), always-on audio, 2K or 768P, 4-15s per-second pricing",
|
|
253
256
|
"wan": "Versatile, good for animations and transformations",
|
|
254
257
|
"wan-turbo": "Faster Wan generation",
|
|
255
258
|
"hailuo-standard": "Standard quality, cost-effective",
|
|
@@ -280,7 +283,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
280
283
|
"seedance-2-fast": "Seedance 2.0 Fast — same capabilities, cheaper and quicker",
|
|
281
284
|
"seedance-2-mini": "Seedance 2.0 Mini — same capabilities, budget tier, 480p/720p",
|
|
282
285
|
"seedance-2-5": "Seedance 2.5 — start/end frame + wide multimodal refs, native audio, up to 30s, 480p/720p/1080p",
|
|
283
|
-
"minimax-h3": "MiniMax
|
|
286
|
+
"minimax-h3": "MiniMax H3 — first/last frame + multimodal refs, always-on audio, 2K or 768P, 4-15s",
|
|
284
287
|
"hailuo-2.3-pro": "Premium Hailuo animation",
|
|
285
288
|
"hailuo-2.3": "Standard Hailuo animation",
|
|
286
289
|
"hailuo-standard": "Cost-effective animation",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* - KIE API docs https://docs.kie.ai/market/kling/kling-3-0
|
|
24
24
|
* - Live KIE-path dialogue probe 2026-07-16 (scripted lines spoken verbatim,
|
|
25
25
|
* lip-synced, on both kling-2.6 and kling-3.0 with sound=true)
|
|
26
|
-
* MiniMax
|
|
26
|
+
* MiniMax H3:
|
|
27
27
|
* - KIE API docs https://docs.kie.ai/market/minimax-h3 (text-to-video /
|
|
28
28
|
* image-to-video / reference-to-video — the API contract is the doctrine
|
|
29
29
|
* source: limits, modes, aspect rules, pricing dimensions). No live probe
|
|
@@ -160,7 +160,7 @@ const KLING_AUDIO_DOCTRINE: ProviderPromptDoctrine = {
|
|
|
160
160
|
|
|
161
161
|
const MINIMAX_H3_DOCTRINE: ProviderPromptDoctrine = {
|
|
162
162
|
providers: ["minimax-h3"],
|
|
163
|
-
heading: "MiniMax
|
|
163
|
+
heading: "MiniMax H3 (minimax-h3)",
|
|
164
164
|
tips: [
|
|
165
165
|
"Natural-language prompts up to 7000 chars. Front-load what matters: subject → action → scene/environment → lighting → camera move → style. One camera movement per shot.",
|
|
166
166
|
"Output 2K (default) or 768P (cheaper tier). Aspect: pure text-to-video needs a concrete ratio (21:9/16:9/4:3/1:1/3:4/9:16); reference runs default to adaptive (match the input).",
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// packages/prompts/src/realism-wording.ts
|
|
2
|
+
/**
|
|
3
|
+
* Realism and viewpoint wording for everyday-photo prompts: skin and finish
|
|
4
|
+
* sentences, the identity-keeping realism instruction for an edit, and the eye-level
|
|
5
|
+
* viewpoint and empty-hands sentences, worded for any person.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Who a sentence is about: gendered ("she" / "her") or neutral ("the person" / "their"). */
|
|
9
|
+
export interface PersonWords {
|
|
10
|
+
readonly subject: string
|
|
11
|
+
readonly possessive: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const NEUTRAL_PERSON: PersonWords = { subject: "the person", possessive: "their" }
|
|
15
|
+
|
|
16
|
+
const cap = (s: string): string => (s.length === 0 ? s : `${s[0]!.toUpperCase()}${s.slice(1)}`)
|
|
17
|
+
|
|
18
|
+
export const REALISM_SKIN =
|
|
19
|
+
"Skin shows its real structure: visible pores that vary in size across the nose and cheeks, fine vellus hair catching the light, faint uneven pigmentation, a little redness around the nostrils, natural oil on the forehead."
|
|
20
|
+
|
|
21
|
+
export const REALISM_FINISH =
|
|
22
|
+
"The plain look of an everyday small-sensor snapshot: colour left as it was recorded, deep focus from the nearest shoulder to the background, a fine grain in the darker areas, bright areas that fade rather than clip."
|
|
23
|
+
|
|
24
|
+
export const REALISM_PASS_INSTRUCTION =
|
|
25
|
+
"Make this photo look like an unretouched everyday snapshot: skin that shows its pores, its " +
|
|
26
|
+
"fine hairs and the small changes in tone real skin has, colour left as it was recorded, everything " +
|
|
27
|
+
"sharp from the shoulders to the background, a little grain in the shadows. Take out anything that makes it " +
|
|
28
|
+
"look rendered — smoothed, waxy or airbrushed skin, a glow, over-sharpened edges — and add no text or icons. " +
|
|
29
|
+
"Otherwise it stays the same person in the same frame: the face and its proportions, hair, clothing, pose and " +
|
|
30
|
+
"background as they are."
|
|
31
|
+
|
|
32
|
+
/** The frame by its viewpoint only — never by a device. */
|
|
33
|
+
export function realismViewpoint(p: PersonWords): string {
|
|
34
|
+
return `${cap(p.subject)} is looking straight at the viewer from eye level, mid-sentence: ${p.possessive} face and shoulders close up, the framing loose and a little crooked, both arms relaxed and down out of the frame. The whole image is the photograph itself, edge to edge, with no text or icons anywhere.`
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function realismEmptyHands(p: PersonWords): string {
|
|
38
|
+
return `${cap(p.possessive)} hands are empty; ${p.subject} is holding nothing.`
|
|
39
|
+
}
|
|
@@ -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
|
+
}
|