@nodaro/prompts 1.19.0 → 1.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +11175 -1797
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +280 -41
- package/dist/index.d.ts +280 -41
- package/dist/index.js +10049 -686
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/adult-only-ratchet.test.ts +26 -1
- package/src/__tests__/character-motion-audit.test.ts +77 -0
- package/src/__tests__/character-motion-docs-example.test.ts +26 -0
- package/src/__tests__/character-motion-exit-visibility.test.ts +183 -0
- package/src/__tests__/character-motion-partner-referent.test.ts +225 -0
- package/src/__tests__/character-motion-timing-catalogs.test.ts +76 -0
- package/src/__tests__/character-motion.test.ts +214 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +136 -0
- package/src/__tests__/frame-delivery.test.ts +93 -0
- package/src/__tests__/graph-composed-unwired-identity.test.ts +166 -0
- package/src/__tests__/parameter-hint-mode.test.ts +28 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +165 -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 +116 -0
- package/src/character-motion/athletic-stunts.ts +159 -0
- package/src/character-motion/camera-interaction.ts +96 -0
- package/src/character-motion/combat-weapons.ts +147 -0
- package/src/character-motion/dance.ts +125 -0
- package/src/character-motion/entrances-exits.ts +92 -0
- package/src/character-motion/evasive-falls.ts +92 -0
- package/src/character-motion/everyday-actions.ts +237 -0
- package/src/character-motion/face-expression.ts +129 -0
- package/src/character-motion/gestures.ts +215 -0
- package/src/character-motion/head-gestures.ts +70 -0
- package/src/character-motion/idle-ambient.ts +89 -0
- package/src/character-motion/posture-shifts.ts +120 -0
- package/src/character-motion/runway.ts +77 -0
- package/src/character-motion/stage-performance.ts +96 -0
- package/src/character-motion/turns-looks.ts +90 -0
- package/src/character-motion/two-person.ts +245 -0
- package/src/character-motion/types.ts +45 -0
- package/src/character-motion/unnatural-horror.ts +57 -0
- package/src/character-motion/vehicles-mounts.ts +105 -0
- package/src/character-motion/walks-runs.ts +97 -0
- package/src/character-motion-diagnostics.ts +49 -0
- package/src/character-motion.ts +362 -0
- package/src/frame-delivery.ts +106 -0
- package/src/index.ts +5 -0
- package/src/parameter-prompt-hint.ts +71 -0
- package/src/picker-analyzer-registry.ts +9 -5
- package/src/picker-catalogs.ts +33 -1
- 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,166 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
|
|
3
|
+
import { getParameterPromptHint } from "../parameter-prompt-hint.js"
|
|
4
|
+
import { getPickerCatalog, type PickerOption } from "../picker-catalogs.js"
|
|
5
|
+
import { composeTransitionHintFromConnections } from "../transitions.js"
|
|
6
|
+
import { composeCharacterFxHintFromConnections } from "../character-fx.js"
|
|
7
|
+
import type { HintEdgeLike, HintGraphContext, HintNodeLike } from "@nodaro/shared"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* THE BOUND ON "compose transition and character-fx from the graph on every
|
|
11
|
+
* path", as a proof rather than a claim.
|
|
12
|
+
*
|
|
13
|
+
* Transition and character-fx joined `EXECUTION_GRAPH_COMPOSED_PARAMETER_TYPES`
|
|
14
|
+
* (@nodaro/shared) and `LABEL_REF_GRAPH_COMPOSED_PARAMETER_TYPES` (backend
|
|
15
|
+
* `services/workflow-engine/label-ref-hint-context.ts`), so every executor now
|
|
16
|
+
* hands them the graph. That deliberately changes the prompt of a workflow that
|
|
17
|
+
* WIRES them — signed off, and the new text is the text the picker's own
|
|
18
|
+
* injection preview has always shown.
|
|
19
|
+
*
|
|
20
|
+
* What was signed off with it is the BOUND: a picker with nothing wired to its
|
|
21
|
+
* own composing handles (`startState` / `endState` for transition, `target` for
|
|
22
|
+
* character-fx) must emit BYTE-IDENTICAL text with and without the graph, so a
|
|
23
|
+
* workflow that wires nothing cannot move at all. This file walks every entry
|
|
24
|
+
* of both catalogs, in both hint modes, under two unwired graph shapes, and
|
|
25
|
+
* asserts that equality — then proves it is not vacuous with wired positive
|
|
26
|
+
* controls that MUST differ.
|
|
27
|
+
*
|
|
28
|
+
* Why two unwired shapes: "unwired" in a real workflow does not mean an
|
|
29
|
+
* isolated node. The picker is normally wired INTO a consumer (an outgoing
|
|
30
|
+
* `cinematography` edge) while nothing feeds its own handles — the walker in
|
|
31
|
+
* `resolveParameterHint` only ever looks at edges whose TARGET is the picker,
|
|
32
|
+
* and this pins that.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
const MODES = ["full", "compact"] as const
|
|
36
|
+
|
|
37
|
+
/** The two pickers this change admitted, with the data field each reads. */
|
|
38
|
+
const SUBJECTS = [
|
|
39
|
+
{ type: "transition", valueField: "transition" },
|
|
40
|
+
{ type: "character-fx", valueField: "characterFx" },
|
|
41
|
+
] as const
|
|
42
|
+
|
|
43
|
+
function optionsFor(type: string): readonly PickerOption[] {
|
|
44
|
+
const catalog = getPickerCatalog(type)
|
|
45
|
+
if (!catalog?.options?.length) throw new Error(`no registered picker catalog options for ${type}`)
|
|
46
|
+
return catalog.options
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pickerNode(type: string, valueField: string, id: string, mode: string): HintNodeLike {
|
|
50
|
+
return { id: "picker", type, data: { [valueField]: id, hintMode: mode } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Nothing at all around the picker. */
|
|
54
|
+
function isolated(node: HintNodeLike): HintGraphContext {
|
|
55
|
+
return { nodes: [node], edges: [] }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The realistic unwired shape: the picker FEEDS a consumer, but nothing feeds it. */
|
|
59
|
+
function wiredIntoConsumer(node: HintNodeLike): HintGraphContext {
|
|
60
|
+
const consumer: HintNodeLike = { id: "consumer", type: "generate-video", data: { prompt: "a man walks forward" } }
|
|
61
|
+
const edges: HintEdgeLike[] = [
|
|
62
|
+
{ source: node.id, target: consumer.id, sourceHandle: null, targetHandle: "cinematography" },
|
|
63
|
+
]
|
|
64
|
+
return { nodes: [node, consumer], edges }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const UNWIRED_SHAPES = [
|
|
68
|
+
{ name: "isolated (no edges at all)", build: isolated },
|
|
69
|
+
{ name: "wired INTO a consumer, nothing wired into it", build: wiredIntoConsumer },
|
|
70
|
+
] as const
|
|
71
|
+
|
|
72
|
+
describe("unwired transition / character-fx pickers are byte-identical with and without the graph", () => {
|
|
73
|
+
for (const { type, valueField } of SUBJECTS) {
|
|
74
|
+
for (const { name, build } of UNWIRED_SHAPES) {
|
|
75
|
+
it(`${type}: every catalog entry × both hint modes, ${name}`, () => {
|
|
76
|
+
const options = optionsFor(type)
|
|
77
|
+
const differences: string[] = []
|
|
78
|
+
let compared = 0
|
|
79
|
+
|
|
80
|
+
for (const option of options) {
|
|
81
|
+
for (const mode of MODES) {
|
|
82
|
+
const node = pickerNode(type, valueField, option.id, mode)
|
|
83
|
+
const withoutGraph = getParameterPromptHint(node)
|
|
84
|
+
const withGraph = getParameterPromptHint(node, build(node))
|
|
85
|
+
compared += 1
|
|
86
|
+
if (withGraph !== withoutGraph) {
|
|
87
|
+
differences.push(`${type}/${option.id}/${mode}: ${JSON.stringify(withoutGraph)} -> ${JSON.stringify(withGraph)}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Non-vacuity of the WALK itself: a catalog that somehow arrived empty
|
|
93
|
+
// would pass a zero-difference assertion trivially.
|
|
94
|
+
expect(compared).toBe(options.length * MODES.length)
|
|
95
|
+
expect(compared).toBeGreaterThan(20)
|
|
96
|
+
expect(differences).toEqual([])
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
it("the catalogs really do carry injecting entries (the comparison is over real text, not all empty strings)", () => {
|
|
102
|
+
// `auto` / `none` inject "" by design and are legitimately compared; this
|
|
103
|
+
// asserts they are not ALL of it, so the identity above is meaningful.
|
|
104
|
+
for (const { type, valueField } of SUBJECTS) {
|
|
105
|
+
const injecting = optionsFor(type).filter(
|
|
106
|
+
(o) => getParameterPromptHint(pickerNode(type, valueField, o.id, "full")).trim().length > 0,
|
|
107
|
+
)
|
|
108
|
+
expect(injecting.length, type).toBeGreaterThan(20)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
describe("positive control — a WIRED picker must differ (or the bound above proves nothing)", () => {
|
|
114
|
+
/** A tone node: free text, so its fragment is identical in both hint modes,
|
|
115
|
+
* which keeps the control honest without re-deriving inherited-mode text. */
|
|
116
|
+
const tone = (id: string, text: string): HintNodeLike => ({ id, type: "tone", data: { tone: text } })
|
|
117
|
+
|
|
118
|
+
it("transition: a startState + endState wire changes the text, in both modes", () => {
|
|
119
|
+
const id = optionsFor("transition").find(
|
|
120
|
+
(o) => getParameterPromptHint(pickerNode("transition", "transition", o.id, "full")).trim().length > 0,
|
|
121
|
+
)!.id
|
|
122
|
+
const start = tone("start", "warm golden morning light")
|
|
123
|
+
const end = tone("end", "cold blue dusk")
|
|
124
|
+
|
|
125
|
+
for (const mode of MODES) {
|
|
126
|
+
const node = pickerNode("transition", "transition", id, mode)
|
|
127
|
+
const ctx: HintGraphContext = {
|
|
128
|
+
nodes: [node, start, end],
|
|
129
|
+
edges: [
|
|
130
|
+
{ source: "start", target: node.id, sourceHandle: null, targetHandle: "startState" },
|
|
131
|
+
{ source: "end", target: node.id, sourceHandle: null, targetHandle: "endState" },
|
|
132
|
+
],
|
|
133
|
+
}
|
|
134
|
+
const composed = getParameterPromptHint(node, ctx)
|
|
135
|
+
expect(composed).not.toBe(getParameterPromptHint(node))
|
|
136
|
+
// Derived from the catalog composer, never a hand-written sentence.
|
|
137
|
+
expect(composed).toBe(
|
|
138
|
+
composeTransitionHintFromConnections(
|
|
139
|
+
id,
|
|
140
|
+
[getParameterPromptHint(start)],
|
|
141
|
+
[getParameterPromptHint(end)],
|
|
142
|
+
undefined,
|
|
143
|
+
mode,
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it("character-fx: a target ref wire changes the text, in both modes", () => {
|
|
150
|
+
const id = optionsFor("character-fx").find(
|
|
151
|
+
(o) => getParameterPromptHint(pickerNode("character-fx", "characterFx", o.id, "full")).trim().length > 0,
|
|
152
|
+
)!.id
|
|
153
|
+
const mira: HintNodeLike = { id: "mira", type: "character", data: { characterName: "Mira" } }
|
|
154
|
+
|
|
155
|
+
for (const mode of MODES) {
|
|
156
|
+
const node = pickerNode("character-fx", "characterFx", id, mode)
|
|
157
|
+
const ctx: HintGraphContext = {
|
|
158
|
+
nodes: [node, mira],
|
|
159
|
+
edges: [{ source: "mira", target: node.id, sourceHandle: null, targetHandle: "target" }],
|
|
160
|
+
}
|
|
161
|
+
const composed = getParameterPromptHint(node, ctx)
|
|
162
|
+
expect(composed).not.toBe(getParameterPromptHint(node))
|
|
163
|
+
expect(composed).toBe(composeCharacterFxHintFromConnections(id, ["Mira"], undefined, mode))
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
})
|
|
@@ -194,6 +194,33 @@ describe("hint mode: compact preserves everything but the base fragment", () =>
|
|
|
194
194
|
expect(compact).toContain("with extreme exaggerated energy, wild flourishes, and dramatic distortion")
|
|
195
195
|
})
|
|
196
196
|
|
|
197
|
+
it("character-motion timing clauses are emitted identically in compact mode", () => {
|
|
198
|
+
const move = firstInjectingId("character-motion")
|
|
199
|
+
const data = { characterMotion: move, position: "full", pace: "explosive" }
|
|
200
|
+
const compact = getParameterPromptHint({ id: "n1", type: "character-motion", data: { ...data, hintMode: "compact" } })
|
|
201
|
+
expect(compact).toContain("the movement plays out across the entire clip")
|
|
202
|
+
expect(compact).toContain("performed with an explosive burst of energy, snapping from stillness into full-speed motion")
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
it("character-motion names target and partner in both modes", () => {
|
|
206
|
+
const ctx: HintGraphContext = {
|
|
207
|
+
nodes: [
|
|
208
|
+
{ id: "c1", type: "character-ref", data: { characterName: "Mira" } },
|
|
209
|
+
{ id: "c2", type: "character-ref", data: { characterName: "Theo" } },
|
|
210
|
+
],
|
|
211
|
+
edges: [
|
|
212
|
+
{ source: "c1", target: "n1", targetHandle: "target" },
|
|
213
|
+
{ source: "c2", target: "n1", targetHandle: "partner" },
|
|
214
|
+
],
|
|
215
|
+
}
|
|
216
|
+
const node: HintNodeLike = { id: "n1", type: "character-motion", data: { characterMotion: "hug-partner" } }
|
|
217
|
+
for (const out of [getParameterPromptHint(node, ctx), getParameterPromptHint(withMode(node, "compact"), ctx)]) {
|
|
218
|
+
expect(out).toContain("Mira")
|
|
219
|
+
expect(out).toContain("Theo")
|
|
220
|
+
expect(out).not.toMatch(/\bthe (subject|partner)\b/)
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
197
224
|
it("the wired character is NAMED in both modes", () => {
|
|
198
225
|
// Regression: compact mode used to run the full mode's substitution — a
|
|
199
226
|
// regex replace of "the subject" — over a `term` that never contains those
|
|
@@ -282,6 +309,7 @@ describe("hint mode: compact preserves everything but the base fragment", () =>
|
|
|
282
309
|
for (const [type, field] of [
|
|
283
310
|
["transition", "transition"],
|
|
284
311
|
["character-fx", "characterFx"],
|
|
312
|
+
["character-motion", "characterMotion"],
|
|
285
313
|
["camera-motion", "cameraMotion"],
|
|
286
314
|
] as const) {
|
|
287
315
|
expect(getParameterPromptHint({ id: "n1", type, data: { [field]: "auto", hintMode: "compact" } })).toBe("")
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest"
|
|
|
2
2
|
import { getParameterPromptHint } from "../parameter-prompt-hint.js"
|
|
3
3
|
import { PARAMETER_NODE_TYPES, getParameterValue } from "@nodaro/shared"
|
|
4
4
|
import { ACTION_FX } from "../action-fx.js"
|
|
5
|
+
import { getCharacterMotionPromptHint, getCharacterMotionTerm } from "../character-motion.js"
|
|
5
6
|
|
|
6
7
|
describe("getParameterPromptHint — action-fx", () => {
|
|
7
8
|
it("returns the catalog hint for a single id", () => {
|
|
@@ -268,3 +269,167 @@ describe("getParameterPromptHint — preText/postText for graph-aware nodes", ()
|
|
|
268
269
|
expect(out).toContain("POST_FX")
|
|
269
270
|
})
|
|
270
271
|
})
|
|
272
|
+
|
|
273
|
+
describe("getParameterPromptHint — character-motion", () => {
|
|
274
|
+
const ctx = {
|
|
275
|
+
nodes: [
|
|
276
|
+
{ id: "c1", type: "character", data: { characterName: "Mira" } },
|
|
277
|
+
{ id: "c2", type: "character", data: { characterName: "Theo" } },
|
|
278
|
+
],
|
|
279
|
+
edges: [
|
|
280
|
+
{ source: "c1", target: "n1", targetHandle: "target" },
|
|
281
|
+
{ source: "c2", target: "n1", targetHandle: "partner" },
|
|
282
|
+
],
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
it("composes the ordered sequence with target and partner names from the graph", () => {
|
|
286
|
+
const out = getParameterPromptHint(
|
|
287
|
+
{ id: "n1", type: "character-motion", data: { characterMotion: ["wave-hello", "hug-partner"] } },
|
|
288
|
+
ctx,
|
|
289
|
+
)
|
|
290
|
+
expect(out).toBe(
|
|
291
|
+
`${getCharacterMotionPromptHint("wave-hello").replace(/\bthe subject\b/g, "Mira")}, then ${getCharacterMotionPromptHint("hug-partner").replace(/\bthe subject\b/g, "Mira").replace(/\bthe partner\b/g, "Theo")}`,
|
|
292
|
+
)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
it("reads position and pace and wraps preText / postText", () => {
|
|
296
|
+
const out = getParameterPromptHint({
|
|
297
|
+
id: "n1",
|
|
298
|
+
type: "character-motion",
|
|
299
|
+
data: { characterMotion: "wave-hello", position: "end", pace: "fast", preText: "PRE_M", postText: "POST_M" },
|
|
300
|
+
})
|
|
301
|
+
expect(out.startsWith("PRE_M")).toBe(true)
|
|
302
|
+
expect(out.endsWith("POST_M")).toBe(true)
|
|
303
|
+
expect(out).toContain("the movement happens in the closing moments of the clip")
|
|
304
|
+
expect(out).toContain("performed quickly")
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
it("compact mode names the target by prefix and the partner in the term", () => {
|
|
308
|
+
const out = getParameterPromptHint(
|
|
309
|
+
{ id: "n1", type: "character-motion", data: { characterMotion: "hug-partner", hintMode: "compact" } },
|
|
310
|
+
ctx,
|
|
311
|
+
)
|
|
312
|
+
expect(out).toBe(`Mira: ${getCharacterMotionTerm("hug-partner").replace(/\bthe partner\b/g, "Theo")}`)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it("ignores edges on other handles", () => {
|
|
316
|
+
const out = getParameterPromptHint(
|
|
317
|
+
{ id: "n1", type: "character-motion", data: { characterMotion: "wave-hello" } },
|
|
318
|
+
{ nodes: ctx.nodes, edges: [{ source: "c1", target: "n1", targetHandle: "in" }] },
|
|
319
|
+
)
|
|
320
|
+
expect(out).toBe(getCharacterMotionPromptHint("wave-hello"))
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// D9 option (b)-lite: a ref wired into `target` OR `partner` that describes a
|
|
325
|
+
// minor (structured `person` age, or an age in its free-text description)
|
|
326
|
+
// makes the composer drop every adultOnly move. wave-hello is neutral;
|
|
327
|
+
// kiss-partner is adultOnly.
|
|
328
|
+
describe("getParameterPromptHint — character-motion minor-age floor", () => {
|
|
329
|
+
const MINOR = { age: "age-child" }
|
|
330
|
+
const ADULT = { age: "age-30s" }
|
|
331
|
+
const motion = { id: "n1", type: "character-motion", data: { characterMotion: ["wave-hello", "kiss-partner"] } }
|
|
332
|
+
const wave = (target: string) => getCharacterMotionPromptHint("wave-hello").replace(/\bthe subject\b/g, target)
|
|
333
|
+
// kiss-partner names the partner twice. A wired name repeats; the unwired
|
|
334
|
+
// fallback is introduced once and referred back to after that.
|
|
335
|
+
const kiss = (target: string, ...partner: readonly string[]) => {
|
|
336
|
+
let i = 0
|
|
337
|
+
return getCharacterMotionPromptHint("kiss-partner")
|
|
338
|
+
.replace(/\bthe subject\b/g, target)
|
|
339
|
+
.replace(/\bthe partner\b/g, () => partner[Math.min(i++, partner.length - 1)]!)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
it("a minor wired to target drops the adult-only move and keeps the neutral one", () => {
|
|
343
|
+
const out = getParameterPromptHint(motion, {
|
|
344
|
+
nodes: [{ id: "c1", type: "character", data: { characterName: "Mira", person: MINOR } }],
|
|
345
|
+
edges: [{ source: "c1", target: "n1", targetHandle: "target" }],
|
|
346
|
+
})
|
|
347
|
+
expect(out).toBe(wave("Mira"))
|
|
348
|
+
expect(out).not.toMatch(/kiss/i)
|
|
349
|
+
})
|
|
350
|
+
|
|
351
|
+
it("an adult wired to target keeps both moves", () => {
|
|
352
|
+
const out = getParameterPromptHint(motion, {
|
|
353
|
+
nodes: [{ id: "c1", type: "character", data: { characterName: "Mira", person: ADULT } }],
|
|
354
|
+
edges: [{ source: "c1", target: "n1", targetHandle: "target" }],
|
|
355
|
+
})
|
|
356
|
+
expect(out).toBe(`${wave("Mira")}, then ${kiss("Mira", "another person", "that same person")}`)
|
|
357
|
+
expect(out).toMatch(/kiss/)
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
it("a minor wired only to partner also drops the adult-only move", () => {
|
|
361
|
+
const out = getParameterPromptHint(motion, {
|
|
362
|
+
nodes: [
|
|
363
|
+
{ id: "c1", type: "character", data: { characterName: "Mira", person: ADULT } },
|
|
364
|
+
{ id: "c2", type: "character", data: { characterName: "Theo", person: MINOR } },
|
|
365
|
+
],
|
|
366
|
+
edges: [
|
|
367
|
+
{ source: "c1", target: "n1", targetHandle: "target" },
|
|
368
|
+
{ source: "c2", target: "n1", targetHandle: "partner" },
|
|
369
|
+
],
|
|
370
|
+
})
|
|
371
|
+
expect(out).toBe(wave("Mira"))
|
|
372
|
+
expect(out).not.toMatch(/kiss/i)
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
it("a minor described only in free text drops the adult-only move", () => {
|
|
376
|
+
for (const field of ["description", "seedPrompt", "canonicalDescription"] as const) {
|
|
377
|
+
const out = getParameterPromptHint(motion, {
|
|
378
|
+
nodes: [{ id: "c1", type: "character", data: { characterName: "Mira", [field]: "a 12-year-old girl in a red coat" } }],
|
|
379
|
+
edges: [{ source: "c1", target: "n1", targetHandle: "target" }],
|
|
380
|
+
})
|
|
381
|
+
expect(out, field).toBe(wave("Mira"))
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
it("an unnamed ref that describes a minor still floors", () => {
|
|
386
|
+
const out = getParameterPromptHint(motion, {
|
|
387
|
+
nodes: [{ id: "c1", type: "character", data: { person: MINOR } }],
|
|
388
|
+
edges: [{ source: "c1", target: "n1", targetHandle: "target" }],
|
|
389
|
+
})
|
|
390
|
+
expect(out).toBe(getCharacterMotionPromptHint("wave-hello"))
|
|
391
|
+
expect(out).not.toMatch(/kiss/i)
|
|
392
|
+
})
|
|
393
|
+
|
|
394
|
+
it("a minor on an unrelated handle does not floor", () => {
|
|
395
|
+
const out = getParameterPromptHint(motion, {
|
|
396
|
+
nodes: [{ id: "c1", type: "character", data: { characterName: "Mira", person: MINOR } }],
|
|
397
|
+
edges: [{ source: "c1", target: "n1", targetHandle: "in" }],
|
|
398
|
+
})
|
|
399
|
+
expect(out).toMatch(/kiss/)
|
|
400
|
+
})
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
// A Creature (data.creatureName) wired to Character Motion's target / partner is
|
|
404
|
+
// named like the other identity refs. The creature read is local to Character
|
|
405
|
+
// Motion: Character FX (and Camera Motion) keep the shared name read unchanged.
|
|
406
|
+
describe("getParameterPromptHint — character-motion names a creature ref", () => {
|
|
407
|
+
const rex = { id: "k1", type: "creature", data: { creatureName: " Rex " } }
|
|
408
|
+
|
|
409
|
+
it("a creature on partner is named in the two-person move", () => {
|
|
410
|
+
const out = getParameterPromptHint(
|
|
411
|
+
{ id: "n1", type: "character-motion", data: { characterMotion: "hug-partner" } },
|
|
412
|
+
{ nodes: [rex], edges: [{ source: "k1", target: "n1", targetHandle: "partner" }] },
|
|
413
|
+
)
|
|
414
|
+
expect(out).toBe(getCharacterMotionPromptHint("hug-partner").replace(/\bthe partner\b/g, "Rex"))
|
|
415
|
+
expect(out).not.toContain("another person")
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it("a creature on target becomes the subject", () => {
|
|
419
|
+
const out = getParameterPromptHint(
|
|
420
|
+
{ id: "n1", type: "character-motion", data: { characterMotion: "wave-hello" } },
|
|
421
|
+
{ nodes: [rex], edges: [{ source: "k1", target: "n1", targetHandle: "target" }] },
|
|
422
|
+
)
|
|
423
|
+
expect(out).toBe(getCharacterMotionPromptHint("wave-hello").replace(/\bthe subject\b/g, "Rex"))
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it("Character FX with a creature on target is unchanged (still unnamed)", () => {
|
|
427
|
+
const fx = { id: "n1", type: "character-fx", data: { characterFx: "werewolf" } }
|
|
428
|
+
const out = getParameterPromptHint(fx, {
|
|
429
|
+
nodes: [rex],
|
|
430
|
+
edges: [{ source: "k1", target: "n1", targetHandle: "target" }],
|
|
431
|
+
})
|
|
432
|
+
expect(out).toBe(getParameterPromptHint(fx))
|
|
433
|
+
expect(out).not.toContain("Rex")
|
|
434
|
+
})
|
|
435
|
+
})
|
|
@@ -146,6 +146,8 @@ const SAMPLE_DATA_BY_TYPE: Record<string, Record<string, unknown>> = {
|
|
|
146
146
|
// CHARACTER_FX[0] is "auto" with an intentionally empty promptHint — use a
|
|
147
147
|
// real effect id so the sync test exercises the hint dispatch.
|
|
148
148
|
"character-fx": { characterFx: "werewolf" },
|
|
149
|
+
// CHARACTER_MOTIONS[0] is the "auto" no-op head — use a real move.
|
|
150
|
+
"character-motion": { characterMotion: "wave-hello" },
|
|
149
151
|
"loop-subject": { loopSubject: firstId(LOOP_SUBJECTS) },
|
|
150
152
|
"scene-count": { count: 5 },
|
|
151
153
|
"duration": { seconds: 8 },
|
|
@@ -86,7 +86,7 @@ describe("registry invariants (all analyzable pickers)", () => {
|
|
|
86
86
|
"lens", "camera-format", "setting", "atmosphere", "style", "mood", "color-look",
|
|
87
87
|
"photographer", "aesthetic", "era", "photo-genre", "backdrop", "render-quality",
|
|
88
88
|
"composition-effects", "post-process-effects", "action-fx", "loop-subject",
|
|
89
|
-
"transition", "character-fx", "pose", "material", "held-prop", "camera-motion",
|
|
89
|
+
"transition", "character-fx", "character-motion", "pose", "material", "held-prop", "camera-motion",
|
|
90
90
|
"animal", "vehicle", "weapon", "furniture",
|
|
91
91
|
]),
|
|
92
92
|
)
|
|
@@ -148,6 +148,6 @@ describe("transition timing catalogs", () => {
|
|
|
148
148
|
const singlesWithDims = PICKER_CATALOGS.filter(
|
|
149
149
|
(c) => c.kind === "single" && c.dimensions,
|
|
150
150
|
).map((c) => c.nodeType)
|
|
151
|
-
expect(singlesWithDims).toEqual(["transition", "character-fx"])
|
|
151
|
+
expect(singlesWithDims).toEqual(["transition", "character-fx", "character-motion"])
|
|
152
152
|
})
|
|
153
153
|
})
|
|
@@ -22,9 +22,18 @@ describe("video reference-image capability (catalog)", () => {
|
|
|
22
22
|
// Replicate reference_images, and the `maxRefImages` merge for grok-i2v /
|
|
23
23
|
// happyhorse-ref2v) — must keep advertising it. Subset check, so wiring a
|
|
24
24
|
// NEW ref-capable model doesn't fail here; only breaking a wired one does.
|
|
25
|
+
// VEO 3.1 QUALITY (`veo3`) is the mirror case, and a costlier one: it DID
|
|
26
|
+
// advertise the feature, the adapter sent generationType:
|
|
27
|
+
// "REFERENCE_2_VIDEO", and KIE answered "Reference to video only supports
|
|
28
|
+
// the Veo Fast model and Veo Lite model." — a 422 after the credits were
|
|
29
|
+
// reserved (production 2026-09-04, app-reports lane G). Reference-to-video
|
|
30
|
+
// exists on the Fast and Lite SKUs only.
|
|
31
|
+
it("excludes veo3 / VEO 3.1 Quality (KIE serves reference-to-video on Fast + Lite only)", () => {
|
|
32
|
+
expect(refModels).not.toContain("veo3")
|
|
33
|
+
})
|
|
34
|
+
|
|
25
35
|
it("includes the wired reference-capable video models", () => {
|
|
26
36
|
for (const id of [
|
|
27
|
-
"veo3",
|
|
28
37
|
"veo3.1",
|
|
29
38
|
"veo3_lite",
|
|
30
39
|
"gemini-omni-video",
|
package/src/age-floor.ts
CHANGED
|
@@ -34,7 +34,7 @@ export interface AdultOnlyEntry {
|
|
|
34
34
|
* sweeps for the flag, and which analyzer keys `FLOORED_PICKER_KEYS` strips
|
|
35
35
|
* flagged ids out of (a `photo-genre` swept for the flag but absent from the
|
|
36
36
|
* strip list let `glamour-portrait` survive on a minor). */
|
|
37
|
-
export const ADULT_SWEPT_CATALOG_IDS = ["styling", "mood", "pose", "photo-genre"] as const
|
|
37
|
+
export const ADULT_SWEPT_CATALOG_IDS = ["styling", "mood", "pose", "photo-genre", "character-motion"] as const
|
|
38
38
|
|
|
39
39
|
/** Membership form of the list above (the sweep does one lookup per catalog). */
|
|
40
40
|
const ADULT_SWEPT_CATALOG_ID_SET: ReadonlySet<string> = new Set(ADULT_SWEPT_CATALOG_IDS)
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { CharacterMotion } from "./types.js"
|
|
2
|
+
|
|
3
|
+
/** animals-pets — authored motion fragments. Stable ids survive copy revisions. */
|
|
4
|
+
export const ANIMALS_PETS_MOTIONS: ReadonlyArray<CharacterMotion> = [
|
|
5
|
+
{ "id": "pet-a-dog", "label": "Pet a dog", "category": "animals-pets", "description": "Flat hand strokes over the dog's skull and neck",
|
|
6
|
+
"promptHint": "the subject lowers an open hand onto the counterpart's head, draws the flat palm back over the skull to the base of the neck, lifts the hand away and repeats the stroke", "term": "pets the counterpart on the head",
|
|
7
|
+
"counterpart": "a dog", "requires": ["dog"] },
|
|
8
|
+
{ "id": "scratch-behind-ears", "label": "Scratch behind ears", "category": "animals-pets", "description": "Fingertips work in small circles behind an ear",
|
|
9
|
+
"promptHint": "the subject reaches to the side of the counterpart's head and works curled fingertips in small circles behind one ear, the wrist rocking, the other hand resting under the muzzle", "term": "scratches behind the ears with the counterpart",
|
|
10
|
+
"counterpart": "an animal", "requires": ["animal"] },
|
|
11
|
+
{ "id": "ruffle-dog-fur", "label": "Ruffle dog fur", "category": "animals-pets", "description": "Fingers scrub through the thick fur at the neck",
|
|
12
|
+
"promptHint": "the subject sinks the fingers deep into the thick fur at the counterpart's neck and scrubs them briskly back and forth through the coat, elbows pumping, then opens the fingers and lifts the hands away", "term": "ruffles the counterpart's fur with both hands",
|
|
13
|
+
"counterpart": "a dog", "requires": ["dog"] },
|
|
14
|
+
{ "id": "scoop-up-pet", "label": "Scoop up pet", "category": "animals-pets", "description": "Bends, slides both hands under, lifts to the chest",
|
|
15
|
+
"promptHint": "the subject bends at the knees, slides one hand under the counterpart's chest and the other under its hindquarters, and straightens upright, drawing the animal in against the subject's chest", "term": "scoops up the counterpart",
|
|
16
|
+
"counterpart": "a small pet", "requires": ["pet"] },
|
|
17
|
+
{ "id": "offer-hand-to-sniff", "label": "Offer hand to sniff", "category": "animals-pets", "description": "Arm extends, knuckles down, hand held still",
|
|
18
|
+
"promptHint": "the subject extends one arm forward at waist height with the fingers curled and the knuckles down, stops the hand short of the counterpart and holds it motionless, shoulders loose", "term": "offers knuckles for the counterpart to sniff",
|
|
19
|
+
"counterpart": "the animal", "requires": ["animal"] },
|
|
20
|
+
{ "id": "offer-a-treat", "label": "Offer treat to dog", "category": "animals-pets", "description": "Holds a treat out for the dog without requiring it to take it.",
|
|
21
|
+
"promptHint": "the subject extends an open palm holding a treat toward the counterpart and holds it steady, waiting for a response", "term": "offers the counterpart a treat on open palm",
|
|
22
|
+
"counterpart": "a dog", "requires": ["dog", "treat"], "aliases": ["Offer a Treat"] },
|
|
23
|
+
{ "id": "get-pulled-by-leash", "label": "Get pulled by leash", "category": "animals-pets", "description": "Leash snaps taut and drags the arm forward",
|
|
24
|
+
"promptHint": "the subject is yanked forward as the leash snaps taut and stumbles a step with one shoulder dragged ahead of the torso, then leans the weight back and digs the heels in", "term": "gets pulled along by the leash" },
|
|
25
|
+
{ "id": "reel-in-leash", "label": "Reel in leash", "category": "animals-pets", "description": "Hand over hand shortens the leash",
|
|
26
|
+
"promptHint": "the subject gathers the leash hand over hand, each fist closing on the line and drawing it back to the hip until the slack is gone, the elbow tucked tight against the ribs", "term": "reels in the leash hand over hand" },
|
|
27
|
+
{ "id": "throw-ball-for-dog", "label": "Throw ball for dog", "category": "animals-pets", "description": "Throws a ball ahead of the dog without adding a fetch return.",
|
|
28
|
+
"promptHint": "the subject faces the same direction as the counterpart, draws the throwing arm back and throws a ball forward, following through with the arm", "term": "throws a ball ahead of the counterpart",
|
|
29
|
+
"counterpart": "a dog", "requires": ["dog", "ball"] },
|
|
30
|
+
{ "id": "kneel-to-greet", "label": "Kneel to greet", "category": "animals-pets", "description": "Drops to one knee and opens both arms",
|
|
31
|
+
"promptHint": "the subject drops onto one knee, spreads both arms wide and leans the chest forward, closing both hands around the counterpart's shoulders and rocking back slightly under the weight", "term": "kneels down to greet the counterpart",
|
|
32
|
+
"counterpart": "a dog", "requires": ["dog"] },
|
|
33
|
+
{ "id": "signal-dog-to-sit", "label": "Signal dog to sit", "category": "animals-pets", "description": "Gives the dog a downward hand signal without requiring a response.",
|
|
34
|
+
"promptHint": "the subject faces the counterpart, raises a flat palm to chest height and lowers it in a deliberate downward signal, then waits", "term": "gives the counterpart a downward sit signal",
|
|
35
|
+
"counterpart": "a dog", "requires": ["dog"] },
|
|
36
|
+
{ "id": "call-dog-over", "label": "Call dog over", "category": "animals-pets", "description": "Crouches and beckons the dog, then waits.",
|
|
37
|
+
"promptHint": "the subject faces the counterpart, lowers into a crouch, pats one thigh and opens both hands in an inviting gesture, then waits", "term": "crouches and beckons the counterpart",
|
|
38
|
+
"counterpart": "a dog", "requires": ["dog"] },
|
|
39
|
+
{ "id": "cuddle-pet-on-lap", "label": "Cuddle pet on lap", "category": "animals-pets", "description": "Curls over a pet on the lap and strokes its back",
|
|
40
|
+
"promptHint": "the subject curls the shoulders forward over the counterpart settled across the lap, runs one hand in long strokes down its back and lets the chin drop toward it", "term": "cuddles the counterpart on the lap",
|
|
41
|
+
"counterpart": "a pet", "requires": ["pet"] },
|
|
42
|
+
{ "id": "hold-cat-to-chest", "label": "Hold cat to chest", "category": "animals-pets", "description": "Gathers a cat up against the sternum",
|
|
43
|
+
"promptHint": "the subject gathers the counterpart up against the chest, one forearm braced under its hindquarters and the other hand flat along its back, and lowers the chin toward its head", "term": "holds the counterpart against the chest",
|
|
44
|
+
"counterpart": "a cat", "requires": ["cat"] },
|
|
45
|
+
{ "id": "lift-cat-up", "label": "Lift cat up", "category": "animals-pets", "description": "Chest and hindquarters supported, lifted to eye level",
|
|
46
|
+
"promptHint": "the subject slides one hand under the counterpart's chest just behind the front legs and the other under its hindquarters, lifts it up to eye level as the arms extend, holds it there a beat before drawing it back in against the chest", "term": "lifts the counterpart up to eye level",
|
|
47
|
+
"counterpart": "a cat", "requires": ["cat"] },
|
|
48
|
+
{ "id": "lift-puppy-to-face", "label": "Lift puppy to face", "category": "animals-pets", "description": "Raises a puppy up nose to nose",
|
|
49
|
+
"promptHint": "the subject raises the counterpart in both hands from lap height up to eye level, lifts the chin and leans in until the two are nose to nose, then lowers the counterpart back down against the subject's chest", "term": "holds the counterpart up to the face",
|
|
50
|
+
"counterpart": "a puppy", "requires": ["puppy"] },
|
|
51
|
+
{ "id": "let-bird-perch", "label": "Let bird perch", "category": "animals-pets", "description": "Index finger held out level and steady",
|
|
52
|
+
"promptHint": "the subject holds one index finger out level in front of the chest and locks the elbow against the ribs to keep the hand from shaking, lifting the whole arm upward as the counterpart steps onto the finger", "term": "holds out a finger for the counterpart",
|
|
53
|
+
"counterpart": "a bird", "requires": ["bird"] },
|
|
54
|
+
{ "id": "feed-the-ducks", "label": "Feed ducks", "category": "animals-pets", "description": "Tosses small pieces of food toward the ducks.",
|
|
55
|
+
"promptHint": "the subject takes small pieces of food from a bag and tosses them underhand toward the counterpart", "term": "tosses small pieces of food toward the counterpart",
|
|
56
|
+
"counterpart": "the ducks", "requires": ["ducks", "food", "bag"], "aliases": ["Feed the Ducks"] },
|
|
57
|
+
{ "id": "stroke-horse-neck", "label": "Stroke horse neck", "category": "animals-pets", "description": "Long flat-palm sweeps down the neck",
|
|
58
|
+
"promptHint": "the subject lays a flat palm high on the counterpart's neck and sweeps it down the length of the crest in long full strokes, the other hand resting at the counterpart's shoulder", "term": "strokes the counterpart's neck",
|
|
59
|
+
"counterpart": "a horse", "requires": ["horse"] },
|
|
60
|
+
{ "id": "brush-the-horse", "label": "Brush the horse", "category": "animals-pets", "description": "Long brush strokes along the lie of the coat",
|
|
61
|
+
"promptHint": "the subject presses a brush to the counterpart's flank and pulls it in long strokes along the lie of the coat, the free hand steadying the counterpart ahead of each pass, the shoulder swinging with the motion", "term": "brushes the counterpart's coat",
|
|
62
|
+
"counterpart": "a horse", "requires": ["horse"] },
|
|
63
|
+
{ "id": "pat-horse-flank", "label": "Pat horse flank", "category": "animals-pets", "description": "Two firm open-palm pats, then rests the hand",
|
|
64
|
+
"promptHint": "the subject brings a flat hand in to pat the counterpart's flank twice with a firm open palm, then leaves the hand resting there and leans a little weight onto it", "term": "pats the counterpart on the flank",
|
|
65
|
+
"counterpart": "a horse", "requires": ["horse"] },
|
|
66
|
+
{ "id": "mount-the-horse", "label": "Mount the horse", "category": "animals-pets", "description": "Foot in the stirrup, swings the leg over",
|
|
67
|
+
"promptHint": "the subject sets one foot into the stirrup, grips the saddle with both hands, springs off the ground and swings the trailing leg over, then settles down and finds the other stirrup", "term": "mounts a horse",
|
|
68
|
+
"deprecated": true, "replacementId": "mount-horse", "aliases": ["mount horse"] },
|
|
69
|
+
{ "id": "back-away-slowly", "label": "Back away slowly from animal", "category": "animals-pets", "description": "Takes small cautious backward steps while facing the animal.",
|
|
70
|
+
"promptHint": "the subject faces the counterpart and takes small cautious backward steps, keeping the upper body steady and the gaze forward", "term": "takes cautious backward steps away from the counterpart",
|
|
71
|
+
"counterpart": "an animal", "requires": ["animal"], "fixedPace": true, "startPose": "standing", "endPose": "standing", "aliases": ["Back Away Slowly", "retreat"] },
|
|
72
|
+
{ "id": "let-it-approach", "label": "Let it approach", "category": "animals-pets", "description": "Goes completely still and waits it out",
|
|
73
|
+
"promptHint": "the subject stops mid-step and goes completely still, arms hanging loose at the sides, the breathing shallow so the chest barely moves, only the eyes tracking the counterpart", "term": "freezes as the counterpart comes near",
|
|
74
|
+
"counterpart": "the animal", "requires": ["animal"] },
|
|
75
|
+
{ "id": "crouch-to-photograph", "label": "Crouch to photograph", "category": "animals-pets", "description": "Drops low and frames the animal with a phone",
|
|
76
|
+
"promptHint": "the subject drops into a low crouch, braces the elbows on the knees and lifts a phone in both hands up in front of the face, holding dead still before tapping the screen with a thumb", "term": "crouches to photograph an animal" },
|
|
77
|
+
{ "id": "tug-the-toy", "label": "Tug toy with dog", "category": "animals-pets", "description": "Pulls one end of a rope toy while the dog holds the other.",
|
|
78
|
+
"promptHint": "the subject holds one end of a rope toy while the counterpart holds the other, leans back with a gentle pull and then steps forward to loosen the tension", "term": "tugs a rope toy held by the counterpart",
|
|
79
|
+
"counterpart": "a dog", "requires": ["dog", "rope toy"], "aliases": ["Tug the Toy"] },
|
|
80
|
+
{ "id": "wipe-muddy-paws", "label": "Wipe muddy paws", "category": "animals-pets", "description": "Kneels and towels one paw at a time",
|
|
81
|
+
"promptHint": "the subject kneels on one knee, lifts the counterpart's paw into a cupped hand, wraps a towel around it and rubs it clean, sets it back down and reaches for the next one", "term": "wipes the counterpart's muddy paws",
|
|
82
|
+
"counterpart": "a dog", "requires": ["dog"] },
|
|
83
|
+
{ "id": "brace-for-jump", "label": "Brace for jump", "category": "animals-pets", "description": "Plants the back foot to absorb a dog jumping up",
|
|
84
|
+
"promptHint": "the subject plants the back foot and drops the pelvis to take the counterpart's weight against the chest, both hands rising to catch its shoulders, the torso rocking back then steadying", "term": "braces as the counterpart jumps up",
|
|
85
|
+
"counterpart": "a dog", "requires": ["dog"] },
|
|
86
|
+
{ "id": "dog-subject-sit", "label": "Dog sits", "category": "animals-pets", "description": "Dog sits.",
|
|
87
|
+
"term": "sits down on the hindquarters", "promptHint": "the subject is a dog that lowers its hindquarters to the ground while keeping the front legs upright",
|
|
88
|
+
"requires": ["dog as target subject"], "aliases": ["animal subject"] },
|
|
89
|
+
{ "id": "dog-subject-lie-down", "label": "Dog lies down", "category": "animals-pets", "description": "Dog lies down.",
|
|
90
|
+
"term": "lies down with all four limbs resting", "promptHint": "the subject is a dog that lowers the chest and hindquarters to rest on the ground",
|
|
91
|
+
"requires": ["dog as target subject"], "aliases": ["animal subject"] },
|
|
92
|
+
{ "id": "dog-subject-wag-tail", "label": "Dog wags tail", "category": "animals-pets", "description": "Dog wags tail.",
|
|
93
|
+
"term": "wags the tail", "promptHint": "the subject is a dog that wags its tail from side to side while keeping the body in place",
|
|
94
|
+
"requires": ["dog as target subject"], "aliases": ["animal subject"] },
|
|
95
|
+
{ "id": "dog-subject-shake", "label": "Dog shakes body", "category": "animals-pets", "description": "Dog shakes body.",
|
|
96
|
+
"term": "shakes the body from head to tail", "promptHint": "the subject is a dog that shakes its body from head through torso to tail and then becomes still",
|
|
97
|
+
"requires": ["dog as target subject"], "aliases": ["animal subject"] },
|
|
98
|
+
{ "id": "cat-subject-stretch", "label": "Cat stretches", "category": "animals-pets", "description": "Cat stretches.",
|
|
99
|
+
"term": "stretches the front legs and back", "promptHint": "the subject is a cat that extends both front legs, lowers the chest and lengthens the back before returning upright",
|
|
100
|
+
"requires": ["cat as target subject"], "aliases": ["animal subject"] },
|
|
101
|
+
{ "id": "horse-subject-walk", "label": "Horse walks", "category": "animals-pets", "description": "Horse walks.",
|
|
102
|
+
"term": "walks with a steady four-beat gait", "promptHint": "the subject is a horse that walks steadily forward in a four-beat gait",
|
|
103
|
+
"requires": ["horse as target subject"], "aliases": ["animal subject"] },
|
|
104
|
+
{ "id": "horse-subject-trot", "label": "Horse trots", "category": "animals-pets", "description": "Horse trots.",
|
|
105
|
+
"term": "trots with alternating diagonal leg pairs", "promptHint": "the subject is a horse that trots forward using alternating diagonal pairs of legs",
|
|
106
|
+
"requires": ["horse as target subject"], "aliases": ["animal subject"] },
|
|
107
|
+
{ "id": "horse-subject-canter", "label": "Horse canters", "category": "animals-pets", "description": "Horse canters.",
|
|
108
|
+
"term": "canters in a rhythmic three-beat gait", "promptHint": "the subject is a horse that canters forward in a rhythmic three-beat gait",
|
|
109
|
+
"requires": ["horse as target subject"], "aliases": ["animal subject"] },
|
|
110
|
+
{ "id": "bird-subject-hop", "label": "Bird hops", "category": "animals-pets", "description": "Bird hops.",
|
|
111
|
+
"term": "hops forward and settles", "promptHint": "the subject is a bird that makes a small hop forward and settles on both feet",
|
|
112
|
+
"requires": ["bird as target subject"], "aliases": ["animal subject"] },
|
|
113
|
+
{ "id": "bird-subject-takeoff", "label": "Bird takes flight", "category": "animals-pets", "description": "Bird takes flight.",
|
|
114
|
+
"term": "pushes off, beating wings to take flight", "promptHint": "the subject is a bird that pushes off its perch and beats its wings to take flight",
|
|
115
|
+
"requires": ["bird as target subject"], "aliases": ["animal subject"] },
|
|
116
|
+
]
|