@nodaro/prompts 1.18.0 → 1.20.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 (56) hide show
  1. package/dist/index.cjs +9861 -1414
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +209 -10
  4. package/dist/index.d.ts +209 -10
  5. package/dist/index.js +9199 -765
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/__tests__/adult-only-ratchet.test.ts +18 -1
  9. package/src/__tests__/assemble-suno-input.test.ts +14 -1
  10. package/src/__tests__/character-motion-docs-example.test.ts +26 -0
  11. package/src/__tests__/character-motion-timing-catalogs.test.ts +76 -0
  12. package/src/__tests__/character-motion.test.ts +207 -0
  13. package/src/__tests__/fixtures/parameter-hint-golden.json +137 -1
  14. package/src/__tests__/hint-join.test.ts +57 -0
  15. package/src/__tests__/parameter-hint-mode.test.ts +28 -0
  16. package/src/__tests__/parameter-prompt-hint.test.ts +159 -0
  17. package/src/__tests__/parameter-registry-sync.test.ts +2 -0
  18. package/src/__tests__/picker-analyzer-registry.test.ts +1 -1
  19. package/src/__tests__/sound-aggregator.test.ts +1 -1
  20. package/src/__tests__/transition-timing-catalogs.test.ts +1 -1
  21. package/src/__tests__/video-reference-features.test.ts +10 -1
  22. package/src/age-floor.ts +1 -1
  23. package/src/assemble-suno-input.ts +3 -2
  24. package/src/camera-motions.ts +44 -42
  25. package/src/character-motion/animals-pets.ts +89 -0
  26. package/src/character-motion/athletic-stunts.ts +218 -0
  27. package/src/character-motion/camera-interaction.ts +137 -0
  28. package/src/character-motion/combat-weapons.ts +206 -0
  29. package/src/character-motion/dance.ts +173 -0
  30. package/src/character-motion/entrances-exits.ts +113 -0
  31. package/src/character-motion/evasive-falls.ts +134 -0
  32. package/src/character-motion/everyday-actions.ts +278 -0
  33. package/src/character-motion/face-expression.ts +182 -0
  34. package/src/character-motion/gestures.ts +308 -0
  35. package/src/character-motion/head-gestures.ts +98 -0
  36. package/src/character-motion/idle-ambient.ts +128 -0
  37. package/src/character-motion/posture-shifts.ts +143 -0
  38. package/src/character-motion/runway.ts +101 -0
  39. package/src/character-motion/stage-performance.ts +131 -0
  40. package/src/character-motion/turns-looks.ts +116 -0
  41. package/src/character-motion/two-person.ts +233 -0
  42. package/src/character-motion/types.ts +42 -0
  43. package/src/character-motion/unnatural-horror.ts +95 -0
  44. package/src/character-motion/vehicles-mounts.ts +101 -0
  45. package/src/character-motion/walks-runs.ts +125 -0
  46. package/src/character-motion.ts +286 -0
  47. package/src/factory-presets/music.ts +35 -35
  48. package/src/hint-join.ts +26 -0
  49. package/src/index.ts +2 -0
  50. package/src/parameter-prompt-hint.ts +67 -1
  51. package/src/picker-analyzer-registry.ts +9 -5
  52. package/src/picker-catalogs.ts +24 -0
  53. package/src/picker-wiring.ts +2 -0
  54. package/src/prompt-wizard-categories.ts +9 -5
  55. package/src/provider-prompt-doctrine.ts +1 -1
  56. package/src/sound-aggregator.ts +1 -1
@@ -35,8 +35,11 @@ import { buildPoseHints } from "./pose.js"
35
35
  import { buildStylingHints } from "./styling.js"
36
36
  import { buildTemporalHints } from "./temporal.js"
37
37
  import { composeCameraMotionHintFromConnections } from "./camera-motions.js"
38
+ import { joinHintFragments } from "./hint-join.js"
38
39
  import { composeTransitionHintFromConnections, type TransitionDuration, type TransitionIntensity, type TransitionPosition, type TransitionTiming } from "./transitions.js"
39
40
  import { composeCharacterFxHintFromConnections, type CharacterFxDuration, type CharacterFxIntensity, type CharacterFxPosition, type CharacterFxTiming } from "./character-fx.js"
41
+ import { composeCharacterMotionHintFromConnections, type CharacterMotionPace, type CharacterMotionPosition, type CharacterMotionTiming } from "./character-motion.js"
42
+ import { containsMinorAgeHint, isMinorAge } from "./age-floor.js"
40
43
  import { buildMaterialHints } from "./materials.js"
41
44
  import { curatedAnimalPromptHint, curatedAnimalTerm, curatedVehicleText, curatedWeaponText, curatedFurnitureText } from "./shared-catalog-overlay.js"
42
45
  import { getPhotoGenrePromptHint, getPhotoGenreTerm } from "./photo-genre.js"
@@ -72,6 +75,33 @@ function extractCharacterRefName(node: HintNodeLike): string | undefined {
72
75
  return undefined
73
76
  }
74
77
 
78
+ /** Character Motion's name read: the shared character / face / object / location
79
+ * read, then a creature's `creatureName`. Local to Character Motion so the
80
+ * Character FX prompt, which shares `extractCharacterRefName`, stays unchanged. */
81
+ function extractCharacterMotionRefName(node: HintNodeLike): string | undefined {
82
+ const name = extractCharacterRefName(node)
83
+ if (name) return name
84
+ const creatureName = asStr(((node.data ?? {}) as Record<string, unknown>).creatureName).trim()
85
+ return creatureName.length > 0 ? creatureName : undefined
86
+ }
87
+
88
+ /** Free-text fields a character / face / object / location ref node describes its subject in.
89
+ * A creature ref uses the same `description` / `canonicalDescription` names. */
90
+ const REF_DESCRIPTION_FIELDS = ["description", "seedPrompt", "canonicalDescription"] as const
91
+
92
+ /** True when a character / face / object / location ref node describes a minor:
93
+ * its structured `person` age (the primary signal) or an age phrase in its
94
+ * free-text description. Feeds the Character Motion floor (adult-only moves
95
+ * dropped) — errs toward flooring, never toward letting an adult-only move through. */
96
+ function extractCharacterRefMinor(node: HintNodeLike): boolean {
97
+ const d = (node.data ?? {}) as Record<string, unknown>
98
+ const person = d.person
99
+ if (person && typeof person === "object" && !Array.isArray(person) && isMinorAge(person as Parameters<typeof isMinorAge>[0])) {
100
+ return true
101
+ }
102
+ return REF_DESCRIPTION_FIELDS.some((field) => containsMinorAgeHint(asStr(d[field])))
103
+ }
104
+
75
105
  /** Compose `[preText, mainHint, postText]` into a comma-joined string,
76
106
  * honoring the user's free-text fragments around the structured hint.
77
107
  * Helpers like `getStylePromptHint` that already include preText/postText
@@ -84,7 +114,7 @@ function withCustomText(data: Record<string, unknown>, mainHint: string): string
84
114
  if (mainHint) fragments.push(mainHint)
85
115
  const post = typeof data.postText === "string" ? data.postText.trim() : ""
86
116
  if (post) fragments.push(post)
87
- return fragments.join(", ")
117
+ return joinHintFragments(fragments)
88
118
  }
89
119
 
90
120
  /**
@@ -218,6 +248,42 @@ function resolveParameterHint(
218
248
  return withCustomText(data, composeCharacterFxHintFromConnections(effectId, targetNames, timing, mode))
219
249
  }
220
250
 
251
+ if (node.type === "character-motion") {
252
+ const raw = data.characterMotion
253
+ const motionId: string | string[] | undefined =
254
+ Array.isArray(raw)
255
+ ? raw.filter((s): s is string => typeof s === "string" && s.length > 0)
256
+ : (asStr(raw) || undefined)
257
+ const timing: CharacterMotionTiming = {
258
+ position: asStr(data.position) as CharacterMotionPosition | undefined,
259
+ pace: asStr(data.pace) as CharacterMotionPace | undefined,
260
+ }
261
+ if (!ctx) {
262
+ return withCustomText(data, composeCharacterMotionHintFromConnections(motionId, [], [], timing, mode))
263
+ }
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
+ }
281
+ return withCustomText(
282
+ data,
283
+ composeCharacterMotionHintFromConnections(motionId, targetNames, partnerNames, timing, mode, { subjectMinor }),
284
+ )
285
+ }
286
+
221
287
  const base = resolveBaseHint(node.type, data, mode)
222
288
  if (base) return base
223
289
  // Pack-extend fallback: a single-dim pack entry the per-catalog getter (which
@@ -29,6 +29,7 @@ import { ACTION_FX } from "./action-fx.js"
29
29
  import { LOOP_SUBJECTS } from "./loop-subject.js"
30
30
  import { TRANSITIONS } from "./transitions.js"
31
31
  import { CHARACTER_FX } from "./character-fx.js"
32
+ import { CHARACTER_MOTIONS } from "./character-motion.js"
32
33
  import { POSES } from "./pose.js"
33
34
  import { MATERIALS } from "./materials.js"
34
35
  import { HELD_PROPS } from "./held-prop.js"
@@ -183,6 +184,7 @@ export const PICKER_ANALYZER_REGISTRY = {
183
184
  "loop-subject": { kind: "flat", toolName: "emit_loop_subject", field: "loopSubject", label: "Loop Subject", entries: LOOP_SUBJECTS as ReadonlyArray<AnalyzerEntry> },
184
185
  transition: { kind: "flat", toolName: "emit_transition", field: "transition", label: "Transition", entries: TRANSITIONS as ReadonlyArray<AnalyzerEntry> },
185
186
  "character-fx": { kind: "flat", toolName: "emit_character_fx", field: "characterFx", label: "Character FX", entries: CHARACTER_FX as ReadonlyArray<AnalyzerEntry> },
187
+ "character-motion": { kind: "flat", toolName: "emit_character_motion", field: "characterMotion", label: "Character Motion", entries: CHARACTER_MOTIONS as ReadonlyArray<AnalyzerEntry> },
186
188
  pose: { kind: "flat", toolName: "emit_pose", field: "pose", label: "Pose", entries: POSES as ReadonlyArray<AnalyzerEntry> },
187
189
  material: { kind: "flat", toolName: "emit_material", field: "material", label: "Material", entries: MATERIALS as ReadonlyArray<AnalyzerEntry> },
188
190
  "held-prop": { kind: "flat", toolName: "emit_held_prop", field: "heldProp", label: "Held Prop", entries: HELD_PROPS as ReadonlyArray<AnalyzerEntry> },
@@ -301,18 +303,20 @@ export type PickerType = keyof typeof PICKER_ANALYZER_REGISTRY
301
303
  export const PICKER_TYPES = Object.keys(PICKER_ANALYZER_REGISTRY) as PickerType[]
302
304
 
303
305
  /**
304
- * Family grouping for BATCHED analysis. A single call across all 38 catalogs
305
- * carries a ~211k-char legend (~53k tokens — measured 2026-08-09, the
306
+ * Family grouping for BATCHED analysis. A single call across the first 38
307
+ * catalogs carried a ~211k-char legend (~53k tokens — measured 2026-08-09, the
306
308
  * measure-first probe from the text-to-picker spec), which is slow, costly,
307
309
  * and dilutes per-section accuracy. The text-to-picker route fans out one
308
- * structured call per family (6-15k tokens each) and merges. Mirrors the
309
- * build-brief's §5 UI grouping so Cine can reuse the same partition.
310
+ * structured call per family (6-15k tokens each) and merges. The character
311
+ * family is the exception: Character Motion's 1005-id legend alone is ~82k
312
+ * chars (~46k input tokens measured 2026-09-15), making it the heaviest call.
313
+ * Mirrors the build-brief's §5 UI grouping so Cine can reuse the same partition.
310
314
  */
311
315
  export const PICKER_ANALYZER_FAMILIES: Readonly<Record<string, ReadonlyArray<PickerType>>> = {
312
316
  scene: ["setting", "atmosphere", "backdrop", "era", "temporal"],
313
317
  look: ["style", "color-look", "mood", "aesthetic", "photographer", "photo-genre", "render-quality", "composition-effects", "post-process-effects"],
314
318
  camera: ["framing", "camera-motion", "lens", "camera-format", "lighting", "exposure-settings"],
315
- character: ["person", "styling", "pose", "character-fx"],
319
+ character: ["person", "styling", "pose", "character-fx", "character-motion"],
316
320
  elements: ["animal", "vehicle", "weapon", "furniture", "held-prop", "material", "action-fx", "loop-subject", "transition"],
317
321
  audio: ["music-genre", "music-mood", "instrumentation", "voice-character", "voice-delivery"],
318
322
  }
@@ -67,6 +67,13 @@ import {
67
67
  CHARACTER_FX_DURATIONS,
68
68
  CHARACTER_FX_INTENSITIES,
69
69
  } from "./character-fx.js"
70
+ import {
71
+ CHARACTER_MOTIONS,
72
+ CHARACTER_MOTION_CATEGORY_LABELS,
73
+ CHARACTER_MOTION_CATEGORY_ORDER,
74
+ CHARACTER_MOTION_POSITIONS,
75
+ CHARACTER_MOTION_PACES,
76
+ } from "./character-motion.js"
70
77
  import { POSES, POSE_CATEGORY_LABELS, POSE_CATEGORY_ORDER } from "./pose.js"
71
78
  import { MATERIALS, MATERIAL_CATEGORY_LABELS, MATERIAL_CATEGORY_ORDER } from "./materials.js"
72
79
  import { ANIMALS, ANIMAL_SUBCATEGORY_LABELS, ANIMAL_SUBCATEGORY_ORDER, getAnimalPromptHint } from "@nodaro/shared"
@@ -588,6 +595,23 @@ const SINGLE_CATALOGS: readonly PickerCatalog[] = [
588
595
  ["intensity", CHARACTER_FX_INTENSITIES],
589
596
  ]),
590
597
  },
598
+ {
599
+ nodeType: "character-motion",
600
+ label: "Character Motion",
601
+ catalogId: "character-motion",
602
+ kind: "single",
603
+ valueField: "characterMotion",
604
+ defaultValue: "auto",
605
+ categoryOrder: CHARACTER_MOTION_CATEGORY_ORDER,
606
+ categoryLabels: CHARACTER_MOTION_CATEGORY_LABELS,
607
+ options: toOptions(CHARACTER_MOTIONS, "category"),
608
+ // Position + Pace, this node's own wording (a movement begins / plays out;
609
+ // an effect manifests; a transition occurs) — never the sibling rows.
610
+ dimensions: perFieldDims([
611
+ ["position", CHARACTER_MOTION_POSITIONS],
612
+ ["pace", CHARACTER_MOTION_PACES],
613
+ ]),
614
+ },
591
615
 
592
616
  // -------- "Subject / Object" family --------
593
617
  {
@@ -51,6 +51,7 @@ import { ACTION_FX, ACTION_FX_CATEGORY_LABELS, ACTION_FX_CATEGORY_ORDER } from "
51
51
  import { LOOP_SUBJECTS, LOOP_SUBJECT_CATEGORY_LABELS, LOOP_SUBJECT_CATEGORY_ORDER } from "./loop-subject.js"
52
52
  import { TRANSITIONS, TRANSITION_CATEGORY_LABELS, TRANSITION_CATEGORY_ORDER } from "./transitions.js"
53
53
  import { CHARACTER_FX, CHARACTER_FX_CATEGORY_LABELS, CHARACTER_FX_CATEGORY_ORDER } from "./character-fx.js"
54
+ import { CHARACTER_MOTIONS, CHARACTER_MOTION_CATEGORY_LABELS, CHARACTER_MOTION_CATEGORY_ORDER } from "./character-motion.js"
54
55
  import { FRAMINGS, FRAMING_CATEGORY_ORDER, FRAMING_FIELD_BY_CATEGORY } from "./framing.js"
55
56
  import { LIGHTINGS, LIGHTING_CATEGORY_ORDER, LIGHTING_FIELD_BY_CATEGORY } from "./lighting.js"
56
57
  import { STYLINGS, STYLING_DIMENSION_ORDER, STYLING_FIELD_BY_DIMENSION } from "./styling.js"
@@ -159,6 +160,7 @@ export const SINGLE_PICKER_WIRING: ReadonlyArray<SingleDimPickerWiring> = [
159
160
  { kind: "single", nodeType: "camera-format", label: "Camera / Film", valueField: "cameraFormat", defaultValue: "35mm-film", catalogId: "camera-format", entries: mapCat(CAMERA_FORMATS) },
160
161
  { kind: "single", nodeType: "transition", label: "Transition", valueField: "transition", defaultValue: "auto", catalogId: "transitions", entries: mapCat(TRANSITIONS, "category"), groupOrder: TRANSITION_CATEGORY_ORDER as ReadonlyArray<string>, groupLabels: TRANSITION_CATEGORY_LABELS },
161
162
  { kind: "single", nodeType: "character-fx", label: "Character FX", valueField: "characterFx", defaultValue: "auto", catalogId: "character-fx", entries: mapCat(CHARACTER_FX, "category"), groupOrder: CHARACTER_FX_CATEGORY_ORDER as ReadonlyArray<string>, groupLabels: CHARACTER_FX_CATEGORY_LABELS },
163
+ { kind: "single", nodeType: "character-motion", label: "Character Motion", valueField: "characterMotion", defaultValue: "auto", catalogId: "character-motion", entries: mapCat(CHARACTER_MOTIONS, "category"), groupOrder: CHARACTER_MOTION_CATEGORY_ORDER as ReadonlyArray<string>, groupLabels: CHARACTER_MOTION_CATEGORY_LABELS },
162
164
 
163
165
  // -------- "Subject / Object" family --------
164
166
  { kind: "single", nodeType: "pose", label: "Pose", valueField: "pose", defaultValue: "standing-upright", catalogId: "pose", entries: mapCat(POSES, "category"), groupOrder: POSE_CATEGORY_ORDER as ReadonlyArray<string>, groupLabels: POSE_CATEGORY_LABELS },
@@ -196,7 +196,6 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
196
196
  "gpt-image-2-5-sunburst-i2i": "Precision GPT Image 2.5 edits — up to 16 sources, tightest control on demanding retouches; slower",
197
197
  "ideogram-edit": "Instruction-based editing with text preservation",
198
198
  "ideogram-remix": "Style remixing while preserving structure",
199
- "ideogram-reframe": "Aspect ratio changes with AI fill",
200
199
  "qwen-i2i": "Versatile transformation",
201
200
  "qwen-edit": "Instruction-based editing",
202
201
  "seedream-edit": "Artistic style editing",
@@ -221,7 +220,6 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
221
220
  "gpt-image-2-5-sunburst-i2i": "Precision GPT Image 2.5 edits — up to 16 sources, tightest control on demanding retouches; slower",
222
221
  "ideogram-edit": "Instruction-based editing with text preservation",
223
222
  "ideogram-remix": "Style remixing while preserving structure",
224
- "ideogram-reframe": "Aspect ratio changes with AI fill",
225
223
  "qwen-i2i": "Versatile transformation",
226
224
  "qwen-edit": "Instruction-based editing",
227
225
  "seedream-edit": "Artistic style editing",
@@ -333,11 +331,17 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
333
331
  "minimax": "General music generation, multiple genres",
334
332
  },
335
333
  "suno-generate": {
336
- "V4": "Standard Suno generation",
337
- "V4_5": "Improved quality and coherence",
334
+ // Current generation first (V6 = default), then the earlier ones — all
335
+ // offered in the picker; mirrors SUNO_MODELS order in @nodaro/shared.
336
+ "V6": "Flagship — greater musical expression, more natural vocals, richer details",
337
+ "V6_WILD": "Bolder, more distinctive, less predictable results for exploration",
338
+ "V6_MINI": "Lightweight and fast — quality/speed balance",
339
+ "V5_5": "Custom-model tuning with strong vocal quality",
340
+ "V5": "Superior musical expression, faster generation",
338
341
  "V4_5PLUS": "Enhanced V4.5 with better production",
339
342
  "V4_5ALL": "Full-featured V4.5",
340
- "V5": "Latest Suno with highest quality",
343
+ "V4_5": "Improved quality and coherence",
344
+ "V4": "Standard Suno generation",
341
345
  },
342
346
  "text-to-audio": {
343
347
  "elevenlabs-sfx": "High quality sound effects and ambient audio",
@@ -227,7 +227,7 @@ Example: "Medium shot, a tired corporate worker, rubbing his temples in exhausti
227
227
 
228
228
  **Frames & references**
229
229
  - Start + end frame: wire both (imageUrls [start, end]) and describe the camera path between them — "a smooth 180-degree arc shot, starting front-facing and circling to end on the POV from behind her".
230
- - Reference images (ingredients): attach character/object/scene refs and name them in the prompt ("using the provided images for the detective and the office, …"). Reference runs DO generate audio.
230
+ - Reference images (ingredients) — **Fast (veo3.1) and Lite (veo3_lite) only**; the Quality SKU (veo3) has no reference-to-video mode at the provider, so wire it with frames alone. Where they are available: attach character/object/scene refs and name them in the prompt ("using the provided images for the detective and the office, …"). Reference runs DO generate audio.
231
231
 
232
232
  **Constraints**
233
233
  - Negative prompting works by positive description: write "a desolate landscape with no buildings or roads", not "no buildings".
@@ -96,7 +96,7 @@ export function composeSoundHintFromConnections(
96
96
  // Music consumers (suno-generate, generate-music) accept BOTH music nodes
97
97
  // AND voice nodes — voice description (gender, age, accent, language,
98
98
  // timbre, delivery archetype) is valid input for music with vocals. Suno
99
- // V5 in particular benefits from rich voice description; the typed
99
+ // V6 in particular benefits from rich voice description; the typed
100
100
  // `vocalGender` field on Suno is also extracted below from voice-character.
101
101
  //
102
102
  // Voice Design rejects music nodes (different domain). Text-to-Audio