@nodaro/prompts 1.21.0 → 1.25.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 +191 -45
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +362 -207
- package/dist/index.d.ts +362 -207
- package/dist/index.js +183 -46
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/assemble-video-input.test.ts +20 -12
- package/src/__tests__/camera-motion-approved.test.ts +46 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +3 -2
- package/src/__tests__/catalog-packs.test.ts +13 -0
- package/src/__tests__/character-fx-timing-catalogs.test.ts +3 -2
- package/src/__tests__/factory-presets.test.ts +28 -3
- package/src/__tests__/fixtures/camera-motion-approved.json +330 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +32 -16
- package/src/__tests__/hint-join.test.ts +3 -2
- package/src/__tests__/node-prompt-fields.test.ts +2 -2
- package/src/__tests__/parameter-hint-mode.test.ts +17 -5
- package/src/__tests__/prompt-style-section.test.ts +2 -2
- package/src/__tests__/transitions-instant.test.ts +242 -0
- package/src/__tests__/transitions-scope.test.ts +160 -0
- package/src/__tests__/transitions.test.ts +9 -9
- package/src/ad-creative-analysis.ts +99 -0
- package/src/camera-motions.ts +21 -21
- package/src/catalog-packs.ts +13 -2
- package/src/direction-registry.ts +2 -2
- package/src/factory-presets/generate-video.ts +22 -0
- package/src/index.ts +3 -0
- package/src/node-prompt-fields.ts +4 -0
- package/src/picker-catalogs.ts +13 -0
- package/src/ref-binding.ts +19 -0
- package/src/transitions.ts +204 -27
- package/src/video-reference-resolver.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodaro/prompts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "Nodaro's prompt-engineering layer — person/picker catalogs with prompt hints, identity-lock clauses, entity prompt builders, brand presets, and prompt/reference assembly shared by the Nodaro platform and SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test": "vitest run"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@nodaro/shared": "^3.
|
|
23
|
+
"@nodaro/shared": "^3.12.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"tsup": "^8.5.0",
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
renderDirectionHints,
|
|
8
8
|
} from "../direction-registry.js"
|
|
9
9
|
import { getStylePromptHint, getStyleTerm } from "../style.js"
|
|
10
|
-
import {
|
|
10
|
+
import { renderTransitionBases } from "../transitions.js"
|
|
11
11
|
import { getCameraMotionPromptHint, getCameraMotionTerm } from "../camera-motions.js"
|
|
12
12
|
import { getFramingPromptHint } from "../framing.js"
|
|
13
13
|
import { getLightingPromptHint } from "../lighting.js"
|
|
@@ -78,28 +78,36 @@ describe("composeVideoPromptText — the verbosity policy", () => {
|
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
it("renders a MOTION dimension as its compact term, not its full hint", () => {
|
|
81
|
-
const out = composeVideoPromptText("a knight", {
|
|
82
|
-
expect(out).toBe(`a knight. ${
|
|
83
|
-
expect(out).not.toContain(
|
|
81
|
+
const out = composeVideoPromptText("a knight", { cameraMotion: CAMERA_MOTION })
|
|
82
|
+
expect(out).toBe(`a knight. ${getCameraMotionTerm(CAMERA_MOTION)}`)
|
|
83
|
+
expect(out).not.toContain(getCameraMotionPromptHint(CAMERA_MOTION))
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("renders a TRANSITION as `term (hint)` in every mode — the bare term did not steer the model", () => {
|
|
87
|
+
const expected = `a knight. ${renderTransitionBases([TRANSITION])[0]}`
|
|
88
|
+
expect(composeVideoPromptText("a knight", { transition: TRANSITION })).toBe(expected)
|
|
89
|
+
for (const hintMode of ["full", "compact"] as const) {
|
|
90
|
+
expect(composeVideoPromptText("a knight", { transition: TRANSITION }, undefined, { hintMode })).toBe(expected)
|
|
91
|
+
}
|
|
84
92
|
})
|
|
85
93
|
|
|
86
94
|
it("applies both halves of the split policy in ONE fold", () => {
|
|
87
95
|
const out = composeVideoPromptText("a knight", {
|
|
88
96
|
style: STYLE,
|
|
89
|
-
|
|
97
|
+
cameraMotion: CAMERA_MOTION,
|
|
90
98
|
})
|
|
91
99
|
expect(out).toContain(getStylePromptHint(STYLE))
|
|
92
|
-
expect(out).toContain(
|
|
93
|
-
expect(out).not.toContain(
|
|
100
|
+
expect(out).toContain(getCameraMotionTerm(CAMERA_MOTION))
|
|
101
|
+
expect(out).not.toContain(getCameraMotionPromptHint(CAMERA_MOTION))
|
|
94
102
|
})
|
|
95
103
|
|
|
96
104
|
it("honors a whole-fold `hintMode` override in both directions", () => {
|
|
97
105
|
// "full" promotes the motion family to its full clause…
|
|
98
106
|
expect(
|
|
99
|
-
composeVideoPromptText("a knight", {
|
|
107
|
+
composeVideoPromptText("a knight", { cameraMotion: CAMERA_MOTION }, undefined, {
|
|
100
108
|
hintMode: "full",
|
|
101
109
|
}),
|
|
102
|
-
).toBe(`a knight. ${
|
|
110
|
+
).toBe(`a knight. ${getCameraMotionPromptHint(CAMERA_MOTION)}`)
|
|
103
111
|
// …and "compact" demotes the look family to its term. Verbosity does not
|
|
104
112
|
// move a clause: the look term still reads in the section.
|
|
105
113
|
expect(
|
|
@@ -124,7 +132,7 @@ describe("composeVideoPromptText — what stays in the body", () => {
|
|
|
124
132
|
transition: TRANSITION,
|
|
125
133
|
})
|
|
126
134
|
expect(out).toBe(
|
|
127
|
-
`a knight. ${getCameraMotionTerm(CAMERA_MOTION)}. ${
|
|
135
|
+
`a knight. ${getCameraMotionTerm(CAMERA_MOTION)}. ${renderTransitionBases([TRANSITION])[0]}`,
|
|
128
136
|
)
|
|
129
137
|
expect(out).not.toContain("[style]")
|
|
130
138
|
})
|
|
@@ -248,13 +256,13 @@ describe("composeVideoPromptText — tolerance", () => {
|
|
|
248
256
|
describe("composeVideoPromptText — an empty or absent body", () => {
|
|
249
257
|
it("returns the hints alone for an empty prompt (never a leading '. ')", () => {
|
|
250
258
|
expect(composeVideoPromptText("", { transition: TRANSITION })).toBe(
|
|
251
|
-
|
|
259
|
+
renderTransitionBases([TRANSITION])[0],
|
|
252
260
|
)
|
|
253
261
|
})
|
|
254
262
|
|
|
255
263
|
it("returns the hints alone for an ABSENT prompt", () => {
|
|
256
264
|
expect(composeVideoPromptText(undefined, { transition: TRANSITION })).toBe(
|
|
257
|
-
|
|
265
|
+
renderTransitionBases([TRANSITION])[0],
|
|
258
266
|
)
|
|
259
267
|
})
|
|
260
268
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every Camera Motion injection was approved on screen (the Camera Motion Lab,
|
|
3
|
+
* 2026-08/09). A cinematic label alone did not drive the model — "camera
|
|
4
|
+
* trucks to the left" barely moved the image and "orbits to the left" went a
|
|
5
|
+
* random way — so each option carries a description of what the camera
|
|
6
|
+
* physically does, tested take by take on a base frame built for its family.
|
|
7
|
+
*
|
|
8
|
+
* The approved wording is pinned here with the clip that approved it. Changing
|
|
9
|
+
* an injection means re-running it on its base frame first; a new option needs
|
|
10
|
+
* its own approved take before it joins this list.
|
|
11
|
+
*/
|
|
12
|
+
import { readFileSync } from "node:fs"
|
|
13
|
+
import { describe, expect, it } from "vitest"
|
|
14
|
+
import { CAMERA_MOTIONS } from "../camera-motions"
|
|
15
|
+
|
|
16
|
+
interface ApprovedInjection {
|
|
17
|
+
readonly id: string
|
|
18
|
+
readonly approvedClip: string
|
|
19
|
+
readonly injection: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const fixture = JSON.parse(
|
|
23
|
+
readFileSync(new URL("./fixtures/camera-motion-approved.json", import.meta.url), "utf8"),
|
|
24
|
+
) as { readonly unapproved: readonly string[]; readonly approved: readonly ApprovedInjection[] }
|
|
25
|
+
|
|
26
|
+
const byId = new Map(CAMERA_MOTIONS.map((motion) => [motion.id, motion]))
|
|
27
|
+
|
|
28
|
+
describe("Camera Motion injections", () => {
|
|
29
|
+
it.each(fixture.approved.map((row) => [row.id, row] as const))("%s sends its lab-approved wording", (id, row) => {
|
|
30
|
+
expect(byId.get(id)?.promptHint).toBe(row.injection)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("has an approved take for every option but the ones never built", () => {
|
|
34
|
+
const approved = new Set(fixture.approved.map((row) => row.id))
|
|
35
|
+
const unaccounted = CAMERA_MOTIONS.map((motion) => motion.id).filter((id) => !approved.has(id))
|
|
36
|
+
expect(unaccounted.sort()).toEqual([...fixture.unapproved].sort())
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
// An edit-type transition's real-world trigger gets drawn into the scene:
|
|
40
|
+
// "finger tap" put a finger on screen, and the compact term is sent as-is.
|
|
41
|
+
it("never names a transition's physical trigger in the compact term", () => {
|
|
42
|
+
for (const id of ["screen-tap", "phone-flip"]) {
|
|
43
|
+
expect(byId.get(id)?.term ?? "").not.toMatch(/finger|phone|hand|tap|click/i)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
|
-
import { composeCameraMotionHintFromConnections } from "../camera-motions.js"
|
|
2
|
+
import { CAMERA_MOTIONS, composeCameraMotionHintFromConnections } from "../camera-motions.js"
|
|
3
3
|
|
|
4
4
|
describe("composeCameraMotionHintFromConnections", () => {
|
|
5
5
|
it("returns empty string for unknown motion", () => {
|
|
@@ -64,7 +64,8 @@ describe("composeCameraMotionHintFromConnections", () => {
|
|
|
64
64
|
["wide shot"],
|
|
65
65
|
["close-up"],
|
|
66
66
|
)
|
|
67
|
-
|
|
67
|
+
// The static hint itself, whatever its approved wording.
|
|
68
|
+
expect(out).toContain(CAMERA_MOTIONS.find((motion) => motion.id === "static")!.promptHint)
|
|
68
69
|
expect(out).toContain("beginning with wide shot")
|
|
69
70
|
expect(out).toContain("ending with close-up")
|
|
70
71
|
})
|
|
@@ -40,6 +40,19 @@ describe("composePickerCatalogs — pure", () => {
|
|
|
40
40
|
expect(out[0].options!.map((o: PickerOption) => o.id)).toEqual(["forest", "beach", "shul"])
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
it("denies auxiliary dimensions on single catalogs and replaces a removed default", () => {
|
|
44
|
+
const hybrid: PickerCatalog = { ...base[0], defaultValue: "beach", dimensions: [
|
|
45
|
+
{ field: "speed", label: "Speed", options: [{ id: "fast", label: "Fast", term: "fast", promptHint: "fast" }] },
|
|
46
|
+
] }
|
|
47
|
+
const [out] = composePickerCatalogs([hybrid], [{ id: "curation", catalogId: "setting", mode: "deny", denyIds: ["beach", "fast"] }])
|
|
48
|
+
expect(out.options?.map((o) => o.id)).toEqual(["forest"])
|
|
49
|
+
expect(out.dimensions?.[0].options).toEqual([])
|
|
50
|
+
expect(out.defaultValue).toBe("forest")
|
|
51
|
+
expect(hybrid.dimensions?.[0].options).toHaveLength(1)
|
|
52
|
+
const [empty] = composePickerCatalogs([hybrid], [{ id: "empty", catalogId: "setting", mode: "deny", denyIds: ["forest", "beach", "fast"] }])
|
|
53
|
+
expect(empty.defaultValue).toBeUndefined()
|
|
54
|
+
})
|
|
55
|
+
|
|
43
56
|
it("replace swaps the catalog wholesale for its catalogId", () => {
|
|
44
57
|
const vendored: PickerCatalogInput = { nodeType: "setting", label: "Setting", catalogId: "setting", kind: "single",
|
|
45
58
|
valueField: "setting", options: [{ id: "forest", label: "Forest", promptHint: "in a forest", term: "forest" }] }
|
|
@@ -86,8 +86,9 @@ describe("character-fx timing catalogs", () => {
|
|
|
86
86
|
// The one constraint that matters most: these scales must never be unified
|
|
87
87
|
// with the transition ones. Position and duration are worded differently
|
|
88
88
|
// on purpose, so pointing character-fx at the transition rows fails here.
|
|
89
|
-
// Intensity is deliberately NOT compared: its
|
|
90
|
-
//
|
|
89
|
+
// Intensity is deliberately NOT compared: its clauses happen to mostly
|
|
90
|
+
// match, and that is a coincidence, not a shared definition (transitions'
|
|
91
|
+
// "natural" already reads differently since 2026-09-24).
|
|
91
92
|
expect(CHARACTER_FX_POSITIONS.map((o) => o.promptHint)).not.toEqual(
|
|
92
93
|
TRANSITION_POSITIONS.map((o) => o.promptHint),
|
|
93
94
|
)
|
|
@@ -5,7 +5,7 @@ import { COMPOSER_PLAN_MAP, COMPOSER_PLAN_FIELDS } from "@nodaro/shared"
|
|
|
5
5
|
import { STYLE_IDS } from "../index.js"
|
|
6
6
|
import { joinPromptParts } from "../resolve-prompt.js"
|
|
7
7
|
import { nodeSupportsPromptAffixes } from "../node-prompt-fields.js"
|
|
8
|
-
import { IMAGE_GEN_PROVIDERS, MODIFY_IMAGE_PROVIDERS, VIDEO_GEN_PROVIDERS, VIDEO_TO_VIDEO_PROVIDERS, MUSIC_PROVIDERS, SUNO_MODELS, TTS_PROVIDERS, TEXT_TO_AUDIO_PROVIDERS, ALL_CAPTION_STYLES, COMBINE_TRANSITION_IDS, AUDIO_CROSSFADE_CURVE_IDS, aspectRatioOptionsByKind, durationsByMode, IMAGE_PROMPT_MAX, MODEL_CATALOG, NATIVE_NEGATIVE_VIDEO_PROVIDERS } from "@nodaro/shared"
|
|
8
|
+
import { IMAGE_GEN_PROVIDERS, MODIFY_IMAGE_PROVIDERS, VIDEO_GEN_PROVIDERS, VIDEO_TO_VIDEO_PROVIDERS, MUSIC_PROVIDERS, SUNO_MODELS, TTS_PROVIDERS, TEXT_TO_AUDIO_PROVIDERS, ALL_CAPTION_STYLES, COMBINE_TRANSITION_IDS, AUDIO_CROSSFADE_CURVE_IDS, aspectRatioOptionsByKind, durationsByMode, IMAGE_PROMPT_MAX, MODEL_CATALOG, NATIVE_NEGATIVE_VIDEO_PROVIDERS, VIDEO_DURATION_AUTO, supportsAutoVideoDuration } from "@nodaro/shared"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* The text a preset ACTUALLY sends: a preset may ship its doctrine as pre/post
|
|
@@ -327,6 +327,8 @@ describe("generate-video factory preset data validity", () => {
|
|
|
327
327
|
new Set<number>([
|
|
328
328
|
...(durationsByMode("t2v")[provider] ?? []),
|
|
329
329
|
...(durationsByMode("i2v")[provider] ?? []),
|
|
330
|
+
// Auto (-1) is a catalog capability, not a member of the seconds list.
|
|
331
|
+
...(supportsAutoVideoDuration(provider) ? [VIDEO_DURATION_AUTO] : []),
|
|
330
332
|
])
|
|
331
333
|
|
|
332
334
|
it("uses a known video provider when set", () => {
|
|
@@ -382,8 +384,10 @@ describe("generate-video factory preset data validity", () => {
|
|
|
382
384
|
NATIVE_NEGATIVE_VIDEO_PROVIDERS.has(provider),
|
|
383
385
|
`${p.id}: missing negativePrompt on a native-negative provider`,
|
|
384
386
|
).toBe(false)
|
|
387
|
+
// The ASSEMBLED prompt — an affix-only preset (the user's own sentence is
|
|
388
|
+
// the whole prompt) carries its constraint tail in the post text.
|
|
385
389
|
expect(
|
|
386
|
-
(p.data
|
|
390
|
+
effectivePrompt(p.data),
|
|
387
391
|
`${p.id}: omits negativePrompt but carries no in-prompt constraint tail`,
|
|
388
392
|
).toContain("subtitle-free")
|
|
389
393
|
}
|
|
@@ -392,7 +396,7 @@ describe("generate-video factory preset data validity", () => {
|
|
|
392
396
|
it("non-style-pinned generate-video prompts are substantive", () => {
|
|
393
397
|
for (const p of presets) {
|
|
394
398
|
if (p.data.style !== undefined) continue
|
|
395
|
-
const prompt = (p.data
|
|
399
|
+
const prompt = effectivePrompt(p.data)
|
|
396
400
|
expect(prompt.trim().length, `${p.id}: prompt too thin (${prompt.length} chars)`).toBeGreaterThanOrEqual(40)
|
|
397
401
|
}
|
|
398
402
|
})
|
|
@@ -1099,3 +1103,24 @@ describe("factory-presets split integrity", () => {
|
|
|
1099
1103
|
}
|
|
1100
1104
|
})
|
|
1101
1105
|
})
|
|
1106
|
+
|
|
1107
|
+
describe("generate-video/edit-video — the Seedance 2.5 edit shape, up front", () => {
|
|
1108
|
+
const preset = getFactoryPresets("generate-video").find((p) => p.id === "generate-video/edit-video")!
|
|
1109
|
+
|
|
1110
|
+
it("ships exactly what edit mode requires: Adaptive ratio + Auto duration on seedance-2-5", () => {
|
|
1111
|
+
expect(preset.data.provider).toBe("seedance-2-5")
|
|
1112
|
+
expect(preset.data.aspectRatio).toBe("adaptive")
|
|
1113
|
+
expect(preset.data.duration).toBe(VIDEO_DURATION_AUTO)
|
|
1114
|
+
expect(supportsAutoVideoDuration(preset.data.provider as string)).toBe(true)
|
|
1115
|
+
})
|
|
1116
|
+
|
|
1117
|
+
it("owns only the pre & post text — the user's sentence is the prompt", () => {
|
|
1118
|
+
expect(preset.data.prompt).toBeUndefined()
|
|
1119
|
+
expect(preset.data.promptPrefix).toBe("edit {video:1} as follows:\n")
|
|
1120
|
+
// The instruction lands on its own line, straight after the prefix.
|
|
1121
|
+
expect(effectivePrompt({ ...preset.data, prompt: "make it black and white" })).toMatch(
|
|
1122
|
+
/^edit \{video:1\} as follows:\nmake it black and white/,
|
|
1123
|
+
)
|
|
1124
|
+
})
|
|
1125
|
+
})
|
|
1126
|
+
|