@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.
- package/dist/index.cjs +9861 -1414
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +209 -10
- package/dist/index.d.ts +209 -10
- package/dist/index.js +9199 -765
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adult-only-ratchet.test.ts +18 -1
- package/src/__tests__/assemble-suno-input.test.ts +14 -1
- package/src/__tests__/character-motion-docs-example.test.ts +26 -0
- package/src/__tests__/character-motion-timing-catalogs.test.ts +76 -0
- package/src/__tests__/character-motion.test.ts +207 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +137 -1
- package/src/__tests__/hint-join.test.ts +57 -0
- package/src/__tests__/parameter-hint-mode.test.ts +28 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +159 -0
- package/src/__tests__/parameter-registry-sync.test.ts +2 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +1 -1
- package/src/__tests__/sound-aggregator.test.ts +1 -1
- package/src/__tests__/transition-timing-catalogs.test.ts +1 -1
- package/src/__tests__/video-reference-features.test.ts +10 -1
- package/src/age-floor.ts +1 -1
- package/src/assemble-suno-input.ts +3 -2
- package/src/camera-motions.ts +44 -42
- package/src/character-motion/animals-pets.ts +89 -0
- package/src/character-motion/athletic-stunts.ts +218 -0
- package/src/character-motion/camera-interaction.ts +137 -0
- package/src/character-motion/combat-weapons.ts +206 -0
- package/src/character-motion/dance.ts +173 -0
- package/src/character-motion/entrances-exits.ts +113 -0
- package/src/character-motion/evasive-falls.ts +134 -0
- package/src/character-motion/everyday-actions.ts +278 -0
- package/src/character-motion/face-expression.ts +182 -0
- package/src/character-motion/gestures.ts +308 -0
- package/src/character-motion/head-gestures.ts +98 -0
- package/src/character-motion/idle-ambient.ts +128 -0
- package/src/character-motion/posture-shifts.ts +143 -0
- package/src/character-motion/runway.ts +101 -0
- package/src/character-motion/stage-performance.ts +131 -0
- package/src/character-motion/turns-looks.ts +116 -0
- package/src/character-motion/two-person.ts +233 -0
- package/src/character-motion/types.ts +42 -0
- package/src/character-motion/unnatural-horror.ts +95 -0
- package/src/character-motion/vehicles-mounts.ts +101 -0
- package/src/character-motion/walks-runs.ts +125 -0
- package/src/character-motion.ts +286 -0
- package/src/factory-presets/music.ts +35 -35
- package/src/hint-join.ts +26 -0
- package/src/index.ts +2 -0
- package/src/parameter-prompt-hint.ts +67 -1
- package/src/picker-analyzer-registry.ts +9 -5
- package/src/picker-catalogs.ts +24 -0
- package/src/picker-wiring.ts +2 -0
- package/src/prompt-wizard-categories.ts +9 -5
- package/src/provider-prompt-doctrine.ts +1 -1
- 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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
]
|
package/src/hint-join.ts
ADDED
|
@@ -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"
|