@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.
Files changed (43) hide show
  1. package/dist/index.cjs +8181 -7229
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +120 -61
  4. package/dist/index.d.ts +120 -61
  5. package/dist/index.js +8082 -7133
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/__tests__/adult-only-ratchet.test.ts +9 -1
  9. package/src/__tests__/character-motion-audit.test.ts +77 -0
  10. package/src/__tests__/character-motion-exit-visibility.test.ts +183 -0
  11. package/src/__tests__/character-motion-partner-referent.test.ts +225 -0
  12. package/src/__tests__/character-motion.test.ts +11 -4
  13. package/src/__tests__/fixtures/parameter-hint-golden.json +3 -3
  14. package/src/__tests__/frame-delivery.test.ts +93 -0
  15. package/src/__tests__/graph-composed-unwired-identity.test.ts +166 -0
  16. package/src/__tests__/parameter-prompt-hint.test.ts +9 -3
  17. package/src/character-motion/animals-pets.ts +112 -85
  18. package/src/character-motion/athletic-stunts.ts +155 -214
  19. package/src/character-motion/camera-interaction.ts +92 -133
  20. package/src/character-motion/combat-weapons.ts +143 -202
  21. package/src/character-motion/dance.ts +121 -169
  22. package/src/character-motion/entrances-exits.ts +88 -109
  23. package/src/character-motion/evasive-falls.ts +88 -130
  24. package/src/character-motion/everyday-actions.ts +233 -274
  25. package/src/character-motion/face-expression.ts +125 -178
  26. package/src/character-motion/gestures.ts +211 -304
  27. package/src/character-motion/head-gestures.ts +66 -94
  28. package/src/character-motion/idle-ambient.ts +85 -124
  29. package/src/character-motion/posture-shifts.ts +116 -139
  30. package/src/character-motion/runway.ts +73 -97
  31. package/src/character-motion/stage-performance.ts +92 -127
  32. package/src/character-motion/turns-looks.ts +86 -112
  33. package/src/character-motion/two-person.ts +241 -229
  34. package/src/character-motion/types.ts +4 -1
  35. package/src/character-motion/unnatural-horror.ts +53 -91
  36. package/src/character-motion/vehicles-mounts.ts +101 -97
  37. package/src/character-motion/walks-runs.ts +93 -121
  38. package/src/character-motion-diagnostics.ts +49 -0
  39. package/src/character-motion.ts +89 -13
  40. package/src/frame-delivery.ts +106 -0
  41. package/src/index.ts +4 -0
  42. package/src/parameter-prompt-hint.ts +23 -17
  43. 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: string[] = []
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 }),
@@ -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. */