@nodaro/prompts 1.35.0 → 1.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +319 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -2
- package/dist/index.d.ts +20 -2
- package/dist/index.js +315 -27
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/creator-presets.test.ts +346 -1
- package/src/__tests__/factory-presets.test.ts +26 -2
- package/src/__tests__/node-prompt-fields.test.ts +2 -2
- package/src/__tests__/realism-wording.test.ts +41 -0
- package/src/factory-presets/generate-image.ts +114 -0
- package/src/factory-presets/generate-video.ts +71 -0
- package/src/factory-presets/index.ts +15 -1
- package/src/factory-presets/shared-image.ts +42 -0
- package/src/factory-presets/text.ts +35 -1
- package/src/factory-presets/video-edit.ts +16 -0
- package/src/index.ts +1 -0
- package/src/node-prompt-fields.ts +2 -0
- package/src/prompt-wizard-categories.ts +2 -2
- package/src/provider-prompt-doctrine.ts +2 -2
- package/src/realism-wording.ts +39 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodaro/prompts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Nodaro's prompt-engineering layer — person/picker catalogs with prompt hints, identity-lock clauses, entity prompt builders, brand presets, and prompt/reference assembly shared by the Nodaro platform and SDK.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "FSL-1.1-Apache-2.0",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test": "vitest run"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@nodaro/shared": "^3.
|
|
23
|
+
"@nodaro/shared": "^3.27.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"tsup": "^8.5.0",
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// packages/prompts/src/__tests__/creator-presets.test.ts
|
|
2
2
|
import { describe, it, expect } from "vitest"
|
|
3
|
-
import { captionRoutesToRemotion, presetApplyClearKeys } from "@nodaro/shared"
|
|
3
|
+
import { aspectRatioOptionsByKind, captionRoutesToRemotion, MODELS_WITH_REFERENCE_IMAGE_SUPPORT, normalizeNodeModelParams, presetApplyClearKeys, resolveNodeRefs, resolveNormalizedImageGen, type ConnectedReference, type SourceImageSize } from "@nodaro/shared"
|
|
4
4
|
import { getFactoryPresets, type FactoryPreset } from "../factory-presets.js"
|
|
5
|
+
import { assembleImageInput } from "../assemble-image-input.js"
|
|
6
|
+
import { computeNodePrompt } from "../resolve-prompt.js"
|
|
7
|
+
import { resolveReferenceTokens } from "../video-reference-resolver.js"
|
|
8
|
+
import { NEUTRAL_PERSON, REALISM_FINISH, REALISM_PASS_INSTRUCTION, REALISM_SKIN, realismEmptyHands, realismViewpoint } from "../realism-wording.js"
|
|
5
9
|
|
|
6
10
|
/** Words that name a recording or viewing device. Plural forms included. */
|
|
7
11
|
const DEVICE_WORDS = /\b(?:smartphones?|iphones?|phones?|cameras?|lens(?:es)?|webcams?|laptops?|tablets?|screens?|monitors?|displays?|tripods?|selfie[ -]sticks?)\b/i
|
|
@@ -149,3 +153,344 @@ describe("Wave 0 — Hook Plate and Body Captions", () => {
|
|
|
149
153
|
for (const k of ["backgroundColor", "look", "positionY", "fontWeight", "maxWordsPerLine"]) expect(overClean[k], k).toBeUndefined()
|
|
150
154
|
})
|
|
151
155
|
})
|
|
156
|
+
|
|
157
|
+
/** Every prompt-bearing field, joined — what the device and slot checks read. */
|
|
158
|
+
const allText = (d: Readonly<Record<string, unknown>>) =>
|
|
159
|
+
["promptPrefix", "prompt", "promptSuffix", "negativePrompt"].map((k) => d[k]).filter((v): v is string => typeof v === "string").join(" ")
|
|
160
|
+
const wiredRunPrompts = (d: Readonly<Record<string, unknown>>): Record<"legacy" | "hybrid" | "urlsOnly", string> => {
|
|
161
|
+
const userPrompt = resolveNodeRefs(computeNodePrompt("generate-image", { ...d }, { refMap: new Map() }), new Map())
|
|
162
|
+
const wired = Math.max(0, ...[...userPrompt.matchAll(/\{image:(\d+)[^{}]*\}/gi)].map((m) => Number(m[1])))
|
|
163
|
+
const refs: ConnectedReference[] = Array.from({ length: wired }, (_, i) => ({
|
|
164
|
+
id: `ref-${i + 1}`, defaultName: `Reference ${i + 1}`, source: "wired-image", url: `https://example.com/ref-${i + 1}.jpg`,
|
|
165
|
+
}))
|
|
166
|
+
const provider = d.provider as string
|
|
167
|
+
return {
|
|
168
|
+
legacy: assembleImageInput({ userPrompt, provider, referenceFormat: "legacy", connectedReferences: refs }).prompt,
|
|
169
|
+
hybrid: assembleImageInput({ userPrompt, provider, referenceFormat: "hybrid", connectedReferences: refs }).prompt,
|
|
170
|
+
urlsOnly: assembleImageInput({ userPrompt, provider, extraReferenceImageUrls: refs.map((r) => r.url) }).prompt,
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/** What the image route makes of a preset's levers with `refs` photos connected (and, when known, the size of the photo
|
|
174
|
+
* being transformed): the credit tier, the ratio it runs at and anything it had to change. */
|
|
175
|
+
const imageRouteRun = (node: string, d: Readonly<Record<string, unknown>>, refs: number, sourceImage?: SourceImageSize) =>
|
|
176
|
+
resolveNormalizedImageGen({
|
|
177
|
+
provider: d.provider as string, aspectRatio: d.aspectRatio, resolution: d.resolution, quality: d.quality, refCount: refs, sourceImage,
|
|
178
|
+
// Generate Image moves a text-to-image model to its image-to-image sibling once a photo is connected; Modify Image already names that sibling.
|
|
179
|
+
swapToI2i: node === "generate-image",
|
|
180
|
+
})
|
|
181
|
+
/** The aspect ratio the workflow write step stores for one node of `type` carrying `d`. */
|
|
182
|
+
const storedAspectRatio = (type: string, d: Readonly<Record<string, unknown>>) =>
|
|
183
|
+
normalizeNodeModelParams([{ id: "node", type, data: { ...d } }]).nodes[0]!.data.aspectRatio
|
|
184
|
+
/** A preset's data without the named wording fields — what is left is its levers. */
|
|
185
|
+
const leversOf = (d: Readonly<Record<string, unknown>>, ...wording: string[]) => Object.fromEntries(Object.entries(d).filter(([k]) => !wording.includes(k)))
|
|
186
|
+
|
|
187
|
+
const REALISM_IDS = [
|
|
188
|
+
"unretouched-portrait", "phone-selfie", "candid-snapshot", "mirror-selfie",
|
|
189
|
+
"creator-at-home", "video-call-frame", "macro-skin-study", "product-in-hand",
|
|
190
|
+
].map((s) => `generate-image/${s}`)
|
|
191
|
+
const REALISM_REFERENCE_IDS = ["generate-image/creator-at-home", "generate-image/product-in-hand"]
|
|
192
|
+
/** Each Realism preset's levers, and the credit tier the image route charges for them (its reference connected where it takes one). */
|
|
193
|
+
const REALISM_RUNS: Readonly<Record<string, readonly [levers: Readonly<Record<string, string>>, creditId: string]>> = {
|
|
194
|
+
"generate-image/unretouched-portrait": [{ provider: "gpt-image-2", resolution: "2K", aspectRatio: "3:4" }, "gpt-image-2:2K"],
|
|
195
|
+
"generate-image/phone-selfie": [{ provider: "gpt-image-2", resolution: "2K", aspectRatio: "3:4" }, "gpt-image-2:2K"],
|
|
196
|
+
"generate-image/candid-snapshot": [{ provider: "gpt-image-2", resolution: "2K", aspectRatio: "3:4" }, "gpt-image-2:2K"],
|
|
197
|
+
"generate-image/mirror-selfie": [{ provider: "gpt-image-2", resolution: "2K", aspectRatio: "9:16" }, "gpt-image-2:2K"],
|
|
198
|
+
"generate-image/creator-at-home": [{ provider: "gpt-image-2", resolution: "2K", aspectRatio: "9:16" }, "gpt-image-2-i2i:2K"],
|
|
199
|
+
"generate-image/video-call-frame": [{ provider: "gpt-image-2", resolution: "2K", aspectRatio: "16:9" }, "gpt-image-2:2K"],
|
|
200
|
+
"generate-image/macro-skin-study": [{ provider: "seedream-5-pro", quality: "high", aspectRatio: "1:1" }, "seedream-5-pro:high"],
|
|
201
|
+
"generate-image/product-in-hand": [{ provider: "gpt-image-2", resolution: "2K", aspectRatio: "3:4" }, "gpt-image-2-i2i:2K"],
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
describe("Wave 1 — Realism & Authentic", () => {
|
|
205
|
+
const group = () => getFactoryPresets("generate-image").filter((p) => p.group === "Realism & Authentic")
|
|
206
|
+
|
|
207
|
+
it("ships exactly these eight presets, in order, each on its model with that model's one lever, and no negativePrompt", () => {
|
|
208
|
+
expect(group().map((p) => p.id)).toEqual(REALISM_IDS)
|
|
209
|
+
expect(Object.keys(REALISM_RUNS)).toEqual(REALISM_IDS)
|
|
210
|
+
for (const p of group()) {
|
|
211
|
+
expect(leversOf(p.data, "prompt"), p.id).toEqual(REALISM_RUNS[p.id]![0])
|
|
212
|
+
expect(p.data.negativePrompt, p.id).toBeUndefined()
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it("the image route runs each one on its levers unchanged, at its model's tier", () => {
|
|
217
|
+
for (const id of REALISM_IDS) {
|
|
218
|
+
const run = imageRouteRun("generate-image", preset("generate-image", id).data, REALISM_REFERENCE_IDS.includes(id) ? 1 : 0)
|
|
219
|
+
expect(run.adjustments, id).toEqual([])
|
|
220
|
+
expect(run.identifier, id).toBe(REALISM_RUNS[id]![1])
|
|
221
|
+
}
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it("composes the selected fragments", () => {
|
|
225
|
+
const text = (slug: string) => preset("generate-image", `generate-image/${slug}`).data.prompt as string
|
|
226
|
+
for (const slug of ["unretouched-portrait", "phone-selfie", "candid-snapshot", "mirror-selfie", "creator-at-home", "macro-skin-study"]) expect(text(slug), slug).toContain(REALISM_SKIN)
|
|
227
|
+
for (const slug of ["unretouched-portrait", "phone-selfie", "candid-snapshot", "mirror-selfie", "creator-at-home", "video-call-frame", "product-in-hand"]) expect(text(slug), slug).toContain(REALISM_FINISH)
|
|
228
|
+
expect(text("phone-selfie")).toContain(realismViewpoint(NEUTRAL_PERSON))
|
|
229
|
+
expect(text("phone-selfie")).toContain(realismEmptyHands(NEUTRAL_PERSON))
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("names no device — the mirror self-portrait may say phone", () => {
|
|
233
|
+
for (const id of REALISM_IDS) {
|
|
234
|
+
const text = allText(preset("generate-image", id).data)
|
|
235
|
+
// A mirror self-portrait shows the phone in hand.
|
|
236
|
+
const scanned = id === "generate-image/mirror-selfie" ? text.replace(/\bphone\b/gi, "") : text
|
|
237
|
+
expect(scanned, id).not.toMatch(DEVICE_WORDS)
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it("every subject slot has a default naming an adult; every setting slot has a default", () => {
|
|
242
|
+
for (const id of [...REALISM_IDS, ...UGC_IDS]) {
|
|
243
|
+
const text = allText(preset(id.split("/")[0]!, id).data)
|
|
244
|
+
for (const m of text.matchAll(/\{subject([^{}]*)\}/g)) {
|
|
245
|
+
expect(m[1], `${id}: {subject} needs a default`).toMatch(/\|\|/)
|
|
246
|
+
expect(m[1]!.split("||")[1], `${id}: the default names an adult`).toMatch(ADULT)
|
|
247
|
+
}
|
|
248
|
+
for (const m of text.matchAll(/\{setting([^{}]*)\}/g)) expect(m[1], `${id}: {setting} needs a default`).toMatch(/\|\|\s*\S/)
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
it("labels every reference token, and reads whole when the references are wired", () => {
|
|
253
|
+
const TOKEN = /\{(?:image|video|audio):\d+(:[^{}]*)?\}/gi
|
|
254
|
+
const count = (s: string, part: string) => s.split(part).length - 1
|
|
255
|
+
for (const id of REALISM_REFERENCE_IDS) expect(MODELS_WITH_REFERENCE_IMAGE_SUPPORT.has(preset("generate-image", id).data.provider as string), id).toBe(true)
|
|
256
|
+
const withTokens: string[] = []
|
|
257
|
+
for (const id of REALISM_IDS) {
|
|
258
|
+
const d = preset("generate-image", id).data
|
|
259
|
+
const text = allText(d)
|
|
260
|
+
const labels: string[] = []
|
|
261
|
+
for (const m of text.matchAll(TOKEN)) {
|
|
262
|
+
expect(m[1], `${id}: ${m[0]} needs a label`).toBeTruthy()
|
|
263
|
+
labels.push(m[1]!.slice(1))
|
|
264
|
+
}
|
|
265
|
+
if (labels.length > 0) withTokens.push(id)
|
|
266
|
+
const bare = text.replace(TOKEN, "")
|
|
267
|
+
for (const [path, out] of Object.entries(wiredRunPrompts(d))) {
|
|
268
|
+
const legacyReferenceLine = path === "legacy" ? 1 : 0
|
|
269
|
+
for (const label of new Set(labels)) {
|
|
270
|
+
const tokens = labels.filter((l) => l === label).length
|
|
271
|
+
expect(count(out, label), `${id} (${path}): ${label}`).toBeGreaterThanOrEqual(count(bare, label) + tokens + legacyReferenceLine)
|
|
272
|
+
}
|
|
273
|
+
expect(out, `${id} (${path})`).not.toMatch(/ {2}/)
|
|
274
|
+
expect(out, `${id} (${path})`).not.toMatch(/[{}]/)
|
|
275
|
+
expect(out, `${id} (${path})`).not.toMatch(/\bfrom\s+[.,;:!?]/)
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
expect(withTokens).toEqual(REALISM_REFERENCE_IDS)
|
|
279
|
+
})
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
describe("Wave 1 — Skin Detail Sheet", () => {
|
|
283
|
+
it("is a Reference Sheet board on seedream-5-pro at high quality, carrying the never-merge clause in pre/post text and no negativePrompt", () => {
|
|
284
|
+
const p = preset("generate-image", "generate-image/skin-detail-sheet")
|
|
285
|
+
expect(p.group).toBe("Reference Sheet")
|
|
286
|
+
expect(leversOf(p.data, "promptPrefix", "promptSuffix")).toEqual({ provider: "seedream-5-pro", quality: "high", aspectRatio: "3:2" })
|
|
287
|
+
expect(MODELS_WITH_REFERENCE_IMAGE_SUPPORT.has(p.data.provider as string)).toBe(true)
|
|
288
|
+
// The connected face photo moves the run to Seedream's image-to-image model, at the same tier.
|
|
289
|
+
const run = imageRouteRun("generate-image", p.data, 1)
|
|
290
|
+
expect(run.adjustments).toEqual([])
|
|
291
|
+
expect(run.identifier).toBe("seedream-5-pro-i2i:high")
|
|
292
|
+
expect(p.data.prompt).toBeUndefined()
|
|
293
|
+
expect(p.data.promptPrefix as string).toContain("never merging or omitting a panel")
|
|
294
|
+
expect(p.data.promptPrefix as string).toContain(REALISM_SKIN)
|
|
295
|
+
expect(p.data.negativePrompt).toBeUndefined()
|
|
296
|
+
expect(allText(p.data)).not.toMatch(DEVICE_WORDS)
|
|
297
|
+
const ids = getFactoryPresets("generate-image").map((x) => x.id)
|
|
298
|
+
expect(ids[ids.indexOf("generate-image/pet-board") + 1]).toBe("generate-image/skin-detail-sheet")
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
describe("Wave 1 — Edits", () => {
|
|
303
|
+
const SLUGS = ["add-skin-realism", "natural-photo-finish", "phone-camera-look", "undo-beauty-filter"]
|
|
304
|
+
const FINISH = { provider: "nano-banana-2-1", aspectRatio: "auto", resolution: "2K" }
|
|
305
|
+
const UNDO = { aspectRatio: "auto", quality: "high" }
|
|
306
|
+
/** Each Edit's levers per node. Modify Image's provider list names Seedream's image-to-image id, not the text-to-image one. */
|
|
307
|
+
const LEVERS: Readonly<Record<string, Readonly<Record<string, Readonly<Record<string, string>>>>>> = {
|
|
308
|
+
"generate-image": { "add-skin-realism": FINISH, "natural-photo-finish": FINISH, "phone-camera-look": FINISH, "undo-beauty-filter": { provider: "seedream-5-pro", ...UNDO } },
|
|
309
|
+
"modify-image": { "add-skin-realism": FINISH, "natural-photo-finish": FINISH, "phone-camera-look": FINISH, "undo-beauty-filter": { provider: "seedream-5-pro-i2i", ...UNDO } },
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
it("ships the four Edits on both image nodes after the Doodle Overlay pair, each on its model and levers, as pre-text only", () => {
|
|
313
|
+
for (const node of ["generate-image", "modify-image"]) {
|
|
314
|
+
const ids = getFactoryPresets(node).map((x) => x.id)
|
|
315
|
+
const i = ids.indexOf(`${node}/doodle-overlay-expressive`)
|
|
316
|
+
expect(ids.slice(i + 1, i + 5)).toEqual(SLUGS.map((s) => `${node}/${s}`))
|
|
317
|
+
for (const s of SLUGS) {
|
|
318
|
+
const p = preset(node, `${node}/${s}`)
|
|
319
|
+
expect(p.group).toBe("Edits")
|
|
320
|
+
expect(typeof p.data.promptPrefix, p.id).toBe("string")
|
|
321
|
+
expect(leversOf(p.data, "promptPrefix"), p.id).toEqual(LEVERS[node]![s])
|
|
322
|
+
// Every Edit asks for "auto", to keep the photo's shape. Undo Beauty Filter's model lists no
|
|
323
|
+
// native "auto"; how each node resolves it is pinned in the Undo Beauty Filter test below.
|
|
324
|
+
expect(p.data.aspectRatio, p.id).toBe("auto")
|
|
325
|
+
if (s !== "undo-beauty-filter") expect(aspectRatioOptionsByKind("image")[p.data.provider as string]?.map((o) => o.value), p.id).toContain("auto")
|
|
326
|
+
expect((p.data.promptPrefix as string).length).toBeLessThanOrEqual(2000)
|
|
327
|
+
expect(allText(p.data), p.id).not.toMatch(DEVICE_WORDS)
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
it("Undo Beauty Filter keeps the photo's shape on both nodes", () => {
|
|
333
|
+
// The write step stores "auto" as written, and the run takes the model's listed ratio nearest the
|
|
334
|
+
// connected photo's size, on Seedream 5 Pro's image-to-image model at high quality.
|
|
335
|
+
const photos: ReadonlyArray<readonly [SourceImageSize, string]> = [[{ width: 1104, height: 1472 }, "3:4"], [{ width: 1600, height: 1000 }, "3:2"]]
|
|
336
|
+
for (const node of ["generate-image", "modify-image"]) {
|
|
337
|
+
const d = preset(node, `${node}/undo-beauty-filter`).data
|
|
338
|
+
expect(storedAspectRatio(node, d), node).toBe("auto")
|
|
339
|
+
for (const [photo, ratio] of photos) {
|
|
340
|
+
const run = imageRouteRun(node, d, 1, photo)
|
|
341
|
+
expect(run.aspectRatio, `${node} ${photo.width}x${photo.height}`).toBe(ratio)
|
|
342
|
+
expect(run.modelId, node).toBe("seedream-5-pro-i2i")
|
|
343
|
+
expect(run.identifier, node).toBe("seedream-5-pro-i2i:high")
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
it("Add Skin Realism is the realism instruction verbatim; the finish edits carry the finish sentence", () => {
|
|
349
|
+
expect(preset("generate-image", "generate-image/add-skin-realism").data.promptPrefix).toBe(REALISM_PASS_INSTRUCTION)
|
|
350
|
+
for (const s of ["natural-photo-finish", "phone-camera-look"])
|
|
351
|
+
expect(preset("generate-image", `generate-image/${s}`).data.promptPrefix as string, s).toContain(REALISM_FINISH)
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
describe("Wave 1 — Realism Audit", () => {
|
|
356
|
+
it("is a structured Creative reading that answers ten yes/no questions and ends with a count", () => {
|
|
357
|
+
const p = preset("image-to-text", "image-to-text/realism-audit")
|
|
358
|
+
expect(p.group).toBe("Creative")
|
|
359
|
+
expect(p.data.detailLevel).toBe("structured")
|
|
360
|
+
const prompt = p.data.customPrompt as string
|
|
361
|
+
expect(prompt).toContain("Tells found: N of 10")
|
|
362
|
+
for (let n = 1; n <= 10; n++) expect(prompt).toContain(`${n}. `)
|
|
363
|
+
expect(prompt.length).toBeLessThanOrEqual(2000)
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
/** The prompt as a run sends it with nothing wired: slot defaults applied, reference tokens dropped to their labels. */
|
|
368
|
+
const unwired = (text: string) =>
|
|
369
|
+
resolveReferenceTokens(resolveNodeRefs(text, new Map([["nothing-wired", ""]])), { image: 0, video: 0, audio: 0 }) ?? ""
|
|
370
|
+
|
|
371
|
+
const UGC_IDS = [
|
|
372
|
+
"ugc-talking-head", "ugc-product-closer", "handheld-unboxing", "get-ready-with-me",
|
|
373
|
+
"street-qa", "reaction-shot", "acts-on-phone",
|
|
374
|
+
].map((s) => `generate-video/${s}`)
|
|
375
|
+
// The action is typing on a phone while looking down at it; its screen faces away.
|
|
376
|
+
const PHONE_CLOSER_SENTENCE = "In the last seconds the person picks up a phone and looks down into its screen while typing on it for a moment, then looks back up at the viewer; the screen faces away from the camera."
|
|
377
|
+
|
|
378
|
+
describe("Wave 2 — UGC & Creator", () => {
|
|
379
|
+
const group = () => getFactoryPresets("generate-video").filter((p) => p.group === "UGC & Creator")
|
|
380
|
+
|
|
381
|
+
it("ships exactly these seven, in order, on Seedance 2.5 at 9:16 and 720p with audio, 8 s except the 5 s answer and reaction, and no negativePrompt", () => {
|
|
382
|
+
const SHORT = ["generate-video/street-qa", "generate-video/reaction-shot"]
|
|
383
|
+
expect(group().map((p) => p.id)).toEqual(UGC_IDS)
|
|
384
|
+
for (const p of group()) {
|
|
385
|
+
expect(p.data, p.id).toMatchObject({ provider: "seedance-2-5", aspectRatio: "9:16", resolution: "720p", generateAudio: true })
|
|
386
|
+
expect(p.data.duration, p.id).toBe(SHORT.includes(p.id) ? 5 : 8)
|
|
387
|
+
expect(p.data.negativePrompt, p.id).toBeUndefined()
|
|
388
|
+
}
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
it("every prompt ends with the subtitle-free tail", () => {
|
|
392
|
+
for (const id of UGC_IDS)
|
|
393
|
+
expect((preset("generate-video", id).data.prompt as string).endsWith("Keep it subtitle-free, do not generate a watermark."), id).toBe(true)
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
it("names no device outside the one pinned sentence", () => {
|
|
397
|
+
// The one pinned sentence is the only text the scan skips: the phone closer, in one preset.
|
|
398
|
+
for (const id of UGC_IDS) {
|
|
399
|
+
let text = allText(preset("generate-video", id).data)
|
|
400
|
+
if (id === "generate-video/acts-on-phone") text = text.split(PHONE_CLOSER_SENTENCE).join(" ")
|
|
401
|
+
expect(text, id).not.toMatch(DEVICE_WORDS)
|
|
402
|
+
}
|
|
403
|
+
expect(preset("generate-video", "generate-video/acts-on-phone").data.prompt as string).toContain(PHONE_CLOSER_SENTENCE)
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it("a default spoken line is one sentence with its numbers in words", () => {
|
|
407
|
+
let lines = 0
|
|
408
|
+
for (const id of UGC_IDS) {
|
|
409
|
+
for (const m of (preset("generate-video", id).data.prompt as string).matchAll(/\{line \|\| ([^{}]*)\}/g)) {
|
|
410
|
+
lines += 1
|
|
411
|
+
expect(m[1], id).toMatch(/^[^.!?]*[.!?]$/)
|
|
412
|
+
expect(m[1], id).not.toMatch(/\d/)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
expect(lines).toBe(5)
|
|
416
|
+
})
|
|
417
|
+
|
|
418
|
+
it("labels every reference token, and reads whole with nothing wired", () => {
|
|
419
|
+
for (const id of UGC_IDS) {
|
|
420
|
+
const text = allText(preset("generate-video", id).data)
|
|
421
|
+
for (const m of text.matchAll(/\{(?:image|video|audio):\d+(:[^{}]*)?\}/gi)) expect(m[1], `${id}: ${m[0]} needs a label`).toBeTruthy()
|
|
422
|
+
const out = unwired(text)
|
|
423
|
+
expect(out, id).not.toMatch(/ {2}/)
|
|
424
|
+
expect(out, id).not.toMatch(/[{}]/)
|
|
425
|
+
expect(out, id).not.toMatch(/\bfrom\s+[.,;:!?]/)
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
it("the rows that carry a spoken line tell the user to type it with numbers as words", () => {
|
|
430
|
+
for (const id of UGC_IDS) {
|
|
431
|
+
const p = preset("generate-video", id)
|
|
432
|
+
if (/\{line \|\|/.test(p.data.prompt as string)) expect(p.description, id).toMatch(/numbers as words/)
|
|
433
|
+
}
|
|
434
|
+
expect(preset("generate-video", "generate-video/street-qa").description).toContain("staged")
|
|
435
|
+
})
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
const WRITER_CLOSER = "closer — the last line. It ends on one simple physical action the speaker can do on camera."
|
|
439
|
+
const closerParts = (s: string) => s.split("\n\n").filter((part) => part.startsWith("closer —"))
|
|
440
|
+
|
|
441
|
+
describe("Wave 2 — UGC Monologue Writer", () => {
|
|
442
|
+
const p = () => preset("llm-chat", "llm-chat/ugc-monologue-writer")
|
|
443
|
+
it("sits in Writing & Marketing after Hook Generator, set to Claude Opus 5.5 at high effort, at temperature 0.8 and 2048 tokens", () => {
|
|
444
|
+
expect(p().group).toBe("Writing & Marketing")
|
|
445
|
+
expect(p().data).toMatchObject({ llmModel: "claude-opus-5.5", reasoningEffort: "high", temperature: 0.8, maxTokens: 2048 })
|
|
446
|
+
const ids = getFactoryPresets("llm-chat").map((x) => x.id)
|
|
447
|
+
expect(ids[ids.indexOf("llm-chat/hook-generator") + 1]).toBe("llm-chat/ugc-monologue-writer")
|
|
448
|
+
})
|
|
449
|
+
it("pins the closer to one simple physical action", () => {
|
|
450
|
+
const s = p().data.systemPrompt as string
|
|
451
|
+
expect(s.length).toBeLessThanOrEqual(10_000)
|
|
452
|
+
expect(s).toContain("at most 8 words")
|
|
453
|
+
for (const k of ['"hook"', '"beats"', '"closer"']) expect(s).toContain(k)
|
|
454
|
+
expect(s).not.toMatch(/per second/i)
|
|
455
|
+
expect(closerParts(s)).toEqual([WRITER_CLOSER])
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
describe("UGC Monologue Writer — Hebrew paragraph", () => {
|
|
460
|
+
it("carries the Hebrew craft paragraph and still passes the Writer's checks", () => {
|
|
461
|
+
const s = preset("llm-chat", "llm-chat/ugc-monologue-writer").data.systemPrompt as string
|
|
462
|
+
expect(s).toContain("When the script is in Hebrew:")
|
|
463
|
+
expect(s.indexOf("When the script is in Hebrew:")).toBeLessThan(s.indexOf("Return only this JSON"))
|
|
464
|
+
expect(s.match(/at most 8 words/g)?.length).toBe(2)
|
|
465
|
+
expect(s).not.toMatch(/per second/i)
|
|
466
|
+
expect(closerParts(s)).toEqual([WRITER_CLOSER])
|
|
467
|
+
expect(s.length).toBeLessThanOrEqual(10_000)
|
|
468
|
+
})
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
describe("Wave 2 — Phone Footage Look", () => {
|
|
472
|
+
it("is a Restyle Look on Wan 2.7 VideoEdit that keeps the source's own sound and length, whose description warns it is for b-roll, with the finish sentence and no device in its text", () => {
|
|
473
|
+
const p = preset("video-to-video", "video-to-video/phone-footage-look")
|
|
474
|
+
expect(p.group).toBe("Restyle Looks")
|
|
475
|
+
expect(leversOf(p.data, "promptPrefix", "negativePrompt")).toEqual({
|
|
476
|
+
provider: "wan-videoedit", v2vResolution: "720p", videoEditDuration: "0", audioSetting: "origin", promptExtend: false,
|
|
477
|
+
})
|
|
478
|
+
expect(p.description).toContain("b-roll")
|
|
479
|
+
expect(p.description).toContain("not for talking clips")
|
|
480
|
+
expect(p.data.promptPrefix as string).toContain(REALISM_FINISH)
|
|
481
|
+
expect((p.data.negativePrompt as string).length).toBeLessThanOrEqual(500)
|
|
482
|
+
expect(allText(p.data)).not.toMatch(DEVICE_WORDS)
|
|
483
|
+
})
|
|
484
|
+
})
|
|
485
|
+
|
|
486
|
+
describe("Wave 2 — UGC Ad script style guide", () => {
|
|
487
|
+
it("keeps its name and slot, is set to Claude Opus 5.5 at high effort, and asks for the payload first, spoken texture, the exact brand, digits and only the user's figures — without the JSON shape", () => {
|
|
488
|
+
const p = preset("generate-script", "generate-script/ugc-ad")
|
|
489
|
+
expect(p.name).toBe("UGC Ad (Hook–Show–Proof–Opinion)")
|
|
490
|
+
expect(p.data).toMatchObject({ llmModel: "claude-opus-5.5", reasoningEffort: "high" })
|
|
491
|
+
const g = p.data.styleGuide as string
|
|
492
|
+
expect(g).toContain("{product}")
|
|
493
|
+
for (const s of ["claim the viewer can check", "contractions", "exactly as given", "as digits", "Use only figures the user supplies", "take the Proof from the topic"]) expect(g).toContain(s)
|
|
494
|
+
expect(g).not.toContain('"hook"')
|
|
495
|
+
})
|
|
496
|
+
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest"
|
|
2
2
|
import { FACTORY_PRESETS, getFactoryPresets, groupFactoryPresets } from "../factory-presets.js"
|
|
3
|
-
import { extractPresetData, PRESET_APPLY_CLEAR_KEYS } from "@nodaro/shared"
|
|
3
|
+
import { extractPresetData, LLM_MODEL_IDS, PRESET_APPLY_CLEAR_KEYS } from "@nodaro/shared"
|
|
4
4
|
import { COMPOSER_PLAN_MAP, COMPOSER_PLAN_FIELDS } from "@nodaro/shared"
|
|
5
5
|
import { STYLE_IDS } from "../index.js"
|
|
6
6
|
import { joinPromptParts } from "../resolve-prompt.js"
|
|
@@ -54,6 +54,13 @@ describe("FACTORY_PRESETS", () => {
|
|
|
54
54
|
}
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
+
it("names an LLM only by an id the routes accept", () => {
|
|
58
|
+
for (const p of Object.values(FACTORY_PRESETS).flat()) {
|
|
59
|
+
if (p.data.llmModel === undefined) continue
|
|
60
|
+
expect(LLM_MODEL_IDS, `${p.id}: llmModel ${String(p.data.llmModel)}`).toContain(p.data.llmModel as never)
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
|
|
57
64
|
it("generate-image factory presets use a known image provider", () => {
|
|
58
65
|
for (const p of getFactoryPresets("generate-image")) {
|
|
59
66
|
if (p.data.provider !== undefined) {
|
|
@@ -280,13 +287,30 @@ describe("generate-image factory preset data validity", () => {
|
|
|
280
287
|
}
|
|
281
288
|
})
|
|
282
289
|
|
|
283
|
-
|
|
290
|
+
// These presets carry their whole wording in the prompt; an added negative list would change the wording they were written with.
|
|
291
|
+
const NEGATIVE_EXEMPT: ReadonlySet<string> = new Set([
|
|
292
|
+
"generate-image/unretouched-portrait", "generate-image/phone-selfie", "generate-image/candid-snapshot",
|
|
293
|
+
"generate-image/mirror-selfie", "generate-image/creator-at-home", "generate-image/video-call-frame",
|
|
294
|
+
"generate-image/macro-skin-study", "generate-image/product-in-hand", "generate-image/skin-detail-sheet",
|
|
295
|
+
"generate-image/add-skin-realism", "generate-image/natural-photo-finish", "generate-image/phone-camera-look",
|
|
296
|
+
"generate-image/undo-beauty-filter",
|
|
297
|
+
])
|
|
298
|
+
|
|
299
|
+
it("every generate-image preset has a non-empty negativePrompt (except the exempt ones, which carry none)", () => {
|
|
284
300
|
for (const p of presets) {
|
|
301
|
+
if (NEGATIVE_EXEMPT.has(p.id)) {
|
|
302
|
+
expect(p.data.negativePrompt, `${p.id}: carries its whole wording in the prompt`).toBeUndefined()
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
285
305
|
const neg = (p.data.negativePrompt as string | undefined) ?? ""
|
|
286
306
|
expect(neg.trim().length, `${p.id}: missing negativePrompt`).toBeGreaterThan(0)
|
|
287
307
|
}
|
|
288
308
|
})
|
|
289
309
|
|
|
310
|
+
it("every negative-exempt id exists", () => {
|
|
311
|
+
for (const id of NEGATIVE_EXEMPT) expect(presets.some((p) => p.id === id), id).toBe(true)
|
|
312
|
+
})
|
|
313
|
+
|
|
290
314
|
it("non-style-pinned generate-image prompts are substantive (no thin one-liners)", () => {
|
|
291
315
|
for (const p of presets) {
|
|
292
316
|
if (p.data.style !== undefined) continue // style field carries the look; short prompt is fine
|
|
@@ -16,10 +16,10 @@ describe("nodeSupportsPromptAffixes", () => {
|
|
|
16
16
|
expect(nodeSupportsPromptAffixes("not-a-node")).toBe(false)
|
|
17
17
|
expect(nodeSupportsPromptAffixes(undefined)).toBe(false)
|
|
18
18
|
})
|
|
19
|
-
it("PROMPT_AFFIX_NODE_TYPES is exactly the registry minus opt-outs (
|
|
19
|
+
it("PROMPT_AFFIX_NODE_TYPES is exactly the registry minus opt-outs (42 types)", () => {
|
|
20
20
|
const expected = Object.entries(NODE_PROMPT_FIELDS).filter(([, s]) => s.affixes !== false).map(([t]) => t)
|
|
21
21
|
expect([...PROMPT_AFFIX_NODE_TYPES].sort()).toEqual(expected.sort())
|
|
22
|
-
expect(PROMPT_AFFIX_NODE_TYPES.size).toBe(
|
|
22
|
+
expect(PROMPT_AFFIX_NODE_TYPES.size).toBe(42)
|
|
23
23
|
})
|
|
24
24
|
it("getPromptFields still resolves", () => expect(getPromptFields("generate-image")?.prompt).toBe("prompt"))
|
|
25
25
|
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// packages/prompts/src/__tests__/realism-wording.test.ts
|
|
2
|
+
import { describe, it, expect } from "vitest"
|
|
3
|
+
import * as wording from "../realism-wording.js"
|
|
4
|
+
|
|
5
|
+
// These strings are read at run time by other Nodaro services; changing one changes their output on the next deploy, so a change needs its owner's review.
|
|
6
|
+
describe("realism wording (frozen)", () => {
|
|
7
|
+
it("exports exactly the selected fragments and the two person functions", () => {
|
|
8
|
+
expect(Object.keys(wording).sort()).toEqual(
|
|
9
|
+
["NEUTRAL_PERSON", "REALISM_FINISH", "REALISM_PASS_INSTRUCTION", "REALISM_SKIN", "realismEmptyHands", "realismViewpoint"].sort(),
|
|
10
|
+
)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it("keeps every fragment's text", () => {
|
|
14
|
+
expect(wording.REALISM_SKIN).toBe(
|
|
15
|
+
"Skin shows its real structure: visible pores that vary in size across the nose and cheeks, fine vellus hair catching the light, faint uneven pigmentation, a little redness around the nostrils, natural oil on the forehead.",
|
|
16
|
+
)
|
|
17
|
+
expect(wording.REALISM_FINISH).toBe(
|
|
18
|
+
"The plain look of an everyday small-sensor snapshot: colour left as it was recorded, deep focus from the nearest shoulder to the background, a fine grain in the darker areas, bright areas that fade rather than clip.",
|
|
19
|
+
)
|
|
20
|
+
expect(wording.REALISM_PASS_INSTRUCTION).toBe(
|
|
21
|
+
"Make this photo look like an unretouched everyday snapshot: skin that shows its pores, its fine hairs and the small changes in tone real skin has, colour left as it was recorded, everything sharp from the shoulders to the background, a little grain in the shadows. Take out anything that makes it look rendered — smoothed, waxy or airbrushed skin, a glow, over-sharpened edges — and add no text or icons. Otherwise it stays the same person in the same frame: the face and its proportions, hair, clothing, pose and background as they are.",
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it("keeps the viewpoint and empty-hands sentences, neutral and gendered", () => {
|
|
26
|
+
expect(wording.realismViewpoint(wording.NEUTRAL_PERSON)).toBe(
|
|
27
|
+
"The person is looking straight at the viewer from eye level, mid-sentence: their face and shoulders close up, the framing loose and a little crooked, both arms relaxed and down out of the frame. The whole image is the photograph itself, edge to edge, with no text or icons anywhere.",
|
|
28
|
+
)
|
|
29
|
+
expect(wording.realismViewpoint({ subject: "she", possessive: "her" })).toBe(
|
|
30
|
+
"She is looking straight at the viewer from eye level, mid-sentence: her face and shoulders close up, the framing loose and a little crooked, both arms relaxed and down out of the frame. The whole image is the photograph itself, edge to edge, with no text or icons anywhere.",
|
|
31
|
+
)
|
|
32
|
+
expect(wording.realismEmptyHands(wording.NEUTRAL_PERSON)).toBe("Their hands are empty; the person is holding nothing.")
|
|
33
|
+
expect(wording.realismEmptyHands({ subject: "he", possessive: "his" })).toBe("His hands are empty; he is holding nothing.")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("names no device", () => {
|
|
37
|
+
const DEVICE_WORDS = /\b(?:smartphones?|iphones?|phones?|cameras?|lens(?:es)?|webcams?|laptops?|tablets?|screens?|monitors?|displays?|tripods?|selfie[ -]sticks?)\b/i
|
|
38
|
+
for (const s of [wording.REALISM_SKIN, wording.REALISM_FINISH, wording.REALISM_PASS_INSTRUCTION, wording.realismViewpoint(wording.NEUTRAL_PERSON), wording.realismEmptyHands(wording.NEUTRAL_PERSON)])
|
|
39
|
+
expect(s).not.toMatch(DEVICE_WORDS)
|
|
40
|
+
})
|
|
41
|
+
})
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import type { FactoryPreset } from "./types.js"
|
|
2
2
|
import { stylizedSubjectFor, editsFor } from "./shared-image.js"
|
|
3
|
+
import { NEUTRAL_PERSON, REALISM_FINISH, REALISM_SKIN, realismEmptyHands, realismViewpoint } from "../realism-wording.js"
|
|
4
|
+
|
|
5
|
+
/** Joins the sentences of one prompt with single spaces. */
|
|
6
|
+
const sentences = (...parts: string[]): string => parts.join(" ")
|
|
7
|
+
/** The model and resolution the Realism & Authentic presets render with. */
|
|
8
|
+
const REALISM_MODEL = { provider: "gpt-image-2", resolution: "2K" } as const
|
|
9
|
+
/** The model and quality the close skin studies render with (Macro Skin Study, Skin Detail Sheet). */
|
|
10
|
+
const SKIN_STUDY_MODEL = { provider: "seedream-5-pro", quality: "high" } as const
|
|
3
11
|
|
|
4
12
|
export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
|
|
5
13
|
// ── Reference Sheet ──────────────────────────────────────────────────────
|
|
@@ -168,6 +176,22 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
|
|
|
168
176
|
"illustration, cartoon, 3d render, garbled text, misspelled labels, watermark, deformed anatomy, inconsistent markings between panels, wrong breed, blurry, low resolution",
|
|
169
177
|
},
|
|
170
178
|
},
|
|
179
|
+
{
|
|
180
|
+
id: "generate-image/skin-detail-sheet",
|
|
181
|
+
name: "Skin Detail Sheet",
|
|
182
|
+
description: "Connect a sharp face photo → a six-panel skin reference.",
|
|
183
|
+
group: "Reference Sheet",
|
|
184
|
+
data: {
|
|
185
|
+
...SKIN_STUDY_MODEL,
|
|
186
|
+
aspectRatio: "3:2",
|
|
187
|
+
promptPrefix: sentences(
|
|
188
|
+
"Create a single skin detail reference sheet titled \"SKIN DETAIL SHEET\" from the attached photo of the person, the single source of truth for identity, skin tone and features. The same person, the same light and the same unretouched skin in every panel. Six panels — render ALL six panel headings exactly as written, never merging or omitting a panel: PANEL 01 — FULL FACE (front, eye level) · PANEL 02 — LEFT CHEEK & NOSE · PANEL 03 — RIGHT CHEEK & JAW · PANEL 04 — FOREHEAD & HAIRLINE · PANEL 05 — EYES & BROWS · PANEL 06 — LIPS & CHIN. In every panel:",
|
|
189
|
+
REALISM_SKIN,
|
|
190
|
+
"Flat, even daylight, neutral colour, everything sharp.",
|
|
191
|
+
),
|
|
192
|
+
promptSuffix: "Plain neutral grey background, thin white panel labels in English. Use this sheet as a visual reference for consistent skin detail of this person across generations.",
|
|
193
|
+
},
|
|
194
|
+
},
|
|
171
195
|
// ── Cast & Consistency (clean grids per the multi-character workflow:
|
|
172
196
|
// sterile neutral-background reference grids built to be FED BACK as identity
|
|
173
197
|
// anchors — unlike the editorial boards above, decoration here is noise.
|
|
@@ -1703,4 +1727,94 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
|
|
|
1703
1727
|
// instruction lives in the prompt, not `style`.
|
|
1704
1728
|
...stylizedSubjectFor("generate-image"),
|
|
1705
1729
|
...editsFor("generate-image"),
|
|
1730
|
+
|
|
1731
|
+
// ── Realism & Authentic ──────────────────────────────────────────────────
|
|
1732
|
+
{
|
|
1733
|
+
id: "generate-image/unretouched-portrait",
|
|
1734
|
+
name: "Unretouched Portrait",
|
|
1735
|
+
description: "Straight-on head-and-shoulders portrait, real skin, everyday finish.",
|
|
1736
|
+
group: "Realism & Authentic",
|
|
1737
|
+
data: {
|
|
1738
|
+
...REALISM_MODEL,
|
|
1739
|
+
aspectRatio: "3:4",
|
|
1740
|
+
prompt: sentences("{subject || An adult woman in her thirties with dark shoulder-length hair}, a straight-on head-and-shoulders portrait at eye level against a plain pale wall, soft daylight from one side.", REALISM_SKIN, REALISM_FINISH),
|
|
1741
|
+
},
|
|
1742
|
+
},
|
|
1743
|
+
{
|
|
1744
|
+
id: "generate-image/phone-selfie",
|
|
1745
|
+
name: "Phone Selfie",
|
|
1746
|
+
description: "Eye-level, arm's-length self-portrait look.",
|
|
1747
|
+
group: "Realism & Authentic",
|
|
1748
|
+
data: {
|
|
1749
|
+
...REALISM_MODEL,
|
|
1750
|
+
aspectRatio: "3:4",
|
|
1751
|
+
prompt: sentences("{subject || An adult woman in her late twenties with her hair tied back}, {setting || on a couch at home, a cushion and a lamp behind}.", realismViewpoint(NEUTRAL_PERSON), realismEmptyHands(NEUTRAL_PERSON), REALISM_SKIN, REALISM_FINISH),
|
|
1752
|
+
},
|
|
1753
|
+
},
|
|
1754
|
+
{
|
|
1755
|
+
id: "generate-image/candid-snapshot",
|
|
1756
|
+
name: "Candid Phone Snapshot",
|
|
1757
|
+
description: "Caught mid-moment, an everyday snapshot.",
|
|
1758
|
+
group: "Realism & Authentic",
|
|
1759
|
+
data: {
|
|
1760
|
+
...REALISM_MODEL,
|
|
1761
|
+
aspectRatio: "3:4",
|
|
1762
|
+
prompt: sentences("{subject || An adult man in his thirties} caught mid-moment, not posing, glancing away, the framing a little crooked, part of the scene cut by the edge, {setting || at a kitchen table at home}.", REALISM_SKIN, REALISM_FINISH),
|
|
1763
|
+
},
|
|
1764
|
+
},
|
|
1765
|
+
{
|
|
1766
|
+
id: "generate-image/mirror-selfie",
|
|
1767
|
+
name: "Mirror Selfie",
|
|
1768
|
+
description: "Mirror self-portrait, phone in hand.",
|
|
1769
|
+
group: "Realism & Authentic",
|
|
1770
|
+
data: {
|
|
1771
|
+
...REALISM_MODEL,
|
|
1772
|
+
aspectRatio: "9:16",
|
|
1773
|
+
prompt: sentences("{subject || An adult woman in her twenties} photographs their reflection in a full-length mirror {setting || in a hallway at home}, a phone held at chest height with its back toward the mirror.", REALISM_SKIN, REALISM_FINISH),
|
|
1774
|
+
},
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
id: "generate-image/creator-at-home",
|
|
1778
|
+
name: "Creator at Home · Waist-Up",
|
|
1779
|
+
description: "A waist-up reference of the same person — wire a face photo into the references.",
|
|
1780
|
+
group: "Realism & Authentic",
|
|
1781
|
+
data: {
|
|
1782
|
+
...REALISM_MODEL,
|
|
1783
|
+
aspectRatio: "9:16",
|
|
1784
|
+
prompt: sentences("Waist-up photo of {image:1:person}, an adult, standing at home in front of a plain wall, arms relaxed, empty hands.", REALISM_SKIN, REALISM_FINISH),
|
|
1785
|
+
},
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
id: "generate-image/video-call-frame",
|
|
1789
|
+
name: "Webcam / Video-Call Frame",
|
|
1790
|
+
description: "Chest-up video-call framing.",
|
|
1791
|
+
group: "Realism & Authentic",
|
|
1792
|
+
data: {
|
|
1793
|
+
...REALISM_MODEL,
|
|
1794
|
+
aspectRatio: "16:9",
|
|
1795
|
+
prompt: sentences("{subject || An adult man in his forties}, chest-up, a little off-centre, slightly wide, seen from a fixed point just above eye level about an arm's length away, a softly lit room behind, flat even light from the front.", REALISM_FINISH),
|
|
1796
|
+
},
|
|
1797
|
+
},
|
|
1798
|
+
{
|
|
1799
|
+
id: "generate-image/macro-skin-study",
|
|
1800
|
+
name: "Macro Skin Study",
|
|
1801
|
+
description: "A close study of one area of skin.",
|
|
1802
|
+
group: "Realism & Authentic",
|
|
1803
|
+
data: {
|
|
1804
|
+
...SKIN_STUDY_MODEL,
|
|
1805
|
+
aspectRatio: "1:1",
|
|
1806
|
+
prompt: sentences("{feature || the left cheek and the side of the nose} of {subject || an adult woman in her thirties}, filling the frame, everything sharp front to back, flat cool daylight, neutral colour, a plain study of the skin, not a glamour shot.", REALISM_SKIN),
|
|
1807
|
+
},
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
id: "generate-image/product-in-hand",
|
|
1811
|
+
name: "Product in Hand — phone photo",
|
|
1812
|
+
description: "Wire a photo of the product into the references.",
|
|
1813
|
+
group: "Realism & Authentic",
|
|
1814
|
+
data: {
|
|
1815
|
+
...REALISM_MODEL,
|
|
1816
|
+
aspectRatio: "3:4",
|
|
1817
|
+
prompt: sentences("{subject || An adult woman in her thirties} holds {image:1:product} at chest height toward the viewer, label facing out, {setting || in a bright room at home}.", REALISM_FINISH),
|
|
1818
|
+
},
|
|
1819
|
+
},
|
|
1706
1820
|
]
|