@nodaro/prompts 1.38.0 → 1.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-LVSH7K7R.cjs → chunk-22KZOAGL.cjs} +81 -42
- package/dist/chunk-22KZOAGL.cjs.map +1 -0
- package/dist/chunk-2P26SZX5.cjs +414 -0
- package/dist/chunk-2P26SZX5.cjs.map +1 -0
- package/dist/{chunk-CVPB2N3Z.cjs → chunk-3INXNDV7.cjs} +102 -4
- package/dist/chunk-3INXNDV7.cjs.map +1 -0
- package/dist/{chunk-7XKHDXCL.cjs → chunk-3U7X6JZN.cjs} +13 -12
- package/dist/chunk-3U7X6JZN.cjs.map +1 -0
- package/dist/chunk-4GKTOVFM.cjs +8 -0
- package/dist/chunk-4GKTOVFM.cjs.map +1 -0
- package/dist/{chunk-HRBBNODV.js → chunk-6GVFJLDL.js} +4 -3
- package/dist/chunk-6GVFJLDL.js.map +1 -0
- package/dist/{chunk-NO6XHX4R.js → chunk-6MPZHFIS.js} +99 -6
- package/dist/chunk-6MPZHFIS.js.map +1 -0
- package/dist/{chunk-4FDNVNJE.js → chunk-7XMTVYS5.js} +3 -76
- package/dist/chunk-7XMTVYS5.js.map +1 -0
- package/dist/{chunk-QDEWBBUR.js → chunk-DUXX6CBX.js} +12 -143
- package/dist/chunk-DUXX6CBX.js.map +1 -0
- package/dist/chunk-EAYEZXWT.js +388 -0
- package/dist/chunk-EAYEZXWT.js.map +1 -0
- package/dist/chunk-GILUVQQJ.js +6 -0
- package/dist/chunk-GILUVQQJ.js.map +1 -0
- package/dist/{chunk-KIYNOOPJ.js → chunk-GTDKDD7Y.js} +12 -6
- package/dist/chunk-GTDKDD7Y.js.map +1 -0
- package/dist/{chunk-TLVX5PXD.cjs → chunk-IUCNJAHW.cjs} +13 -7
- package/dist/chunk-IUCNJAHW.cjs.map +1 -0
- package/dist/{chunk-ZFIXQRBZ.cjs → chunk-KUBZVWME.cjs} +2 -80
- package/dist/chunk-KUBZVWME.cjs.map +1 -0
- package/dist/{chunk-JKCR6WRZ.cjs → chunk-NMWKXAYA.cjs} +2 -2
- package/dist/{chunk-JKCR6WRZ.cjs.map → chunk-NMWKXAYA.cjs.map} +1 -1
- package/dist/{chunk-LK2CZK5U.cjs → chunk-PWWM7Q3T.cjs} +7 -7
- package/dist/{chunk-LK2CZK5U.cjs.map → chunk-PWWM7Q3T.cjs.map} +1 -1
- package/dist/{chunk-N7TOWXDJ.js → chunk-QDUTMOF4.js} +3 -3
- package/dist/{chunk-N7TOWXDJ.js.map → chunk-QDUTMOF4.js.map} +1 -1
- package/dist/{chunk-MENBHEAY.cjs → chunk-WRM52ZBN.cjs} +19 -161
- package/dist/chunk-WRM52ZBN.cjs.map +1 -0
- package/dist/chunk-XIWWDGT5.cjs +86 -0
- package/dist/chunk-XIWWDGT5.cjs.map +1 -0
- package/dist/{chunk-CEPVQQVO.js → chunk-XRLOMTBZ.js} +2 -2
- package/dist/{chunk-CEPVQQVO.js.map → chunk-XRLOMTBZ.js.map} +1 -1
- package/dist/{chunk-QBEZMR2F.js → chunk-XU54YJXQ.js} +54 -15
- package/dist/chunk-XU54YJXQ.js.map +1 -0
- package/dist/chunk-Z3SZXWRU.js +80 -0
- package/dist/chunk-Z3SZXWRU.js.map +1 -0
- package/dist/entries/assembly.cjs +26 -25
- package/dist/entries/assembly.d.cts +32 -4
- package/dist/entries/assembly.d.ts +32 -4
- package/dist/entries/assembly.js +5 -4
- package/dist/entries/doctrine.cjs +46 -46
- package/dist/entries/doctrine.d.cts +3 -3
- package/dist/entries/doctrine.d.ts +3 -3
- package/dist/entries/doctrine.js +1 -1
- package/dist/entries/picker-catalogs.cjs +42 -42
- package/dist/entries/picker-catalogs.js +3 -3
- package/dist/entries/presets.cjs +49 -26
- package/dist/entries/presets.d.cts +51 -1
- package/dist/entries/presets.d.ts +51 -1
- package/dist/entries/presets.js +4 -1
- package/dist/entries/prompt-builder.cjs +135 -98
- package/dist/entries/prompt-builder.d.cts +22 -11
- package/dist/entries/prompt-builder.d.ts +22 -11
- package/dist/entries/prompt-builder.js +4 -3
- package/dist/entries/prompt-fields.cjs +41 -40
- package/dist/entries/prompt-fields.js +2 -1
- package/dist/index.cjs +331 -273
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +11 -9
- package/dist/{subject-registry-BUswuEVD.d.cts → subject-registry-CQi_CZfJ.d.cts} +78 -9
- package/dist/{subject-registry-D7V8PEG-.d.ts → subject-registry-DFiGx49v.d.ts} +78 -9
- package/package.json +2 -2
- package/src/__tests__/assemble-image-input-style-references.test.ts +173 -0
- package/src/__tests__/catalog-id-guard.test.ts +30 -0
- package/src/__tests__/factory-preset-previews.test.ts +123 -0
- package/src/__tests__/factory-preset-references.test.ts +222 -0
- package/src/__tests__/user-direction-refs.test.ts +180 -0
- package/src/assemble-image-input.ts +114 -13
- package/src/catalog-id-guard.ts +18 -2
- package/src/color-names.ts +192 -0
- package/src/direction-registry.ts +66 -5
- package/src/entries/prompt-builder.ts +2 -0
- package/src/factory-presets/generate-image.ts +29 -0
- package/src/factory-presets/index.ts +2 -0
- package/src/factory-presets/preview-files.generated.ts +4 -0
- package/src/factory-presets/previews.ts +40 -0
- package/src/factory-presets/references.ts +80 -0
- package/src/factory-presets/shared-image.ts +4 -0
- package/src/factory-presets/types.ts +8 -0
- package/src/image-reference-doctrine.ts +3 -3
- package/src/image-token-pattern.ts +13 -0
- package/src/prompt-builder.ts +4 -5
- package/src/prompt-style-section.ts +3 -3
- package/src/user-direction-refs.ts +110 -0
- package/dist/chunk-4FDNVNJE.js.map +0 -1
- package/dist/chunk-7XKHDXCL.cjs.map +0 -1
- package/dist/chunk-CVPB2N3Z.cjs.map +0 -1
- package/dist/chunk-HRBBNODV.js.map +0 -1
- package/dist/chunk-KIYNOOPJ.js.map +0 -1
- package/dist/chunk-LVSH7K7R.cjs.map +0 -1
- package/dist/chunk-MENBHEAY.cjs.map +0 -1
- package/dist/chunk-NO6XHX4R.js.map +0 -1
- package/dist/chunk-QBEZMR2F.js.map +0 -1
- package/dist/chunk-QDEWBBUR.js.map +0 -1
- package/dist/chunk-TLVX5PXD.cjs.map +0 -1
- package/dist/chunk-UJKFHPZ7.js +0 -37
- package/dist/chunk-UJKFHPZ7.js.map +0 -1
- package/dist/chunk-W5SCZIOB.cjs +0 -43
- package/dist/chunk-W5SCZIOB.cjs.map +0 -1
- package/dist/chunk-ZFIXQRBZ.cjs.map +0 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-owned direction references: `direction.colorLook: "palette:<id>"` and
|
|
3
|
+
* `direction.style: "moodboard:<id>"`. The grammar, the wording of the
|
|
4
|
+
* resolved clauses, and how a resolved clause takes its row's place in the
|
|
5
|
+
* fold — without changing a single byte of a fold that carries none.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect } from "vitest"
|
|
8
|
+
import {
|
|
9
|
+
parseUserDirectionRef,
|
|
10
|
+
userDirectionRefValue,
|
|
11
|
+
renderPaletteClause,
|
|
12
|
+
renderMoodboardClause,
|
|
13
|
+
styleReferenceDescription,
|
|
14
|
+
} from "../user-direction-refs.js"
|
|
15
|
+
import { nearestColorName } from "../color-names.js"
|
|
16
|
+
import {
|
|
17
|
+
DIRECTION_FIELDS,
|
|
18
|
+
directionKeyForUserRef,
|
|
19
|
+
userRefKindOfDirectionKey,
|
|
20
|
+
renderDirectionHints,
|
|
21
|
+
renderDirectionHintClauses,
|
|
22
|
+
} from "../direction-registry.js"
|
|
23
|
+
import { partitionStyleClauses } from "../prompt-style-section.js"
|
|
24
|
+
import { getColorLookPromptHint } from "../color-look.js"
|
|
25
|
+
import { getStylePromptHint } from "../style.js"
|
|
26
|
+
|
|
27
|
+
const UUID = "0f9e8d7c-6b5a-4c3d-9e2f-1a2b3c4d5e6f"
|
|
28
|
+
const PALETTE = `palette:${UUID}`
|
|
29
|
+
const MOODBOARD = `moodboard:${UUID}`
|
|
30
|
+
const IMAGE = { surface: "image" as const }
|
|
31
|
+
|
|
32
|
+
describe("parseUserDirectionRef", () => {
|
|
33
|
+
it("reads a palette and a moodboard reference", () => {
|
|
34
|
+
expect(parseUserDirectionRef(PALETTE)).toEqual({ kind: "palette", id: UUID, wellFormed: true })
|
|
35
|
+
expect(parseUserDirectionRef(MOODBOARD)).toEqual({ kind: "moodboard", id: UUID, wellFormed: true })
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it("marks a prefix followed by anything but a uuid as malformed, not as a catalog id", () => {
|
|
39
|
+
expect(parseUserDirectionRef("palette:warm")).toEqual({ kind: "palette", id: "warm", wellFormed: false })
|
|
40
|
+
expect(parseUserDirectionRef("moodboard:")).toEqual({ kind: "moodboard", id: "", wellFormed: false })
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("is null for a catalog id, an empty string and a non-string", () => {
|
|
44
|
+
expect(parseUserDirectionRef("teal-orange")).toBeNull()
|
|
45
|
+
expect(parseUserDirectionRef("")).toBeNull()
|
|
46
|
+
expect(parseUserDirectionRef(undefined)).toBeNull()
|
|
47
|
+
expect(parseUserDirectionRef(["palette:x"])).toBeNull()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("builds the wire value back", () => {
|
|
51
|
+
expect(userDirectionRefValue("palette", UUID)).toBe(PALETTE)
|
|
52
|
+
expect(userDirectionRefValue("moodboard", UUID)).toBe(MOODBOARD)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
describe("which direction keys take a reference", () => {
|
|
57
|
+
it("colorLook takes a palette and style takes a moodboard", () => {
|
|
58
|
+
expect(directionKeyForUserRef("palette")).toBe("colorLook")
|
|
59
|
+
expect(directionKeyForUserRef("moodboard")).toBe("style")
|
|
60
|
+
expect(userRefKindOfDirectionKey("colorLook")).toBe("palette")
|
|
61
|
+
expect(userRefKindOfDirectionKey("style")).toBe("moodboard")
|
|
62
|
+
expect(userRefKindOfDirectionKey("mood")).toBeUndefined()
|
|
63
|
+
expect(userRefKindOfDirectionKey("not-a-key")).toBeUndefined()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("only single-pick rows take one, so a reference is never sliced away by a pick budget", () => {
|
|
67
|
+
const rows = DIRECTION_FIELDS.filter((f) => "userRef" in f)
|
|
68
|
+
expect(rows.map((f) => f.key).sort()).toEqual(["colorLook", "style"])
|
|
69
|
+
for (const row of rows) expect(row.maxPicks, row.key).toBe(1)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
describe("nearestColorName", () => {
|
|
74
|
+
it("names the primaries and the neutrals plainly", () => {
|
|
75
|
+
expect(nearestColorName("#000000")).toBe("black")
|
|
76
|
+
expect(nearestColorName("#FFFFFF")).toBe("white")
|
|
77
|
+
expect(nearestColorName("#FF0000")).toBe("red")
|
|
78
|
+
expect(nearestColorName("#0000FF")).toBe("blue")
|
|
79
|
+
expect(nearestColorName("#808080")).toBe("gray")
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("accepts lowercase hex and answers the same name", () => {
|
|
83
|
+
expect(nearestColorName("#ff0000")).toBe(nearestColorName("#FF0000"))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("answers a name for any valid colour and nothing for an invalid one", () => {
|
|
87
|
+
expect(nearestColorName("#0F4C5C").length).toBeGreaterThan(0)
|
|
88
|
+
expect(nearestColorName("#E3B23C").length).toBeGreaterThan(0)
|
|
89
|
+
expect(nearestColorName("teal")).toBe("")
|
|
90
|
+
expect(nearestColorName("#12345")).toBe("")
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it("puts a dark blue-green near teal, not near a warm colour", () => {
|
|
94
|
+
expect(nearestColorName("#0F4C5C")).toMatch(/teal|petrol|blue|green/)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe("renderPaletteClause", () => {
|
|
99
|
+
it("names every colour with its hex, in the palette's order", () => {
|
|
100
|
+
const clause = renderPaletteClause(["#0f4c5c", "#E3B23C"])
|
|
101
|
+
expect(clause).toBe(`color palette: ${nearestColorName("#0F4C5C")} #0F4C5C, ${nearestColorName("#E3B23C")} #E3B23C`)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it("skips an entry that is not a #RRGGBB colour, and renders nothing for none", () => {
|
|
105
|
+
expect(renderPaletteClause(["#FF0000", "red", "#00FF00"])).toBe("color palette: red #FF0000, " + `${nearestColorName("#00FF00")} #00FF00`)
|
|
106
|
+
expect(renderPaletteClause([])).toBe("")
|
|
107
|
+
expect(renderPaletteClause(["nope"])).toBe("")
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe("renderMoodboardClause", () => {
|
|
112
|
+
it("is the description, on one line, trimmed", () => {
|
|
113
|
+
expect(renderMoodboardClause(" Moody Nordic interiors,\n\nsoft daylight ")).toBe("Moody Nordic interiors, soft daylight")
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("cannot open a section of its own: newlines collapse", () => {
|
|
117
|
+
expect(renderMoodboardClause("calm\n\n[style]:\nloud")).not.toContain("\n")
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("renders nothing without a description", () => {
|
|
121
|
+
expect(renderMoodboardClause(null)).toBe("")
|
|
122
|
+
expect(renderMoodboardClause(undefined)).toBe("")
|
|
123
|
+
expect(renderMoodboardClause(" ")).toBe("")
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe("styleReferenceDescription", () => {
|
|
128
|
+
it("tells the model what to take from the first style reference, and keeps the rest short", () => {
|
|
129
|
+
expect(styleReferenceDescription(0)).toMatch(/style reference/)
|
|
130
|
+
expect(styleReferenceDescription(0)).toMatch(/not its subject/)
|
|
131
|
+
expect(styleReferenceDescription(1)).toBe("another style reference")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("carries the image's caption, on one line", () => {
|
|
135
|
+
expect(styleReferenceDescription(1, " linen\nand oak ")).toBe("another style reference (linen and oak)")
|
|
136
|
+
expect(styleReferenceDescription(0, "soft daylight")).toContain("(soft daylight)")
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe("the fold with resolved references", () => {
|
|
141
|
+
const refs = new Map([
|
|
142
|
+
[PALETTE, "color palette: deep teal #0F4C5C"],
|
|
143
|
+
[MOODBOARD, "Moody Nordic interiors"],
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
it("renders a resolved palette in the colorLook row and a moodboard in the style row", () => {
|
|
147
|
+
expect(renderDirectionHints({ colorLook: PALETTE, style: MOODBOARD }, { ...IMAGE, refs })).toEqual([
|
|
148
|
+
"color palette: deep teal #0F4C5C",
|
|
149
|
+
"Moody Nordic interiors",
|
|
150
|
+
])
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it("keeps the table's fold order around a resolved clause", () => {
|
|
154
|
+
const hints = renderDirectionHints({ style: MOODBOARD, colorLook: "warm", era: "1970s" }, { ...IMAGE, refs })
|
|
155
|
+
expect(hints[0]).toBe(getColorLookPromptHint("warm"))
|
|
156
|
+
expect(hints[1]).toBe("Moody Nordic interiors")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("lands both on the film line of the [style] section", () => {
|
|
160
|
+
const slots = partitionStyleClauses({ colorLook: PALETTE, style: MOODBOARD }, { ...IMAGE, refs })
|
|
161
|
+
expect(slots.map((c) => c.slot)).toEqual(["film", "film"])
|
|
162
|
+
const clauses = renderDirectionHintClauses({ colorLook: PALETTE }, { ...IMAGE, refs })
|
|
163
|
+
expect(clauses[0]).toMatchObject({ key: "colorLook", family: "look", styleGroup: "film" })
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it("renders nothing for a reference it was not handed, like an unknown catalog id", () => {
|
|
167
|
+
expect(renderDirectionHints({ colorLook: PALETTE }, IMAGE)).toEqual([])
|
|
168
|
+
expect(renderDirectionHints({ colorLook: PALETTE }, { ...IMAGE, refs: new Map() })).toEqual([])
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("renders nothing for a reference on a row that does not take that kind", () => {
|
|
172
|
+
expect(renderDirectionHints({ style: PALETTE, colorLook: MOODBOARD, mood: PALETTE }, { ...IMAGE, refs })).toEqual([])
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it("leaves a fold with no reference byte-identical, refs or not", () => {
|
|
176
|
+
const direction = { colorLook: "warm", style: "anime", mood: ["happy", "joyful"] }
|
|
177
|
+
expect(renderDirectionHints(direction, { ...IMAGE, refs })).toEqual(renderDirectionHints(direction, IMAGE))
|
|
178
|
+
expect(renderDirectionHints({ style: "anime" }, { ...IMAGE, refs })).toEqual([getStylePromptHint("anime")])
|
|
179
|
+
})
|
|
180
|
+
})
|
|
@@ -60,6 +60,8 @@ import {
|
|
|
60
60
|
type SlottedPromptClause,
|
|
61
61
|
} from "./prompt-style-section.js"
|
|
62
62
|
import { keepableDirectionHints } from "./hint-shedding.js"
|
|
63
|
+
import { styleReferenceDescription, type StyleReferenceInput } from "./user-direction-refs.js"
|
|
64
|
+
import { imageReferenceLimit } from "@nodaro/shared/models"
|
|
63
65
|
import type { CharacterDef, ConnectedReference, DescribedReference, IdentityMeta } from "@nodaro/shared/types"
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -112,6 +114,23 @@ export interface AssembleImageInput {
|
|
|
112
114
|
* is a no-op for it and the result is byte-identical to today).
|
|
113
115
|
*/
|
|
114
116
|
direction?: DirectionFields
|
|
117
|
+
/**
|
|
118
|
+
* Rendered clauses for the caller's OWN saved resources named in `direction`
|
|
119
|
+
* (`colorLook: "palette:<id>"`, `style: "moodboard:<id>"`), keyed by the
|
|
120
|
+
* direction value. Resolving them is a database read, so the platform does it
|
|
121
|
+
* BEFORE this pure assembly and hands the clauses in; each one folds in its
|
|
122
|
+
* row's slot exactly like a catalog clause (the `[style]` film line), sheds
|
|
123
|
+
* like one, and a reference missing from the map folds nothing.
|
|
124
|
+
*/
|
|
125
|
+
directionRefs?: ReadonlyMap<string, string>
|
|
126
|
+
/**
|
|
127
|
+
* Style reference images (a moodboard's images). They only FILL the
|
|
128
|
+
* reference seats the caller's own references leave free — after them, never
|
|
129
|
+
* displacing, renumbering or evicting one — up to the model's reference
|
|
130
|
+
* limit; the rest are not sent. `styleReferencesSent` on the result says how
|
|
131
|
+
* many went. Each carries a short style line (`styleReferenceDescription`).
|
|
132
|
+
*/
|
|
133
|
+
styleReferences?: readonly StyleReferenceInput[]
|
|
115
134
|
/**
|
|
116
135
|
* Flat subject ids (Person / Styling / props) → folded into the prompt AHEAD
|
|
117
136
|
* of the direction clauses: the subject is the noun phrase the cinematography
|
|
@@ -216,6 +235,7 @@ function renderImageHintPieces(
|
|
|
216
235
|
subject: SubjectFields | undefined,
|
|
217
236
|
direction: DirectionFields | undefined,
|
|
218
237
|
structured: StructuredPromptFields | undefined,
|
|
238
|
+
directionRefs: ReadonlyMap<string, string> | undefined,
|
|
219
239
|
): ImageHintPieces {
|
|
220
240
|
return {
|
|
221
241
|
hintClauses: [
|
|
@@ -228,6 +248,7 @@ function renderImageHintPieces(
|
|
|
228
248
|
...partitionStyleClauses(direction, {
|
|
229
249
|
surface: "image",
|
|
230
250
|
mode: IMAGE_HINT_MODE_DEFAULT,
|
|
251
|
+
...(directionRefs !== undefined ? { refs: directionRefs } : {}),
|
|
231
252
|
}),
|
|
232
253
|
].filter((c) => c.text.length > 0),
|
|
233
254
|
structuredFragment: structured ? renderStructuredFields(structured) : "",
|
|
@@ -275,7 +296,9 @@ function composePromptText(
|
|
|
275
296
|
* Order: (1) compose the prompt text (no-op when no subject/direction/structured),
|
|
276
297
|
* (2) `buildImagePrompt(...)` — exactly the call the three sites make today,
|
|
277
298
|
* (3) shed hint clauses and re-assemble while the provider cap overflows,
|
|
278
|
-
* (4)
|
|
299
|
+
* (4) with `styleReferences`, fill the free reference seats with them
|
|
300
|
+
* (`fillStyleReferences`; absent = this step does not run at all),
|
|
301
|
+
* (5) optional post-assembly empty-prompt throw (gated by `throwOnEmpty`).
|
|
279
302
|
*
|
|
280
303
|
* TRUNCATION ORDERING (step 3): `buildImagePrompt`'s cap clamp cuts the TAIL,
|
|
281
304
|
* which is ORDER-BLIND — on a low-cap provider (seedream = 3000) a maximal
|
|
@@ -293,8 +316,96 @@ function composePromptText(
|
|
|
293
316
|
*/
|
|
294
317
|
export function assembleImageInput(
|
|
295
318
|
input: AssembleImageInput,
|
|
296
|
-
):
|
|
297
|
-
const
|
|
319
|
+
): AssembleImageResult {
|
|
320
|
+
const style = input.styleReferences
|
|
321
|
+
const result = style !== undefined && style.length > 0
|
|
322
|
+
? fillStyleReferences(input, style)
|
|
323
|
+
: assembleOnce(input)
|
|
324
|
+
|
|
325
|
+
// Post-assembly empty-prompt check (opt-in): a bound entity / `@`-mention /
|
|
326
|
+
// subject or direction chip — or a style reference's line — could have
|
|
327
|
+
// filled the assembled prompt even if the user typed nothing, so only reject
|
|
328
|
+
// when the FINAL prompt is truly empty.
|
|
329
|
+
if (input.throwOnEmpty && !result.prompt.trim()) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
"No prompt — type one, bind a character, or pick a cinematography direction",
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return result
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** `assembleImageInput`'s result: the builder's, plus the style-reference count. */
|
|
339
|
+
export interface AssembleImageResult extends BuildImagePromptResult {
|
|
340
|
+
/**
|
|
341
|
+
* How many of `styleReferences` were sent (they follow the caller's own
|
|
342
|
+
* references at the end of `referenceImageUrls`). Present only when
|
|
343
|
+
* `styleReferences` were given, so every other result is unchanged.
|
|
344
|
+
*/
|
|
345
|
+
styleReferencesSent?: number
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Fill the reference seats the caller's own references leave free with style
|
|
350
|
+
* references, without touching the caller's own.
|
|
351
|
+
*
|
|
352
|
+
* PASS 1 assembles the caller's input alone: its `referenceImageUrls` are the
|
|
353
|
+
* references the caller's request really sends (after the per-provider cap,
|
|
354
|
+
* mention resolution and dedupe). The free seats are the model's limit minus
|
|
355
|
+
* those. PASS 2 appends k style references as manual extra references — the
|
|
356
|
+
* builder's own mechanism, so they get its numbered/lettered lines in either
|
|
357
|
+
* reference format — for the LARGEST k that leaves the caller's references
|
|
358
|
+
* exactly where pass 1 put them, followed by the k style images. Why a check
|
|
359
|
+
* and not arithmetic: the builder caps the RAW reference list with an
|
|
360
|
+
* attach-priority slice (non-variants first), so extra entries can push out a
|
|
361
|
+
* character variant the prompt mentions even when seats looked free. Assembly
|
|
362
|
+
* is pure and k ≤ 12, so trying each k downward is cheap and exact.
|
|
363
|
+
*/
|
|
364
|
+
function fillStyleReferences(
|
|
365
|
+
input: AssembleImageInput,
|
|
366
|
+
style: readonly StyleReferenceInput[],
|
|
367
|
+
): AssembleImageResult {
|
|
368
|
+
const base = assembleOnce(input)
|
|
369
|
+
const own = base.referenceImageUrls ?? []
|
|
370
|
+
const seats = imageReferenceLimit(input.provider) - own.length
|
|
371
|
+
const seen = new Set(own)
|
|
372
|
+
const candidates: StyleReferenceInput[] = []
|
|
373
|
+
for (const s of style) {
|
|
374
|
+
if (typeof s.url !== "string" || s.url.length === 0 || seen.has(s.url)) continue
|
|
375
|
+
seen.add(s.url)
|
|
376
|
+
candidates.push(s)
|
|
377
|
+
}
|
|
378
|
+
for (let k = Math.min(seats, candidates.length); k > 0; k--) {
|
|
379
|
+
const extras = candidates.slice(0, k).map(styleConnectedReference)
|
|
380
|
+
const filled = assembleOnce({
|
|
381
|
+
...input,
|
|
382
|
+
connectedReferences: [...(input.connectedReferences ?? []), ...extras],
|
|
383
|
+
})
|
|
384
|
+
const urls = filled.referenceImageUrls ?? []
|
|
385
|
+
const keepsOwn = urls.length === own.length + k
|
|
386
|
+
&& own.every((url, i) => urls[i] === url)
|
|
387
|
+
&& extras.every((ref, j) => urls[own.length + j] === ref.url)
|
|
388
|
+
if (keepsOwn) return { ...filled, styleReferencesSent: k }
|
|
389
|
+
}
|
|
390
|
+
return { ...base, styleReferencesSent: 0 }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/** A style reference as the builder's manual extra reference. */
|
|
394
|
+
function styleConnectedReference(s: StyleReferenceInput, index: number): ConnectedReference {
|
|
395
|
+
return {
|
|
396
|
+
id: `style-reference-${index + 1}`,
|
|
397
|
+
defaultName: `Style reference ${index + 1}`,
|
|
398
|
+
source: "manual",
|
|
399
|
+
url: s.url,
|
|
400
|
+
isExtraRef: true,
|
|
401
|
+
defaultRole: "style",
|
|
402
|
+
description: styleReferenceDescription(index, s.caption),
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/** One assembly of `input` as given (no style-reference fill, no empty check). */
|
|
407
|
+
function assembleOnce(input: AssembleImageInput): BuildImagePromptResult {
|
|
408
|
+
const pieces = renderImageHintPieces(input.subject, input.direction, input.structured, input.directionRefs)
|
|
298
409
|
|
|
299
410
|
const assembleWith = (keptHintClauses: number) => buildImagePromptWithOverflow({
|
|
300
411
|
prompt: composePromptText(input.userPrompt, pieces, keptHintClauses),
|
|
@@ -359,15 +470,5 @@ export function assembleImageInput(
|
|
|
359
470
|
}
|
|
360
471
|
// `overflowChars` is assembly bookkeeping, not part of the callers' contract.
|
|
361
472
|
const { overflowChars, ...result } = fitted
|
|
362
|
-
|
|
363
|
-
// Post-assembly empty-prompt check (opt-in): a bound entity / `@`-mention /
|
|
364
|
-
// subject or direction chip could have filled the assembled prompt even if the user
|
|
365
|
-
// typed nothing — so only reject when the FINAL prompt is truly empty.
|
|
366
|
-
if (input.throwOnEmpty && !result.prompt.trim()) {
|
|
367
|
-
throw new Error(
|
|
368
|
-
"No prompt — type one, bind a character, or pick a cinematography direction",
|
|
369
|
-
)
|
|
370
|
-
}
|
|
371
|
-
|
|
372
473
|
return result
|
|
373
474
|
}
|
package/src/catalog-id-guard.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { PICKER_CATALOGS, getPickerCatalog } from "./picker-catalogs.js"
|
|
2
2
|
import { getRegisteredCatalogPacks, catalogPacksVersion } from "./catalog-packs.js"
|
|
3
|
+
import { userRefKindOfDirectionKey } from "./direction-registry.js"
|
|
4
|
+
import { parseUserDirectionRef } from "./user-direction-refs.js"
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* THE WALL: refuse work that names a catalog id this deployment does not
|
|
@@ -113,18 +115,32 @@ export function catalogGuardActive(): boolean {
|
|
|
113
115
|
return getRegisteredCatalogPacks().length > 0
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
/**
|
|
119
|
+
* A `direction` value that names the caller's OWN saved resource on the key
|
|
120
|
+
* that takes that kind (`colorLook: "palette:<id>"`, `style: "moodboard:<id>"`)
|
|
121
|
+
* is not a catalog id, so curation has nothing to say about it — the platform
|
|
122
|
+
* resolves it against the caller's rows instead. On any other key, and in a
|
|
123
|
+
* parameter node's own fields, the same value is still an unknown id.
|
|
124
|
+
*/
|
|
125
|
+
function isOwnUserRef(field: string, id: string): boolean {
|
|
126
|
+
const ref = parseUserDirectionRef(id)
|
|
127
|
+
return ref !== null && userRefKindOfDirectionKey(field) === ref.kind
|
|
128
|
+
}
|
|
129
|
+
|
|
116
130
|
function checkRecord(
|
|
117
131
|
nodeType: string,
|
|
118
132
|
nodeId: string | undefined,
|
|
119
133
|
data: Record<string, unknown>,
|
|
120
134
|
fields: ReadonlyMap<string, string>,
|
|
121
135
|
out: ForeignCatalogId[],
|
|
136
|
+
skip?: (field: string, id: string) => boolean,
|
|
122
137
|
): void {
|
|
123
138
|
for (const [field, catalogId] of fields) {
|
|
124
139
|
const raw = data[field]
|
|
125
140
|
if (raw === undefined || raw === null) continue
|
|
126
141
|
const offered = composedIds(catalogId)
|
|
127
142
|
for (const id of values(raw)) {
|
|
143
|
+
if (skip?.(field, id)) continue
|
|
128
144
|
if (!offered.has(id)) out.push({ nodeId, nodeType, field, id, catalogId })
|
|
129
145
|
}
|
|
130
146
|
}
|
|
@@ -152,7 +168,7 @@ export function findForeignCatalogIds(
|
|
|
152
168
|
for (const key of ["direction", "subject"] as const) {
|
|
153
169
|
const rec = data[key]
|
|
154
170
|
if (rec && typeof rec === "object" && !Array.isArray(rec)) {
|
|
155
|
-
checkRecord(type, nodeId, rec as Record<string, unknown>, table.byField, out)
|
|
171
|
+
checkRecord(type, nodeId, rec as Record<string, unknown>, table.byField, out, key === "direction" ? isOwnUserRef : undefined)
|
|
156
172
|
}
|
|
157
173
|
}
|
|
158
174
|
}
|
|
@@ -170,7 +186,7 @@ export function findForeignCatalogIdsInBody(
|
|
|
170
186
|
for (const key of ["direction", "subject"] as const) {
|
|
171
187
|
const rec = body[key]
|
|
172
188
|
if (rec && typeof rec === "object" && !Array.isArray(rec)) {
|
|
173
|
-
checkRecord(nodeType, undefined, rec as Record<string, unknown>, table.byField, out)
|
|
189
|
+
checkRecord(nodeType, undefined, rec as Record<string, unknown>, table.byField, out, key === "direction" ? isOwnUserRef : undefined)
|
|
174
190
|
}
|
|
175
191
|
}
|
|
176
192
|
return out
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain colour names for a hex value — what a saved colour palette reads as
|
|
3
|
+
* in a prompt ("deep teal #0F4C5C"). A model follows a named colour more
|
|
4
|
+
* reliably than a bare hex code, and the hex keeps the exact value.
|
|
5
|
+
*
|
|
6
|
+
* NEAREST NAME, NOT A CLASSIFIER: a fixed table of everyday colour names, each
|
|
7
|
+
* with a representative value, and the one closest to the input in CIE Lab
|
|
8
|
+
* (ΔE 1976 — Euclidean distance in Lab, which tracks perceived difference far
|
|
9
|
+
* better than RGB). Deterministic, dependency-free, and a name is always one a
|
|
10
|
+
* person would say. Ties go to the earlier row.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Everyday colour names and a representative sRGB value for each. */
|
|
14
|
+
const NAMED: ReadonlyArray<readonly [name: string, hex: string]> = [
|
|
15
|
+
// Neutrals
|
|
16
|
+
["black", "#000000"],
|
|
17
|
+
["charcoal", "#36454F"],
|
|
18
|
+
["dark gray", "#555555"],
|
|
19
|
+
["slate gray", "#708090"],
|
|
20
|
+
["gray", "#808080"],
|
|
21
|
+
["warm gray", "#9A9088"],
|
|
22
|
+
["silver", "#C0C0C0"],
|
|
23
|
+
["light gray", "#D3D3D3"],
|
|
24
|
+
["off-white", "#F2F0EB"],
|
|
25
|
+
["white", "#FFFFFF"],
|
|
26
|
+
["ivory", "#FFFFF0"],
|
|
27
|
+
["cream", "#FFFDD0"],
|
|
28
|
+
["eggshell", "#F0EAD6"],
|
|
29
|
+
["linen", "#FAF0E6"],
|
|
30
|
+
["champagne", "#F7E7CE"],
|
|
31
|
+
["wheat", "#F5DEB3"],
|
|
32
|
+
// Earth tones and browns
|
|
33
|
+
["sand", "#C2B280"],
|
|
34
|
+
["tan", "#D2B48C"],
|
|
35
|
+
["khaki", "#C3B091"],
|
|
36
|
+
["taupe", "#8B8589"],
|
|
37
|
+
["camel", "#C19A6B"],
|
|
38
|
+
["caramel", "#C68E17"],
|
|
39
|
+
["brown", "#964B00"],
|
|
40
|
+
["chocolate", "#7B3F00"],
|
|
41
|
+
["coffee", "#6F4E37"],
|
|
42
|
+
["espresso", "#4E312D"],
|
|
43
|
+
["walnut", "#773F1A"],
|
|
44
|
+
["chestnut", "#954535"],
|
|
45
|
+
["mahogany", "#C04000"],
|
|
46
|
+
["rust", "#B7410E"],
|
|
47
|
+
["terracotta", "#E2725B"],
|
|
48
|
+
["copper", "#B87333"],
|
|
49
|
+
["bronze", "#CD7F32"],
|
|
50
|
+
["ochre", "#CC7722"],
|
|
51
|
+
// Oranges
|
|
52
|
+
["burnt orange", "#CC5500"],
|
|
53
|
+
["orange", "#FFA500"],
|
|
54
|
+
["tangerine", "#F28500"],
|
|
55
|
+
["apricot", "#FBCEB1"],
|
|
56
|
+
["peach", "#FFCBA4"],
|
|
57
|
+
["coral", "#FF7F50"],
|
|
58
|
+
["salmon", "#FA8072"],
|
|
59
|
+
// Reds
|
|
60
|
+
["red", "#FF0000"],
|
|
61
|
+
["scarlet", "#FF2400"],
|
|
62
|
+
["crimson", "#DC143C"],
|
|
63
|
+
["cherry red", "#D2042D"],
|
|
64
|
+
["brick red", "#CB4154"],
|
|
65
|
+
["burgundy", "#800020"],
|
|
66
|
+
["maroon", "#800000"],
|
|
67
|
+
["wine red", "#722F37"],
|
|
68
|
+
["oxblood", "#4A0000"],
|
|
69
|
+
// Pinks
|
|
70
|
+
["raspberry", "#E30B5C"],
|
|
71
|
+
["rose", "#FF007F"],
|
|
72
|
+
["hot pink", "#FF69B4"],
|
|
73
|
+
["pink", "#FFC0CB"],
|
|
74
|
+
["blush pink", "#E8B4B8"],
|
|
75
|
+
["dusty rose", "#C08081"],
|
|
76
|
+
["mauve", "#B784A7"],
|
|
77
|
+
// Purples
|
|
78
|
+
["magenta", "#FF00FF"],
|
|
79
|
+
["orchid", "#DA70D6"],
|
|
80
|
+
["plum", "#8E4585"],
|
|
81
|
+
["eggplant", "#614051"],
|
|
82
|
+
["dusty purple", "#6E5A7E"],
|
|
83
|
+
["lilac", "#C8A2C8"],
|
|
84
|
+
["lavender", "#B57EDC"],
|
|
85
|
+
["pale lavender", "#E6E6FA"],
|
|
86
|
+
["amethyst", "#9966CC"],
|
|
87
|
+
["violet", "#8F00FF"],
|
|
88
|
+
["purple", "#800080"],
|
|
89
|
+
["indigo", "#4B0082"],
|
|
90
|
+
// Blues
|
|
91
|
+
["navy blue", "#000080"],
|
|
92
|
+
["midnight blue", "#191970"],
|
|
93
|
+
["prussian blue", "#003153"],
|
|
94
|
+
["dusky blue", "#4B5E7D"],
|
|
95
|
+
["royal blue", "#4169E1"],
|
|
96
|
+
["cobalt blue", "#0047AB"],
|
|
97
|
+
["blue", "#0000FF"],
|
|
98
|
+
["cornflower blue", "#6495ED"],
|
|
99
|
+
["denim blue", "#1560BD"],
|
|
100
|
+
["steel blue", "#4682B4"],
|
|
101
|
+
["slate blue", "#6A5ACD"],
|
|
102
|
+
["periwinkle", "#CCCCFF"],
|
|
103
|
+
["sky blue", "#87CEEB"],
|
|
104
|
+
["baby blue", "#89CFF0"],
|
|
105
|
+
["powder blue", "#B0E0E6"],
|
|
106
|
+
["ice blue", "#D6ECEF"],
|
|
107
|
+
["azure", "#007FFF"],
|
|
108
|
+
["cerulean", "#007BA7"],
|
|
109
|
+
["petrol blue", "#005F6A"],
|
|
110
|
+
// Cyans and teals
|
|
111
|
+
["cyan", "#00FFFF"],
|
|
112
|
+
["turquoise", "#40E0D0"],
|
|
113
|
+
["aquamarine", "#7FFFD4"],
|
|
114
|
+
["teal", "#008080"],
|
|
115
|
+
["dark teal", "#014D4E"],
|
|
116
|
+
["dark slate gray", "#2F4F4F"],
|
|
117
|
+
// Greens
|
|
118
|
+
["sea green", "#2E8B57"],
|
|
119
|
+
["emerald green", "#50C878"],
|
|
120
|
+
["jade green", "#00A86B"],
|
|
121
|
+
["mint green", "#98FF98"],
|
|
122
|
+
["seafoam green", "#93E9BE"],
|
|
123
|
+
["sage green", "#9CAF88"],
|
|
124
|
+
["olive green", "#708238"],
|
|
125
|
+
["olive", "#808000"],
|
|
126
|
+
["moss green", "#8A9A5B"],
|
|
127
|
+
["forest green", "#228B22"],
|
|
128
|
+
["hunter green", "#355E3B"],
|
|
129
|
+
["bottle green", "#006A4E"],
|
|
130
|
+
["dark green", "#013220"],
|
|
131
|
+
["green", "#008000"],
|
|
132
|
+
["kelly green", "#4CBB17"],
|
|
133
|
+
["lime green", "#32CD32"],
|
|
134
|
+
["chartreuse", "#7FFF00"],
|
|
135
|
+
["pistachio", "#93C572"],
|
|
136
|
+
// Yellows and golds
|
|
137
|
+
["yellow", "#FFFF00"],
|
|
138
|
+
["lemon yellow", "#FFF44F"],
|
|
139
|
+
["pale yellow", "#FDFD96"],
|
|
140
|
+
["canary yellow", "#FFEF00"],
|
|
141
|
+
["mustard yellow", "#E1AD01"],
|
|
142
|
+
["gold", "#FFD700"],
|
|
143
|
+
["amber", "#FFBF00"],
|
|
144
|
+
["honey", "#F0A830"],
|
|
145
|
+
["saffron", "#F4C430"],
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
const HEX_RE = /^#[0-9a-f]{6}$/i
|
|
149
|
+
|
|
150
|
+
type Lab = readonly [l: number, a: number, b: number]
|
|
151
|
+
|
|
152
|
+
function channel(v: number): number {
|
|
153
|
+
const c = v / 255
|
|
154
|
+
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function labPivot(t: number): number {
|
|
158
|
+
return t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** sRGB hex → CIE Lab (D65). Only called with a value that passed `HEX_RE`. */
|
|
162
|
+
function toLab(hex: string): Lab {
|
|
163
|
+
const r = channel(parseInt(hex.slice(1, 3), 16))
|
|
164
|
+
const g = channel(parseInt(hex.slice(3, 5), 16))
|
|
165
|
+
const b = channel(parseInt(hex.slice(5, 7), 16))
|
|
166
|
+
const x = labPivot((0.4124564 * r + 0.3575761 * g + 0.1804375 * b) / 0.95047)
|
|
167
|
+
const y = labPivot(0.2126729 * r + 0.7151522 * g + 0.072175 * b)
|
|
168
|
+
const z = labPivot((0.0193339 * r + 0.119192 * g + 0.9503041 * b) / 1.08883)
|
|
169
|
+
return [116 * y - 16, 500 * (x - y), 200 * (y - z)]
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let namedLab: ReadonlyArray<readonly [string, Lab]> | null = null
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The everyday name nearest to `hex` (`#RRGGBB`, any case), or `""` when the
|
|
176
|
+
* value is not a `#RRGGBB` colour.
|
|
177
|
+
*/
|
|
178
|
+
export function nearestColorName(hex: string): string {
|
|
179
|
+
if (!HEX_RE.test(hex)) return ""
|
|
180
|
+
namedLab ??= NAMED.map(([name, value]) => [name, toLab(value)] as const)
|
|
181
|
+
const [l, a, b] = toLab(hex)
|
|
182
|
+
let best = ""
|
|
183
|
+
let bestDistance = Infinity
|
|
184
|
+
for (const [name, [nl, na, nb]] of namedLab) {
|
|
185
|
+
const d = (l - nl) ** 2 + (a - na) ** 2 + (b - nb) ** 2
|
|
186
|
+
if (d < bestDistance) {
|
|
187
|
+
bestDistance = d
|
|
188
|
+
best = name
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return best
|
|
192
|
+
}
|