@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
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { FACTORY_PRESETS, getFactoryPresets, groupFactoryPresets } from "../factory-presets.js"
|
|
3
|
+
import { extractPresetData, PRESET_APPLY_CLEAR_KEYS } from "@nodaro/shared"
|
|
4
|
+
import { COMPOSER_PLAN_MAP, COMPOSER_PLAN_FIELDS } from "@nodaro/shared"
|
|
5
|
+
import { STYLE_IDS } from "../index.js"
|
|
6
|
+
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"
|
|
7
|
+
|
|
8
|
+
describe("FACTORY_PRESETS", () => {
|
|
9
|
+
it("has presets for generate-image", () => {
|
|
10
|
+
expect(getFactoryPresets("generate-image").length).toBeGreaterThan(0)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("returns [] for an unknown node type", () => {
|
|
14
|
+
expect(getFactoryPresets("does-not-exist")).toEqual([])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it("every factory preset has a stable unique id, name, and object data", () => {
|
|
18
|
+
const ids = new Set<string>()
|
|
19
|
+
for (const [, presets] of Object.entries(FACTORY_PRESETS)) {
|
|
20
|
+
for (const p of presets) {
|
|
21
|
+
expect(p.id).toMatch(/.+\/.+/) // "<nodeType>/<slug>"
|
|
22
|
+
expect(ids.has(p.id)).toBe(false)
|
|
23
|
+
ids.add(p.id)
|
|
24
|
+
expect(typeof p.name).toBe("string")
|
|
25
|
+
expect(p.name.length).toBeGreaterThan(0)
|
|
26
|
+
expect(typeof p.data).toBe("object")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("factory preset data never contains excluded/runtime keys", () => {
|
|
32
|
+
for (const [, presets] of Object.entries(FACTORY_PRESETS)) {
|
|
33
|
+
for (const p of presets) {
|
|
34
|
+
// extract is a no-op iff data already excludes runtime/label/fieldMappings
|
|
35
|
+
expect(extractPresetData(p.data)).toEqual(p.data)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it("generate-image factory presets use a known image provider", () => {
|
|
41
|
+
for (const p of getFactoryPresets("generate-image")) {
|
|
42
|
+
if (p.data.provider !== undefined) {
|
|
43
|
+
expect(IMAGE_GEN_PROVIDERS).toContain(p.data.provider as never)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
describe("Seedance Director factory presets", () => {
|
|
50
|
+
const pack = getFactoryPresets("generate-video").filter(
|
|
51
|
+
(p) => p.group === "Seedance Director",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
it("ships at least 10 presets in the Seedance Director group", () => {
|
|
55
|
+
expect(pack.length).toBeGreaterThanOrEqual(10)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("every pack preset targets a Seedance 2 provider with valid catalog settings", () => {
|
|
59
|
+
for (const p of pack) {
|
|
60
|
+
const provider = p.data.provider as string
|
|
61
|
+
expect(["seedance-2", "seedance-2-fast"]).toContain(provider)
|
|
62
|
+
const entry = MODEL_CATALOG[provider]!
|
|
63
|
+
expect(entry.durations).toContain(p.data.duration as number)
|
|
64
|
+
expect(entry.aspectRatios).toContain(p.data.aspectRatio as string)
|
|
65
|
+
expect(p.data.generateAudio).toBe(true)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it("never sets negativePrompt (Seedance has no native negative param — constraints live in the prompt)", () => {
|
|
70
|
+
for (const p of pack) expect(p.data.negativePrompt).toBeUndefined()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("never uses timestamped shots (officially unstable) and always carries the constraint tail", () => {
|
|
74
|
+
for (const p of pack) {
|
|
75
|
+
const prompt = p.data.prompt as string
|
|
76
|
+
expect(prompt).not.toMatch(/\(\s*\d+\s*[-–]\s*\d+\s*s\s*\)/i) // "(0-3s)" style
|
|
77
|
+
expect(prompt.length).toBeLessThanOrEqual(2000)
|
|
78
|
+
expect(prompt).toContain("subtitle-free")
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it("uses curly braces ONLY for {slot || default} placeholders (no {} dialogue — Nodaro variable syntax)", () => {
|
|
83
|
+
for (const p of pack) {
|
|
84
|
+
const stripped = (p.data.prompt as string).replace(/\{[^{}]*\|\|[^{}]*\}/g, "")
|
|
85
|
+
expect(stripped).not.toContain("{")
|
|
86
|
+
expect(stripped).not.toContain("}")
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe("groupFactoryPresets", () => {
|
|
92
|
+
it("returns a single null bucket when nothing is grouped", () => {
|
|
93
|
+
const out = groupFactoryPresets([
|
|
94
|
+
{ id: "a/1", name: "One", data: {} },
|
|
95
|
+
{ id: "a/2", name: "Two", data: {} },
|
|
96
|
+
])
|
|
97
|
+
expect(out).toHaveLength(1)
|
|
98
|
+
expect(out[0]!.group).toBeNull()
|
|
99
|
+
expect(out[0]!.presets.map((p) => p.id)).toEqual(["a/1", "a/2"])
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it("buckets presets by group, preserving first-appearance order", () => {
|
|
103
|
+
const out = groupFactoryPresets([
|
|
104
|
+
{ id: "a/1", name: "One", group: "Folder A", data: {} },
|
|
105
|
+
{ id: "a/2", name: "Two", group: "Folder B", data: {} },
|
|
106
|
+
{ id: "a/3", name: "Three", group: "Folder A", data: {} },
|
|
107
|
+
])
|
|
108
|
+
expect(out.map((g) => g.group)).toEqual(["Folder A", "Folder B"])
|
|
109
|
+
expect(out[0]!.presets.map((p) => p.id)).toEqual(["a/1", "a/3"])
|
|
110
|
+
expect(out[1]!.presets.map((p) => p.id)).toEqual(["a/2"])
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it("keeps ungrouped presets in their own leading null bucket", () => {
|
|
114
|
+
const out = groupFactoryPresets([
|
|
115
|
+
{ id: "a/1", name: "One", data: {} },
|
|
116
|
+
{ id: "a/2", name: "Two", group: "Folder A", data: {} },
|
|
117
|
+
])
|
|
118
|
+
expect(out.map((g) => g.group)).toEqual([null, "Folder A"])
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it("defaults groupKind to folder and respects an explicit section", () => {
|
|
122
|
+
const out = groupFactoryPresets([
|
|
123
|
+
{ id: "a/1", name: "One", group: "F", data: {} },
|
|
124
|
+
{ id: "a/2", name: "Two", group: "S", groupKind: "section", data: {} },
|
|
125
|
+
])
|
|
126
|
+
expect(out[0]!.groupKind).toBe("folder")
|
|
127
|
+
expect(out[1]!.groupKind).toBe("section")
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe("generate-image factory preset data validity", () => {
|
|
132
|
+
const imageAR = aspectRatioOptionsByKind("image")
|
|
133
|
+
// Mirrors the frontend's getAspectRatiosForModel fallback (model-options.ts).
|
|
134
|
+
const DEFAULT_AR = ["1:1", "16:9", "9:16", "4:3"]
|
|
135
|
+
const presets = getFactoryPresets("generate-image")
|
|
136
|
+
|
|
137
|
+
it("pairs every aspectRatio with a provider that actually supports it", () => {
|
|
138
|
+
for (const p of presets) {
|
|
139
|
+
const provider = p.data.provider as string | undefined
|
|
140
|
+
const ar = p.data.aspectRatio as string | undefined
|
|
141
|
+
if (!provider || !ar || ar === "auto") continue
|
|
142
|
+
const valid = imageAR[provider]?.map((o) => o.value) ?? DEFAULT_AR
|
|
143
|
+
expect(
|
|
144
|
+
valid,
|
|
145
|
+
`${p.id}: aspectRatio "${ar}" is not supported by provider "${provider}" — valid: ${valid.join(", ")}`,
|
|
146
|
+
).toContain(ar)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it("only references known style ids", () => {
|
|
151
|
+
for (const p of presets) {
|
|
152
|
+
const style = p.data.style as string | undefined
|
|
153
|
+
if (style === undefined) continue
|
|
154
|
+
expect(STYLE_IDS, `${p.id}: unknown style "${style}"`).toContain(style)
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it("respects prompt (IMAGE_PROMPT_MAX) and negativePrompt (5000) length caps", () => {
|
|
159
|
+
for (const p of presets) {
|
|
160
|
+
const prompt = (p.data.prompt as string | undefined) ?? ""
|
|
161
|
+
const neg = (p.data.negativePrompt as string | undefined) ?? ""
|
|
162
|
+
expect(prompt.length, `${p.id}: prompt exceeds cap`).toBeLessThanOrEqual(IMAGE_PROMPT_MAX)
|
|
163
|
+
expect(neg.length, `${p.id}: negativePrompt exceeds 5000 chars`).toBeLessThanOrEqual(5000)
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it("includes the Reference Sheet group with Character + Location boards", () => {
|
|
168
|
+
const char = presets.find((p) => p.id === "generate-image/character-board")
|
|
169
|
+
const loc = presets.find((p) => p.id === "generate-image/location-board")
|
|
170
|
+
expect(char?.group).toBe("Reference Sheet")
|
|
171
|
+
expect(char?.data.provider).toBe("nano-banana-pro")
|
|
172
|
+
expect(char?.data.resolution).toBe("2K")
|
|
173
|
+
expect(loc?.group).toBe("Reference Sheet")
|
|
174
|
+
expect(loc?.data.provider).toBe("nano-banana-pro")
|
|
175
|
+
expect(loc?.data.resolution).toBe("2K")
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("includes the four new Reference Sheet boards", () => {
|
|
179
|
+
for (const id of [
|
|
180
|
+
"generate-image/product-board", "generate-image/outfit-board",
|
|
181
|
+
"generate-image/scene-board", "generate-image/creature-board",
|
|
182
|
+
]) {
|
|
183
|
+
const b = presets.find((p) => p.id === id)
|
|
184
|
+
expect(b, `${id} missing`).toBeTruthy()
|
|
185
|
+
expect(b!.group).toBe("Reference Sheet")
|
|
186
|
+
expect(b!.data.provider).toBe("nano-banana-pro")
|
|
187
|
+
expect(b!.data.resolution).toBe("2K")
|
|
188
|
+
expect((b!.data.negativePrompt as string)?.length).toBeGreaterThan(0)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("includes the five audit-expansion Reference Sheet boards", () => {
|
|
193
|
+
for (const id of [
|
|
194
|
+
"generate-image/pose-board", "generate-image/vehicle-board",
|
|
195
|
+
"generate-image/food-board", "generate-image/mascot-board",
|
|
196
|
+
"generate-image/pet-board",
|
|
197
|
+
]) {
|
|
198
|
+
const b = presets.find((p) => p.id === id)
|
|
199
|
+
expect(b, `${id} missing`).toBeTruthy()
|
|
200
|
+
expect(b!.group).toBe("Reference Sheet")
|
|
201
|
+
expect(b!.data.provider).toBe("nano-banana-pro")
|
|
202
|
+
expect(b!.data.resolution).toBe("2K")
|
|
203
|
+
expect((b!.data.negativePrompt as string)?.length).toBeGreaterThan(0)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("every Reference Sheet board enforces the render-all-panel-headings clause", () => {
|
|
208
|
+
// 2026-06-10 provider experiment: nano-banana-pro merged the DETAILS panel
|
|
209
|
+
// heading into a neighbor on a board run. The in-prompt "never merging or
|
|
210
|
+
// omitting a panel" clause is the fix — guard it so a rewrite can't drop it.
|
|
211
|
+
const boards = presets.filter((p) => p.group === "Reference Sheet")
|
|
212
|
+
expect(boards.length).toBeGreaterThanOrEqual(11)
|
|
213
|
+
for (const b of boards) {
|
|
214
|
+
expect(b.data.prompt as string, `${b.id}: missing the never-merge-panels clause`).toContain(
|
|
215
|
+
"never merging or omitting a panel",
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("includes the Cast & Consistency grids on nano-banana-2", () => {
|
|
221
|
+
// Grids are FED BACK as identity references — they ride nano-banana-2
|
|
222
|
+
// (cheap, consistency-strong) at 4K so reused panel faces stay sharp.
|
|
223
|
+
for (const id of ["generate-image/character-reference-grid", "generate-image/cast-mega-grid"]) {
|
|
224
|
+
const g = presets.find((p) => p.id === id)
|
|
225
|
+
expect(g, `${id} missing`).toBeTruthy()
|
|
226
|
+
expect(g!.group).toBe("Cast & Consistency")
|
|
227
|
+
expect(g!.data.provider).toBe("nano-banana-2")
|
|
228
|
+
expect(g!.data.aspectRatio).toBe("3:4")
|
|
229
|
+
expect(g!.data.resolution).toBe("4K")
|
|
230
|
+
}
|
|
231
|
+
const scene = presets.find((p) => p.id === "generate-image/cast-scene")
|
|
232
|
+
expect(scene?.group).toBe("Cast & Consistency")
|
|
233
|
+
expect(scene?.data.provider).toBe("nano-banana-2")
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("ships the Handmade & Stop-Motion family with the in-prompt NOT-digital-CG clause", () => {
|
|
237
|
+
// The "NOT digital CG" clause is what holds the tactile handmade look —
|
|
238
|
+
// a negativePrompt alone is not enough. Guard it so edits can't drop it.
|
|
239
|
+
const handmade = presets.filter((p) => p.group === "Handmade & Stop-Motion")
|
|
240
|
+
expect(handmade.length).toBeGreaterThanOrEqual(6)
|
|
241
|
+
for (const p of handmade) {
|
|
242
|
+
expect(p.data.prompt as string, `${p.id}: missing the NOT digital CG clause`).toContain("NOT digital CG")
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("shares the Doodle Overlay edits with modify-image (same catalog, provider valid for both)", () => {
|
|
247
|
+
for (const nodeType of ["generate-image", "modify-image"] as const) {
|
|
248
|
+
for (const slug of ["doodle-overlay", "doodle-overlay-expressive"]) {
|
|
249
|
+
const p = getFactoryPresets(nodeType).find((x) => x.id === `${nodeType}/${slug}`)
|
|
250
|
+
expect(p, `${nodeType}/${slug} missing`).toBeTruthy()
|
|
251
|
+
expect(p!.group).toBe("Edits")
|
|
252
|
+
expect(p!.data.provider).toBe("nano-banana-pro")
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it("every generate-image preset has a non-empty negativePrompt", () => {
|
|
258
|
+
for (const p of presets) {
|
|
259
|
+
const neg = (p.data.negativePrompt as string | undefined) ?? ""
|
|
260
|
+
expect(neg.trim().length, `${p.id}: missing negativePrompt`).toBeGreaterThan(0)
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it("non-style-pinned generate-image prompts are substantive (no thin one-liners)", () => {
|
|
265
|
+
for (const p of presets) {
|
|
266
|
+
if (p.data.style !== undefined) continue // style field carries the look; short prompt is fine
|
|
267
|
+
const prompt = (p.data.prompt as string | undefined) ?? ""
|
|
268
|
+
expect(prompt.trim().length, `${p.id}: prompt too thin (${prompt.length} chars)`).toBeGreaterThanOrEqual(40)
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
it("groups every preset under a folder (no stray ungrouped presets)", () => {
|
|
273
|
+
for (const p of presets) {
|
|
274
|
+
expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
})
|
|
278
|
+
|
|
279
|
+
describe("modify-image factory preset data validity", () => {
|
|
280
|
+
const presets = getFactoryPresets("modify-image")
|
|
281
|
+
|
|
282
|
+
it("uses a known modify-image provider", () => {
|
|
283
|
+
for (const p of presets) {
|
|
284
|
+
if (p.data.provider === undefined) continue
|
|
285
|
+
expect(MODIFY_IMAGE_PROVIDERS, `${p.id}: unknown provider`).toContain(p.data.provider as never)
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
it("only references known style ids", () => {
|
|
290
|
+
for (const p of presets) {
|
|
291
|
+
const style = p.data.style as string | undefined
|
|
292
|
+
if (style === undefined) continue
|
|
293
|
+
expect(STYLE_IDS, `${p.id}: unknown style "${style}"`).toContain(style)
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
it("respects the prompt 2000-char cap", () => {
|
|
298
|
+
for (const p of presets) {
|
|
299
|
+
const prompt = (p.data.prompt as string | undefined) ?? ""
|
|
300
|
+
expect(prompt.length, `${p.id}: prompt exceeds 2000 chars`).toBeLessThanOrEqual(2000)
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
})
|
|
304
|
+
|
|
305
|
+
describe("generate-video factory preset data validity", () => {
|
|
306
|
+
const presets = getFactoryPresets("generate-video")
|
|
307
|
+
const vidAR = aspectRatioOptionsByKind("video")
|
|
308
|
+
// Mirrors the frontend's getAspectRatiosForVideoModel fallback (model-options.ts).
|
|
309
|
+
const VIDEO_AR_FALLBACK = ["16:9", "9:16", "1:1"]
|
|
310
|
+
const validDurations = (provider: string) =>
|
|
311
|
+
new Set<number>([
|
|
312
|
+
...(durationsByMode("t2v")[provider] ?? []),
|
|
313
|
+
...(durationsByMode("i2v")[provider] ?? []),
|
|
314
|
+
])
|
|
315
|
+
|
|
316
|
+
it("uses a known video provider when set", () => {
|
|
317
|
+
for (const p of presets) {
|
|
318
|
+
if (p.data.provider === undefined) continue
|
|
319
|
+
expect(VIDEO_GEN_PROVIDERS, `${p.id}: unknown provider`).toContain(p.data.provider as never)
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
it("pairs every aspectRatio with a provider that supports it", () => {
|
|
324
|
+
for (const p of presets) {
|
|
325
|
+
const provider = p.data.provider as string | undefined
|
|
326
|
+
const ar = p.data.aspectRatio as string | undefined
|
|
327
|
+
if (!provider || !ar) continue
|
|
328
|
+
const valid = vidAR[provider]?.map((o) => o.value) ?? VIDEO_AR_FALLBACK
|
|
329
|
+
expect(
|
|
330
|
+
valid,
|
|
331
|
+
`${p.id}: aspectRatio "${ar}" not supported by "${provider}" (valid: ${valid.join(", ")})`,
|
|
332
|
+
).toContain(ar)
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
it("pairs every duration with a provider that supports it", () => {
|
|
337
|
+
for (const p of presets) {
|
|
338
|
+
const provider = p.data.provider as string | undefined
|
|
339
|
+
const dur = p.data.duration as number | undefined
|
|
340
|
+
if (!provider || dur === undefined) continue
|
|
341
|
+
const valid = validDurations(provider)
|
|
342
|
+
if (valid.size === 0) continue // provider derives duration from input — no lever
|
|
343
|
+
expect(
|
|
344
|
+
[...valid],
|
|
345
|
+
`${p.id}: duration ${dur}s not supported by "${provider}" (valid: ${[...valid].join(", ")})`,
|
|
346
|
+
).toContain(dur)
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
it("respects prompt/negativePrompt length caps (2500)", () => {
|
|
351
|
+
for (const p of presets) {
|
|
352
|
+
expect(((p.data.prompt as string) ?? "").length, `${p.id}: prompt too long`).toBeLessThanOrEqual(2500)
|
|
353
|
+
expect(((p.data.negativePrompt as string) ?? "").length, `${p.id}: negativePrompt too long`).toBeLessThanOrEqual(2500)
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it("every generate-video preset has a non-empty negativePrompt, or in-prompt constraints when the provider has no native negative support", () => {
|
|
358
|
+
for (const p of presets) {
|
|
359
|
+
const neg = ((p.data.negativePrompt as string | undefined) ?? "").trim()
|
|
360
|
+
if (neg.length > 0) continue
|
|
361
|
+
// Providers without a native negative_prompt param (e.g. seedance-*) get
|
|
362
|
+
// "Avoid: …" appended to the prompt by applyVideoNegativePrompt — the
|
|
363
|
+
// official Seedance doctrine puts constraints in the prompt text instead.
|
|
364
|
+
const provider = p.data.provider as string
|
|
365
|
+
expect(
|
|
366
|
+
NATIVE_NEGATIVE_VIDEO_PROVIDERS.has(provider),
|
|
367
|
+
`${p.id}: missing negativePrompt on a native-negative provider`,
|
|
368
|
+
).toBe(false)
|
|
369
|
+
expect(
|
|
370
|
+
(p.data.prompt as string) ?? "",
|
|
371
|
+
`${p.id}: omits negativePrompt but carries no in-prompt constraint tail`,
|
|
372
|
+
).toContain("subtitle-free")
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
it("non-style-pinned generate-video prompts are substantive", () => {
|
|
377
|
+
for (const p of presets) {
|
|
378
|
+
if (p.data.style !== undefined) continue
|
|
379
|
+
const prompt = (p.data.prompt as string | undefined) ?? ""
|
|
380
|
+
expect(prompt.trim().length, `${p.id}: prompt too thin (${prompt.length} chars)`).toBeGreaterThanOrEqual(40)
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
|
|
384
|
+
it("includes the board-driven Scene Recipes on seedance-2", () => {
|
|
385
|
+
// Step-2 companions to the generate-image boards/grids — seedance-2 is the
|
|
386
|
+
// one provider with native audio + quoted-line lip-sync + up-to-9 refs.
|
|
387
|
+
for (const id of [
|
|
388
|
+
"generate-video/viral-meteor-scene",
|
|
389
|
+
"generate-video/cartoon-short-opening",
|
|
390
|
+
"generate-video/cartoon-short-chase",
|
|
391
|
+
"generate-video/cartoon-short-resolution",
|
|
392
|
+
"generate-video/two-character-dialogue",
|
|
393
|
+
"generate-video/disaster-reveal",
|
|
394
|
+
"generate-video/chase-scene",
|
|
395
|
+
]) {
|
|
396
|
+
const p = presets.find((x) => x.id === id)
|
|
397
|
+
expect(p, `${id} missing`).toBeTruthy()
|
|
398
|
+
expect(p!.group).toBe("Scene Recipes")
|
|
399
|
+
expect(p!.data.provider).toBe("seedance-2")
|
|
400
|
+
}
|
|
401
|
+
})
|
|
402
|
+
|
|
403
|
+
it("cartoon Scene Recipes hold the look with an in-prompt NOT-photorealistic clause", () => {
|
|
404
|
+
// Mirror of the image catalog's "NOT digital CG" guard — the clause in the
|
|
405
|
+
// positive prompt is what locks cartoon rendering; negativePrompt alone drifts.
|
|
406
|
+
for (const id of [
|
|
407
|
+
"generate-video/cartoon-short-opening",
|
|
408
|
+
"generate-video/cartoon-short-chase",
|
|
409
|
+
"generate-video/cartoon-short-resolution",
|
|
410
|
+
]) {
|
|
411
|
+
const p = presets.find((x) => x.id === id)
|
|
412
|
+
expect(p?.data.prompt as string, `${id}: missing the NOT photorealistic clause`).toContain("NOT photorealistic")
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
|
|
416
|
+
it("reveal-timing negatives survive on the surprise Scene Recipes", () => {
|
|
417
|
+
// The source guide's load-bearing line — without it the event fires too early.
|
|
418
|
+
const meteor = presets.find((x) => x.id === "generate-video/viral-meteor-scene")
|
|
419
|
+
expect(meteor?.data.negativePrompt as string).toContain("noticing the meteor before impact")
|
|
420
|
+
const reveal = presets.find((x) => x.id === "generate-video/disaster-reveal")
|
|
421
|
+
expect(reveal?.data.negativePrompt as string).toContain("noticing the event before the shockwave")
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it("groups every preset under a folder", () => {
|
|
425
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
426
|
+
})
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
describe("suno-generate factory preset data validity", () => {
|
|
430
|
+
const presets = getFactoryPresets("suno-generate")
|
|
431
|
+
|
|
432
|
+
it("uses a known Suno model when set", () => {
|
|
433
|
+
for (const p of presets) {
|
|
434
|
+
if (p.data.model === undefined) continue
|
|
435
|
+
expect(SUNO_MODELS, `${p.id}: unknown model`).toContain(p.data.model as never)
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
|
|
439
|
+
it("respects Suno field caps (style/negativeStyle 500, title 200, lyrics/prompt 3000)", () => {
|
|
440
|
+
for (const p of presets) {
|
|
441
|
+
expect(((p.data.style as string) ?? "").length, `${p.id}: style > 500`).toBeLessThanOrEqual(500)
|
|
442
|
+
expect(((p.data.negativeStyle as string) ?? "").length, `${p.id}: negativeStyle > 500`).toBeLessThanOrEqual(500)
|
|
443
|
+
expect(((p.data.title as string) ?? "").length, `${p.id}: title > 200`).toBeLessThanOrEqual(200)
|
|
444
|
+
expect(((p.data.lyrics as string) ?? "").length, `${p.id}: lyrics > 3000`).toBeLessThanOrEqual(3000)
|
|
445
|
+
expect(((p.data.prompt as string) ?? "").length, `${p.id}: prompt > 3000`).toBeLessThanOrEqual(3000)
|
|
446
|
+
}
|
|
447
|
+
})
|
|
448
|
+
|
|
449
|
+
it("keeps weights within 0-1 and vocalGender valid", () => {
|
|
450
|
+
for (const p of presets) {
|
|
451
|
+
for (const k of ["styleWeight", "weirdnessConstraint", "audioWeight"] as const) {
|
|
452
|
+
const v = p.data[k] as number | undefined
|
|
453
|
+
if (v === undefined) continue
|
|
454
|
+
expect(v, `${p.id}: ${k} out of 0-1`).toBeGreaterThanOrEqual(0)
|
|
455
|
+
expect(v, `${p.id}: ${k} out of 0-1`).toBeLessThanOrEqual(1)
|
|
456
|
+
}
|
|
457
|
+
if (p.data.vocalGender !== undefined) {
|
|
458
|
+
expect(["male", "female"], `${p.id}: bad vocalGender`).toContain(p.data.vocalGender as never)
|
|
459
|
+
}
|
|
460
|
+
if (p.data.instrumental !== undefined) {
|
|
461
|
+
expect(typeof p.data.instrumental, `${p.id}: instrumental must be boolean`).toBe("boolean")
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
})
|
|
465
|
+
|
|
466
|
+
it("groups every preset under a folder", () => {
|
|
467
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
describe("generate-music factory preset data validity", () => {
|
|
472
|
+
const presets = getFactoryPresets("generate-music")
|
|
473
|
+
|
|
474
|
+
it("uses a known music provider when set", () => {
|
|
475
|
+
for (const p of presets) {
|
|
476
|
+
if (p.data.provider === undefined) continue
|
|
477
|
+
expect(MUSIC_PROVIDERS, `${p.id}: unknown provider`).toContain(p.data.provider as never)
|
|
478
|
+
}
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it("keeps duration within the route's 1-30s range", () => {
|
|
482
|
+
for (const p of presets) {
|
|
483
|
+
const dur = p.data.duration as number | undefined
|
|
484
|
+
if (dur === undefined) continue
|
|
485
|
+
expect(dur, `${p.id}: duration ${dur} below 1`).toBeGreaterThanOrEqual(1)
|
|
486
|
+
expect(dur, `${p.id}: duration ${dur} above 30`).toBeLessThanOrEqual(30)
|
|
487
|
+
}
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it("uses a boolean instrumental flag when set", () => {
|
|
491
|
+
for (const p of presets) {
|
|
492
|
+
if (p.data.instrumental === undefined) continue
|
|
493
|
+
expect(typeof p.data.instrumental, `${p.id}: instrumental must be boolean`).toBe("boolean")
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it("respects prompt (2000) and lyrics (2000) caps", () => {
|
|
498
|
+
for (const p of presets) {
|
|
499
|
+
expect(((p.data.prompt as string) ?? "").length, `${p.id}: prompt too long`).toBeLessThanOrEqual(2000)
|
|
500
|
+
expect(((p.data.lyrics as string) ?? "").length, `${p.id}: lyrics too long`).toBeLessThanOrEqual(2000)
|
|
501
|
+
}
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
it("groups every preset under a folder", () => {
|
|
505
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
describe("text-to-speech factory preset data validity", () => {
|
|
510
|
+
const presets = getFactoryPresets("text-to-speech")
|
|
511
|
+
const unit = (k: string, v: unknown, id: string) => {
|
|
512
|
+
if (v === undefined) return
|
|
513
|
+
expect(v, `${id}: ${k} must be a number`).toBeTypeOf("number")
|
|
514
|
+
expect(v as number, `${id}: ${k} out of 0-1`).toBeGreaterThanOrEqual(0)
|
|
515
|
+
expect(v as number, `${id}: ${k} out of 0-1`).toBeLessThanOrEqual(1)
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
it("uses a known TTS provider when set", () => {
|
|
519
|
+
for (const p of presets) {
|
|
520
|
+
if (p.data.provider === undefined) continue
|
|
521
|
+
expect(TTS_PROVIDERS, `${p.id}: unknown provider`).toContain(p.data.provider as never)
|
|
522
|
+
}
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it("keeps stability / similarityBoost / style in 0-1 and speed in 0.7-1.2", () => {
|
|
526
|
+
for (const p of presets) {
|
|
527
|
+
unit("stability", p.data.stability, p.id)
|
|
528
|
+
unit("similarityBoost", p.data.similarityBoost, p.id)
|
|
529
|
+
unit("style", p.data.style, p.id)
|
|
530
|
+
const speed = p.data.speed as number | undefined
|
|
531
|
+
if (speed !== undefined) {
|
|
532
|
+
expect(speed, `${p.id}: speed below 0.7`).toBeGreaterThanOrEqual(0.7)
|
|
533
|
+
expect(speed, `${p.id}: speed above 1.2`).toBeLessThanOrEqual(1.2)
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
})
|
|
537
|
+
|
|
538
|
+
it("does not pin a user-specific voiceId", () => {
|
|
539
|
+
for (const p of presets) {
|
|
540
|
+
expect(p.data.voiceId, `${p.id}: presets must not hardcode a voiceId`).toBeUndefined()
|
|
541
|
+
}
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
it("groups every preset under a folder", () => {
|
|
545
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
546
|
+
})
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
describe("text-to-audio factory preset data validity", () => {
|
|
550
|
+
const presets = getFactoryPresets("text-to-audio")
|
|
551
|
+
|
|
552
|
+
it("uses a known text-to-audio provider when set", () => {
|
|
553
|
+
for (const p of presets) {
|
|
554
|
+
if (p.data.provider === undefined) continue
|
|
555
|
+
expect(TEXT_TO_AUDIO_PROVIDERS, `${p.id}: unknown provider`).toContain(p.data.provider as never)
|
|
556
|
+
}
|
|
557
|
+
})
|
|
558
|
+
|
|
559
|
+
it("keeps duration in 0.5-30 and promptInfluence in 0-1, loop boolean", () => {
|
|
560
|
+
for (const p of presets) {
|
|
561
|
+
const dur = p.data.duration as number | undefined
|
|
562
|
+
if (dur !== undefined) {
|
|
563
|
+
expect(dur, `${p.id}: duration below 0.5`).toBeGreaterThanOrEqual(0.5)
|
|
564
|
+
expect(dur, `${p.id}: duration above 30`).toBeLessThanOrEqual(30)
|
|
565
|
+
}
|
|
566
|
+
const pi = p.data.promptInfluence as number | undefined
|
|
567
|
+
if (pi !== undefined) {
|
|
568
|
+
expect(pi, `${p.id}: promptInfluence out of 0-1`).toBeGreaterThanOrEqual(0)
|
|
569
|
+
expect(pi, `${p.id}: promptInfluence out of 0-1`).toBeLessThanOrEqual(1)
|
|
570
|
+
}
|
|
571
|
+
if (p.data.loop !== undefined) {
|
|
572
|
+
expect(typeof p.data.loop, `${p.id}: loop must be boolean`).toBe("boolean")
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
it("respects the prompt 2000-char cap", () => {
|
|
578
|
+
for (const p of presets) {
|
|
579
|
+
expect(((p.data.prompt as string) ?? "").length, `${p.id}: prompt too long`).toBeLessThanOrEqual(2000)
|
|
580
|
+
}
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
it("groups every preset under a folder", () => {
|
|
584
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
585
|
+
})
|
|
586
|
+
})
|
|
587
|
+
|
|
588
|
+
describe("llm-chat factory preset data validity", () => {
|
|
589
|
+
const presets = getFactoryPresets("llm-chat")
|
|
590
|
+
|
|
591
|
+
it("keeps temperature in 0-2 and maxTokens in 1-16384", () => {
|
|
592
|
+
for (const p of presets) {
|
|
593
|
+
const t = p.data.temperature as number | undefined
|
|
594
|
+
if (t !== undefined) {
|
|
595
|
+
expect(t, `${p.id}: temperature below 0`).toBeGreaterThanOrEqual(0)
|
|
596
|
+
expect(t, `${p.id}: temperature above 2`).toBeLessThanOrEqual(2)
|
|
597
|
+
}
|
|
598
|
+
const mt = p.data.maxTokens as number | undefined
|
|
599
|
+
if (mt !== undefined) {
|
|
600
|
+
expect(mt, `${p.id}: maxTokens below 1`).toBeGreaterThanOrEqual(1)
|
|
601
|
+
expect(mt, `${p.id}: maxTokens above 16384`).toBeLessThanOrEqual(16384)
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
it("respects the systemPrompt 10000-char cap", () => {
|
|
607
|
+
for (const p of presets) {
|
|
608
|
+
expect(((p.data.systemPrompt as string) ?? "").length, `${p.id}: systemPrompt too long`).toBeLessThanOrEqual(10000)
|
|
609
|
+
}
|
|
610
|
+
})
|
|
611
|
+
|
|
612
|
+
it("groups every preset under a folder", () => {
|
|
613
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
614
|
+
})
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
describe("generate-script factory preset data validity", () => {
|
|
618
|
+
const presets = getFactoryPresets("generate-script")
|
|
619
|
+
|
|
620
|
+
it("keeps sceneCount in 1-20 and targetLength in 5-600", () => {
|
|
621
|
+
for (const p of presets) {
|
|
622
|
+
const sc = p.data.sceneCount as number | undefined
|
|
623
|
+
if (sc !== undefined) {
|
|
624
|
+
expect(sc, `${p.id}: sceneCount below 1`).toBeGreaterThanOrEqual(1)
|
|
625
|
+
expect(sc, `${p.id}: sceneCount above 20`).toBeLessThanOrEqual(20)
|
|
626
|
+
}
|
|
627
|
+
const tl = p.data.targetLength as number | undefined
|
|
628
|
+
if (tl !== undefined) {
|
|
629
|
+
expect(tl, `${p.id}: targetLength below 5`).toBeGreaterThanOrEqual(5)
|
|
630
|
+
expect(tl, `${p.id}: targetLength above 600`).toBeLessThanOrEqual(600)
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it("uses a valid structure and a tone within 200 chars", () => {
|
|
636
|
+
for (const p of presets) {
|
|
637
|
+
if (p.data.structure !== undefined) {
|
|
638
|
+
expect(["freeform", "8-step", "custom"], `${p.id}: bad structure`).toContain(p.data.structure as never)
|
|
639
|
+
}
|
|
640
|
+
expect(((p.data.tone as string) ?? "").length, `${p.id}: tone too long`).toBeLessThanOrEqual(200)
|
|
641
|
+
}
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
it("groups every preset under a folder", () => {
|
|
645
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
646
|
+
})
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
describe("image-to-text factory preset data validity", () => {
|
|
650
|
+
const presets = getFactoryPresets("image-to-text")
|
|
651
|
+
|
|
652
|
+
it("uses a valid detailLevel and customPrompt within 2000 chars", () => {
|
|
653
|
+
for (const p of presets) {
|
|
654
|
+
if (p.data.detailLevel !== undefined) {
|
|
655
|
+
expect(["brief", "detailed", "structured"], `${p.id}: bad detailLevel`).toContain(p.data.detailLevel as never)
|
|
656
|
+
}
|
|
657
|
+
expect(((p.data.customPrompt as string) ?? "").length, `${p.id}: customPrompt too long`).toBeLessThanOrEqual(2000)
|
|
658
|
+
}
|
|
659
|
+
})
|
|
660
|
+
|
|
661
|
+
it("groups every preset under a folder", () => {
|
|
662
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
663
|
+
})
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
describe("voice-design factory preset data validity", () => {
|
|
667
|
+
const presets = getFactoryPresets("voice-design")
|
|
668
|
+
|
|
669
|
+
it("keeps voiceDescription within 1000 chars", () => {
|
|
670
|
+
for (const p of presets) {
|
|
671
|
+
const vd = (p.data.voiceDescription as string) ?? ""
|
|
672
|
+
expect(vd.length, `${p.id}: voiceDescription too long`).toBeLessThanOrEqual(1000)
|
|
673
|
+
expect(vd.length, `${p.id}: voiceDescription empty`).toBeGreaterThan(0)
|
|
674
|
+
}
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
it("groups every preset under a folder", () => {
|
|
678
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
679
|
+
})
|
|
680
|
+
})
|
|
681
|
+
|
|
682
|
+
describe("voice-changer factory preset data validity", () => {
|
|
683
|
+
const presets = getFactoryPresets("voice-changer")
|
|
684
|
+
|
|
685
|
+
it("keeps stability/similarityBoost/style within 0-1 and removeBackgroundNoise boolean", () => {
|
|
686
|
+
for (const p of presets) {
|
|
687
|
+
for (const k of ["stability", "similarityBoost", "style"] as const) {
|
|
688
|
+
const v = p.data[k] as number | undefined
|
|
689
|
+
if (v === undefined) continue
|
|
690
|
+
expect(v, `${p.id}: ${k} out of 0-1`).toBeGreaterThanOrEqual(0)
|
|
691
|
+
expect(v, `${p.id}: ${k} out of 0-1`).toBeLessThanOrEqual(1)
|
|
692
|
+
}
|
|
693
|
+
if (p.data.removeBackgroundNoise !== undefined) {
|
|
694
|
+
expect(typeof p.data.removeBackgroundNoise, `${p.id}: removeBackgroundNoise must be boolean`).toBe("boolean")
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
})
|
|
698
|
+
|
|
699
|
+
it("groups every preset under a folder", () => {
|
|
700
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
701
|
+
})
|
|
702
|
+
})
|
|
703
|
+
|
|
704
|
+
describe("add-captions factory preset data validity", () => {
|
|
705
|
+
const presets = getFactoryPresets("add-captions")
|
|
706
|
+
|
|
707
|
+
it("uses a known caption style and position, fontSize within 12-200", () => {
|
|
708
|
+
for (const p of presets) {
|
|
709
|
+
if (p.data.style !== undefined) {
|
|
710
|
+
expect(ALL_CAPTION_STYLES, `${p.id}: unknown style`).toContain(p.data.style as never)
|
|
711
|
+
}
|
|
712
|
+
if (p.data.position !== undefined) {
|
|
713
|
+
expect(["bottom", "top", "center"], `${p.id}: bad position`).toContain(p.data.position as never)
|
|
714
|
+
}
|
|
715
|
+
const fs = p.data.fontSize as number | undefined
|
|
716
|
+
if (fs !== undefined) {
|
|
717
|
+
expect(fs, `${p.id}: fontSize ${fs} out of 12-200`).toBeGreaterThanOrEqual(12)
|
|
718
|
+
expect(fs, `${p.id}: fontSize ${fs} out of 12-200`).toBeLessThanOrEqual(200)
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
it("groups every preset under a folder", () => {
|
|
724
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
725
|
+
})
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
describe("video-to-video factory preset data validity", () => {
|
|
729
|
+
const presets = getFactoryPresets("video-to-video")
|
|
730
|
+
|
|
731
|
+
it("uses a known video-to-video provider when set", () => {
|
|
732
|
+
for (const p of presets) {
|
|
733
|
+
if (p.data.provider === undefined) continue
|
|
734
|
+
expect(VIDEO_TO_VIDEO_PROVIDERS, `${p.id}: unknown provider`).toContain(p.data.provider as never)
|
|
735
|
+
}
|
|
736
|
+
})
|
|
737
|
+
|
|
738
|
+
it("respects prompt (5000) and negativePrompt (500) caps", () => {
|
|
739
|
+
for (const p of presets) {
|
|
740
|
+
expect(((p.data.prompt as string) ?? "").length, `${p.id}: prompt > 5000`).toBeLessThanOrEqual(5000)
|
|
741
|
+
expect(((p.data.negativePrompt as string) ?? "").length, `${p.id}: negativePrompt > 500`).toBeLessThanOrEqual(500)
|
|
742
|
+
}
|
|
743
|
+
})
|
|
744
|
+
|
|
745
|
+
it("groups every preset under a folder", () => {
|
|
746
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
747
|
+
})
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
describe("combine-videos factory preset data validity", () => {
|
|
751
|
+
const presets = getFactoryPresets("combine-videos")
|
|
752
|
+
|
|
753
|
+
it("uses a known transition, audioMode, and audio crossfade curve when set", () => {
|
|
754
|
+
for (const p of presets) {
|
|
755
|
+
if (p.data.transition !== undefined) {
|
|
756
|
+
expect(COMBINE_TRANSITION_IDS, `${p.id}: unknown transition`).toContain(p.data.transition as never)
|
|
757
|
+
}
|
|
758
|
+
if (p.data.audioCrossfadeCurve !== undefined) {
|
|
759
|
+
expect(AUDIO_CROSSFADE_CURVE_IDS, `${p.id}: unknown audioCrossfadeCurve`).toContain(p.data.audioCrossfadeCurve as never)
|
|
760
|
+
}
|
|
761
|
+
if (p.data.audioMode !== undefined) {
|
|
762
|
+
expect(["keep", "crossfade", "remove"], `${p.id}: bad audioMode`).toContain(p.data.audioMode as never)
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
})
|
|
766
|
+
|
|
767
|
+
it("keeps trim frames int 0-120 and transitionDuration 0-5", () => {
|
|
768
|
+
for (const p of presets) {
|
|
769
|
+
for (const k of ["trimStartFrames", "trimEndFrames"] as const) {
|
|
770
|
+
const v = p.data[k] as number | undefined
|
|
771
|
+
if (v === undefined) continue
|
|
772
|
+
expect(Number.isInteger(v), `${p.id}: ${k} not int`).toBe(true)
|
|
773
|
+
expect(v, `${p.id}: ${k} out of 0-120`).toBeGreaterThanOrEqual(0)
|
|
774
|
+
expect(v, `${p.id}: ${k} out of 0-120`).toBeLessThanOrEqual(120)
|
|
775
|
+
}
|
|
776
|
+
const td = p.data.transitionDuration as number | undefined
|
|
777
|
+
if (td !== undefined) {
|
|
778
|
+
expect(td, `${p.id}: transitionDuration out of 0-5`).toBeGreaterThanOrEqual(0)
|
|
779
|
+
expect(td, `${p.id}: transitionDuration out of 0-5`).toBeLessThanOrEqual(5)
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
})
|
|
783
|
+
|
|
784
|
+
it("groups every preset under a folder", () => {
|
|
785
|
+
for (const p of presets) expect(p.group, `${p.id}: missing group`).toBeTruthy()
|
|
786
|
+
})
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
describe("motion-graphics factory preset data validity", () => {
|
|
790
|
+
const presets = getFactoryPresets("motion-graphics")
|
|
791
|
+
// MotionGraphicsData.aspectRatio union (frontend/src/types/nodes.ts).
|
|
792
|
+
const ASPECTS = ["16:9", "9:16", "1:1", "4:5"]
|
|
793
|
+
// The 6 groups, in spec §4 table order — exact set + order is the catalog contract.
|
|
794
|
+
const GROUP_ORDER = [
|
|
795
|
+
"Titles & Text", "Intros & Logos", "Social & CTA", "UI & Icons", "FX Overlays", "Backgrounds",
|
|
796
|
+
]
|
|
797
|
+
// The authoritative slot contract from the spec §4 table — preset id → required sids.
|
|
798
|
+
const SLOT_CONTRACT: Record<string, string[]> = {
|
|
799
|
+
"motion-graphics/lower-third": ["primaryColor", "nameText", "roleText"],
|
|
800
|
+
"motion-graphics/title-card": ["primaryColor", "titleText", "subtitleText"],
|
|
801
|
+
"motion-graphics/kinetic-typography": ["accentColor", "wordOne", "wordTwo", "wordThree"],
|
|
802
|
+
"motion-graphics/quote-card": ["accentColor", "quoteText", "attributionText"],
|
|
803
|
+
"motion-graphics/end-card-cta": ["primaryColor", "headlineText", "ctaText"],
|
|
804
|
+
"motion-graphics/logo-sting": ["brandColor", "brandName"],
|
|
805
|
+
"motion-graphics/channel-intro": ["primaryColor", "channelName", "taglineText"],
|
|
806
|
+
"motion-graphics/countdown": ["ringColor", "numberColor"],
|
|
807
|
+
"motion-graphics/subscribe-reminder": ["buttonColor", "buttonText"],
|
|
808
|
+
"motion-graphics/like-follow-bug": ["accentColor", "handleText"],
|
|
809
|
+
"motion-graphics/sale-badge": ["badgeColor", "saleText", "detailText"],
|
|
810
|
+
"motion-graphics/story-highlight": ["primaryColor", "headlineText", "subText"],
|
|
811
|
+
"motion-graphics/loader-spinner": ["dotColor", "trailColor"],
|
|
812
|
+
"motion-graphics/success-check": ["strokeColor", "ringColor"],
|
|
813
|
+
"motion-graphics/error-cross": ["strokeColor", "ringColor"],
|
|
814
|
+
"motion-graphics/progress-bar": ["barColor", "trackColor", "labelText"],
|
|
815
|
+
"motion-graphics/notification-pop": ["cardColor", "titleText", "bodyText"],
|
|
816
|
+
"motion-graphics/confetti-burst": ["colorA", "colorB", "colorC", "colorD"],
|
|
817
|
+
"motion-graphics/sparkle-shimmer": ["sparkleColor"],
|
|
818
|
+
"motion-graphics/speed-lines": ["lineColor"],
|
|
819
|
+
"motion-graphics/gradient-blob-loop": ["colorA", "colorB"],
|
|
820
|
+
"motion-graphics/geometric-pattern-loop": ["shapeColor", "backdropColor"],
|
|
821
|
+
}
|
|
822
|
+
// Rows the spec §4 table marks "transparent" — must be the fully-transparent overlay bg.
|
|
823
|
+
const TRANSPARENT_IDS = new Set([
|
|
824
|
+
"motion-graphics/lower-third",
|
|
825
|
+
"motion-graphics/subscribe-reminder",
|
|
826
|
+
"motion-graphics/like-follow-bug",
|
|
827
|
+
"motion-graphics/sale-badge",
|
|
828
|
+
"motion-graphics/loader-spinner",
|
|
829
|
+
"motion-graphics/success-check",
|
|
830
|
+
"motion-graphics/error-cross",
|
|
831
|
+
"motion-graphics/progress-bar",
|
|
832
|
+
"motion-graphics/notification-pop",
|
|
833
|
+
"motion-graphics/confetti-burst",
|
|
834
|
+
"motion-graphics/sparkle-shimmer",
|
|
835
|
+
"motion-graphics/speed-lines",
|
|
836
|
+
])
|
|
837
|
+
|
|
838
|
+
it("ships exactly the 22 catalog presets named in the spec table", () => {
|
|
839
|
+
expect(presets.length).toBe(22)
|
|
840
|
+
expect(new Set(presets.map((p) => p.id))).toEqual(new Set(Object.keys(SLOT_CONTRACT)))
|
|
841
|
+
})
|
|
842
|
+
|
|
843
|
+
it("every preset targets the Lottie engine", () => {
|
|
844
|
+
for (const p of presets) expect(p.data.engine, `${p.id}: not lottie`).toBe("lottie")
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
it("every preset has a non-empty description and group", () => {
|
|
848
|
+
for (const p of presets) {
|
|
849
|
+
expect((p.description ?? "").trim().length, `${p.id}: missing description`).toBeGreaterThan(0)
|
|
850
|
+
expect((p.group ?? "").trim().length, `${p.id}: missing group`).toBeGreaterThan(0)
|
|
851
|
+
}
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
it("uses exactly the six spec groups, in table order", () => {
|
|
855
|
+
const seen: string[] = []
|
|
856
|
+
for (const p of presets) if (p.group && !seen.includes(p.group)) seen.push(p.group)
|
|
857
|
+
expect(seen).toEqual(GROUP_ORDER)
|
|
858
|
+
})
|
|
859
|
+
|
|
860
|
+
it("aspectRatio is in the MotionGraphicsData union", () => {
|
|
861
|
+
for (const p of presets) {
|
|
862
|
+
expect(ASPECTS, `${p.id}: bad aspectRatio "${String(p.data.aspectRatio)}"`).toContain(p.data.aspectRatio as never)
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
it("durationSeconds is between 2 and 8", () => {
|
|
867
|
+
for (const p of presets) {
|
|
868
|
+
const d = p.data.durationSeconds as number
|
|
869
|
+
expect(d, `${p.id}: duration ${d} below 2`).toBeGreaterThanOrEqual(2)
|
|
870
|
+
expect(d, `${p.id}: duration ${d} above 8`).toBeLessThanOrEqual(8)
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
|
|
874
|
+
it("backgroundColor parses as a hex color", () => {
|
|
875
|
+
for (const p of presets) {
|
|
876
|
+
const bg = p.data.backgroundColor as string
|
|
877
|
+
expect(bg, `${p.id}: bg "${bg}" not hex`).toMatch(/^#(?:[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/)
|
|
878
|
+
}
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
it("never carries motionPlan (generated state is solved generically, not per-preset)", () => {
|
|
882
|
+
for (const p of presets) {
|
|
883
|
+
expect("motionPlan" in p.data, `${p.id}: must not carry motionPlan`).toBe(false)
|
|
884
|
+
}
|
|
885
|
+
})
|
|
886
|
+
|
|
887
|
+
it("every motionPrompt is a rich art-direction brief (>200 chars) ending in an Expose slots sentence", () => {
|
|
888
|
+
for (const p of presets) {
|
|
889
|
+
const prompt = p.data.motionPrompt as string
|
|
890
|
+
expect(prompt.length, `${p.id}: prompt too thin (${prompt.length} chars)`).toBeGreaterThan(200)
|
|
891
|
+
expect(prompt, `${p.id}: missing "Expose slots:"`).toMatch(/Expose slots:/)
|
|
892
|
+
}
|
|
893
|
+
})
|
|
894
|
+
|
|
895
|
+
it("each prompt literally names every slot from its spec-table contract (stable-contract guard)", () => {
|
|
896
|
+
for (const p of presets) {
|
|
897
|
+
const sids = SLOT_CONTRACT[p.id]
|
|
898
|
+
expect(sids, `${p.id}: not in the slot-contract table`).toBeTruthy()
|
|
899
|
+
const prompt = p.data.motionPrompt as string
|
|
900
|
+
for (const sid of sids!) {
|
|
901
|
+
expect(prompt, `${p.id}: prompt is missing sid "${sid}"`).toContain(sid)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
})
|
|
905
|
+
|
|
906
|
+
it("transparent-overlay presets use #00000000 and state transparency in the prompt", () => {
|
|
907
|
+
for (const p of presets) {
|
|
908
|
+
if (!TRANSPARENT_IDS.has(p.id)) continue
|
|
909
|
+
expect(p.data.backgroundColor, `${p.id}: overlay bg must be #00000000`).toBe("#00000000")
|
|
910
|
+
expect(p.data.motionPrompt as string, `${p.id}: prompt must mention transparency`).toMatch(/[Tt]ransparent/)
|
|
911
|
+
}
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
it("Backgrounds presets are opaque and declare a seamless loop", () => {
|
|
915
|
+
for (const p of presets) {
|
|
916
|
+
if (p.group !== "Backgrounds") continue
|
|
917
|
+
expect(p.data.backgroundColor, `${p.id}: backdrop must be opaque`).not.toBe("#00000000")
|
|
918
|
+
expect(p.data.motionPrompt as string, `${p.id}: must declare a seamless loop`).toMatch(/[Ss]eamless loop/)
|
|
919
|
+
}
|
|
920
|
+
})
|
|
921
|
+
})
|
|
922
|
+
|
|
923
|
+
describe("lottie-overlay factory preset data validity", () => {
|
|
924
|
+
const presets = getFactoryPresets("lottie-overlay")
|
|
925
|
+
// The 4 folders, in catalog order.
|
|
926
|
+
const GROUP_ORDER = ["Connected Graphic", "Celebration & FX", "Reactions & Social", "Emphasis & UI"]
|
|
927
|
+
|
|
928
|
+
it("ships at least 12 presets across the four folders, in order", () => {
|
|
929
|
+
expect(presets.length).toBeGreaterThanOrEqual(12)
|
|
930
|
+
const seen: string[] = []
|
|
931
|
+
for (const p of presets) if (p.group && !seen.includes(p.group)) seen.push(p.group)
|
|
932
|
+
expect(seen).toEqual(GROUP_ORDER)
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
it("every preset has a non-empty description and group", () => {
|
|
936
|
+
for (const p of presets) {
|
|
937
|
+
expect((p.description ?? "").trim().length, `${p.id}: missing description`).toBeGreaterThan(0)
|
|
938
|
+
expect((p.group ?? "").trim().length, `${p.id}: missing group`).toBeGreaterThan(0)
|
|
939
|
+
}
|
|
940
|
+
})
|
|
941
|
+
|
|
942
|
+
it("data carries ONLY overlayPrompt (duration must track the source video; fps/llmModel stay the user's)", () => {
|
|
943
|
+
// durationSeconds in a preset would silently truncate the plan timeline on
|
|
944
|
+
// longer footage; fps has a node default; llmModel is the user's tier choice.
|
|
945
|
+
for (const p of presets) {
|
|
946
|
+
expect(Object.keys(p.data), `${p.id}: unexpected data keys`).toEqual(["overlayPrompt"])
|
|
947
|
+
}
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
it("respects the route's 2000-char prompt cap and stays substantive", () => {
|
|
951
|
+
for (const p of presets) {
|
|
952
|
+
const prompt = p.data.overlayPrompt as string
|
|
953
|
+
expect(prompt.length, `${p.id}: prompt exceeds the route cap`).toBeLessThanOrEqual(2000)
|
|
954
|
+
expect(prompt.trim().length, `${p.id}: prompt too thin`).toBeGreaterThanOrEqual(40)
|
|
955
|
+
}
|
|
956
|
+
})
|
|
957
|
+
|
|
958
|
+
it("uses curly braces ONLY for {variable || default} tokens (Nodaro variable syntax)", () => {
|
|
959
|
+
for (const p of presets) {
|
|
960
|
+
const stripped = (p.data.overlayPrompt as string).replace(/\{[^{}]*\|\|[^{}]*\}/g, "")
|
|
961
|
+
expect(stripped, `${p.id}: stray "{" outside a variable token`).not.toContain("{")
|
|
962
|
+
expect(stripped, `${p.id}: stray "}" outside a variable token`).not.toContain("}")
|
|
963
|
+
}
|
|
964
|
+
})
|
|
965
|
+
|
|
966
|
+
it("every prompt exposes at least one {variable || default} token (the catalog's signature feature)", () => {
|
|
967
|
+
for (const p of presets) {
|
|
968
|
+
expect(p.data.overlayPrompt as string, `${p.id}: no variable token`).toMatch(/\{[^{}]*\|\|[^{}]*\}/)
|
|
969
|
+
}
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
it("Connected Graphic presets pin the only-connected-graphic exclusivity clause; the rest name a built-in asset", () => {
|
|
973
|
+
// Without the clause the planner may substitute catalog animations for the
|
|
974
|
+
// wired asset; without a named asset the catalog presets drift to whatever
|
|
975
|
+
// the LLM fancies. Both are the load-bearing lines — guard them.
|
|
976
|
+
for (const p of presets) {
|
|
977
|
+
const prompt = p.data.overlayPrompt as string
|
|
978
|
+
if (p.group === "Connected Graphic") {
|
|
979
|
+
expect(prompt, `${p.id}: missing the only-connected-graphic clause`).toContain("only the connected graphic")
|
|
980
|
+
} else {
|
|
981
|
+
expect(prompt, `${p.id}: must name a built-in catalog asset`).toContain("built-in")
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
})
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
describe("PRESET_APPLY_CLEAR_KEYS", () => {
|
|
988
|
+
it("equals every COMPOSER_PLAN_MAP plan field plus lottieUrl (drift guard)", () => {
|
|
989
|
+
// Deliberately re-derived from COMPOSER_PLAN_MAP (not COMPOSER_PLAN_FIELDS):
|
|
990
|
+
// asserting against the same constant production reads would be tautological.
|
|
991
|
+
const expected = [...new Set(Object.values(COMPOSER_PLAN_MAP).map((m) => m.planField)), "lottieUrl"]
|
|
992
|
+
expect([...PRESET_APPLY_CLEAR_KEYS].sort()).toEqual([...expected].sort())
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
it("covers the motion-graphics plan field + lottieUrl explicitly", () => {
|
|
996
|
+
expect(PRESET_APPLY_CLEAR_KEYS).toContain("motionPlan")
|
|
997
|
+
expect(PRESET_APPLY_CLEAR_KEYS).toContain("lottieUrl")
|
|
998
|
+
})
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
describe("presets don't bake config-field values (framing) into the prompt", () => {
|
|
1002
|
+
// The aspectRatio field is the single source of truth for framing. If a prompt also NAMES a
|
|
1003
|
+
// ratio, the model follows the prompt — so changing the aspect-ratio dropdown silently fails
|
|
1004
|
+
// (e.g. a prompt that says "3:4" ignores a switch to 16:9). No prompt may name an aspect ratio.
|
|
1005
|
+
const RATIO_TOKENS = ["1:1", "16:9", "9:16", "4:3", "3:4", "3:2", "2:3", "4:5", "5:4", "21:9", "2.39:1", "2.35:1", "1.85:1"]
|
|
1006
|
+
const ratioRe = new RegExp(`(^|[^0-9.])(${RATIO_TOKENS.map((t) => t.replace(/\./g, "\\.")).join("|")})([^0-9]|$)`)
|
|
1007
|
+
|
|
1008
|
+
it("no factory preset prompt names an aspect ratio", () => {
|
|
1009
|
+
const offenders: string[] = []
|
|
1010
|
+
for (const [, presets] of Object.entries(FACTORY_PRESETS)) {
|
|
1011
|
+
for (const p of presets) {
|
|
1012
|
+
const prompt = (p.data as Record<string, unknown>).prompt
|
|
1013
|
+
if (typeof prompt !== "string") continue
|
|
1014
|
+
const m = prompt.match(ratioRe)
|
|
1015
|
+
if (m) offenders.push(`${p.id} → "${m[2]}"`)
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
expect(
|
|
1019
|
+
offenders,
|
|
1020
|
+
`prompts must not name an aspect ratio (the aspectRatio field controls framing):\n${offenders.join("\n")}`,
|
|
1021
|
+
).toEqual([])
|
|
1022
|
+
})
|
|
1023
|
+
})
|
|
1024
|
+
|
|
1025
|
+
describe("factory-presets split integrity", () => {
|
|
1026
|
+
// The exact node-type keys, in the exact insertion order the single-file
|
|
1027
|
+
// catalog declared them. The split's index.ts must reproduce this order.
|
|
1028
|
+
const EXPECTED_KEYS = [
|
|
1029
|
+
"generate-image", "modify-image", "generate-video", "text-to-speech",
|
|
1030
|
+
"text-to-audio", "generate-music", "suno-generate", "llm-chat",
|
|
1031
|
+
"generate-script", "image-to-text", "voice-design", "video-to-video",
|
|
1032
|
+
"switchx", "voice-changer", "add-captions", "combine-videos",
|
|
1033
|
+
"motion-graphics", "lottie-overlay",
|
|
1034
|
+
]
|
|
1035
|
+
|
|
1036
|
+
it("exposes exactly the expected node-type keys in order", () => {
|
|
1037
|
+
expect(Object.keys(FACTORY_PRESETS)).toEqual(EXPECTED_KEYS)
|
|
1038
|
+
})
|
|
1039
|
+
|
|
1040
|
+
it("every declared node type has at least one preset", () => {
|
|
1041
|
+
for (const k of EXPECTED_KEYS) {
|
|
1042
|
+
expect(getFactoryPresets(k).length, `${k} is empty`).toBeGreaterThan(0)
|
|
1043
|
+
}
|
|
1044
|
+
})
|
|
1045
|
+
})
|