@nodaro/prompts 1.11.0 → 1.12.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 +383 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +516 -28
- package/dist/index.d.ts +516 -28
- package/dist/index.js +368 -51
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/__snapshots__/entity-convergence-image.test.ts.snap +19 -0
- package/src/__tests__/animal-getters-parity.test.ts +82 -0
- package/src/__tests__/assemble-image-input-cap.test.ts +212 -0
- package/src/__tests__/assemble-video-input-cap.test.ts +356 -0
- package/src/__tests__/entity-convergence-image.test.ts +374 -0
- package/src/__tests__/location-convergence-image.test.ts +29 -1
- package/src/__tests__/location-default-role-image.test.ts +166 -0
- package/src/__tests__/mention-splice-spacing.test.ts +257 -0
- package/src/__tests__/read-node-subject.test.ts +140 -0
- package/src/__tests__/subject-fold.test.ts +232 -0
- package/src/__tests__/subject-registry.test.ts +312 -0
- package/src/assemble-image-input.ts +133 -30
- package/src/assemble-video-input.ts +173 -18
- package/src/direction-registry.ts +18 -1
- package/src/hint-shedding.ts +68 -0
- package/src/index.ts +2 -0
- package/src/parameter-prompt-hint.ts +8 -7
- package/src/picker-catalogs.ts +14 -7
- package/src/prompt-builder.ts +544 -61
- package/src/read-node-direction.ts +60 -1
- package/src/subject-registry.ts +464 -0
|
@@ -17,14 +17,26 @@
|
|
|
17
17
|
* bug this channel exists to fix. The image side is structurally identical
|
|
18
18
|
* (`assembleImageInput` = `composePromptText` → `buildImagePrompt`).
|
|
19
19
|
*
|
|
20
|
+
* That ordering is also why cap-aware shedding here takes a `frame` callback
|
|
21
|
+
* rather than a provider id: the shed must run at the FOLD site (before the
|
|
22
|
+
* resolver) but be decided on the RESOLVED length (after it), so the binding
|
|
23
|
+
* text the resolver adds is inside the budget and can never be the thing that
|
|
24
|
+
* gets dropped. See {@link VideoPromptCapOptions}. Both catalog channels —
|
|
25
|
+
* SUBJECT and direction — fold into the one sheddable list that budget walks.
|
|
26
|
+
*
|
|
20
27
|
* THE VERBOSITY POLICY LIVES HERE, NOT IN THE CLIENT: motion dimensions render
|
|
21
28
|
* their compact professional term, look dimensions their full clause
|
|
22
|
-
* (`VIDEO_HINT_MODE_DEFAULT`, resolved per row's `family` by the registry).
|
|
29
|
+
* (`VIDEO_HINT_MODE_DEFAULT`, resolved per row's `family` by the registry). The
|
|
30
|
+
* SUBJECT fold has its own policy — compact on video
|
|
31
|
+
* (`SUBJECT_VIDEO_HINT_MODE_DEFAULT`), because a fully specified person at full
|
|
32
|
+
* verbosity is ~30 paragraph clauses and the start frame already carries the
|
|
33
|
+
* subject's identity into the clip.
|
|
23
34
|
* It is a threaded PARAMETER with a pure default — never deployment state:
|
|
24
35
|
* `__tests__/content-free-contract.test.ts` hard-fails any environment read
|
|
25
36
|
* under `packages/prompts/src`, and this module has nothing to read anyway.
|
|
26
37
|
*
|
|
27
|
-
* EXACT NO-OP CONTRACT: with no direction and no structured fields
|
|
38
|
+
* EXACT NO-OP CONTRACT: with no subject, no direction and no structured fields
|
|
39
|
+
* the caller's
|
|
28
40
|
* `userPrompt` comes back VERBATIM AND UNTRIMMED — `undefined` included, since
|
|
29
41
|
* a video prompt is optional on the route. That is what keeps every existing
|
|
30
42
|
* caller byte-identical (the "backward-compatible: no connectedReferences →
|
|
@@ -33,8 +45,9 @@
|
|
|
33
45
|
* `__tests__/assemble-video-input.test.ts`).
|
|
34
46
|
*
|
|
35
47
|
* WHAT IS DELIBERATELY NOT HERE: the dimension table, the fold order, the
|
|
36
|
-
* dedupe and the surface filter all live in `direction-registry.ts`
|
|
37
|
-
*
|
|
48
|
+
* dedupe and the surface filter all live in `direction-registry.ts` (and
|
|
49
|
+
* `subject-registry.ts` for the subject channel) — ONE renderer per channel
|
|
50
|
+
* serves both surfaces, so the image and video folds cannot drift.
|
|
38
51
|
* Clients render their "will inject into prompt" preview by importing
|
|
39
52
|
* `renderDirectionHints` + `joinPromptHints` directly.
|
|
40
53
|
*/
|
|
@@ -44,19 +57,112 @@ import {
|
|
|
44
57
|
type DirectionFields,
|
|
45
58
|
type DirectionHintMode,
|
|
46
59
|
} from "./direction-registry.js"
|
|
60
|
+
import {
|
|
61
|
+
renderSubjectHints,
|
|
62
|
+
SUBJECT_VIDEO_HINT_MODE_DEFAULT,
|
|
63
|
+
type SubjectFields,
|
|
64
|
+
type SubjectHintMode,
|
|
65
|
+
} from "./subject-registry.js"
|
|
47
66
|
import { joinPromptHints } from "./prompt-hint-join.js"
|
|
67
|
+
import { keepableDirectionHints } from "./hint-shedding.js"
|
|
48
68
|
import {
|
|
49
69
|
renderStructuredFields,
|
|
50
70
|
type StructuredPromptFields,
|
|
51
71
|
} from "./prompt-builder-structured-fields.js"
|
|
52
72
|
|
|
53
73
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
74
|
+
* Cap-aware shedding, opt-in. Absent → the composer is exactly what it always
|
|
75
|
+
* was (every existing caller stays byte-identical, and the no-op path below is
|
|
76
|
+
* never even reached differently).
|
|
77
|
+
*
|
|
78
|
+
* WHY A NUMBER AND A CALLBACK, NOT A PROVIDER ID — the two halves of the video
|
|
79
|
+
* surface's problem, which the image half did not have:
|
|
80
|
+
*
|
|
81
|
+
* - `cap` is the caller's EFFECTIVE ceiling, not `getMaxVideoPromptChars` read
|
|
82
|
+
* here. The routes compute it with `effectiveVideoPromptCeiling`, which
|
|
83
|
+
* mirrors `applyVideoNegativePrompt`'s reservation of the `"\nAvoid: …"`
|
|
84
|
+
* suffix for a provider with no native negative param. Re-deriving the cap
|
|
85
|
+
* inside this package would put a second copy of that reservation one
|
|
86
|
+
* refactor away from drifting from the clamp it is supposed to predict.
|
|
87
|
+
*
|
|
88
|
+
* - `frame` is the REFERENCE RESOLVER, and it is what makes the shed correct
|
|
89
|
+
* end-to-end. The fold runs BEFORE `resolveVideoReferenceCore` (see the
|
|
90
|
+
* module header — folding afterwards strands the scene description past the
|
|
91
|
+
* identity directives). The resolver then ADDS binding text: legacy's
|
|
92
|
+
* "Use these characters:" block, hybrid's lock lines and the canonical role
|
|
93
|
+
* phrases it APPENDS. That added text is exactly what an order-blind tail cut
|
|
94
|
+
* destroys first, so it must be inside the budget — but it must never be
|
|
95
|
+
* shed. Measuring THROUGH the caller's framing gives both properties at once:
|
|
96
|
+
* the shed decision sees the final length, while the only thing it can drop
|
|
97
|
+
* is a hint clause it rendered itself.
|
|
98
|
+
*
|
|
99
|
+
* Re-framing a SUBSET of the hints is sound because a hint can never change how
|
|
100
|
+
* the resolver reads the rest of the body: no registered catalog hint, term or
|
|
101
|
+
* label contains a `{image:N}` / `{ref:` / `@slug:N` shape
|
|
102
|
+
* (`__tests__/direction-hint-token-safety.test.ts` pins that for every catalog),
|
|
103
|
+
* so dropping one cannot renumber or unbind a reference.
|
|
104
|
+
*/
|
|
105
|
+
export interface VideoPromptCapOptions {
|
|
106
|
+
/**
|
|
107
|
+
* The maximum length the FRAMED prompt may reach. Sheds only while the framed
|
|
108
|
+
* body exceeds it; `undefined` (the default) disables shedding entirely.
|
|
109
|
+
*/
|
|
110
|
+
readonly cap?: number
|
|
111
|
+
/**
|
|
112
|
+
* The downstream framing the cap is measured through — the caller's reference
|
|
113
|
+
* assembly. Identity when omitted (a caller with a cap but no references).
|
|
114
|
+
* Must be PURE: it is called once per shed iteration, and the caller re-runs
|
|
115
|
+
* its own real assembly on the returned body afterwards.
|
|
116
|
+
*/
|
|
117
|
+
readonly frame?: (body: string | undefined) => string | undefined
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Fold a video run's subject and cinematic-direction ids (and optional
|
|
122
|
+
* structured fields) into its prompt body.
|
|
56
123
|
*
|
|
57
|
-
* The
|
|
58
|
-
*
|
|
59
|
-
*
|
|
124
|
+
* The SUBJECT hints land first (who is in the shot — the noun phrase the
|
|
125
|
+
* cinematography modifies), then the direction hints in the registry's
|
|
126
|
+
* canonical table order (camera motion leads), and the structured fragment
|
|
127
|
+
* lands LAST — the same ordering `composePromptText` uses for stills.
|
|
128
|
+
*
|
|
129
|
+
* `subject` rides `opts` rather than a fourth positional parameter on purpose:
|
|
130
|
+
* every existing caller passes `(prompt, direction)` or
|
|
131
|
+
* `(prompt, direction, structured)` positionally, and a new positional would
|
|
132
|
+
* have made the two levers' order a memorization test.
|
|
133
|
+
*
|
|
134
|
+
* TRUNCATION ORDERING (opt-in via `opts.cap`): the provider clamp
|
|
135
|
+
* (`applyVideoNegativePrompt`) slices the prompt TAIL, which is ORDER-BLIND —
|
|
136
|
+
* on a low-cap provider (kling = 1000) a broad direction renders more than the
|
|
137
|
+
* whole ceiling and the cut severs reference bindings and the end of the user's
|
|
138
|
+
* prose while decorative clauses survive. With a cap the composer decides
|
|
139
|
+
* instead: it knows which clauses are hints because it just rendered them, and
|
|
140
|
+
* drops them LAST-FOLDED FIRST until the framed prompt fits. Everything else —
|
|
141
|
+
* the user's prose, the structured fragment (user CONTENT, never a garnish) and
|
|
142
|
+
* every byte the resolver's framing adds — outranks a hint.
|
|
143
|
+
*
|
|
144
|
+
* SUBJECT CLAUSES ARE SHED CANDIDATES TOO, and they shed AFTER the direction
|
|
145
|
+
* clauses. Both folds are catalog decoration of the same class — ids the
|
|
146
|
+
* platform rendered into wording — so exempting one would just move the
|
|
147
|
+
* overflow into the order-blind clamp, which is the bug this machinery exists
|
|
148
|
+
* to prevent. They ride the SAME `hintClauses` list the shed already walks
|
|
149
|
+
* (subject first, direction second, tail-first shedding), so there is exactly
|
|
150
|
+
* one shed arithmetic (`hint-shedding.ts`) across both channels and both
|
|
151
|
+
* surfaces. Neither channel ever sheds before the prose, the references or the
|
|
152
|
+
* structured fragment.
|
|
153
|
+
*
|
|
154
|
+
* WHAT THE BUDGET DELIBERATELY EXCLUDES: the route's later opt-in identity
|
|
155
|
+
* injection (an async DB read that appends a canonical description) and any
|
|
156
|
+
* registered `applyPromptPolicies` transform both run AFTER the reference
|
|
157
|
+
* assembly and are not modelled here. Pricing them in would mean folding an
|
|
158
|
+
* await into this pure composer; instead the provider clamp stays their last
|
|
159
|
+
* resort, exactly as today. Same for a body that still overflows with ZERO
|
|
160
|
+
* hints left — long prose, or many bound references on their own.
|
|
161
|
+
*
|
|
162
|
+
* UNDER-CAP PARITY: the first pass folds every hint, so a prompt that fits is
|
|
163
|
+
* byte-identical to a capless call, and a caller with no
|
|
164
|
+
* `subject`/`direction`/`structured` takes the same exact no-op path it always
|
|
165
|
+
* did.
|
|
60
166
|
*
|
|
61
167
|
* @param userPrompt The user's prompt. Optional: an image-to-video run may
|
|
62
168
|
* legitimately have none, and it is returned as-is when nothing folds.
|
|
@@ -65,25 +171,74 @@ import {
|
|
|
65
171
|
* nothing — never a throw.
|
|
66
172
|
* @param structured Path-1 structured fields. Not a `/v1/generate-video` wire
|
|
67
173
|
* field today; the canvas orchestrator passes it directly.
|
|
68
|
-
* @param opts.hintMode Override the verbosity policy (a whole-fold
|
|
174
|
+
* @param opts.hintMode Override the direction verbosity policy (a whole-fold
|
|
69
175
|
* `PickerHintMode`, or a `{ look, motion }` split).
|
|
176
|
+
* @param opts.subject Flat subject ids (Person / Styling / props), same
|
|
177
|
+
* inertness contract as `direction`.
|
|
178
|
+
* @param opts.subjectHintMode Override the subject verbosity policy.
|
|
179
|
+
* @param opts.cap / `opts.frame` See {@link VideoPromptCapOptions}.
|
|
70
180
|
*/
|
|
71
181
|
export function composeVideoPromptText(
|
|
72
182
|
userPrompt: string | undefined,
|
|
73
183
|
direction: DirectionFields | undefined,
|
|
74
184
|
structured?: StructuredPromptFields,
|
|
75
|
-
opts?: {
|
|
185
|
+
opts?: {
|
|
186
|
+
readonly hintMode?: DirectionHintMode
|
|
187
|
+
readonly subject?: SubjectFields
|
|
188
|
+
readonly subjectHintMode?: SubjectHintMode
|
|
189
|
+
} & VideoPromptCapOptions,
|
|
76
190
|
): string | undefined {
|
|
77
|
-
|
|
191
|
+
// ONE sheddable list, subject FIRST then direction — because the shed walks it
|
|
192
|
+
// from the TAIL, so this order IS the survival order: a direction clause
|
|
193
|
+
// leaves before a subject clause. Deliberate, and the same order the image
|
|
194
|
+
// side uses (`renderImageHintPieces`): the subject is the noun phrase the
|
|
195
|
+
// cinematography modifies, so losing "who is in the shot" to keep a
|
|
196
|
+
// decorative grade would be the wrong trade. With no `subject` the list IS
|
|
197
|
+
// the direction fold, so every pre-subject caller is byte-identical.
|
|
198
|
+
const hintClauses = [
|
|
199
|
+
...renderSubjectHints(opts?.subject, {
|
|
200
|
+
surface: "video",
|
|
201
|
+
mode: opts?.subjectHintMode ?? SUBJECT_VIDEO_HINT_MODE_DEFAULT,
|
|
202
|
+
}),
|
|
78
203
|
...renderDirectionHints(direction, {
|
|
79
204
|
surface: "video",
|
|
80
205
|
mode: opts?.hintMode ?? VIDEO_HINT_MODE_DEFAULT,
|
|
81
206
|
}),
|
|
82
|
-
structured ? renderStructuredFields(structured) : "",
|
|
83
207
|
].filter((p) => p.length > 0)
|
|
84
|
-
//
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
208
|
+
// User CONTENT, not a garnish: never sheddable, always last.
|
|
209
|
+
const structuredFragment = structured ? renderStructuredFields(structured) : ""
|
|
210
|
+
|
|
211
|
+
const composeWith = (kept: number): string | undefined => {
|
|
212
|
+
const hints = [...hintClauses.slice(0, kept), structuredFragment].filter(
|
|
213
|
+
(p) => p.length > 0,
|
|
214
|
+
)
|
|
215
|
+
// Nothing to fold → the caller's value straight back, `undefined` included.
|
|
216
|
+
// Do NOT collapse this into `joinPromptHints(userPrompt ?? "", hints)`: that
|
|
217
|
+
// would turn an absent prompt into `""` and break the no-op contract above.
|
|
218
|
+
// A FULL shed lands here too, which is what keeps the no-op contract intact
|
|
219
|
+
// at `kept === 0` — the route's `composed !== prompt` guard then correctly
|
|
220
|
+
// leaves `input_data.userPrompt` unpinned.
|
|
221
|
+
if (hints.length === 0) return userPrompt
|
|
222
|
+
return joinPromptHints(userPrompt ?? "", hints)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const cap = opts?.cap
|
|
226
|
+
if (cap === undefined) return composeWith(hintClauses.length)
|
|
227
|
+
|
|
228
|
+
// Fold everything first (the under-cap byte-parity pass), then shed from the
|
|
229
|
+
// tail of the fold order while the FRAMED prompt overflows the ceiling.
|
|
230
|
+
// `keepableDirectionHints` — the ONE shed arithmetic, shared with the image
|
|
231
|
+
// assembler — strictly decreases `kept` whenever there is a deficit, so this
|
|
232
|
+
// terminates at `kept === 0` in the worst case, at which point nothing
|
|
233
|
+
// droppable is left and the provider clamp stands.
|
|
234
|
+
const frame = opts?.frame ?? ((body: string | undefined) => body)
|
|
235
|
+
let kept = hintClauses.length
|
|
236
|
+
let body = composeWith(kept)
|
|
237
|
+
let framedLength = frame(body)?.length ?? 0
|
|
238
|
+
while (framedLength > cap && kept > 0) {
|
|
239
|
+
kept = keepableDirectionHints(hintClauses, kept, framedLength - cap)
|
|
240
|
+
body = composeWith(kept)
|
|
241
|
+
framedLength = frame(body)?.length ?? 0
|
|
242
|
+
}
|
|
243
|
+
return body
|
|
89
244
|
}
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
* token, not a bare id. A single-id channel cannot carry it.
|
|
33
33
|
* - Subject / Styling / prop dimensions (`animal`, `heldProp`, `material`,
|
|
34
34
|
* Person, Styling) — a separate `subject` channel, deliberately out of scope
|
|
35
|
-
* here.
|
|
35
|
+
* here. It now exists: `subject-registry.ts`, same table-driven shape, its
|
|
36
|
+
* key set DISJOINT from this one (pinned by a test) so nothing folds twice.
|
|
36
37
|
*
|
|
37
38
|
* PACK BLINDNESS (parity, not a regression): `get*PromptHint` reads the frozen
|
|
38
39
|
* base arrays, so ids added by a deployment-registered catalog pack resolve to
|
|
@@ -158,6 +159,22 @@ const temporal = perId(getTemporalPromptHint, getTemporalTerm)
|
|
|
158
159
|
* so they are NOT aliases of `shotSize` / `lightingStyle`, and an alias table
|
|
159
160
|
* would wrongly suppress a legal second selection. Overlap is handled instead
|
|
160
161
|
* by the exact-string dedupe in `renderDirectionHints`.
|
|
162
|
+
*
|
|
163
|
+
* SECOND MEANING OF POSITION: BOTH cap-aware assemblers — `assembleImageInput`
|
|
164
|
+
* (stills) and `composeVideoPromptText` (video) — shed hint clauses from the
|
|
165
|
+
* TAIL of this order when a provider's prompt cap overflows, through the one
|
|
166
|
+
* shared arithmetic in `hint-shedding.ts`. So a row's position is also its
|
|
167
|
+
* survival order under the cap on EVERY surface: reordering rows for one
|
|
168
|
+
* surface silently changes what the other drops first, and the row a
|
|
169
|
+
* video-surface reorder would most likely touch (`cameraMotion`) leads the
|
|
170
|
+
* fold. That is a consequence of reusing the fold order, not a ranking — this
|
|
171
|
+
* table stays a compatibility order; anything that needs a real importance
|
|
172
|
+
* ranking should add an explicit priority column rather than reorder these rows.
|
|
173
|
+
*
|
|
174
|
+
* WHERE THIS TABLE SITS IN THE COMBINED ORDER: both assemblers fold the SUBJECT
|
|
175
|
+
* channel (`subject-registry.ts`) BEFORE this one and shed the combined list
|
|
176
|
+
* tail-first, so every direction row here is dropped before any subject clause.
|
|
177
|
+
* Deliberate — see `hint-shedding.ts` for the argument.
|
|
161
178
|
*/
|
|
162
179
|
export const DIRECTION_FIELDS = [
|
|
163
180
|
{ key: "cameraMotion", surface: "video", family: "motion", maxPicks: 1, render: perId(getCameraMotionPromptHint, getCameraMotionTerm) },
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shed arithmetic shared by the image (`assembleImageInput`) and video
|
|
3
|
+
* (`composeVideoPromptText`) cap-aware assemblers, so the two surfaces cannot
|
|
4
|
+
* drift in WHICH clause goes first when a provider's prompt cap overflows.
|
|
5
|
+
*
|
|
6
|
+
* Only the arithmetic lives here. Each surface keeps its own loop, because what
|
|
7
|
+
* they MEASURE differs: the image side reads `buildImagePrompt`'s
|
|
8
|
+
* `overflowChars` (the cap clamp reports how much it cut), while the video side
|
|
9
|
+
* measures the resolver-FRAMED body against the route's effective ceiling. Both
|
|
10
|
+
* hand this function the same question — "how many of the first `kept` clauses
|
|
11
|
+
* may stay if `deficit` characters have to leave the body?" — and both re-assemble
|
|
12
|
+
* and re-check afterwards.
|
|
13
|
+
*
|
|
14
|
+
* WHAT COUNTS AS A SHEDDABLE CLAUSE (both surfaces, one answer): every clause
|
|
15
|
+
* the platform RENDERED from catalog ids — the SUBJECT fold and the cinematic
|
|
16
|
+
* DIRECTION fold alike. They are decoration of the same class, so exempting
|
|
17
|
+
* either would not save it: the overflow would simply land in the provider's
|
|
18
|
+
* order-blind tail clamp, severing reference bindings or the end of the user's
|
|
19
|
+
* prose instead — precisely the bug this machinery exists to prevent. Never
|
|
20
|
+
* sheddable: the user's prose, the bound references and the framing text the
|
|
21
|
+
* reference resolver adds, and the structured fragment (user CONTENT).
|
|
22
|
+
*/
|
|
23
|
+
import { PROMPT_HINT_SEPARATOR } from "./prompt-hint-join.js"
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* How many of the first `kept` hint clauses may STAY if `deficit`
|
|
27
|
+
* characters have to leave the body. Walks the fold order from the TAIL,
|
|
28
|
+
* subtracting each clause plus the separator it brought, and stops as soon as
|
|
29
|
+
* enough has been reclaimed.
|
|
30
|
+
*
|
|
31
|
+
* The name is historical (direction was the first and for a while the only
|
|
32
|
+
* channel); the list both callers pass is now the COMBINED fold —
|
|
33
|
+
* `[...subject, ...direction]` on both surfaces — so the shed order is that
|
|
34
|
+
* combined order REVERSED: the direction block empties first, then the subject
|
|
35
|
+
* block. Deliberate, and the reason the two folds share one list: a fully
|
|
36
|
+
* specified person renders ~30 clauses, so a subject fold left unsheddable
|
|
37
|
+
* would be the single biggest way to push an overflow into the order-blind
|
|
38
|
+
* clamp, while a decorative grade or ISO value survives.
|
|
39
|
+
*
|
|
40
|
+
* Within the direction block the order is `DIRECTION_FIELDS` order REVERSED
|
|
41
|
+
* (and within the subject block, `SUBJECT_FIELDS` reversed). Note what that
|
|
42
|
+
* is and is not: each table's order is a COMPATIBILITY order (grouped by family,
|
|
43
|
+
* with the legacy `DirectionFields` block pinned last so every pre-registry
|
|
44
|
+
* caller's fold stays byte-identical) — it is NOT a ranking of how load-bearing
|
|
45
|
+
* a dimension is, and this function does not claim one. Tail-first is chosen
|
|
46
|
+
* because it is deterministic, matches the fold order the API documents, and
|
|
47
|
+
* needs no second ordering to drift out of sync with the table. A caller mixing
|
|
48
|
+
* legacy keys with the newer ones can therefore lose e.g. `lightingId` before a
|
|
49
|
+
* decorative `isoValue` clause; if that ever matters, the fix is an explicit
|
|
50
|
+
* priority column on `DIRECTION_FIELDS`, not a second hand-kept list here.
|
|
51
|
+
*
|
|
52
|
+
* Deliberately approximate (assembly is not perfectly additive); the caller
|
|
53
|
+
* re-assembles and re-checks, and this function strictly decreases `kept`
|
|
54
|
+
* whenever `deficit > 0`, so that loop terminates.
|
|
55
|
+
*/
|
|
56
|
+
export function keepableDirectionHints(
|
|
57
|
+
hintClauses: readonly string[],
|
|
58
|
+
kept: number,
|
|
59
|
+
deficit: number,
|
|
60
|
+
): number {
|
|
61
|
+
let remaining = deficit
|
|
62
|
+
let next = kept
|
|
63
|
+
while (next > 0 && remaining > 0) {
|
|
64
|
+
next -= 1
|
|
65
|
+
remaining -= hintClauses[next]!.length + PROMPT_HINT_SEPARATOR.length
|
|
66
|
+
}
|
|
67
|
+
return next
|
|
68
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -19,7 +19,9 @@ export * from "./brand-tokens.js"
|
|
|
19
19
|
export * from "./prompt-builder.js"
|
|
20
20
|
export * from "./prompt-builder-structured-fields.js"
|
|
21
21
|
export * from "./direction-registry.js"
|
|
22
|
+
export * from "./subject-registry.js"
|
|
22
23
|
export * from "./prompt-hint-join.js"
|
|
24
|
+
export * from "./hint-shedding.js"
|
|
23
25
|
export * from "./video-reference-resolver.js"
|
|
24
26
|
export * from "./sound-aggregator.js"
|
|
25
27
|
export * from "./assemble-suno-input.js"
|
|
@@ -38,7 +38,7 @@ import { composeCameraMotionHintFromConnections } from "./camera-motions.js"
|
|
|
38
38
|
import { composeTransitionHintFromConnections, type TransitionDuration, type TransitionIntensity, type TransitionPosition, type TransitionTiming } from "./transitions.js"
|
|
39
39
|
import { composeCharacterFxHintFromConnections, type CharacterFxDuration, type CharacterFxIntensity, type CharacterFxPosition, type CharacterFxTiming } from "./character-fx.js"
|
|
40
40
|
import { buildMaterialHints } from "./materials.js"
|
|
41
|
-
import {
|
|
41
|
+
import { getAnimalPromptHint, getAnimalTerm } from "@nodaro/shared"
|
|
42
42
|
import { getVehicle } from "@nodaro/shared"
|
|
43
43
|
import { getWeapon } from "@nodaro/shared"
|
|
44
44
|
import { getFurniture } from "@nodaro/shared"
|
|
@@ -322,15 +322,16 @@ function resolveBaseHint(
|
|
|
322
322
|
return withCustomText(data, byMode(mode, getLoopSubjectPromptHint, getLoopSubjectTerm)(asStr(data.loopSubject)))
|
|
323
323
|
case "material":
|
|
324
324
|
return withCustomText(data, buildMaterialHints(data.material, mode))
|
|
325
|
-
|
|
326
|
-
|
|
325
|
+
// Animal is the one Object-entity catalog whose phrasing has a single
|
|
326
|
+
// owner: `@nodaro/shared`'s `getAnimalPromptHint` / `getAnimalTerm`, which
|
|
327
|
+
// the picker-catalog funnel calls too. Both getters already return "" on a
|
|
328
|
+
// miss, so the entry lookup and the `animal ? … : ""` guard are the
|
|
329
|
+
// getters' job now, not this switch's.
|
|
330
|
+
case "animal":
|
|
327
331
|
return withCustomText(
|
|
328
332
|
data,
|
|
329
|
-
animal
|
|
330
|
-
? byMode(mode, `featuring a ${animal.label.toLowerCase()}, ${animal.description}`, objectEntityTerm(animal))
|
|
331
|
-
: "",
|
|
333
|
+
byMode(mode, getAnimalPromptHint, getAnimalTerm)(asStr(data.animal)),
|
|
332
334
|
)
|
|
333
|
-
}
|
|
334
335
|
case "vehicle": {
|
|
335
336
|
const vehicle = getVehicle(asStr(data.vehicle))
|
|
336
337
|
return withCustomText(
|
package/src/picker-catalogs.ts
CHANGED
|
@@ -69,7 +69,7 @@ import {
|
|
|
69
69
|
} from "./character-fx.js"
|
|
70
70
|
import { POSES, POSE_CATEGORY_LABELS, POSE_CATEGORY_ORDER } from "./pose.js"
|
|
71
71
|
import { MATERIALS, MATERIAL_CATEGORY_LABELS, MATERIAL_CATEGORY_ORDER } from "./materials.js"
|
|
72
|
-
import { ANIMALS, ANIMAL_SUBCATEGORY_LABELS, ANIMAL_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
72
|
+
import { ANIMALS, ANIMAL_SUBCATEGORY_LABELS, ANIMAL_SUBCATEGORY_ORDER, getAnimalPromptHint } from "@nodaro/shared"
|
|
73
73
|
import { VEHICLES, VEHICLE_SUBCATEGORY_LABELS, VEHICLE_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
74
74
|
import { WEAPONS, WEAPON_SUBCATEGORY_LABELS, WEAPON_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
75
75
|
import { FURNITURE, FURNITURE_SUBCATEGORY_LABELS, FURNITURE_SUBCATEGORY_ORDER } from "@nodaro/shared"
|
|
@@ -193,6 +193,13 @@ function toOptions<T extends BaseCatalogEntry>(
|
|
|
193
193
|
* label + description. Reproduce the exact phrasing from
|
|
194
194
|
* `getParameterPromptHint` so the registry stays faithful + every option's
|
|
195
195
|
* `promptHint` is non-empty.
|
|
196
|
+
*
|
|
197
|
+
* ANIMALS is the exception, and the direction of travel for the other three:
|
|
198
|
+
* its phrasing now has ONE owner, `@nodaro/shared`'s `getAnimalPromptHint`,
|
|
199
|
+
* which this funnel and `getParameterPromptHint` both call instead of
|
|
200
|
+
* re-authoring the sentence. `phrase` therefore takes the ENTRY (not
|
|
201
|
+
* label+description), so a catalog whose phrasing has moved to a getter can be
|
|
202
|
+
* pointed at it.
|
|
196
203
|
*/
|
|
197
204
|
interface ObjectCatalogEntry {
|
|
198
205
|
readonly id: string
|
|
@@ -204,14 +211,14 @@ interface ObjectCatalogEntry {
|
|
|
204
211
|
}
|
|
205
212
|
function objectOptions(
|
|
206
213
|
arr: ReadonlyArray<ObjectCatalogEntry>,
|
|
207
|
-
phrase: (
|
|
214
|
+
phrase: (entry: ObjectCatalogEntry) => string,
|
|
208
215
|
): ReadonlyArray<PickerOption> {
|
|
209
216
|
return arr.map((e) => ({
|
|
210
217
|
id: e.id,
|
|
211
218
|
label: e.label,
|
|
212
219
|
description: e.description,
|
|
213
220
|
category: e.subcategory,
|
|
214
|
-
promptHint: phrase(e
|
|
221
|
+
promptHint: phrase(e),
|
|
215
222
|
// Object entities have no `promptHint` field of their own (it is
|
|
216
223
|
// synthesized above), so the term cannot come from `resolveTerm`'s
|
|
217
224
|
// empty-hint rule: an authored `term` wins, and otherwise the label IS the
|
|
@@ -608,7 +615,7 @@ const SINGLE_CATALOGS: readonly PickerCatalog[] = [
|
|
|
608
615
|
defaultValue: "dog-golden-retriever",
|
|
609
616
|
categoryOrder: ANIMAL_SUBCATEGORY_ORDER,
|
|
610
617
|
categoryLabels: ANIMAL_SUBCATEGORY_LABELS,
|
|
611
|
-
options: objectOptions(ANIMALS, (
|
|
618
|
+
options: objectOptions(ANIMALS, (e) => getAnimalPromptHint(e.id)),
|
|
612
619
|
},
|
|
613
620
|
{
|
|
614
621
|
nodeType: "vehicle",
|
|
@@ -619,7 +626,7 @@ const SINGLE_CATALOGS: readonly PickerCatalog[] = [
|
|
|
619
626
|
defaultValue: "sedan",
|
|
620
627
|
categoryOrder: VEHICLE_SUBCATEGORY_ORDER,
|
|
621
628
|
categoryLabels: VEHICLE_SUBCATEGORY_LABELS,
|
|
622
|
-
options: objectOptions(VEHICLES, (
|
|
629
|
+
options: objectOptions(VEHICLES, (e) => `featuring a ${e.label.toLowerCase()}, ${e.description}`),
|
|
623
630
|
},
|
|
624
631
|
{
|
|
625
632
|
nodeType: "weapon",
|
|
@@ -630,7 +637,7 @@ const SINGLE_CATALOGS: readonly PickerCatalog[] = [
|
|
|
630
637
|
defaultValue: "katana",
|
|
631
638
|
categoryOrder: WEAPON_SUBCATEGORY_ORDER,
|
|
632
639
|
categoryLabels: WEAPON_SUBCATEGORY_LABELS,
|
|
633
|
-
options: objectOptions(WEAPONS, (
|
|
640
|
+
options: objectOptions(WEAPONS, (e) => `with a ${e.label.toLowerCase()}, ${e.description}`),
|
|
634
641
|
},
|
|
635
642
|
{
|
|
636
643
|
nodeType: "furniture",
|
|
@@ -641,7 +648,7 @@ const SINGLE_CATALOGS: readonly PickerCatalog[] = [
|
|
|
641
648
|
defaultValue: "sofa",
|
|
642
649
|
categoryOrder: FURNITURE_SUBCATEGORY_ORDER,
|
|
643
650
|
categoryLabels: FURNITURE_SUBCATEGORY_LABELS,
|
|
644
|
-
options: objectOptions(FURNITURE, (
|
|
651
|
+
options: objectOptions(FURNITURE, (e) => `including a ${e.label.toLowerCase()}, ${e.description}`),
|
|
645
652
|
},
|
|
646
653
|
{
|
|
647
654
|
nodeType: "held-prop",
|