@nodaro/prompts 1.19.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 +9770 -1344
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +187 -7
- package/dist/index.d.ts +187 -7
- package/dist/index.js +9098 -684
- 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__/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 +136 -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__/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/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/index.ts +1 -0
- package/src/parameter-prompt-hint.ts +65 -0
- 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 +0 -2
- package/src/provider-prompt-doctrine.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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,7 @@ export * from "./camera-format.js"
|
|
|
51
51
|
export * from "./camera-motions.js"
|
|
52
52
|
export * from "./hint-join.js"
|
|
53
53
|
export * from "./character-fx.js"
|
|
54
|
+
export * from "./character-motion.js"
|
|
54
55
|
export * from "./color-look.js"
|
|
55
56
|
export * from "./composition-effects.js"
|
|
56
57
|
export * from "./era.js"
|
|
@@ -38,6 +38,8 @@ import { composeCameraMotionHintFromConnections } from "./camera-motions.js"
|
|
|
38
38
|
import { joinHintFragments } from "./hint-join.js"
|
|
39
39
|
import { composeTransitionHintFromConnections, type TransitionDuration, type TransitionIntensity, type TransitionPosition, type TransitionTiming } from "./transitions.js"
|
|
40
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"
|
|
41
43
|
import { buildMaterialHints } from "./materials.js"
|
|
42
44
|
import { curatedAnimalPromptHint, curatedAnimalTerm, curatedVehicleText, curatedWeaponText, curatedFurnitureText } from "./shared-catalog-overlay.js"
|
|
43
45
|
import { getPhotoGenrePromptHint, getPhotoGenreTerm } from "./photo-genre.js"
|
|
@@ -73,6 +75,33 @@ function extractCharacterRefName(node: HintNodeLike): string | undefined {
|
|
|
73
75
|
return undefined
|
|
74
76
|
}
|
|
75
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
|
+
|
|
76
105
|
/** Compose `[preText, mainHint, postText]` into a comma-joined string,
|
|
77
106
|
* honoring the user's free-text fragments around the structured hint.
|
|
78
107
|
* Helpers like `getStylePromptHint` that already include preText/postText
|
|
@@ -219,6 +248,42 @@ function resolveParameterHint(
|
|
|
219
248
|
return withCustomText(data, composeCharacterFxHintFromConnections(effectId, targetNames, timing, mode))
|
|
220
249
|
}
|
|
221
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
|
+
|
|
222
287
|
const base = resolveBaseHint(node.type, data, mode)
|
|
223
288
|
if (base) return base
|
|
224
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
|
|
305
|
-
*
|
|
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.
|
|
309
|
-
*
|
|
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
|
}
|
package/src/picker-catalogs.ts
CHANGED
|
@@ -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
|
{
|
package/src/picker-wiring.ts
CHANGED
|
@@ -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",
|
|
@@ -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".
|