@nodaro/prompts 1.20.0 → 1.21.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 +8181 -7229
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +120 -61
- package/dist/index.d.ts +120 -61
- package/dist/index.js +8082 -7133
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adult-only-ratchet.test.ts +9 -1
- package/src/__tests__/character-motion-audit.test.ts +77 -0
- package/src/__tests__/character-motion-exit-visibility.test.ts +183 -0
- package/src/__tests__/character-motion-partner-referent.test.ts +225 -0
- package/src/__tests__/character-motion.test.ts +11 -4
- package/src/__tests__/fixtures/parameter-hint-golden.json +3 -3
- package/src/__tests__/frame-delivery.test.ts +93 -0
- package/src/__tests__/graph-composed-unwired-identity.test.ts +166 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +9 -3
- package/src/character-motion/animals-pets.ts +112 -85
- package/src/character-motion/athletic-stunts.ts +155 -214
- package/src/character-motion/camera-interaction.ts +92 -133
- package/src/character-motion/combat-weapons.ts +143 -202
- package/src/character-motion/dance.ts +121 -169
- package/src/character-motion/entrances-exits.ts +88 -109
- package/src/character-motion/evasive-falls.ts +88 -130
- package/src/character-motion/everyday-actions.ts +233 -274
- package/src/character-motion/face-expression.ts +125 -178
- package/src/character-motion/gestures.ts +211 -304
- package/src/character-motion/head-gestures.ts +66 -94
- package/src/character-motion/idle-ambient.ts +85 -124
- package/src/character-motion/posture-shifts.ts +116 -139
- package/src/character-motion/runway.ts +73 -97
- package/src/character-motion/stage-performance.ts +92 -127
- package/src/character-motion/turns-looks.ts +86 -112
- package/src/character-motion/two-person.ts +241 -229
- package/src/character-motion/types.ts +4 -1
- package/src/character-motion/unnatural-horror.ts +53 -91
- package/src/character-motion/vehicles-mounts.ts +101 -97
- package/src/character-motion/walks-runs.ts +93 -121
- package/src/character-motion-diagnostics.ts +49 -0
- package/src/character-motion.ts +89 -13
- package/src/frame-delivery.ts +106 -0
- package/src/index.ts +4 -0
- package/src/parameter-prompt-hint.ts +23 -17
- package/src/picker-catalogs.ts +10 -2
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HOW A START/END FRAME TRAVELS — one plan, read by both ends.
|
|
3
|
+
*
|
|
4
|
+
* A frame can reach a model two ways: as a real frame, or as a reference image
|
|
5
|
+
* bound in prose as the opening (or closing) frame. Which one is right is
|
|
6
|
+
* measured per model — the Seedance 2.0 family crop-zooms a frame 2% and drifts
|
|
7
|
+
* 11-26% darker within six frames, but holds its look from frame 0 when the same
|
|
8
|
+
* image rides as a reference; every other model measured reproduces the opening
|
|
9
|
+
* frame better as a frame (see `resolveFrameDelivery` in @nodaro/shared).
|
|
10
|
+
*
|
|
11
|
+
* The composition — where the frames sit in the reference list, which sentence
|
|
12
|
+
* binds them, when the switch is refused — lives HERE rather than at the
|
|
13
|
+
* dispatch site, because two places need the same answer:
|
|
14
|
+
*
|
|
15
|
+
* - `backend/src/lib/video-frame-dispatch.ts` builds the actual request,
|
|
16
|
+
* - the editor's config panel tells the user which mode their node will run in.
|
|
17
|
+
*
|
|
18
|
+
* When those two disagree the panel lies, which is worse than having no panel
|
|
19
|
+
* text at all. One function, two callers, no drift.
|
|
20
|
+
*/
|
|
21
|
+
import { VIDEO_REF_LIMITS_BY_PROVIDER, resolveFrameDelivery, type FrameDelivery } from "@nodaro/shared"
|
|
22
|
+
import { REF_BINDING } from "./ref-binding.js"
|
|
23
|
+
import { promptBindsFirstFrame } from "./seedance-2-inputs.js"
|
|
24
|
+
|
|
25
|
+
export interface FrameDeliveryPlanArgs {
|
|
26
|
+
readonly provider: string | undefined
|
|
27
|
+
/** The node's / caller's choice. `auto` (or absent) resolves per model. */
|
|
28
|
+
readonly requested?: FrameDelivery
|
|
29
|
+
/** Whether the model accepts reference images at all. */
|
|
30
|
+
readonly supportsReferenceImages: boolean
|
|
31
|
+
readonly startFrameUrl?: string
|
|
32
|
+
readonly endFrameUrl?: string
|
|
33
|
+
/** The user's OWN reference images, in their existing order. */
|
|
34
|
+
readonly userReferenceUrls?: readonly string[]
|
|
35
|
+
/** Consulted only to avoid a duplicate opening-frame sentence. */
|
|
36
|
+
readonly prompt?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FrameDeliveryPlan {
|
|
40
|
+
/** What will actually happen — never `auto`. */
|
|
41
|
+
readonly delivery: Exclude<FrameDelivery, "auto">
|
|
42
|
+
/** The full reference list to send. Equals the user's own list under `frame`. */
|
|
43
|
+
readonly referenceImageUrls: readonly string[]
|
|
44
|
+
/** Sentence(s) to append to the prompt. Empty under `frame`. */
|
|
45
|
+
readonly promptSuffix: string
|
|
46
|
+
/** Set when reference delivery was WANTED but refused, so a caller can say why. */
|
|
47
|
+
readonly refusedReason?: "no-reference-support" | "image-cap"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve delivery and compose the request shape that follows from it.
|
|
52
|
+
*
|
|
53
|
+
* Two refusals, both deliberate:
|
|
54
|
+
* - a model with no reference-image support keeps frame mode (there is nowhere
|
|
55
|
+
* for the frame to go);
|
|
56
|
+
* - a switch that would push past the model's image cap keeps frame mode,
|
|
57
|
+
* because the alternative is dropping one of the USER's reference images to
|
|
58
|
+
* make room for ours.
|
|
59
|
+
*
|
|
60
|
+
* The frames are appended AFTER the user's own images so their `@image_N`
|
|
61
|
+
* ordinals never shift, and the opening sentence is skipped when the prompt
|
|
62
|
+
* already binds its own first frame — a second binding at another position
|
|
63
|
+
* dilutes the first back into coin-flip behaviour (field finding 2026-07-20).
|
|
64
|
+
*/
|
|
65
|
+
export function planFrameDelivery(args: FrameDeliveryPlanArgs): FrameDeliveryPlan {
|
|
66
|
+
const userRefs = (args.userReferenceUrls ?? []).filter(Boolean)
|
|
67
|
+
const requested = args.requested ?? "auto"
|
|
68
|
+
const delivery = resolveFrameDelivery({
|
|
69
|
+
provider: args.provider,
|
|
70
|
+
requested,
|
|
71
|
+
supportsReferenceImages: args.supportsReferenceImages,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const frames = [args.startFrameUrl, args.endFrameUrl].filter((u): u is string => Boolean(u))
|
|
75
|
+
|
|
76
|
+
if (delivery === "frame" || frames.length === 0) {
|
|
77
|
+
return {
|
|
78
|
+
delivery: "frame",
|
|
79
|
+
referenceImageUrls: userRefs,
|
|
80
|
+
promptSuffix: "",
|
|
81
|
+
...(requested === "reference" && !args.supportsReferenceImages
|
|
82
|
+
? { refusedReason: "no-reference-support" as const }
|
|
83
|
+
: {}),
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const cap = VIDEO_REF_LIMITS_BY_PROVIDER[args.provider ?? ""]?.images
|
|
88
|
+
if (cap !== undefined && userRefs.length + frames.length > cap) {
|
|
89
|
+
return { delivery: "frame", referenceImageUrls: userRefs, promptSuffix: "", refusedReason: "image-cap" }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const referenceImageUrls = [...userRefs]
|
|
93
|
+
const sentences: string[] = []
|
|
94
|
+
if (args.startFrameUrl) {
|
|
95
|
+
referenceImageUrls.push(args.startFrameUrl)
|
|
96
|
+
if (!promptBindsFirstFrame(args.prompt)) {
|
|
97
|
+
sentences.push(REF_BINDING.frame(referenceImageUrls.length, "opening"))
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (args.endFrameUrl) {
|
|
101
|
+
referenceImageUrls.push(args.endFrameUrl)
|
|
102
|
+
sentences.push(REF_BINDING.frame(referenceImageUrls.length, "closing"))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { delivery: "reference", referenceImageUrls, promptSuffix: sentences.join(" ") }
|
|
106
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -92,3 +92,7 @@ export * from "./object-asset-presets.js"
|
|
|
92
92
|
export * from "./factory-snippets/index.js"
|
|
93
93
|
export * from "./picker-wiring.js"
|
|
94
94
|
export * from "./surround-fill.js"
|
|
95
|
+
|
|
96
|
+
// --- Start/end frame delivery: one plan for the dispatch and the editor ---
|
|
97
|
+
export * from "./frame-delivery.js"
|
|
98
|
+
export * from "./character-motion-diagnostics.js"
|
|
@@ -102,6 +102,28 @@ function extractCharacterRefMinor(node: HintNodeLike): boolean {
|
|
|
102
102
|
return REF_DESCRIPTION_FIELDS.some((field) => containsMinorAgeHint(asStr(d[field])))
|
|
103
103
|
}
|
|
104
104
|
|
|
105
|
+
/** The preview, diagnostics and execution use identical graph bindings. */
|
|
106
|
+
export function getCharacterMotionBindings(node: HintNodeLike, ctx?: HintGraphContext) {
|
|
107
|
+
const targetNames: string[] = []
|
|
108
|
+
const partnerNames: string[] = []
|
|
109
|
+
const targetIds = new Set<string>()
|
|
110
|
+
const partnerIds = new Set<string>()
|
|
111
|
+
let subjectMinor = false
|
|
112
|
+
for (const edge of ctx?.edges ?? []) {
|
|
113
|
+
if (edge.target !== node.id || (edge.targetHandle !== "target" && edge.targetHandle !== "partner")) continue
|
|
114
|
+
const src = ctx?.nodes.find(n => n.id === edge.source)
|
|
115
|
+
if (!src) continue
|
|
116
|
+
if (edge.targetHandle === "target") targetIds.add(src.id)
|
|
117
|
+
else partnerIds.add(src.id)
|
|
118
|
+
if (extractCharacterRefMinor(src)) subjectMinor = true
|
|
119
|
+
const name = extractCharacterMotionRefName(src)
|
|
120
|
+
if (!name) continue
|
|
121
|
+
const names = edge.targetHandle === "target" ? targetNames : partnerNames
|
|
122
|
+
if (!names.includes(name)) names.push(name)
|
|
123
|
+
}
|
|
124
|
+
return { targetNames, partnerNames, subjectMinor, selfPairing: [...targetIds].some(id => partnerIds.has(id)) }
|
|
125
|
+
}
|
|
126
|
+
|
|
105
127
|
/** Compose `[preText, mainHint, postText]` into a comma-joined string,
|
|
106
128
|
* honoring the user's free-text fragments around the structured hint.
|
|
107
129
|
* Helpers like `getStylePromptHint` that already include preText/postText
|
|
@@ -261,23 +283,7 @@ function resolveParameterHint(
|
|
|
261
283
|
if (!ctx) {
|
|
262
284
|
return withCustomText(data, composeCharacterMotionHintFromConnections(motionId, [], [], timing, mode))
|
|
263
285
|
}
|
|
264
|
-
const targetNames
|
|
265
|
-
const partnerNames: string[] = []
|
|
266
|
-
// Minor-age floor: ANY ref wired to target or partner that describes a
|
|
267
|
-
// minor drops every adultOnly move from the composed sequence.
|
|
268
|
-
let subjectMinor = false
|
|
269
|
-
for (const edge of ctx.edges) {
|
|
270
|
-
if (edge.target !== node.id) continue
|
|
271
|
-
if (edge.targetHandle !== "target" && edge.targetHandle !== "partner") continue
|
|
272
|
-
const src = ctx.nodes.find((n) => n.id === edge.source)
|
|
273
|
-
if (!src) continue
|
|
274
|
-
// Before the name check: an unnamed ref that describes a minor still floors.
|
|
275
|
-
if (!subjectMinor && extractCharacterRefMinor(src)) subjectMinor = true
|
|
276
|
-
const name = extractCharacterMotionRefName(src)
|
|
277
|
-
if (!name) continue
|
|
278
|
-
if (edge.targetHandle === "target") targetNames.push(name)
|
|
279
|
-
else partnerNames.push(name)
|
|
280
|
-
}
|
|
286
|
+
const { targetNames, partnerNames, subjectMinor } = getCharacterMotionBindings(node, ctx)
|
|
281
287
|
return withCustomText(
|
|
282
288
|
data,
|
|
283
289
|
composeCharacterMotionHintFromConnections(motionId, targetNames, partnerNames, timing, mode, { subjectMinor }),
|
package/src/picker-catalogs.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CharacterMotionMetadata } from "./character-motion/types.js"
|
|
1
2
|
/**
|
|
2
3
|
* Public, discoverable registry of the parameter-picker catalogs.
|
|
3
4
|
*
|
|
@@ -98,6 +99,9 @@ import { setComposedCatalogResolver } from "./catalog-overlay.js"
|
|
|
98
99
|
import { deriveTerm, resolveTerm } from "./term.js"
|
|
99
100
|
|
|
100
101
|
export interface PickerOption {
|
|
102
|
+
/** Character Motion only: authored prerequisites, state and search metadata. */
|
|
103
|
+
readonly motion?: CharacterMotionMetadata
|
|
104
|
+
|
|
101
105
|
readonly id: string
|
|
102
106
|
readonly label: string
|
|
103
107
|
readonly description?: string
|
|
@@ -604,7 +608,10 @@ const SINGLE_CATALOGS: readonly PickerCatalog[] = [
|
|
|
604
608
|
defaultValue: "auto",
|
|
605
609
|
categoryOrder: CHARACTER_MOTION_CATEGORY_ORDER,
|
|
606
610
|
categoryLabels: CHARACTER_MOTION_CATEGORY_LABELS,
|
|
607
|
-
options: toOptions(CHARACTER_MOTIONS, "category"),
|
|
611
|
+
options: toOptions(CHARACTER_MOTIONS, "category").map((option, index) => {
|
|
612
|
+
const { id: _id, label: _label, category: _category, description: _description, promptHint: _hint, term: _term, adultOnly: _adult, twoPerson: _duo, ...motion } = CHARACTER_MOTIONS[index]!
|
|
613
|
+
return { ...option, motion }
|
|
614
|
+
}),
|
|
608
615
|
// Position + Pace, this node's own wording (a movement begins / plays out;
|
|
609
616
|
// an effect manifests; a transition occurs) — never the sibling rows.
|
|
610
617
|
dimensions: perFieldDims([
|
|
@@ -939,8 +946,9 @@ function projectOption(o: PickerOption, detail: PickerCatalogDetail): ProjectedP
|
|
|
939
946
|
promptHint: o.promptHint,
|
|
940
947
|
term: o.term,
|
|
941
948
|
icon: o.icon,
|
|
949
|
+
...(o.motion ? { motion: o.motion } : {}),
|
|
942
950
|
}
|
|
943
|
-
: { id: o.id, label: o.label, category: o.category, term: o.term, icon: o.icon }
|
|
951
|
+
: { id: o.id, label: o.label, category: o.category, term: o.term, icon: o.icon, ...(o.motion ? { motion: o.motion } : {}) }
|
|
944
952
|
}
|
|
945
953
|
|
|
946
954
|
/** Project a catalog to the wire shape: compact by default, optional category/field filter. */
|