@nodaro/prompts 1.15.0 → 1.17.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 +243 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -3
- package/dist/index.d.ts +149 -3
- package/dist/index.js +239 -70
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/assemble-image-input.test.ts +30 -0
- package/src/__tests__/described-references-video.test.ts +250 -0
- package/src/__tests__/described-references.test.ts +595 -0
- package/src/__tests__/doctrine-roster-completeness.test.ts +1 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +313 -35
- package/src/__tests__/minor-age-floor.test.ts +4 -4
- package/src/__tests__/person-body-axes.test.ts +2 -2
- package/src/__tests__/person-exposure-hints.test.ts +59 -4
- package/src/__tests__/retired-hint-strings.test.ts +68 -0
- package/src/__tests__/subject-registry.test.ts +1 -1
- package/src/__tests__/w1b-rephrase.test.ts +119 -0
- package/src/age-floor.ts +65 -2
- package/src/assemble-image-input.ts +11 -1
- package/src/described-references.ts +138 -0
- package/src/index.ts +1 -0
- package/src/person.ts +18 -17
- package/src/pose.ts +1 -1
- package/src/prompt-builder.ts +196 -19
- package/src/styling.ts +38 -2
- package/src/video-reference-resolver.ts +85 -10
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Described references (`DescribedReference` — a name + a description, no
|
|
3
|
+
* media) and the per-use `descriptionOverride`, on the IMAGE lane, in BOTH
|
|
4
|
+
* reference formats.
|
|
5
|
+
*
|
|
6
|
+
* Contract under test:
|
|
7
|
+
* - a described reference reaches the model as `<Name> — <description>.` —
|
|
8
|
+
* bulleted into the legacy "Use these characters:" block, a trailing scene
|
|
9
|
+
* directive in hybrid — even when the request carries NO connected
|
|
10
|
+
* references at all (the story-landing case: a cast role nothing is bound
|
|
11
|
+
* to yet).
|
|
12
|
+
* - `descriptionOverride` is the reference's identity description for THIS
|
|
13
|
+
* use: it fills the description slot a legacy bullet (and the hybrid
|
|
14
|
+
* extras' `, <desc>` clause) already has, winning over the entity's stored
|
|
15
|
+
* canonical description AND over the ref's own `description`; where hybrid
|
|
16
|
+
* has no such slot it adds ONE `reference image A — <override>.` line.
|
|
17
|
+
* - neither field present → byte-identical output.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from "vitest"
|
|
21
|
+
import { getMaxImagePromptChars } from "@nodaro/shared"
|
|
22
|
+
import type { ConnectedReference, DescribedReference } from "@nodaro/shared"
|
|
23
|
+
import { buildImagePrompt } from "../prompt-builder.js"
|
|
24
|
+
import {
|
|
25
|
+
appendReferenceLines,
|
|
26
|
+
referenceDescriptionLine,
|
|
27
|
+
renderDescribedReferenceLines,
|
|
28
|
+
renderReferenceCaptionLines,
|
|
29
|
+
} from "../described-references.js"
|
|
30
|
+
|
|
31
|
+
const PROVIDER = "nano-banana-pro" // in MODELS_WITH_REFERENCE_IMAGE_SUPPORT
|
|
32
|
+
|
|
33
|
+
const described: DescribedReference[] = [
|
|
34
|
+
{ name: "Natalie", description: "a tall woman in a red coat" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
describe("renderDescribedReferenceLines", () => {
|
|
38
|
+
it("renders `<Name> — <description>.`", () => {
|
|
39
|
+
expect(renderDescribedReferenceLines(described)).toEqual([
|
|
40
|
+
"Natalie — a tall woman in a red coat.",
|
|
41
|
+
])
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("drops an entry missing either half and dedups by name", () => {
|
|
45
|
+
expect(
|
|
46
|
+
renderDescribedReferenceLines([
|
|
47
|
+
{ name: " ", description: "nameless" },
|
|
48
|
+
{ name: "Jack", description: " " },
|
|
49
|
+
{ name: "Natalie", description: "a tall woman" },
|
|
50
|
+
{ name: "natalie", description: "a second take" },
|
|
51
|
+
]),
|
|
52
|
+
).toEqual(["Natalie — a tall woman."])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("returns nothing for an absent or empty list", () => {
|
|
56
|
+
expect(renderDescribedReferenceLines(undefined)).toEqual([])
|
|
57
|
+
expect(renderDescribedReferenceLines([])).toEqual([])
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("referenceDescriptionLine", () => {
|
|
62
|
+
it("takes the binding as the subject for a per-use override", () => {
|
|
63
|
+
expect(referenceDescriptionLine("reference image A", "in a red coat")).toBe(
|
|
64
|
+
"reference image A — in a red coat.",
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("is empty when either half is blank", () => {
|
|
69
|
+
expect(referenceDescriptionLine("reference image A", " ")).toBe("")
|
|
70
|
+
expect(referenceDescriptionLine("", "in a red coat")).toBe("")
|
|
71
|
+
expect(referenceDescriptionLine("reference image A", undefined)).toBe("")
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
describe("renderReferenceCaptionLines", () => {
|
|
76
|
+
it("renders index-aligned `@video_N:` / `@audio_N:` lines", () => {
|
|
77
|
+
expect(
|
|
78
|
+
renderReferenceCaptionLines(
|
|
79
|
+
["the establishing drone shot", "the close-up"],
|
|
80
|
+
["the score"],
|
|
81
|
+
{ video: 2, audio: 1 },
|
|
82
|
+
),
|
|
83
|
+
).toEqual([
|
|
84
|
+
"@video_1: the establishing drone shot.",
|
|
85
|
+
"@video_2: the close-up.",
|
|
86
|
+
"@audio_1: the score.",
|
|
87
|
+
])
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it("keeps the alignment when a caption is blank and stops at the shipped count", () => {
|
|
91
|
+
expect(
|
|
92
|
+
renderReferenceCaptionLines(["", "the close-up", "dropped by the cap"], undefined, {
|
|
93
|
+
video: 2,
|
|
94
|
+
audio: 0,
|
|
95
|
+
}),
|
|
96
|
+
).toEqual(["@video_2: the close-up."])
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe("appendReferenceLines", () => {
|
|
101
|
+
it("creates a legacy block ahead of the body", () => {
|
|
102
|
+
expect(appendReferenceLines("A woman walks.", ["Natalie — tall."], "legacy")).toBe(
|
|
103
|
+
"Use these characters:\n- Natalie — tall.\n\nA woman walks.",
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it("consolidates into an existing legacy block", () => {
|
|
108
|
+
const prompt = "Use these characters:\n- Image 1 (Kira) — match exactly.\n\nA woman walks."
|
|
109
|
+
expect(appendReferenceLines(prompt, ["Natalie — tall."], "legacy")).toBe(
|
|
110
|
+
"Use these characters:\n- Image 1 (Kira) — match exactly.\n- Natalie — tall.\n\nA woman walks.",
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("appends hybrid lines to the body", () => {
|
|
115
|
+
expect(appendReferenceLines("A woman walks.", ["Natalie — tall."], "hybrid")).toBe(
|
|
116
|
+
"A woman walks.\nNatalie — tall.",
|
|
117
|
+
)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("returns the prompt untouched with no lines", () => {
|
|
121
|
+
expect(appendReferenceLines("A woman walks.", [], "legacy")).toBe("A woman walks.")
|
|
122
|
+
expect(appendReferenceLines("A woman walks.", [], "hybrid")).toBe("A woman walks.")
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe("buildImagePrompt — described references with no connected references", () => {
|
|
127
|
+
it("bullets them into a legacy block", () => {
|
|
128
|
+
const { prompt, referenceImageUrls } = buildImagePrompt({
|
|
129
|
+
provider: PROVIDER,
|
|
130
|
+
prompt: "Natalie walks down the pier.",
|
|
131
|
+
describedReferences: described,
|
|
132
|
+
})
|
|
133
|
+
expect(prompt).toBe(
|
|
134
|
+
"Use these characters:\n- Natalie — a tall woman in a red coat.\n\nNatalie walks down the pier.",
|
|
135
|
+
)
|
|
136
|
+
// A described reference carries no url — nothing to attach.
|
|
137
|
+
expect(referenceImageUrls).toBeUndefined()
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
it("appends them as hybrid trailing directives", () => {
|
|
141
|
+
const { prompt } = buildImagePrompt({
|
|
142
|
+
provider: PROVIDER,
|
|
143
|
+
prompt: "Natalie walks down the pier.",
|
|
144
|
+
describedReferences: described,
|
|
145
|
+
referenceFormat: "hybrid",
|
|
146
|
+
})
|
|
147
|
+
expect(prompt).toBe("Natalie walks down the pier.\nNatalie — a tall woman in a red coat.")
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it("keeps them inside the body, ahead of the [style] section", () => {
|
|
151
|
+
const { prompt } = buildImagePrompt({
|
|
152
|
+
provider: PROVIDER,
|
|
153
|
+
prompt: "Natalie walks down the pier.\n\n[style]:\nsoft dusk light",
|
|
154
|
+
describedReferences: described,
|
|
155
|
+
referenceFormat: "hybrid",
|
|
156
|
+
})
|
|
157
|
+
expect(prompt).toBe(
|
|
158
|
+
"Natalie walks down the pier.\nNatalie — a tall woman in a red coat.\n\n[style]:\nsoft dusk light",
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("is byte-identical to the same call without the field when the list is empty", () => {
|
|
163
|
+
const base = { provider: PROVIDER, prompt: "Natalie walks down the pier." }
|
|
164
|
+
expect(buildImagePrompt({ ...base, describedReferences: [] }).prompt).toBe(
|
|
165
|
+
buildImagePrompt(base).prompt,
|
|
166
|
+
)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("rides the provider prompt cap like any other body text", () => {
|
|
170
|
+
const cap = getMaxImagePromptChars(PROVIDER)
|
|
171
|
+
const { prompt } = buildImagePrompt({
|
|
172
|
+
provider: PROVIDER,
|
|
173
|
+
prompt: "A woman walks.",
|
|
174
|
+
describedReferences: [{ name: "Natalie", description: "x".repeat(cap + 1000) }],
|
|
175
|
+
})
|
|
176
|
+
expect(prompt.length).toBeLessThanOrEqual(cap)
|
|
177
|
+
expect(prompt.endsWith("...")).toBe(true)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe("buildImagePrompt — described references alongside connected references", () => {
|
|
182
|
+
const kira: ConnectedReference = {
|
|
183
|
+
id: "c1",
|
|
184
|
+
defaultName: "Kira",
|
|
185
|
+
source: "wired-character",
|
|
186
|
+
characterSlug: "kira",
|
|
187
|
+
url: "https://r2/kira.png",
|
|
188
|
+
characterCanonicalDescription: "a woman with short black hair",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it("consolidates into the one legacy character block", () => {
|
|
192
|
+
const { prompt } = buildImagePrompt({
|
|
193
|
+
provider: PROVIDER,
|
|
194
|
+
prompt: "Kira meets Natalie.",
|
|
195
|
+
connectedReferences: [kira],
|
|
196
|
+
describedReferences: described,
|
|
197
|
+
})
|
|
198
|
+
expect(prompt).toBe(
|
|
199
|
+
"Use these characters:\n"
|
|
200
|
+
+ "- Image 1 (Kira) — a woman with short black hair. The subject must remain exactly the same person"
|
|
201
|
+
+ " — preserve 100% facial identity, bone structure, skin tone, proportions, and all unique features."
|
|
202
|
+
+ " Do not alter eyes, nose, mouth, or facial shape. Maintain natural skin texture, including pores"
|
|
203
|
+
+ " and imperfections.\n"
|
|
204
|
+
+ "- Natalie — a tall woman in a red coat.\n\n"
|
|
205
|
+
+ "Kira meets Natalie.",
|
|
206
|
+
)
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("makes ONE block when the references produce the 'Use these references' wrap", () => {
|
|
210
|
+
// A non-character ref with no wired character takes the legacy PREPEND
|
|
211
|
+
// branch ("Use these references for the output image:… Compose them
|
|
212
|
+
// naturally…"). The described block must be the block that wrap folds
|
|
213
|
+
// into — never a second header stacked above it.
|
|
214
|
+
const { prompt } = buildImagePrompt({
|
|
215
|
+
provider: PROVIDER,
|
|
216
|
+
prompt: "A still life with {image:1:vase}.",
|
|
217
|
+
connectedReferences: [
|
|
218
|
+
{ id: "o1", defaultName: "Vase", source: "wired-object", url: "https://r2/vase.png", description: "a blue vase" },
|
|
219
|
+
],
|
|
220
|
+
describedReferences: described,
|
|
221
|
+
})
|
|
222
|
+
expect(prompt.match(/Use these characters:/g)).toHaveLength(1)
|
|
223
|
+
expect(prompt).not.toContain("Use these references for the output image:")
|
|
224
|
+
expect(prompt).toBe(
|
|
225
|
+
"Use these characters:\n"
|
|
226
|
+
+ "- Natalie — a tall woman in a red coat.\n"
|
|
227
|
+
+ "- Image 1 (vase — a blue vase) — match exactly. Maintain perfect likeness.\n\n"
|
|
228
|
+
+ "A still life with Image 1 (vase).",
|
|
229
|
+
)
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("lands after the hybrid role phrases", () => {
|
|
233
|
+
const { prompt } = buildImagePrompt({
|
|
234
|
+
provider: PROVIDER,
|
|
235
|
+
prompt: "Kira meets Natalie.",
|
|
236
|
+
connectedReferences: [kira],
|
|
237
|
+
describedReferences: described,
|
|
238
|
+
referenceFormat: "hybrid",
|
|
239
|
+
})
|
|
240
|
+
expect(prompt).toBe(
|
|
241
|
+
"Kira meets Natalie.\n"
|
|
242
|
+
+ "the person from reference image A\n"
|
|
243
|
+
+ "Natalie — a tall woman in a red coat.",
|
|
244
|
+
)
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
describe("descriptionOverride — legacy", () => {
|
|
249
|
+
const withOverride: ConnectedReference = {
|
|
250
|
+
id: "c1",
|
|
251
|
+
defaultName: "Kira",
|
|
252
|
+
source: "wired-character",
|
|
253
|
+
characterSlug: "kira",
|
|
254
|
+
url: "https://r2/kira.png",
|
|
255
|
+
characterCanonicalDescription: "a woman with short black hair",
|
|
256
|
+
descriptionOverride: "a woman with a shaved head and a scar",
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
it("replaces the canonical description on the canonical fallback", () => {
|
|
260
|
+
const { prompt } = buildImagePrompt({
|
|
261
|
+
provider: PROVIDER,
|
|
262
|
+
prompt: "Kira walks.",
|
|
263
|
+
connectedReferences: [withOverride],
|
|
264
|
+
})
|
|
265
|
+
expect(prompt).toContain("- Image 1 (Kira) — a woman with a shaved head and a scar.")
|
|
266
|
+
expect(prompt).not.toContain("short black hair")
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
it("replaces the canonical description on an @-mention", () => {
|
|
270
|
+
const { prompt } = buildImagePrompt({
|
|
271
|
+
provider: PROVIDER,
|
|
272
|
+
prompt: "@kira:1 walks.",
|
|
273
|
+
connectedReferences: [withOverride],
|
|
274
|
+
})
|
|
275
|
+
expect(prompt).toContain("- Image 1 (Kira) — a woman with a shaved head and a scar.")
|
|
276
|
+
expect(prompt).not.toContain("short black hair")
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
it("is honored in a mode that suppresses the canonical description", () => {
|
|
280
|
+
// "emotion" gates the canonical description off — an explicit per-use
|
|
281
|
+
// override is the caller speaking, so it rides every mode that emits a
|
|
282
|
+
// bullet at all.
|
|
283
|
+
const { prompt } = buildImagePrompt({
|
|
284
|
+
provider: PROVIDER,
|
|
285
|
+
prompt: "Kira walks.",
|
|
286
|
+
connectedReferences: [{ ...withOverride, defaultUsageMode: "emotion" }],
|
|
287
|
+
})
|
|
288
|
+
expect(prompt).toContain("- Image 1 (Kira) — a woman with a shaved head and a scar.")
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
it("emits nothing extra in a mode that emits no bullet", () => {
|
|
292
|
+
const { prompt } = buildImagePrompt({
|
|
293
|
+
provider: PROVIDER,
|
|
294
|
+
prompt: "Kira walks.",
|
|
295
|
+
connectedReferences: [{ ...withOverride, defaultUsageMode: "none" }],
|
|
296
|
+
})
|
|
297
|
+
expect(prompt).toBe("Kira walks.")
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it("wins over an extra-ref's own description", () => {
|
|
301
|
+
const { prompt } = buildImagePrompt({
|
|
302
|
+
provider: PROVIDER,
|
|
303
|
+
prompt: "A still life.",
|
|
304
|
+
connectedReferences: [
|
|
305
|
+
{
|
|
306
|
+
id: "m1",
|
|
307
|
+
defaultName: "Vase",
|
|
308
|
+
source: "manual",
|
|
309
|
+
url: "https://r2/vase.png",
|
|
310
|
+
isExtraRef: true,
|
|
311
|
+
description: "a blue vase",
|
|
312
|
+
descriptionOverride: "a cracked terracotta urn",
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
})
|
|
316
|
+
expect(prompt).toContain("- Image 1 (reference): a cracked terracotta urn.")
|
|
317
|
+
expect(prompt).not.toContain("blue vase")
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
it("wins over a MENTIONED location's canonical description", () => {
|
|
321
|
+
const { prompt } = buildImagePrompt({
|
|
322
|
+
provider: PROVIDER,
|
|
323
|
+
prompt: "A wide shot of @old-library:1.",
|
|
324
|
+
connectedReferences: [
|
|
325
|
+
{
|
|
326
|
+
id: "l1",
|
|
327
|
+
defaultName: "Old Library",
|
|
328
|
+
source: "wired-location",
|
|
329
|
+
locationSlug: "old-library",
|
|
330
|
+
url: "https://r2/library.png",
|
|
331
|
+
locationCanonicalDescription: "a dusty reading room",
|
|
332
|
+
descriptionOverride: "a flooded reading room at night",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
})
|
|
336
|
+
expect(prompt).toContain("- Image 1 (Old Library) — a flooded reading room at night.")
|
|
337
|
+
expect(prompt).not.toContain("dusty reading room")
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it("fills the descriptor of a `{image:N:label}` identity directive", () => {
|
|
341
|
+
// The token path reads its description through `collectIdentities`, which is
|
|
342
|
+
// what feeds the `{image:N:label}` directive — so the override lands in the
|
|
343
|
+
// subject's parenthetical, ahead of the ref's own `description`.
|
|
344
|
+
const { prompt } = buildImagePrompt({
|
|
345
|
+
provider: PROVIDER,
|
|
346
|
+
prompt: "A still life with {image:1:vase}.",
|
|
347
|
+
connectedReferences: [
|
|
348
|
+
{
|
|
349
|
+
id: "o1",
|
|
350
|
+
defaultName: "Vase",
|
|
351
|
+
source: "wired-object",
|
|
352
|
+
url: "https://r2/vase.png",
|
|
353
|
+
description: "a blue vase",
|
|
354
|
+
descriptionOverride: "a cracked urn",
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
})
|
|
358
|
+
expect(prompt).toContain("- Image 1 (vase — a cracked urn) — match exactly.")
|
|
359
|
+
expect(prompt).not.toContain("blue vase")
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
it("wins over an UNMENTIONED wired location's canonical description", () => {
|
|
363
|
+
const { prompt } = buildImagePrompt({
|
|
364
|
+
provider: PROVIDER,
|
|
365
|
+
prompt: "A wide shot.",
|
|
366
|
+
connectedReferences: [
|
|
367
|
+
{
|
|
368
|
+
id: "l1",
|
|
369
|
+
defaultName: "Old Library",
|
|
370
|
+
source: "wired-location",
|
|
371
|
+
locationSlug: "old-library",
|
|
372
|
+
url: "https://r2/library.png",
|
|
373
|
+
locationCanonicalDescription: "a dusty reading room",
|
|
374
|
+
descriptionOverride: "a flooded reading room at night",
|
|
375
|
+
},
|
|
376
|
+
],
|
|
377
|
+
})
|
|
378
|
+
expect(prompt).toContain("a flooded reading room at night")
|
|
379
|
+
expect(prompt).not.toContain("dusty reading room")
|
|
380
|
+
})
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
describe("descriptionOverride — hybrid", () => {
|
|
384
|
+
const kira: ConnectedReference = {
|
|
385
|
+
id: "c1",
|
|
386
|
+
defaultName: "Kira",
|
|
387
|
+
source: "wired-character",
|
|
388
|
+
characterSlug: "kira",
|
|
389
|
+
url: "https://r2/kira.png",
|
|
390
|
+
characterCanonicalDescription: "a woman with short black hair",
|
|
391
|
+
descriptionOverride: "a woman with a shaved head and a scar",
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
it("adds one binding-subject line for a canonical fallback", () => {
|
|
395
|
+
const { prompt } = buildImagePrompt({
|
|
396
|
+
provider: PROVIDER,
|
|
397
|
+
prompt: "Kira walks.",
|
|
398
|
+
connectedReferences: [kira],
|
|
399
|
+
referenceFormat: "hybrid",
|
|
400
|
+
})
|
|
401
|
+
expect(prompt).toBe(
|
|
402
|
+
"Kira walks.\n"
|
|
403
|
+
+ "the person from reference image A\n"
|
|
404
|
+
+ "reference image A — a woman with a shaved head and a scar.",
|
|
405
|
+
)
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it("adds one binding-subject line for an @-mention", () => {
|
|
409
|
+
const { prompt } = buildImagePrompt({
|
|
410
|
+
provider: PROVIDER,
|
|
411
|
+
prompt: "@kira:1 walks.",
|
|
412
|
+
connectedReferences: [kira],
|
|
413
|
+
referenceFormat: "hybrid",
|
|
414
|
+
})
|
|
415
|
+
expect(prompt).toBe(
|
|
416
|
+
"the person from reference image A walks.\n"
|
|
417
|
+
+ "reference image A — a woman with a shaved head and a scar.",
|
|
418
|
+
)
|
|
419
|
+
})
|
|
420
|
+
|
|
421
|
+
it("adds one binding-subject line for a wired location", () => {
|
|
422
|
+
const { prompt } = buildImagePrompt({
|
|
423
|
+
provider: PROVIDER,
|
|
424
|
+
prompt: "A wide shot.",
|
|
425
|
+
connectedReferences: [
|
|
426
|
+
{
|
|
427
|
+
id: "l1",
|
|
428
|
+
defaultName: "Old Library",
|
|
429
|
+
source: "wired-location",
|
|
430
|
+
locationSlug: "old-library",
|
|
431
|
+
url: "https://r2/library.png",
|
|
432
|
+
locationCanonicalDescription: "a dusty reading room",
|
|
433
|
+
descriptionOverride: "a flooded reading room at night",
|
|
434
|
+
},
|
|
435
|
+
],
|
|
436
|
+
referenceFormat: "hybrid",
|
|
437
|
+
})
|
|
438
|
+
expect(prompt).toContain("reference image A — a flooded reading room at night.")
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
it("adds one binding-subject line for a wired object", () => {
|
|
442
|
+
const { prompt } = buildImagePrompt({
|
|
443
|
+
provider: PROVIDER,
|
|
444
|
+
prompt: "A still life.",
|
|
445
|
+
connectedReferences: [
|
|
446
|
+
{
|
|
447
|
+
id: "o1",
|
|
448
|
+
defaultName: "Vase",
|
|
449
|
+
source: "wired-object",
|
|
450
|
+
url: "https://r2/vase.png",
|
|
451
|
+
descriptionOverride: "a cracked terracotta urn",
|
|
452
|
+
},
|
|
453
|
+
],
|
|
454
|
+
referenceFormat: "hybrid",
|
|
455
|
+
})
|
|
456
|
+
expect(prompt).toContain("reference image A — a cracked terracotta urn.")
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
it("fills the extras' own description clause instead of adding a line", () => {
|
|
460
|
+
const { prompt } = buildImagePrompt({
|
|
461
|
+
provider: PROVIDER,
|
|
462
|
+
prompt: "A still life.",
|
|
463
|
+
connectedReferences: [
|
|
464
|
+
{
|
|
465
|
+
id: "m1",
|
|
466
|
+
defaultName: "Vase",
|
|
467
|
+
source: "manual",
|
|
468
|
+
url: "https://r2/vase.png",
|
|
469
|
+
isExtraRef: true,
|
|
470
|
+
description: "a blue vase",
|
|
471
|
+
descriptionOverride: "a cracked terracotta urn",
|
|
472
|
+
},
|
|
473
|
+
],
|
|
474
|
+
referenceFormat: "hybrid",
|
|
475
|
+
})
|
|
476
|
+
expect(prompt).toBe("A still life.\na cracked terracotta urn (reference image A).")
|
|
477
|
+
expect(prompt).not.toContain("blue vase")
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
// References the hybrid format renders NO role phrase for: a `{image:N:label}`
|
|
481
|
+
// token expands the ref inline (and suppresses both canonical renders), and an
|
|
482
|
+
// unmentioned `wired-image` / `manual` ref has no canonical render at all. The
|
|
483
|
+
// per-use override still has to reach the model — same one-line grammar, bound
|
|
484
|
+
// to the seat the reference actually ships in.
|
|
485
|
+
const boat: ConnectedReference = {
|
|
486
|
+
id: "i1",
|
|
487
|
+
defaultName: "Boat",
|
|
488
|
+
source: "wired-image",
|
|
489
|
+
url: "https://r2/boat.png",
|
|
490
|
+
descriptionOverride: "a wrecked boat at dawn",
|
|
491
|
+
}
|
|
492
|
+
const vase: ConnectedReference = {
|
|
493
|
+
id: "o1",
|
|
494
|
+
defaultName: "Vase",
|
|
495
|
+
source: "wired-object",
|
|
496
|
+
url: "https://r2/vase.png",
|
|
497
|
+
descriptionOverride: "a cracked urn",
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
it("adds one binding-subject line for an object a {image:N} token expanded", () => {
|
|
501
|
+
const { prompt } = buildImagePrompt({
|
|
502
|
+
provider: PROVIDER,
|
|
503
|
+
prompt: "A still life with {image:1:vase}.",
|
|
504
|
+
connectedReferences: [vase],
|
|
505
|
+
referenceFormat: "hybrid",
|
|
506
|
+
})
|
|
507
|
+
expect(prompt).toBe(
|
|
508
|
+
"A still life with the vase from reference image A.\n"
|
|
509
|
+
+ "reference image A — a cracked urn.",
|
|
510
|
+
)
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it("adds one binding-subject line for a wired image a {image:N} token expanded", () => {
|
|
514
|
+
const { prompt } = buildImagePrompt({
|
|
515
|
+
provider: PROVIDER,
|
|
516
|
+
prompt: "{image:1:boat} drifts.",
|
|
517
|
+
connectedReferences: [boat],
|
|
518
|
+
referenceFormat: "hybrid",
|
|
519
|
+
})
|
|
520
|
+
expect(prompt).toBe(
|
|
521
|
+
"The boat from reference image A drifts.\n"
|
|
522
|
+
+ "reference image A — a wrecked boat at dawn.",
|
|
523
|
+
)
|
|
524
|
+
})
|
|
525
|
+
|
|
526
|
+
it("adds one binding-subject line for an unmentioned wired image", () => {
|
|
527
|
+
const { prompt } = buildImagePrompt({
|
|
528
|
+
provider: PROVIDER,
|
|
529
|
+
prompt: "A boat drifts.",
|
|
530
|
+
connectedReferences: [boat],
|
|
531
|
+
referenceFormat: "hybrid",
|
|
532
|
+
})
|
|
533
|
+
expect(prompt).toBe(
|
|
534
|
+
"A boat drifts.\nreference image A — a wrecked boat at dawn.",
|
|
535
|
+
)
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it("adds one binding-subject line for a manual reference that is not an extra", () => {
|
|
539
|
+
const { prompt } = buildImagePrompt({
|
|
540
|
+
provider: PROVIDER,
|
|
541
|
+
prompt: "A still life.",
|
|
542
|
+
connectedReferences: [
|
|
543
|
+
{
|
|
544
|
+
id: "m1",
|
|
545
|
+
defaultName: "Vase",
|
|
546
|
+
source: "manual",
|
|
547
|
+
url: "https://r2/vase.png",
|
|
548
|
+
description: "a blue vase",
|
|
549
|
+
descriptionOverride: "a cracked urn",
|
|
550
|
+
},
|
|
551
|
+
],
|
|
552
|
+
referenceFormat: "hybrid",
|
|
553
|
+
})
|
|
554
|
+
expect(prompt).toBe("A still life.\nreference image A — a cracked urn.")
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
it("leaves those same requests byte-identical without an override", () => {
|
|
558
|
+
const withoutOverride = (ref: ConnectedReference, text: string) =>
|
|
559
|
+
buildImagePrompt({
|
|
560
|
+
provider: PROVIDER,
|
|
561
|
+
prompt: text,
|
|
562
|
+
connectedReferences: [{ ...ref, descriptionOverride: undefined }],
|
|
563
|
+
referenceFormat: "hybrid",
|
|
564
|
+
}).prompt
|
|
565
|
+
expect(withoutOverride(vase, "A still life with {image:1:vase}.")).toBe(
|
|
566
|
+
"A still life with the vase from reference image A.",
|
|
567
|
+
)
|
|
568
|
+
expect(withoutOverride(boat, "{image:1:boat} drifts.")).toBe(
|
|
569
|
+
"The boat from reference image A drifts.",
|
|
570
|
+
)
|
|
571
|
+
expect(withoutOverride(boat, "A boat drifts.")).toBe("A boat drifts.")
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
it("tells a reference that is BOTH @-mentioned and token-covered once", () => {
|
|
575
|
+
const line = "reference image A — a wrecked boat at dawn."
|
|
576
|
+
const { prompt } = buildImagePrompt({
|
|
577
|
+
provider: PROVIDER,
|
|
578
|
+
prompt: "@boat:1 drifts past {image:1:boat}.",
|
|
579
|
+
connectedReferences: [boat],
|
|
580
|
+
referenceFormat: "hybrid",
|
|
581
|
+
})
|
|
582
|
+
expect(prompt.split(line)).toHaveLength(2)
|
|
583
|
+
})
|
|
584
|
+
|
|
585
|
+
it("tells a token-covered entity the @-mention already bound once", () => {
|
|
586
|
+
const line = "reference image A — a cracked urn."
|
|
587
|
+
const { prompt } = buildImagePrompt({
|
|
588
|
+
provider: PROVIDER,
|
|
589
|
+
prompt: "@vase:1 sits beside {image:1:vase}.",
|
|
590
|
+
connectedReferences: [vase],
|
|
591
|
+
referenceFormat: "hybrid",
|
|
592
|
+
})
|
|
593
|
+
expect(prompt.split(line)).toHaveLength(2)
|
|
594
|
+
})
|
|
595
|
+
})
|
|
@@ -26,6 +26,7 @@ const DELIBERATELY_GENERIC: ReadonlyMap<string, string> = new Map([
|
|
|
26
26
|
["minimax", "legacy 5s tier, no vendor guide"],
|
|
27
27
|
["seedance", "legacy Seedance 1.x, superseded"],
|
|
28
28
|
["ltx-2.3-fast", "no vendor prompt guide published"],
|
|
29
|
+
["ltx-2.3-pro", "no vendor prompt guide published"],
|
|
29
30
|
["sora2", "roster utility — no first-party guide via KIE"],
|
|
30
31
|
["sora2-pro", "roster utility — no first-party guide via KIE"],
|
|
31
32
|
])
|