@nodaro/prompts 1.0.1
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/LICENSE +105 -0
- package/README.md +27 -0
- package/dist/index.cjs +21240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3599 -0
- package/dist/index.d.ts +3599 -0
- package/dist/index.js +20854 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/action-fx.test.ts +155 -0
- package/src/__tests__/apply-picker-json.test.ts +50 -0
- package/src/__tests__/assemble-image-input.test.ts +248 -0
- package/src/__tests__/assemble-suno-input.test.ts +283 -0
- package/src/__tests__/brand-tokens.test.ts +81 -0
- package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
- package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
- package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
- package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
- package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
- package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
- package/src/__tests__/catalog-gapfill.test.ts +53 -0
- package/src/__tests__/character-convergence-image.test.ts +217 -0
- package/src/__tests__/character-default-role-image.test.ts +167 -0
- package/src/__tests__/character-default-role-video.test.ts +168 -0
- package/src/__tests__/character-default-role.test.ts +70 -0
- package/src/__tests__/character-fx.test.ts +200 -0
- package/src/__tests__/entity-prompts-location.test.ts +97 -0
- package/src/__tests__/entity-prompts.test.ts +240 -0
- package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
- package/src/__tests__/factory-presets.test.ts +1045 -0
- package/src/__tests__/factory-snippets.test.ts +68 -0
- package/src/__tests__/framing-multi.test.ts +71 -0
- package/src/__tests__/framing-vantage.test.ts +39 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
- package/src/__tests__/identity-lock.test.ts +46 -0
- package/src/__tests__/instrumentation.test.ts +68 -0
- package/src/__tests__/lighting-multi.test.ts +68 -0
- package/src/__tests__/location-convergence-image.test.ts +124 -0
- package/src/__tests__/mention-lock-flag.test.ts +499 -0
- package/src/__tests__/multi-picker-spec.test.ts +71 -0
- package/src/__tests__/music-genre.test.ts +103 -0
- package/src/__tests__/music-mood.test.ts +87 -0
- package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
- package/src/__tests__/parameter-registry-sync.test.ts +243 -0
- package/src/__tests__/person-age.test.ts +80 -0
- package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
- package/src/__tests__/person-body-axes.test.ts +67 -0
- package/src/__tests__/person-facial-geometry.test.ts +156 -0
- package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
- package/src/__tests__/person-sections.test.ts +18 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
- package/src/__tests__/picker-catalogs-project.test.ts +85 -0
- package/src/__tests__/picker-catalogs.test.ts +53 -0
- package/src/__tests__/picker-limits.test.ts +20 -0
- package/src/__tests__/prompt-builder-segments.test.ts +183 -0
- package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
- package/src/__tests__/prompt-builder.test.ts +1773 -0
- package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
- package/src/__tests__/resolve-prompt-append.test.ts +58 -0
- package/src/__tests__/resolve-prompt.test.ts +44 -0
- package/src/__tests__/role-picker-shared.test.ts +208 -0
- package/src/__tests__/seedance-2-inputs.test.ts +173 -0
- package/src/__tests__/seedance-extend.test.ts +44 -0
- package/src/__tests__/sound-aggregator.test.ts +350 -0
- package/src/__tests__/style-presets.test.ts +34 -0
- package/src/__tests__/temporal-multi.test.ts +74 -0
- package/src/__tests__/transitions.test.ts +213 -0
- package/src/__tests__/video-reference-features.test.ts +41 -0
- package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
- package/src/__tests__/video-reference-resolver.test.ts +233 -0
- package/src/__tests__/video-reference-roles.test.ts +96 -0
- package/src/__tests__/voice-character.test.ts +48 -0
- package/src/__tests__/voice-delivery.test.ts +41 -0
- package/src/__tests__/wardrobe.test.ts +25 -0
- package/src/action-fx.ts +255 -0
- package/src/aesthetic.ts +435 -0
- package/src/assemble-image-input.ts +236 -0
- package/src/assemble-suno-input.ts +147 -0
- package/src/atmosphere.ts +104 -0
- package/src/backdrop.ts +131 -0
- package/src/brand-tokens.ts +154 -0
- package/src/camera-format.ts +76 -0
- package/src/camera-motions.ts +615 -0
- package/src/character-fx.ts +274 -0
- package/src/color-look.ts +103 -0
- package/src/composition-effects.ts +67 -0
- package/src/entity-prompts.ts +231 -0
- package/src/era.ts +292 -0
- package/src/exposure-settings.ts +142 -0
- package/src/factory-presets/generate-image.ts +1644 -0
- package/src/factory-presets/generate-video.ts +1116 -0
- package/src/factory-presets/index.ts +46 -0
- package/src/factory-presets/lottie-overlay.ts +166 -0
- package/src/factory-presets/motion-graphics.ts +350 -0
- package/src/factory-presets/music.ts +734 -0
- package/src/factory-presets/sfx.ts +136 -0
- package/src/factory-presets/shared-image.ts +207 -0
- package/src/factory-presets/switchx.ts +65 -0
- package/src/factory-presets/text.ts +292 -0
- package/src/factory-presets/types.ts +51 -0
- package/src/factory-presets/video-edit.ts +136 -0
- package/src/factory-presets/voice.ts +172 -0
- package/src/factory-presets.ts +2 -0
- package/src/factory-snippets/catalog.ts +105 -0
- package/src/factory-snippets/index.ts +16 -0
- package/src/factory-snippets/types.ts +33 -0
- package/src/framing.ts +634 -0
- package/src/held-prop.ts +187 -0
- package/src/identity-lock.ts +213 -0
- package/src/index.ts +66 -0
- package/src/instrumentation.ts +337 -0
- package/src/lens.ts +59 -0
- package/src/lighting.ts +229 -0
- package/src/loop-subject.ts +276 -0
- package/src/materials.ts +184 -0
- package/src/mood.ts +186 -0
- package/src/music-genre.ts +662 -0
- package/src/music-mood.ts +137 -0
- package/src/object-asset-presets.ts +81 -0
- package/src/parameter-prompt-hint.ts +281 -0
- package/src/person.ts +1368 -0
- package/src/photo-genre.ts +151 -0
- package/src/photographer.ts +612 -0
- package/src/picker-analyzer-registry.ts +374 -0
- package/src/picker-catalogs.ts +858 -0
- package/src/pose.ts +246 -0
- package/src/post-process-effects.ts +94 -0
- package/src/prompt-builder-structured-fields.ts +116 -0
- package/src/prompt-builder.ts +2964 -0
- package/src/prompt-templates.ts +50 -0
- package/src/prompt-wizard-categories.ts +334 -0
- package/src/provider-prompt-doctrine.ts +85 -0
- package/src/render-quality.ts +89 -0
- package/src/resolve-prompt.ts +120 -0
- package/src/seedance-2-inputs.ts +100 -0
- package/src/setting.ts +130 -0
- package/src/sound-aggregator.ts +241 -0
- package/src/style-presets.ts +162 -0
- package/src/style.ts +99 -0
- package/src/styling.ts +585 -0
- package/src/temporal.ts +151 -0
- package/src/transitions.ts +333 -0
- package/src/video-reference-resolver.ts +823 -0
- package/src/voice-character.ts +237 -0
- package/src/voice-delivery.ts +142 -0
- package/src/wardrobe.ts +179 -0
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nodaro/prompts",
|
|
3
|
+
"version": "1.0.1",
|
|
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
|
+
"type": "module",
|
|
6
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
7
|
+
"main": "./dist/index.cjs",
|
|
8
|
+
"module": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"require": "./dist/index.cjs"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"dev": "tsup --watch",
|
|
20
|
+
"test": "vitest run"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@nodaro/shared": "*"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"tsup": "^8.5.0",
|
|
27
|
+
"typescript": "^5.7.3",
|
|
28
|
+
"vitest": "^4.1.2"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=22"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/nodaroai/app.nodaro.ai.git",
|
|
36
|
+
"directory": "packages/prompts"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/nodaroai/app.nodaro.ai/tree/main/packages/prompts#readme",
|
|
39
|
+
"bugs": "https://github.com/nodaroai/app.nodaro.ai/issues",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"files": [
|
|
44
|
+
"dist",
|
|
45
|
+
"README.md",
|
|
46
|
+
"src",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`buildImagePrompt characterization (pre/post segment refactor) > produces stable output across the fixture matrix > connected references directives 1`] = `
|
|
4
|
+
{
|
|
5
|
+
"nativeNegativePrompt": undefined,
|
|
6
|
+
"prompt": "Use these references for the output image:
|
|
7
|
+
- Image 1 (background — a stone castle) — use as the background/setting.
|
|
8
|
+
|
|
9
|
+
Compose them naturally into a single image: a knight at Image 1 (background)",
|
|
10
|
+
"referenceImageUrls": [
|
|
11
|
+
"https://example.com/castle.png",
|
|
12
|
+
],
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
exports[`buildImagePrompt characterization (pre/post segment refactor) > produces stable output across the fixture matrix > empty prompt + style 1`] = `
|
|
17
|
+
{
|
|
18
|
+
"nativeNegativePrompt": undefined,
|
|
19
|
+
"prompt": "
|
|
20
|
+
Style: film noir style, high-contrast black-and-white imagery, deep shadows, venetian-blind lighting and moody 1940s cinema feel",
|
|
21
|
+
"referenceImageUrls": undefined,
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
exports[`buildImagePrompt characterization (pre/post segment refactor) > produces stable output across the fixture matrix > negative appended as Avoid (no native field) 1`] = `
|
|
26
|
+
{
|
|
27
|
+
"nativeNegativePrompt": undefined,
|
|
28
|
+
"prompt": "a knight
|
|
29
|
+
Avoid: blurry, watermark",
|
|
30
|
+
"referenceImageUrls": undefined,
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
exports[`buildImagePrompt characterization (pre/post segment refactor) > produces stable output across the fixture matrix > negative native (imagen4) 1`] = `
|
|
35
|
+
{
|
|
36
|
+
"nativeNegativePrompt": "blurry",
|
|
37
|
+
"prompt": "a knight",
|
|
38
|
+
"referenceImageUrls": undefined,
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
exports[`buildImagePrompt characterization (pre/post segment refactor) > produces stable output across the fixture matrix > plain prompt 1`] = `
|
|
43
|
+
{
|
|
44
|
+
"nativeNegativePrompt": undefined,
|
|
45
|
+
"prompt": "a knight on a cliff",
|
|
46
|
+
"referenceImageUrls": undefined,
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
|
|
50
|
+
exports[`buildImagePrompt characterization (pre/post segment refactor) > produces stable output across the fixture matrix > style appended 1`] = `
|
|
51
|
+
{
|
|
52
|
+
"nativeNegativePrompt": undefined,
|
|
53
|
+
"prompt": "a knight
|
|
54
|
+
Style: cinematic film style, dramatic lighting with cinematic color grading, widescreen aesthetic and film-like depth of field",
|
|
55
|
+
"referenceImageUrls": undefined,
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
exports[`buildImagePrompt characterization (pre/post segment refactor) > produces stable output across the fixture matrix > truncation 1`] = `
|
|
60
|
+
{
|
|
61
|
+
"nativeNegativePrompt": undefined,
|
|
62
|
+
"prompt": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
63
|
+
"referenceImageUrls": undefined,
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import {
|
|
3
|
+
ACTION_FX,
|
|
4
|
+
ACTION_FX_IDS,
|
|
5
|
+
ACTION_FX_CATEGORY_LABELS,
|
|
6
|
+
ACTION_FX_CATEGORY_ORDER,
|
|
7
|
+
getActionFx,
|
|
8
|
+
getActionFxLabel,
|
|
9
|
+
getActionFxPromptHint,
|
|
10
|
+
buildActionFxHints,
|
|
11
|
+
} from "../action-fx.js"
|
|
12
|
+
import type { ActionFxCategory } from "../action-fx.js"
|
|
13
|
+
|
|
14
|
+
const VALID_CATEGORIES: ReadonlyArray<ActionFxCategory> = [
|
|
15
|
+
"disaster", "fire-blasts", "electric", "combat", "sci-fi", "magic",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
describe("ACTION_FX catalog", () => {
|
|
19
|
+
it("has at least 60 entries", () => {
|
|
20
|
+
expect(ACTION_FX.length).toBeGreaterThanOrEqual(60)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("every entry has all 5 fields populated and non-empty", () => {
|
|
24
|
+
for (const fx of ACTION_FX) {
|
|
25
|
+
expect(fx.id, `entry: ${JSON.stringify(fx)}`).toBeTruthy()
|
|
26
|
+
expect(fx.label, `id: ${fx.id}`).toBeTruthy()
|
|
27
|
+
expect(fx.category, `id: ${fx.id}`).toBeTruthy()
|
|
28
|
+
expect(fx.description, `id: ${fx.id}`).toBeTruthy()
|
|
29
|
+
expect(fx.promptHint, `id: ${fx.id}`).toBeTruthy()
|
|
30
|
+
expect(fx.promptHint.length, `id: ${fx.id}`).toBeGreaterThan(20)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("every entry's category is a valid ActionFxCategory", () => {
|
|
35
|
+
for (const fx of ACTION_FX) {
|
|
36
|
+
expect(VALID_CATEGORIES, `id: ${fx.id}, category: ${fx.category}`).toContain(fx.category)
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("all ids are unique", () => {
|
|
41
|
+
const ids = ACTION_FX.map((fx) => fx.id)
|
|
42
|
+
expect(new Set(ids).size).toBe(ids.length)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("ACTION_FX_IDS matches catalog order", () => {
|
|
46
|
+
expect(ACTION_FX_IDS).toEqual(ACTION_FX.map((fx) => fx.id))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("ACTION_FX_CATEGORY_ORDER matches the expected order", () => {
|
|
50
|
+
expect(ACTION_FX_CATEGORY_ORDER).toEqual([
|
|
51
|
+
"disaster", "fire-blasts", "electric", "combat", "sci-fi", "magic",
|
|
52
|
+
])
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("ACTION_FX_CATEGORY_LABELS has a label for every category", () => {
|
|
56
|
+
for (const cat of VALID_CATEGORIES) {
|
|
57
|
+
expect(ACTION_FX_CATEGORY_LABELS[cat]).toBeTruthy()
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it("every category has at least one entry", () => {
|
|
62
|
+
const categoriesPresent = new Set(ACTION_FX.map((fx) => fx.category))
|
|
63
|
+
for (const cat of VALID_CATEGORIES) {
|
|
64
|
+
expect(categoriesPresent, `category ${cat} has no entries`).toContain(cat)
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe("getActionFx", () => {
|
|
70
|
+
it("returns the entry for a valid id", () => {
|
|
71
|
+
const first = ACTION_FX[0]
|
|
72
|
+
expect(getActionFx(first.id)).toEqual(first)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("returns undefined for an unknown id", () => {
|
|
76
|
+
expect(getActionFx("not-a-real-id")).toBeUndefined()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("returns undefined for null/undefined input", () => {
|
|
80
|
+
expect(getActionFx(null)).toBeUndefined()
|
|
81
|
+
expect(getActionFx(undefined)).toBeUndefined()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
describe("getActionFxLabel", () => {
|
|
86
|
+
it("returns the label for a valid id", () => {
|
|
87
|
+
const first = ACTION_FX[0]
|
|
88
|
+
expect(getActionFxLabel(first.id)).toBe(first.label)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
it("returns the fallback when id is unknown and fallback provided", () => {
|
|
92
|
+
expect(getActionFxLabel("nope", "Fallback Label")).toBe("Fallback Label")
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it("returns titleized id when no fallback and unknown", () => {
|
|
96
|
+
expect(getActionFxLabel("hello-world")).toBe("Hello World")
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe("getActionFxPromptHint", () => {
|
|
101
|
+
it("returns the promptHint for a valid id", () => {
|
|
102
|
+
const first = ACTION_FX[0]
|
|
103
|
+
expect(getActionFxPromptHint(first.id)).toBe(first.promptHint)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("returns empty string for unknown id", () => {
|
|
107
|
+
expect(getActionFxPromptHint("nope")).toBe("")
|
|
108
|
+
})
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
describe("buildActionFxHints", () => {
|
|
112
|
+
it("returns empty array for undefined", () => {
|
|
113
|
+
expect(buildActionFxHints(undefined)).toEqual([])
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it("returns empty array for null", () => {
|
|
117
|
+
expect(buildActionFxHints(null)).toEqual([])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it("returns empty array for empty string", () => {
|
|
121
|
+
expect(buildActionFxHints("")).toEqual([])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it("returns single-element array for a known string id", () => {
|
|
125
|
+
const first = ACTION_FX[0]
|
|
126
|
+
expect(buildActionFxHints(first.id)).toEqual([first.promptHint])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("returns empty array for an unknown string id", () => {
|
|
130
|
+
expect(buildActionFxHints("not-a-real-id")).toEqual([])
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("returns multiple promptHints in order for an array of ids", () => {
|
|
134
|
+
const first = ACTION_FX[0]
|
|
135
|
+
const last = ACTION_FX[ACTION_FX.length - 1]
|
|
136
|
+
expect(buildActionFxHints([first.id, last.id])).toEqual([first.promptHint, last.promptHint])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it("deduplicates repeated ids before applying the cap", () => {
|
|
140
|
+
const first = ACTION_FX[0]
|
|
141
|
+
const second = ACTION_FX[1]
|
|
142
|
+
expect(buildActionFxHints([first.id, first.id, second.id]))
|
|
143
|
+
.toEqual([first.promptHint, second.promptHint])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("caps at 2 ids (drops the rest, enforced independently of the UI cap)", () => {
|
|
147
|
+
const ids = ACTION_FX.slice(0, 5).map((fx) => fx.id)
|
|
148
|
+
expect(buildActionFxHints(ids)).toHaveLength(2)
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it("filters out unknown ids inside an array", () => {
|
|
152
|
+
const first = ACTION_FX[0]
|
|
153
|
+
expect(buildActionFxHints([first.id, "not-real"])).toEqual([first.promptHint])
|
|
154
|
+
})
|
|
155
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { buildPickerAnalyzerSpec, applyPickerJson } from "../picker-analyzer-registry.js"
|
|
3
|
+
|
|
4
|
+
const spec = buildPickerAnalyzerSpec("person")
|
|
5
|
+
|
|
6
|
+
describe("applyPickerJson", () => {
|
|
7
|
+
it("override: sets detected dims, clears undetected dimension fields, preserves preText/label", () => {
|
|
8
|
+
const current = { label: "P", type: "man", hairBase: "base-short-straight", preText: "keep me", customAge: 8, age: "age-custom" }
|
|
9
|
+
const patch = applyPickerJson(current, { type: "woman" }, "override", spec)
|
|
10
|
+
expect(patch.type).toBe("woman")
|
|
11
|
+
expect(patch.hairBase).toBeUndefined() // undetected → cleared
|
|
12
|
+
expect("preText" in patch).toBe(false) // never touched
|
|
13
|
+
expect("label" in patch).toBe(false)
|
|
14
|
+
expect(patch.customAge).toBeUndefined() // age dimension reset
|
|
15
|
+
expect(patch.lips).toBeUndefined() // legacy cleared in override
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it("overwrite-detected: only writes detected dims, leaves others untouched", () => {
|
|
19
|
+
const current = { hairBase: "base-short-straight" }
|
|
20
|
+
const patch = applyPickerJson(current, { type: "woman" }, "overwrite-detected", spec)
|
|
21
|
+
expect(patch.type).toBe("woman")
|
|
22
|
+
expect("hairBase" in patch).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("fill-empty: only fills currently-empty fields", () => {
|
|
26
|
+
const current = { type: "man" }
|
|
27
|
+
const patch = applyPickerJson(current, { type: "woman", frame: "frame-slim" }, "fill-empty", spec)
|
|
28
|
+
expect("type" in patch).toBe(false) // already set
|
|
29
|
+
expect(patch.frame).toBe("frame-slim")
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it("coerces multi-limit dims to arrays and single to scalars", () => {
|
|
33
|
+
const patch = applyPickerJson({}, { ethnicity: ["east-asian", "mediterranean"], type: "man" }, "overwrite-detected", spec)
|
|
34
|
+
expect(Array.isArray(patch.ethnicity)).toBe(true)
|
|
35
|
+
expect(typeof patch.type).toBe("string")
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
// Cardinality-mismatch leniency: a misbehaving LLM may emit a bare string for
|
|
39
|
+
// a multi-limit dimension, or an array for a single-limit dimension. coerce()
|
|
40
|
+
// normalizes both shapes so the patch always matches the field's cardinality.
|
|
41
|
+
it("coerces a bare string into an array for a multi-limit dim (ethnicity, limit 2)", () => {
|
|
42
|
+
const patch = applyPickerJson({}, { ethnicity: "east-asian" }, "overwrite-detected", spec)
|
|
43
|
+
expect(patch.ethnicity).toEqual(["east-asian"])
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it("coerces an array into the first-element scalar for a single-limit dim (type, limit 1)", () => {
|
|
47
|
+
const patch = applyPickerJson({}, { type: ["man", "woman"] }, "overwrite-detected", spec)
|
|
48
|
+
expect(patch.type).toBe("man")
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { assembleImageInput } from "../assemble-image-input.js"
|
|
3
|
+
import { buildImagePrompt } from "../prompt-builder.js"
|
|
4
|
+
import { getFramingPromptHint } from "../framing.js"
|
|
5
|
+
import { getLightingPromptHint } from "../lighting.js"
|
|
6
|
+
import type { ConnectedReference } from "@nodaro/shared"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `assembleImageInput` is the keystone of removing the assembly mirror: the
|
|
10
|
+
* platform callers (execute-node / payload-builder) and Studio all route their
|
|
11
|
+
* `generate-image` assembly through it. These tests pin BOTH layers:
|
|
12
|
+
* (a) the id-based composition (direction / structured) — ported from
|
|
13
|
+
* Studio's `assembly.test.ts` as the oracle, and
|
|
14
|
+
* (b) the BY-CONSTRUCTION PARITY the caller refactor relies on: with no
|
|
15
|
+
* direction/structured, the wrapper === the old inline `buildImagePrompt`
|
|
16
|
+
* call + empty-check, byte-for-byte.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// flux-2-max supports reference images (used to assert refs survive the gate).
|
|
20
|
+
const REF_PROVIDER = "flux-2-max"
|
|
21
|
+
|
|
22
|
+
describe("assembleImageInput — id-based composition (Studio oracle)", () => {
|
|
23
|
+
it("returns the bare userPrompt verbatim with no direction/structured", () => {
|
|
24
|
+
const result = assembleImageInput({
|
|
25
|
+
userPrompt: "a knight",
|
|
26
|
+
provider: REF_PROVIDER,
|
|
27
|
+
})
|
|
28
|
+
expect(result).toEqual({
|
|
29
|
+
nativeNegativePrompt: undefined,
|
|
30
|
+
prompt: "a knight",
|
|
31
|
+
referenceImageUrls: undefined,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("bakes a framing hint and forwards a bound reference's URL", () => {
|
|
36
|
+
const ref: ConnectedReference = {
|
|
37
|
+
id: "ref-hero",
|
|
38
|
+
defaultName: "Image 1",
|
|
39
|
+
source: "manual",
|
|
40
|
+
url: "https://r2.example/hero.png",
|
|
41
|
+
}
|
|
42
|
+
const result = assembleImageInput({
|
|
43
|
+
userPrompt: "a knight",
|
|
44
|
+
provider: REF_PROVIDER,
|
|
45
|
+
connectedReferences: [ref],
|
|
46
|
+
direction: { framingId: "medium-shot" },
|
|
47
|
+
})
|
|
48
|
+
expect(result.prompt).toBe(`a knight. ${getFramingPromptHint("medium-shot")}`)
|
|
49
|
+
expect(result.referenceImageUrls).toEqual(["https://r2.example/hero.png"])
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("folds Shot Type + Angle independently (separate framing pills)", () => {
|
|
53
|
+
const result = assembleImageInput({
|
|
54
|
+
userPrompt: "a knight",
|
|
55
|
+
provider: REF_PROVIDER,
|
|
56
|
+
direction: { framingId: "medium-shot", framingAngleId: "low-angle" },
|
|
57
|
+
})
|
|
58
|
+
expect(result.prompt).toBe(
|
|
59
|
+
`a knight. ${getFramingPromptHint("medium-shot")}. ${getFramingPromptHint("low-angle")}`,
|
|
60
|
+
)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("folds multi-dimension direction (Composer Framing path)", () => {
|
|
64
|
+
const kira: ConnectedReference = {
|
|
65
|
+
id: "kira-id",
|
|
66
|
+
defaultName: "Kira",
|
|
67
|
+
source: "wired-character",
|
|
68
|
+
url: "https://r2.example/kira.png",
|
|
69
|
+
description: "a young woman with red hair",
|
|
70
|
+
characterSlug: "kira",
|
|
71
|
+
variantSlug: undefined,
|
|
72
|
+
characterCanonicalDescription: "a young woman with red hair",
|
|
73
|
+
variantDescription: null,
|
|
74
|
+
variantDisplayName: "canonical",
|
|
75
|
+
}
|
|
76
|
+
const result = assembleImageInput({
|
|
77
|
+
userPrompt: "walking through the forest",
|
|
78
|
+
provider: REF_PROVIDER,
|
|
79
|
+
connectedReferences: [kira],
|
|
80
|
+
direction: { framingId: "wide-shot", lightingId: "golden-hour" },
|
|
81
|
+
})
|
|
82
|
+
expect(result.prompt).toContain("walking through the forest")
|
|
83
|
+
expect(result.prompt).toContain(getFramingPromptHint("wide-shot"))
|
|
84
|
+
expect(result.prompt).toContain(getLightingPromptHint("golden-hour"))
|
|
85
|
+
expect(result.referenceImageUrls).toEqual(["https://r2.example/kira.png"])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it("appends a rendered structured-fields fragment", () => {
|
|
89
|
+
const result = assembleImageInput({
|
|
90
|
+
userPrompt: "a portrait",
|
|
91
|
+
provider: REF_PROVIDER,
|
|
92
|
+
structured: { person: { age: 30, gender: "woman", expression: "calm" } },
|
|
93
|
+
})
|
|
94
|
+
expect(result.prompt).toBe("a portrait. Subject: 30 years old, woman, calm expression.")
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
describe("assembleImageInput — empty-prompt throw (opt-in)", () => {
|
|
99
|
+
it("throws on a truly-empty FINAL prompt when throwOnEmpty=true", () => {
|
|
100
|
+
expect(() =>
|
|
101
|
+
assembleImageInput({ userPrompt: " ", provider: REF_PROVIDER, throwOnEmpty: true }),
|
|
102
|
+
).toThrow(/No prompt/)
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("does NOT throw on a blank prompt when throwOnEmpty is omitted, and preserves it VERBATIM (backend parity)", () => {
|
|
106
|
+
const result = assembleImageInput({ userPrompt: " ", provider: REF_PROVIDER })
|
|
107
|
+
// No hints → exact no-op: the blank prompt is preserved byte-for-byte (the
|
|
108
|
+
// old platform path passed it straight to buildImagePrompt, which doesn't
|
|
109
|
+
// trim). Emptiness is decided by the throwOnEmpty check's own `.trim()`, not
|
|
110
|
+
// by mutating the prompt here.
|
|
111
|
+
expect(result.prompt).toBe(" ")
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it("does NOT throw when a bound entity fills an otherwise-empty prompt", () => {
|
|
115
|
+
const kira: ConnectedReference = {
|
|
116
|
+
id: "kira-id",
|
|
117
|
+
defaultName: "Kira",
|
|
118
|
+
source: "wired-character",
|
|
119
|
+
url: "https://r2.example/kira.png",
|
|
120
|
+
characterSlug: "kira",
|
|
121
|
+
variantSlug: undefined,
|
|
122
|
+
characterCanonicalDescription: "a young woman with red hair",
|
|
123
|
+
variantDescription: null,
|
|
124
|
+
variantDisplayName: "canonical",
|
|
125
|
+
}
|
|
126
|
+
const result = assembleImageInput({
|
|
127
|
+
userPrompt: "",
|
|
128
|
+
provider: REF_PROVIDER,
|
|
129
|
+
connectedReferences: [kira],
|
|
130
|
+
throwOnEmpty: true,
|
|
131
|
+
})
|
|
132
|
+
// The canonical-fallback block filled the prompt → no throw.
|
|
133
|
+
expect(result.prompt.length).toBeGreaterThan(0)
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
describe("assembleImageInput — extras threading + per-provider ref gate", () => {
|
|
138
|
+
it("threads extra URLs into referenceImageUrls for a supporting provider", () => {
|
|
139
|
+
const result = assembleImageInput({
|
|
140
|
+
userPrompt: "a city street",
|
|
141
|
+
provider: REF_PROVIDER,
|
|
142
|
+
extraReferenceImageUrls: ["https://r2.example/upload-a.png"],
|
|
143
|
+
})
|
|
144
|
+
expect(result.referenceImageUrls).toEqual(["https://r2.example/upload-a.png"])
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("drops refs for a provider with NO reference support (gate delegated to buildImagePrompt)", () => {
|
|
148
|
+
// `flux-schnell` is a text-to-image model with no reference support, so the
|
|
149
|
+
// builder returns `referenceImageUrls: undefined`.
|
|
150
|
+
const result = assembleImageInput({
|
|
151
|
+
userPrompt: "a city street",
|
|
152
|
+
provider: "flux-schnell",
|
|
153
|
+
extraReferenceImageUrls: ["https://r2.example/upload-a.png"],
|
|
154
|
+
})
|
|
155
|
+
expect(result.referenceImageUrls).toBeUndefined()
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* BY-CONSTRUCTION PARITY: this is the contract the two platform-caller
|
|
161
|
+
* refactors rely on. With NO direction/structured, `assembleImageInput`
|
|
162
|
+
* must return EXACTLY what the old inline `buildImagePrompt` call produced
|
|
163
|
+
* for the same inputs — byte-for-byte on all three result fields.
|
|
164
|
+
*/
|
|
165
|
+
describe("assembleImageInput — platform-caller parity (no direction)", () => {
|
|
166
|
+
const kira: ConnectedReference = {
|
|
167
|
+
id: "ref-kira",
|
|
168
|
+
defaultName: "Kira",
|
|
169
|
+
source: "wired-character",
|
|
170
|
+
description: "young woman with warm smile",
|
|
171
|
+
url: "https://r2/kira-portrait.png",
|
|
172
|
+
characterSlug: "kira",
|
|
173
|
+
variantSlug: undefined,
|
|
174
|
+
characterCanonicalDescription: "young woman, brown eyes, auburn hair",
|
|
175
|
+
variantDescription: null,
|
|
176
|
+
variantDisplayName: "canonical",
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const fixtures: Array<{ name: string; cfg: Parameters<typeof buildImagePrompt>[0] }> = [
|
|
180
|
+
{
|
|
181
|
+
name: "plain prompt, no refs",
|
|
182
|
+
cfg: { prompt: "a knight on a hill", provider: REF_PROVIDER },
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "directRefs via referenceImageUrls + style + negativePrompt",
|
|
186
|
+
cfg: {
|
|
187
|
+
prompt: "a city at night",
|
|
188
|
+
provider: REF_PROVIDER,
|
|
189
|
+
style: "cinematic",
|
|
190
|
+
negativePrompt: "blurry, low quality",
|
|
191
|
+
referenceImageUrls: ["https://r2/a.png", "https://r2/b.png"],
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
name: "connectedReferences (wired character canonical fallback)",
|
|
196
|
+
cfg: {
|
|
197
|
+
prompt: "@kira:1 walking through the rain",
|
|
198
|
+
provider: REF_PROVIDER,
|
|
199
|
+
connectedReferences: [kira],
|
|
200
|
+
suppressedCanonicalCharacterIds: [],
|
|
201
|
+
identityMeta: [],
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
name: "empty prompt (backend never throws here)",
|
|
206
|
+
cfg: { prompt: "", provider: REF_PROVIDER, referenceImageUrls: [] },
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
// Regression guard: the old platform path passed the prompt straight to
|
|
210
|
+
// buildImagePrompt (no trim). The wrapper's no-direction path MUST return
|
|
211
|
+
// it verbatim — trimming here would change the assembled prompt + the
|
|
212
|
+
// recorded jobs.input_data byte-for-byte.
|
|
213
|
+
name: "trailing-whitespace prompt — NOT trimmed (exact no-op parity)",
|
|
214
|
+
cfg: { prompt: "a knight on a hill \n", provider: REF_PROVIDER },
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: "leading-whitespace prompt — NOT trimmed (exact no-op parity)",
|
|
218
|
+
cfg: { prompt: " a city at night", provider: REF_PROVIDER },
|
|
219
|
+
},
|
|
220
|
+
]
|
|
221
|
+
|
|
222
|
+
for (const { name, cfg } of fixtures) {
|
|
223
|
+
it(`matches the old inline buildImagePrompt for: ${name}`, () => {
|
|
224
|
+
const oldResult = buildImagePrompt(cfg)
|
|
225
|
+
|
|
226
|
+
const wrapped = assembleImageInput({
|
|
227
|
+
userPrompt: cfg.prompt,
|
|
228
|
+
provider: cfg.provider,
|
|
229
|
+
connectedReferences: cfg.connectedReferences,
|
|
230
|
+
extraReferenceImageUrls: cfg.referenceImageUrls as string[] | undefined,
|
|
231
|
+
negativePrompt: cfg.negativePrompt,
|
|
232
|
+
style: cfg.style,
|
|
233
|
+
referenceOrder: cfg.referenceOrder,
|
|
234
|
+
identityMeta: cfg.identityMeta,
|
|
235
|
+
suppressedCanonicalCharacterIds: cfg.suppressedCanonicalCharacterIds,
|
|
236
|
+
suppressedCanonicalLocationIds: cfg.suppressedCanonicalLocationIds,
|
|
237
|
+
characterDefs: cfg.characterDefs,
|
|
238
|
+
userTemplates: cfg.userTemplates,
|
|
239
|
+
flowTemplates: cfg.flowTemplates,
|
|
240
|
+
ancestorRefs: cfg.ancestorRefs,
|
|
241
|
+
skipCharacterMentions: cfg.skipCharacterMentions,
|
|
242
|
+
// no `throwOnEmpty` → backend parity (no throw)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
expect(wrapped).toEqual(oldResult)
|
|
246
|
+
})
|
|
247
|
+
}
|
|
248
|
+
})
|