@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
@@ -0,0 +1,286 @@
1
+ /**
2
+ * Canonical catalog of CHARACTER MOTION choices — what the subject DOES across
3
+ * a video clip: walks in, turns to camera, breaks into a smile, ducks, draws and
4
+ * fires, runway-walks, dances. Temporal by definition.
5
+ *
6
+ * Distinct from:
7
+ * - Pose — a static SNAPSHOT of body position (serves still images too).
8
+ * - Character FX — supernatural change to the subject's body (werewolf, fire breath).
9
+ * - Action FX — a scene event (explosion, lightning); motion is the BODY acting.
10
+ * - Camera Motion — the camera moves, never the subject.
11
+ *
12
+ * Shared between the picker UI, the standalone Character Motion parameter node,
13
+ * and the prompt-hint injection on both the frontend DAG executor and the
14
+ * backend orchestrator. Video-only: never injected into still-image consumers.
15
+ *
16
+ * Every non-empty `promptHint` references "the subject" at least once — the
17
+ * composer does a global regex replace of "the subject" with the wired target's
18
+ * display name. Two-person moves ALSO contain the literal words "the partner",
19
+ * replaced with the wired partner's name, or "another person" when unwired.
20
+ *
21
+ * Multi-pick is an ORDERED SEQUENCE: value field accepts `string | string[]`
22
+ * (cap 3), joined with ", then " in BOTH hint modes (a movement happens after
23
+ * the previous one; Character FX joins with ", and " because effects coincide).
24
+ *
25
+ * `adultOnly` marks the W1-a minor-age floor. The composer drops flagged ids
26
+ * when its caller reports a minor subject (`floor.subjectMinor`); the AI Fill
27
+ * strip drops them from analyzer output. Hand-curated; the `adult-only-ratchet`
28
+ * test only ratchets. Never set on neutral movement or the `auto` / `none` heads.
29
+ */
30
+
31
+ import { resolveTerm, type PickerHintMode } from "./term.js"
32
+ import { overlayEntry } from "./catalog-overlay.js"
33
+ export type { CharacterMotion, CharacterMotionCategory } from "./character-motion/types.js"
34
+ import type { CharacterMotion, CharacterMotionCategory } from "./character-motion/types.js"
35
+ import { ENTRANCES_EXITS_MOTIONS } from "./character-motion/entrances-exits.js"
36
+ import { TURNS_LOOKS_MOTIONS } from "./character-motion/turns-looks.js"
37
+ import { HEAD_GESTURES_MOTIONS } from "./character-motion/head-gestures.js"
38
+ import { WALKS_RUNS_MOTIONS } from "./character-motion/walks-runs.js"
39
+ import { RUNWAY_MOTIONS } from "./character-motion/runway.js"
40
+ import { DANCE_MOTIONS } from "./character-motion/dance.js"
41
+ import { FACE_EXPRESSION_MOTIONS } from "./character-motion/face-expression.js"
42
+ import { GESTURES_MOTIONS } from "./character-motion/gestures.js"
43
+ import { CAMERA_INTERACTION_MOTIONS } from "./character-motion/camera-interaction.js"
44
+ import { COMBAT_WEAPONS_MOTIONS } from "./character-motion/combat-weapons.js"
45
+ import { ATHLETIC_STUNTS_MOTIONS } from "./character-motion/athletic-stunts.js"
46
+ import { EVASIVE_FALLS_MOTIONS } from "./character-motion/evasive-falls.js"
47
+ import { POSTURE_SHIFTS_MOTIONS } from "./character-motion/posture-shifts.js"
48
+ import { EVERYDAY_ACTIONS_MOTIONS } from "./character-motion/everyday-actions.js"
49
+ import { VEHICLES_MOUNTS_MOTIONS } from "./character-motion/vehicles-mounts.js"
50
+ import { ANIMALS_PETS_MOTIONS } from "./character-motion/animals-pets.js"
51
+ import { TWO_PERSON_MOTIONS } from "./character-motion/two-person.js"
52
+ import { IDLE_AMBIENT_MOTIONS } from "./character-motion/idle-ambient.js"
53
+ import { STAGE_PERFORMANCE_MOTIONS } from "./character-motion/stage-performance.js"
54
+ import { UNNATURAL_HORROR_MOTIONS } from "./character-motion/unnatural-horror.js"
55
+
56
+ export const CHARACTER_MOTION_CATEGORY_ORDER: ReadonlyArray<CharacterMotionCategory> = [
57
+ "entrances-exits",
58
+ "turns-looks",
59
+ "head-gestures",
60
+ "walks-runs",
61
+ "runway",
62
+ "dance",
63
+ "face-expression",
64
+ "gestures",
65
+ "camera-interaction",
66
+ "combat-weapons",
67
+ "athletic-stunts",
68
+ "evasive-falls",
69
+ "posture-shifts",
70
+ "everyday-actions",
71
+ "vehicles-mounts",
72
+ "animals-pets",
73
+ "two-person",
74
+ "idle-ambient",
75
+ "stage-performance",
76
+ "unnatural-horror",
77
+ ] as const
78
+
79
+ export const CHARACTER_MOTION_CATEGORY_LABELS: Readonly<Record<CharacterMotionCategory, string>> = {
80
+ "entrances-exits": "Entrances & Exits",
81
+ "turns-looks": "Turns & Looks",
82
+ "head-gestures": "Head Gestures",
83
+ "walks-runs": "Walks & Runs",
84
+ "runway": "Runway & Model",
85
+ "dance": "Dance",
86
+ "face-expression": "Face & Expression",
87
+ "gestures": "Gestures",
88
+ "camera-interaction": "Camera Interaction",
89
+ "combat-weapons": "Combat & Weapons",
90
+ "athletic-stunts": "Athletic & Stunts",
91
+ "evasive-falls": "Evasive & Falls",
92
+ "posture-shifts": "Posture Shifts",
93
+ "everyday-actions": "Everyday Actions",
94
+ "vehicles-mounts": "Vehicles & Mounts",
95
+ "animals-pets": "Animals & Pets",
96
+ "two-person": "Two-Person",
97
+ "idle-ambient": "Idle & Ambient",
98
+ "stage-performance": "Stage & Performance",
99
+ "unnatural-horror": "Unnatural & Horror",
100
+ } as const
101
+
102
+ /**
103
+ * The catalog: two no-op heads + every category array, in category order.
104
+ * 1003 injecting entries across 20 categories.
105
+ */
106
+ export const CHARACTER_MOTIONS: ReadonlyArray<CharacterMotion> = [
107
+ // Defaults — both inject nothing (empty promptHint ⇒ empty term).
108
+ { id: "auto", label: "Auto", category: "entrances-exits", description: "Let the model choose the movement", promptHint: "" },
109
+ { id: "none", label: "None", category: "entrances-exits", description: "No scripted character movement", promptHint: "" },
110
+ ...ENTRANCES_EXITS_MOTIONS,
111
+ ...TURNS_LOOKS_MOTIONS,
112
+ ...HEAD_GESTURES_MOTIONS,
113
+ ...WALKS_RUNS_MOTIONS,
114
+ ...RUNWAY_MOTIONS,
115
+ ...DANCE_MOTIONS,
116
+ ...FACE_EXPRESSION_MOTIONS,
117
+ ...GESTURES_MOTIONS,
118
+ ...CAMERA_INTERACTION_MOTIONS,
119
+ ...COMBAT_WEAPONS_MOTIONS,
120
+ ...ATHLETIC_STUNTS_MOTIONS,
121
+ ...EVASIVE_FALLS_MOTIONS,
122
+ ...POSTURE_SHIFTS_MOTIONS,
123
+ ...EVERYDAY_ACTIONS_MOTIONS,
124
+ ...VEHICLES_MOUNTS_MOTIONS,
125
+ ...ANIMALS_PETS_MOTIONS,
126
+ ...TWO_PERSON_MOTIONS,
127
+ ...IDLE_AMBIENT_MOTIONS,
128
+ ...STAGE_PERFORMANCE_MOTIONS,
129
+ ...UNNATURAL_HORROR_MOTIONS,
130
+ ]
131
+
132
+ const characterMotionById = new Map<string, CharacterMotion>(
133
+ CHARACTER_MOTIONS.map((m) => [m.id, m]),
134
+ )
135
+
136
+ export function getCharacterMotion(id: string | undefined | null): CharacterMotion | undefined {
137
+ if (!id) return undefined
138
+ return overlayEntry("character-motion", id, characterMotionById.get(id))
139
+ }
140
+
141
+ export function getCharacterMotionLabel(id: string | undefined | null, fallback?: string): string {
142
+ const m = getCharacterMotion(id)
143
+ if (m) return m.label
144
+ if (fallback !== undefined) return fallback
145
+ return (id ?? "").replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase())
146
+ }
147
+
148
+ export function getCharacterMotionPromptHint(id: string | undefined | null): string {
149
+ return getCharacterMotion(id)?.promptHint ?? ""
150
+ }
151
+
152
+ /** Compact counterpart of `getCharacterMotionPromptHint` — same lookup, same
153
+ * empty-string-on-miss, and the no-op `auto` / `none` heads inject nothing. */
154
+ export function getCharacterMotionTerm(id: string | undefined | null): string {
155
+ return resolveTerm(getCharacterMotion(id))
156
+ }
157
+
158
+ export const CHARACTER_MOTION_IDS: ReadonlyArray<string> = CHARACTER_MOTIONS.map((m) => m.id)
159
+
160
+ /** Cap on ordered picks. Shared by the composer (`.slice(0, N)`), the picker
161
+ * (`maxSelected`) and the config panel — the three must agree. */
162
+ export const CHARACTER_MOTION_MAX_PICKS = 3
163
+
164
+ // ---------------------------------------------------------------------------
165
+ // Timing scales — Position + Pace. Own wording; NOT the character-fx or the
166
+ // transition rows (an effect manifests, a transition occurs, a MOVEMENT is
167
+ // performed). `POSITION_CLAUSES` / `PACE_CLAUSES` are DERIVED from these arrays
168
+ // so the clause the composer injects and the hint the catalog advertises are
169
+ // the same string by construction.
170
+ // ---------------------------------------------------------------------------
171
+
172
+ export interface CharacterMotionTimingOption {
173
+ readonly id: string
174
+ readonly label: string
175
+ readonly description: string
176
+ readonly promptHint: string
177
+ readonly term?: string
178
+ }
179
+
180
+ export const CHARACTER_MOTION_POSITIONS = [
181
+ { id: "auto", label: "Auto", description: "Let the model place the movement", promptHint: "", term: "" },
182
+ { id: "start", label: "Start", description: "Begins at the opening of the clip", promptHint: "the movement begins at the opening of the clip", term: "starting as the clip opens" },
183
+ { id: "middle", label: "Middle", description: "Begins midway through the clip", promptHint: "the movement begins midway through the clip", term: "starting midway through the clip" },
184
+ { id: "end", label: "End", description: "Happens in the closing moments of the clip", promptHint: "the movement happens in the closing moments of the clip", term: "in the closing moments of the clip" },
185
+ { id: "full", label: "Full", description: "Plays out across the entire clip", promptHint: "the movement plays out across the entire clip", term: "playing out across the whole clip" },
186
+ ] as const satisfies ReadonlyArray<CharacterMotionTimingOption>
187
+
188
+ export const CHARACTER_MOTION_PACES = [
189
+ { id: "auto", label: "Auto", description: "Let the model set the tempo", promptHint: "", term: "" },
190
+ { id: "slow-motion", label: "Slow Motion", description: "The action itself is rendered in slow motion", promptHint: "the action is rendered in slow motion, every phase of the movement stretched and drawn out", term: "in slow motion" },
191
+ { id: "slow", label: "Slow", description: "Performed slowly and deliberately", promptHint: "performed slowly and deliberately, each phase of the movement given its full time", term: "slow and deliberate" },
192
+ { id: "natural", label: "Natural", description: "Performed at an everyday tempo", promptHint: "performed at a natural everyday tempo, neither rushed nor drawn out", term: "at a natural everyday tempo" },
193
+ { id: "fast", label: "Fast", description: "Performed quickly, brisk and urgent", promptHint: "performed quickly, with brisk urgent tempo and sharp transitions between phases", term: "fast and brisk" },
194
+ { id: "explosive", label: "Explosive", description: "A sudden burst from stillness into full-speed motion", promptHint: "performed with an explosive burst of energy, snapping from stillness into full-speed motion", term: "with an explosive burst" },
195
+ ] as const satisfies ReadonlyArray<CharacterMotionTimingOption>
196
+
197
+ export type CharacterMotionPosition = (typeof CHARACTER_MOTION_POSITIONS)[number]["id"]
198
+ export type CharacterMotionPace = (typeof CHARACTER_MOTION_PACES)[number]["id"]
199
+
200
+ export interface CharacterMotionTiming {
201
+ position?: CharacterMotionPosition
202
+ pace?: CharacterMotionPace
203
+ }
204
+
205
+ /** Private twin of the helper in character-fx.ts / transitions.ts — the
206
+ * catalogs must stay independent. Total over the array by construction. */
207
+ function clausesOf<T extends CharacterMotionTimingOption>(
208
+ options: ReadonlyArray<T>,
209
+ ): Record<Exclude<T["id"], "auto">, string> {
210
+ return Object.fromEntries(
211
+ options.filter((o) => o.id !== "auto").map((o) => [o.id, o.promptHint]),
212
+ ) as Record<Exclude<T["id"], "auto">, string>
213
+ }
214
+
215
+ const POSITION_CLAUSES = clausesOf(CHARACTER_MOTION_POSITIONS)
216
+ const PACE_CLAUSES = clausesOf(CHARACTER_MOTION_PACES)
217
+
218
+ /** Widening guard — `as const` on the two arrays is load-bearing (see character-fx.ts). */
219
+ type NarrowIds<T> = string extends T ? never : true
220
+ const _timingIdsStayNarrow: [NarrowIds<CharacterMotionPosition>, NarrowIds<CharacterMotionPace>] = [true, true]
221
+ void _timingIdsStayNarrow
222
+
223
+ // ---------------------------------------------------------------------------
224
+ // Graph-aware composer — target + partner handles, ordered multi-pick, timing
225
+ // ---------------------------------------------------------------------------
226
+
227
+ const PARTNER_FALLBACK = "another person"
228
+
229
+ /** What the caller knows about the wired target. `subjectMinor: true` drops
230
+ * every `adultOnly` pick after the cap; otherwise the output is unchanged. */
231
+ export interface CharacterMotionFloor {
232
+ readonly subjectMinor?: boolean
233
+ }
234
+
235
+ /**
236
+ * Compose a character-motion prompt fragment from an ORDERED list of 1–3 move
237
+ * ids, the display names wired to the `target` and `partner` handles, and the
238
+ * optional Position / Pace timing.
239
+ *
240
+ * Full mode: each hint has "the subject" rewritten to the target name(s) (only
241
+ * when a target is wired) and "the partner" rewritten to the partner name (or
242
+ * "another person" — ALWAYS, so the literal words "the partner" never ship);
243
+ * both BEFORE the ", then " join. Compact mode: terms joined with ", then ",
244
+ * prefixed `"{target}: "` when a target is wired. A term never contains "the
245
+ * subject" (the prefix names the target) but a two-person term always contains
246
+ * "the partner", which is substituted exactly as in full mode.
247
+ * Timing clauses follow in the fixed order position, pace, in both modes.
248
+ * `floor.subjectMinor` drops adult-only picks after the cap; none left ⇒ "".
249
+ */
250
+ export function composeCharacterMotionHintFromConnections(
251
+ motionId: string | ReadonlyArray<string> | undefined | null,
252
+ targetHints: ReadonlyArray<string>,
253
+ partnerHints: ReadonlyArray<string>,
254
+ timing?: CharacterMotionTiming,
255
+ mode: PickerHintMode = "full",
256
+ floor?: CharacterMotionFloor,
257
+ ): string {
258
+ const picked = Array.isArray(motionId)
259
+ ? Array.from(new Set(motionId as ReadonlyArray<string>)).slice(0, CHARACTER_MOTION_MAX_PICKS)
260
+ : motionId ? [motionId as string] : []
261
+ // Cap the user's picks FIRST, then floor: a dropped pick never lets a later one backfill.
262
+ const ids = floor?.subjectMinor === true
263
+ ? picked.filter((id) => getCharacterMotion(id)?.adultOnly !== true)
264
+ : picked
265
+ const resolveBase = mode === "compact" ? getCharacterMotionTerm : getCharacterMotionPromptHint
266
+ const entries = ids.map((id) => ({ id, base: resolveBase(id) })).filter((e) => e.base.length > 0)
267
+ if (entries.length === 0) return ""
268
+
269
+ const targetClause = targetHints.filter((h) => h && h.length > 0).join(" and ")
270
+ const partnerClause = partnerHints.filter((h) => h && h.length > 0).join(" and ")
271
+ const partnerName = partnerClause || PARTNER_FALLBACK
272
+
273
+ const substituted = entries.map(({ base }) => {
274
+ const named = mode !== "compact" && targetClause
275
+ ? base.replace(/\bthe subject\b/g, targetClause)
276
+ : base
277
+ return named.replace(/\bthe partner\b/g, partnerName)
278
+ })
279
+
280
+ const joined = substituted.join(", then ")
281
+ const combinedBase = mode === "compact" && targetClause ? `${targetClause}: ${joined}` : joined
282
+ const parts: string[] = [combinedBase]
283
+ if (timing?.position && timing.position !== "auto") parts.push(POSITION_CLAUSES[timing.position])
284
+ if (timing?.pace && timing.pace !== "auto") parts.push(PACE_CLAUSES[timing.pace])
285
+ return parts.join(", ")
286
+ }
@@ -401,7 +401,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
401
401
  description: "Mellow focus beat.",
402
402
  group: "By Use-Case",
403
403
  data: {
404
- model: "V5_5",
404
+ model: "V6",
405
405
  instrumental: true,
406
406
  style: "lo-fi hip-hop, chill and mellow, jazzy Rhodes chords, vinyl crackle, soft boom-bap drums, Andante 80 BPM, instrumental",
407
407
  },
@@ -412,7 +412,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
412
412
  description: "Short confident opener.",
413
413
  group: "By Use-Case",
414
414
  data: {
415
- model: "V5_5",
415
+ model: "V6",
416
416
  instrumental: true,
417
417
  style: "modern podcast intro, upbeat and confident, catchy synth hook, punchy beat, Allegro, instrumental",
418
418
  },
@@ -423,7 +423,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
423
423
  description: "Epic orchestral build.",
424
424
  group: "By Use-Case",
425
425
  data: {
426
- model: "V5_5",
426
+ model: "V6",
427
427
  instrumental: true,
428
428
  style: "epic cinematic trailer, dramatic and intense, full orchestra, taiko drums, brass swells, crescendo into a powerful hit, instrumental",
429
429
  },
@@ -434,7 +434,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
434
434
  description: "Bright, motivational.",
435
435
  group: "By Use-Case",
436
436
  data: {
437
- model: "V5_5",
437
+ model: "V6",
438
438
  instrumental: true,
439
439
  style: "upbeat corporate, optimistic and motivational, bright piano, claps, clean electronic beat, Allegro 120 BPM, instrumental",
440
440
  },
@@ -445,7 +445,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
445
445
  description: "Feel-good acoustic bed.",
446
446
  group: "By Use-Case",
447
447
  data: {
448
- model: "V5_5",
448
+ model: "V6",
449
449
  instrumental: true,
450
450
  style: "feel-good vlog background, light and breezy, acoustic guitar, claps, warm and unobtrusive, Andante, instrumental",
451
451
  },
@@ -456,7 +456,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
456
456
  description: "Calm meditative pads.",
457
457
  group: "By Use-Case",
458
458
  data: {
459
- model: "V5_5",
459
+ model: "V6",
460
460
  instrumental: true,
461
461
  style: "ambient soundscape, calm and meditative, soft evolving synth pads, gentle textures, reverb-heavy, Adagio, instrumental",
462
462
  },
@@ -467,7 +467,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
467
467
  description: "Festival build and drop.",
468
468
  group: "By Use-Case",
469
469
  data: {
470
- model: "V5_5",
470
+ model: "V6",
471
471
  instrumental: true,
472
472
  style: "festival EDM, euphoric and energetic, big supersaw leads, sidechain bass, build-up and powerful drop, Allegro 128 BPM, instrumental",
473
473
  },
@@ -478,7 +478,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
478
478
  description: "Playful 8-bit loop.",
479
479
  group: "By Use-Case",
480
480
  data: {
481
- model: "V5_5",
481
+ model: "V6",
482
482
  instrumental: true,
483
483
  style: "video game chiptune, playful and adventurous, 8-bit synths, catchy melody, energetic, instrumental",
484
484
  },
@@ -491,7 +491,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
491
491
  description: "Dusty laid-back groove.",
492
492
  group: "By Genre",
493
493
  data: {
494
- model: "V5_5",
494
+ model: "V6",
495
495
  instrumental: true,
496
496
  style: "lo-fi hip-hop, jazzy and mellow, dusty Rhodes chords, vinyl warmth, laid-back boom-bap groove, instrumental",
497
497
  },
@@ -502,7 +502,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
502
502
  description: "Club-energy four-on-the-floor.",
503
503
  group: "By Genre",
504
504
  data: {
505
- model: "V5_5",
505
+ model: "V6",
506
506
  instrumental: true,
507
507
  style: "house EDM, energetic and euphoric, four-on-the-floor beat, catchy plucky synths, club energy, Allegro 124 BPM, instrumental",
508
508
  },
@@ -513,7 +513,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
513
513
  description: "Driving anthemic guitars.",
514
514
  group: "By Genre",
515
515
  data: {
516
- model: "V5_5",
516
+ model: "V6",
517
517
  instrumental: true,
518
518
  style: "rock anthem, energetic and driving, distorted electric guitars, pounding drums, anthemic, instrumental",
519
519
  },
@@ -524,7 +524,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
524
524
  description: "Late-night sophistication.",
525
525
  group: "By Genre",
526
526
  data: {
527
- model: "V5_5",
527
+ model: "V6",
528
528
  instrumental: true,
529
529
  style: "smooth jazz, relaxed and sophisticated, warm saxophone, brushed drums, walking bass, late-night, instrumental",
530
530
  },
@@ -535,7 +535,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
535
535
  description: "Spacious and serene.",
536
536
  group: "By Genre",
537
537
  data: {
538
- model: "V5_5",
538
+ model: "V6",
539
539
  instrumental: true,
540
540
  style: "ambient, serene and spacious, lush synth pads, field-recording textures, slow and meditative, instrumental",
541
541
  },
@@ -546,7 +546,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
546
546
  description: "Grand cinematic ensemble.",
547
547
  group: "By Genre",
548
548
  data: {
549
- model: "V5_5",
549
+ model: "V6",
550
550
  instrumental: true,
551
551
  style: "epic orchestral, grand and cinematic, sweeping strings, brass, woodwinds, timpani, dramatic, instrumental",
552
552
  },
@@ -557,7 +557,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
557
557
  description: "80s neon nostalgia.",
558
558
  group: "By Genre",
559
559
  data: {
560
- model: "V5_5",
560
+ model: "V6",
561
561
  instrumental: true,
562
562
  style: "80s synthwave, nostalgic and cool, retro analog synths, pulsing bass, gated drums, neon, Andante 110 BPM, instrumental",
563
563
  },
@@ -568,7 +568,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
568
568
  description: "Groovy and danceable.",
569
569
  group: "By Genre",
570
570
  data: {
571
- model: "V5_5",
571
+ model: "V6",
572
572
  instrumental: true,
573
573
  style: "funk and soul, groovy and fun, slap bass, wah guitar, tight horn section, danceable, instrumental",
574
574
  },
@@ -581,7 +581,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
581
581
  description: "Catchy vocal pop — add lyrics.",
582
582
  group: "Vocals & Songs",
583
583
  data: {
584
- model: "V5_5",
584
+ model: "V6",
585
585
  instrumental: false,
586
586
  style: "upbeat pop, catchy and bright, shimmering synths, four-on-the-floor, female vocals, Allegro 120 BPM, verse-chorus-verse with bridge",
587
587
  },
@@ -592,7 +592,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
592
592
  description: "Hard-hitting verses — add bars.",
593
593
  group: "Vocals & Songs",
594
594
  data: {
595
- model: "V5_5",
595
+ model: "V6",
596
596
  instrumental: false,
597
597
  style: "modern hip-hop, confident, hard-hitting 808s, trap hi-hats, male rap vocals, verse-hook-verse",
598
598
  },
@@ -603,7 +603,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
603
603
  description: "Heartfelt vocals — add lyrics.",
604
604
  group: "Vocals & Songs",
605
605
  data: {
606
- model: "V5_5",
606
+ model: "V6",
607
607
  instrumental: false,
608
608
  style: "emotional ballad, tender and heartfelt, piano and strings, soulful vocals, Adagio, verse-chorus with a big bridge",
609
609
  },
@@ -614,7 +614,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
614
614
  description: "Soaring chorus — add lyrics.",
615
615
  group: "Vocals & Songs",
616
616
  data: {
617
- model: "V5_5",
617
+ model: "V6",
618
618
  instrumental: false,
619
619
  style: "anthemic rock, powerful, driving guitars, big drums, male vocals, soaring chorus, verse-pre-chorus-chorus",
620
620
  },
@@ -625,7 +625,7 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
625
625
  description: "Warm and intimate — add lyrics.",
626
626
  group: "Vocals & Songs",
627
627
  data: {
628
- model: "V5_5",
628
+ model: "V6",
629
629
  instrumental: false,
630
630
  style: "acoustic singer-songwriter, warm and intimate, fingerpicked guitar, soft vocals, Andante, verse-chorus",
631
631
  },
@@ -636,42 +636,42 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
636
636
  name: "Trailer Riser / Braam",
637
637
  description: "Pure rising tension hit.",
638
638
  group: "By Use-Case",
639
- data: { model: "V5_5", instrumental: true, style: "cinematic trailer riser and braam, tense and building, rising whoosh into a deep brass impact, no melody, instrumental" },
639
+ data: { model: "V6", instrumental: true, style: "cinematic trailer riser and braam, tense and building, rising whoosh into a deep brass impact, no melody, instrumental" },
640
640
  },
641
641
  {
642
642
  id: "suno-generate/calm-corporate",
643
643
  name: "Calm Corporate / Tech",
644
644
  description: "Soft, professional bed.",
645
645
  group: "By Use-Case",
646
- data: { model: "V5_5", instrumental: true, style: "calm corporate tech, professional and clean, soft synth pads, gentle pulse, Andante, instrumental" },
646
+ data: { model: "V6", instrumental: true, style: "calm corporate tech, professional and clean, soft synth pads, gentle pulse, Andante, instrumental" },
647
647
  },
648
648
  {
649
649
  id: "suno-generate/holiday",
650
650
  name: "Holiday / Festive",
651
651
  description: "Warm seasonal cheer.",
652
652
  group: "By Use-Case",
653
- data: { model: "V5_5", instrumental: true, style: "festive holiday music, warm and cheerful, sleigh bells, warm orchestration, cozy, instrumental" },
653
+ data: { model: "V6", instrumental: true, style: "festive holiday music, warm and cheerful, sleigh bells, warm orchestration, cozy, instrumental" },
654
654
  },
655
655
  {
656
656
  id: "suno-generate/kids",
657
657
  name: "Kids / Children's",
658
658
  description: "Playful and simple.",
659
659
  group: "By Use-Case",
660
- data: { model: "V5_5", instrumental: true, style: "children's music, playful and happy, simple cheerful melody, xylophone and ukulele, bouncy, instrumental" },
660
+ data: { model: "V6", instrumental: true, style: "children's music, playful and happy, simple cheerful melody, xylophone and ukulele, bouncy, instrumental" },
661
661
  },
662
662
  {
663
663
  id: "suno-generate/meditation-spa",
664
664
  name: "Meditation / Spa",
665
665
  description: "Healing singing bowls.",
666
666
  group: "By Use-Case",
667
- data: { model: "V5_5", instrumental: true, style: "meditation and spa, serene and healing, singing bowls, soft drones, Adagio, reverb-heavy, instrumental" },
667
+ data: { model: "V6", instrumental: true, style: "meditation and spa, serene and healing, singing bowls, soft drones, Adagio, reverb-heavy, instrumental" },
668
668
  },
669
669
  {
670
670
  id: "suno-generate/workout",
671
671
  name: "Workout / Gym",
672
672
  description: "High-energy motivation.",
673
673
  group: "By Use-Case",
674
- data: { model: "V5_5", instrumental: true, style: "high-energy workout, motivational and driving, pumping electronic beat, big synths, Allegro 140 BPM, instrumental" },
674
+ data: { model: "V6", instrumental: true, style: "high-energy workout, motivational and driving, pumping electronic beat, big synths, Allegro 140 BPM, instrumental" },
675
675
  },
676
676
  // ── More By Genre ────────────────────────────────────────────────────────
677
677
  {
@@ -679,42 +679,42 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
679
679
  name: "Trap Beat",
680
680
  description: "Booming 808s, dark.",
681
681
  group: "By Genre",
682
- data: { model: "V5_5", instrumental: true, style: "trap beat, dark and hard, booming 808s, crisp rapid hi-hats, moody, instrumental" },
682
+ data: { model: "V6", instrumental: true, style: "trap beat, dark and hard, booming 808s, crisp rapid hi-hats, moody, instrumental" },
683
683
  },
684
684
  {
685
685
  id: "suno-generate/dnb",
686
686
  name: "Drum & Bass",
687
687
  description: "Fast breakbeats, deep bass.",
688
688
  group: "By Genre",
689
- data: { model: "V5_5", instrumental: true, style: "drum and bass, fast and energetic, breakbeats, deep rolling bassline, Presto 174 BPM, instrumental" },
689
+ data: { model: "V6", instrumental: true, style: "drum and bass, fast and energetic, breakbeats, deep rolling bassline, Presto 174 BPM, instrumental" },
690
690
  },
691
691
  {
692
692
  id: "suno-generate/afrobeats",
693
693
  name: "Afrobeats",
694
694
  description: "Warm syncopated groove.",
695
695
  group: "By Genre",
696
- data: { model: "V5_5", instrumental: true, style: "afrobeats, groovy and warm, syncopated percussion, warm bass, bright melodies, danceable, instrumental" },
696
+ data: { model: "V6", instrumental: true, style: "afrobeats, groovy and warm, syncopated percussion, warm bass, bright melodies, danceable, instrumental" },
697
697
  },
698
698
  {
699
699
  id: "suno-generate/country",
700
700
  name: "Country / Americana",
701
701
  description: "Acoustic, heartfelt.",
702
702
  group: "By Genre",
703
- data: { model: "V5_5", instrumental: true, style: "country americana, warm and heartfelt, acoustic and slide guitar, storytelling feel, instrumental" },
703
+ data: { model: "V6", instrumental: true, style: "country americana, warm and heartfelt, acoustic and slide guitar, storytelling feel, instrumental" },
704
704
  },
705
705
  {
706
706
  id: "suno-generate/phonk",
707
707
  name: "Phonk",
708
708
  description: "Dark drift-phonk for reels.",
709
709
  group: "By Genre",
710
- data: { model: "V5_5", instrumental: true, style: "drift phonk, dark and aggressive, distorted 808 cowbell, menacing beat, lo-fi grit, instrumental" },
710
+ data: { model: "V6", instrumental: true, style: "drift phonk, dark and aggressive, distorted 808 cowbell, menacing beat, lo-fi grit, instrumental" },
711
711
  },
712
712
  {
713
713
  id: "suno-generate/reggae",
714
714
  name: "Reggae / Dub",
715
715
  description: "Laid-back island groove.",
716
716
  group: "By Genre",
717
- data: { model: "V5_5", instrumental: true, style: "reggae dub, laid-back and sunny, off-beat skank guitar, deep dub bassline, relaxed island groove, instrumental" },
717
+ data: { model: "V6", instrumental: true, style: "reggae dub, laid-back and sunny, off-beat skank guitar, deep dub bassline, relaxed island groove, instrumental" },
718
718
  },
719
719
  // ── More Vocals & Songs ──────────────────────────────────────────────────
720
720
  {
@@ -722,13 +722,13 @@ export const SUNO_GENERATE_PRESETS: readonly FactoryPreset[] = [
722
722
  name: "R&B / Soul (vocal)",
723
723
  description: "Smooth soulful vocals — add lyrics.",
724
724
  group: "Vocals & Songs",
725
- data: { model: "V5_5", instrumental: false, style: "smooth R&B soul, sensual and warm, lush chords, soft drums, soulful vocals, Andante, verse-chorus with ad-libs" },
725
+ data: { model: "V6", instrumental: false, style: "smooth R&B soul, sensual and warm, lush chords, soft drums, soulful vocals, Andante, verse-chorus with ad-libs" },
726
726
  },
727
727
  {
728
728
  id: "suno-generate/song-kpop",
729
729
  name: "K-Pop (vocal)",
730
730
  description: "Polished hook — add lyrics.",
731
731
  group: "Vocals & Songs",
732
- data: { model: "V5_5", instrumental: false, style: "K-pop, polished and energetic, bright synths, punchy drums, layered vocals, Allegro 120 BPM, verse-pre-chorus-chorus" },
732
+ data: { model: "V6", instrumental: false, style: "K-pop, polished and energetic, bright synths, punchy drums, layered vocals, Allegro 120 BPM, verse-pre-chorus-chorus" },
733
733
  },
734
734
  ]
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Join prompt-hint fragments into one injected string.
3
+ *
4
+ * Catalog hints were historically single clauses ("slow camera pan left,
5
+ * rotating horizontally") and every composer glued fragments with `", "`. The
6
+ * tested camera-motion injections are multi-sentence paragraphs that END IN A
7
+ * PERIOD, so the comma join produced `"...no pull-out., beginning with ..."`.
8
+ *
9
+ * This helper keeps the comma between clause-style fragments — every string
10
+ * the composers emitted before it existed is byte-identical — and switches to
11
+ * a single space after a fragment that already closes a sentence. It never
12
+ * strips or adds terminal punctuation: a tested injection ships exactly as
13
+ * approved, and a user's preText/postText is never rewritten.
14
+ */
15
+
16
+ const ENDS_SENTENCE = /[.!?]$/
17
+
18
+ export function joinHintFragments(fragments: ReadonlyArray<string>): string {
19
+ let out = ""
20
+ for (const fragment of fragments) {
21
+ if (!fragment) continue
22
+ if (!out) { out = fragment; continue }
23
+ out += ENDS_SENTENCE.test(out) ? ` ${fragment}` : `, ${fragment}`
24
+ }
25
+ return out
26
+ }
package/src/index.ts CHANGED
@@ -49,7 +49,9 @@ export * from "./atmosphere.js"
49
49
  export * from "./backdrop.js"
50
50
  export * from "./camera-format.js"
51
51
  export * from "./camera-motions.js"
52
+ export * from "./hint-join.js"
52
53
  export * from "./character-fx.js"
54
+ export * from "./character-motion.js"
53
55
  export * from "./color-look.js"
54
56
  export * from "./composition-effects.js"
55
57
  export * from "./era.js"