@nodaro/prompts 1.36.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodaro/prompts",
3
- "version": "1.36.0",
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",
@@ -4,6 +4,7 @@ import { aspectRatioOptionsByKind, captionRoutesToRemotion, MODELS_WITH_REFERENC
4
4
  import { getFactoryPresets, type FactoryPreset } from "../factory-presets.js"
5
5
  import { assembleImageInput } from "../assemble-image-input.js"
6
6
  import { computeNodePrompt } from "../resolve-prompt.js"
7
+ import { resolveReferenceTokens } from "../video-reference-resolver.js"
7
8
  import { NEUTRAL_PERSON, REALISM_FINISH, REALISM_PASS_INSTRUCTION, REALISM_SKIN, realismEmptyHands, realismViewpoint } from "../realism-wording.js"
8
9
 
9
10
  /** Words that name a recording or viewing device. Plural forms included. */
@@ -238,8 +239,8 @@ describe("Wave 1 — Realism & Authentic", () => {
238
239
  })
239
240
 
240
241
  it("every subject slot has a default naming an adult; every setting slot has a default", () => {
241
- for (const id of REALISM_IDS) {
242
- const text = allText(preset("generate-image", id).data)
242
+ for (const id of [...REALISM_IDS, ...UGC_IDS]) {
243
+ const text = allText(preset(id.split("/")[0]!, id).data)
243
244
  for (const m of text.matchAll(/\{subject([^{}]*)\}/g)) {
244
245
  expect(m[1], `${id}: {subject} needs a default`).toMatch(/\|\|/)
245
246
  expect(m[1]!.split("||")[1], `${id}: the default names an adult`).toMatch(ADULT)
@@ -362,3 +363,134 @@ describe("Wave 1 — Realism Audit", () => {
362
363
  expect(prompt.length).toBeLessThanOrEqual(2000)
363
364
  })
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) {
@@ -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 (41 types)", () => {
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(41)
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
  })
@@ -1,5 +1,18 @@
1
1
  import type { FactoryPreset } from "./types.js"
2
2
 
3
+ // ── UGC & Creator: shared sentences, kept word for word ─────────────────
4
+ const UGC_TAIL = "Keep it subtitle-free, do not generate a watermark."
5
+ const UGC_PAUSE = "A short natural pause before the first word and after the last."
6
+ const ugcSpeech = (line: string): string =>
7
+ `The person says, in a natural American accent, lively and conversational, quicker on connecting words and slower on numbers: "{line || ${line}}"`
8
+ /** Seedance 2.5, vertical, 8 s, 720p with audio — pinned, so every surface renders and bills the same clip. */
9
+ const UGC_VIDEO = { provider: "seedance-2-5", aspectRatio: "9:16", duration: 8, resolution: "720p", generateAudio: true } as const
10
+ /** The same clip at 5 s, for a one-line answer or a quick reaction that 8 s would leave mostly silent. */
11
+ const UGC_SHORT_VIDEO = { ...UGC_VIDEO, duration: 5 } as const
12
+ const ugcPrompt = (...parts: string[]): string => [...parts, UGC_TAIL].join(" ")
13
+ const TALKING_AT_HOME =
14
+ "{image:1:person} talks straight to the viewer at eye level at home, face and shoulders close, the framing a little crooked, deep focus, plain colour, a faint natural sway;"
15
+
3
16
  export const GENERATE_VIDEO_PRESETS: readonly FactoryPreset[] = [
4
17
  // ── Camera Moves (composable cinematography fragments) ───────────────────
5
18
  {
@@ -1135,4 +1148,62 @@ export const GENERATE_VIDEO_PRESETS: readonly FactoryPreset[] = [
1135
1148
  "Keep everything else in the clip unchanged: the same people, motion, framing and timing. Keep it subtitle-free, do not generate a watermark.",
1136
1149
  },
1137
1150
  },
1151
+
1152
+ // ── UGC & Creator (everyday talking-to-camera clips) ──────────────────────
1153
+ {
1154
+ id: "generate-video/ugc-talking-head",
1155
+ name: "UGC Talking Head",
1156
+ description: "Talks to the viewer at home — type the line to be spoken; write numbers as words.",
1157
+ group: "UGC & Creator",
1158
+ data: { ...UGC_VIDEO, prompt: ugcPrompt(`${TALKING_AT_HOME} hands move naturally while talking and hold nothing.`, ugcSpeech("I cut my weekly reporting from three hours to twenty minutes."), UGC_PAUSE) },
1159
+ },
1160
+ {
1161
+ id: "generate-video/ugc-product-closer",
1162
+ name: "UGC Product Closer",
1163
+ description: "Holds your product up while talking — wire a face photo and a product photo; type the line, numbers as words.",
1164
+ group: "UGC & Creator",
1165
+ data: { ...UGC_VIDEO, prompt: ugcPrompt(`${TALKING_AT_HOME} hands move naturally while talking and hold nothing until the first word.`, "As they say the first word, one hand lifts {image:2:product} into view and holds it up beside their face, its front turned to the viewer, while they talk; the other hand keeps moving naturally.", ugcSpeech("Honestly, this is the one I keep coming back to."), UGC_PAUSE, "After the last word, they lower the product with a small smile.") },
1166
+ },
1167
+ {
1168
+ id: "generate-video/handheld-unboxing",
1169
+ name: "Handheld Unboxing",
1170
+ description: "Opens a box with their own hands and reveals your product — wire a face photo and a product photo.",
1171
+ group: "UGC & Creator",
1172
+ data: { ...UGC_VIDEO, prompt: ugcPrompt(
1173
+ "Close on a table at home, seen the whole time through the eyes of {image:1:person} as they look down at it.",
1174
+ "From the first moment their own two hands are on the lid of a plain box, both forearms coming up from the bottom of the frame.",
1175
+ "One adult's left and right hand, no one else's; only their hands and forearms are seen.",
1176
+ "They open the box at once and lift out {image:2:product} with both hands, clear of the box by the middle of the clip.",
1177
+ "Until the end they hold it up in front of them, the whole of it in frame, tilting it slowly in the light with the same side toward them.",
1178
+ "Daylight, deep focus from their hands to the background, plain colour, the framing a little crooked, a faint natural sway.",
1179
+ ) },
1180
+ },
1181
+ {
1182
+ id: "generate-video/get-ready-with-me",
1183
+ name: "Get Ready With Me",
1184
+ description: "Gets ready at a mirror while chatting — type the line, numbers as words.",
1185
+ group: "UGC & Creator",
1186
+ data: { ...UGC_VIDEO, prompt: ugcPrompt("{image:1:person} at a bathroom mirror applies a little makeup while chatting to the viewer, everyday light, deep focus, plain colour.", ugcSpeech("Two minutes and I'm ready to go."), UGC_PAUSE) },
1187
+ },
1188
+ {
1189
+ id: "generate-video/street-qa",
1190
+ name: "Street-Style Q&A",
1191
+ description: "A staged street-interview look — cast it yourself; type a short answer, numbers as words.",
1192
+ group: "UGC & Creator",
1193
+ data: { ...UGC_SHORT_VIDEO, prompt: ugcPrompt("A staged street-interview look: {image:1:person} stands on a sidewalk answering an interviewer who stays out of frame, beside the viewer; only the interviewer's hand is seen, holding a plain microphone out toward them from a lower corner of the frame, passers-by soft in the background.", "Their eyes stay on the interviewer the whole time, off to the side the microphone comes from.", "The answer starts at once, with no wait before the first word.", ugcSpeech("I'd say about twenty minutes a day, tops."), "After the last word, a small laugh and a shrug, eyes still on the interviewer.") },
1194
+ },
1195
+ {
1196
+ id: "generate-video/reaction-shot",
1197
+ name: "Reaction Shot",
1198
+ description: "A quick reaction to something out of frame.",
1199
+ group: "UGC & Creator",
1200
+ data: { ...UGC_SHORT_VIDEO, prompt: ugcPrompt("{image:1:person} sits on a couch at home, a cushion and a lamp behind them, looking off to one side at something just out of frame; at eye level, the framing a little crooked, deep focus, plain colour, a faint natural sway.", "Almost at once, what they see catches them by surprise: their eyes go wide, a hand flies to their mouth and they break into a laugh, their shoulders shaking.", "Still laughing, they glance briefly at the viewer, then look back off to the side, shaking their head.", "A short exclamation at most.") },
1201
+ },
1202
+ {
1203
+ id: "generate-video/acts-on-phone",
1204
+ name: "Acts on Phone, Screen Hidden",
1205
+ description: "Taps a phone whose screen faces away — type the line, numbers as words.",
1206
+ group: "UGC & Creator",
1207
+ data: { ...UGC_VIDEO, prompt: ugcPrompt(`${TALKING_AT_HOME} hands move naturally while talking and hold nothing until the last seconds.`, ugcSpeech("That's the whole setup, and it took me five minutes."), UGC_PAUSE, "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.") },
1208
+ },
1138
1209
  ]
@@ -98,6 +98,27 @@ export const LLM_CHAT_PRESETS: readonly FactoryPreset[] = [
98
98
  temperature: 0.9,
99
99
  },
100
100
  },
101
+ {
102
+ id: "llm-chat/ugc-monologue-writer",
103
+ name: "UGC Monologue Writer",
104
+ description: "A talking-to-camera script as JSON: hook, beats, closer.",
105
+ group: "Writing & Marketing",
106
+ data: {
107
+ systemPrompt:
108
+ "You write the script of a short vertical video in which one person talks straight to the camera about a product, a site or an idea. Return it as JSON with three parts.\n\n" +
109
+ "hook — the first line: one short spoken clause of at most 8 words. It leads with the payload the viewer can check (a number, a price, a time saved, a before and after, a named result) and opens on the claim itself — no greeting, no filler, no introduction first.\n\n" +
110
+ "beats — one beat per point the video makes, in order. When the user lists screenshots, write one beat per screenshot, in the same order. The first beat names the product. Every beat is one short spoken sentence a person says in one easy breath; nothing is crammed in.\n\n" +
111
+ "closer — the last line. It ends on one simple physical action the speaker can do on camera.\n\n" +
112
+ "Leave room for a short pause at the start and the end of each clip. Sound said, not written: contractions, one connecting word, sentences of mixed length, and one live, imperfect moment such as a small self-correction or an aside. Carry emphasis with word choice and repetition, never with capitals or stage directions. Write only what is spoken: no speaker names, brackets, emoji or sound effects.\n\n" +
113
+ "Spell the brand exactly as the user gives it, every time. Write numbers as digits, except inside the brand's own name. Use only figures the user supplies. Write in the language the user writes in.\n\n" +
114
+ "When the script is in Hebrew: write spoken Israeli Hebrew, the way a person talks to their phone, not ad copy. Every first-person verb and adjective agrees with the speaker's gender, and never switches. Speak to the viewer in the plural (\"תנסו\"), so the viewer is not given a gender. Keep sentences short. Write numbers and prices as digits (\"3\", \"₪49\", \"20%\"). Keep the brand exactly as given, usually in Latin letters; when a Hebrew prefix joins it, use a hyphen (\"ב-Planwell\"). Leave out vowel points. The hook is at most 8 words, as in English, a word with a joined prefix counting as one word, and opens on the result, the number or the contrast — never on a greeting or a filler word.\n\n" +
115
+ "Return only this JSON, no code fences, no commentary:\n{\"hook\": \"…\", \"beats\": [\"…\", \"…\"], \"closer\": \"…\"}",
116
+ llmModel: "claude-opus-5.5",
117
+ reasoningEffort: "high",
118
+ temperature: 0.8,
119
+ maxTokens: 2048,
120
+ },
121
+ },
101
122
 
102
123
  // ── Utility ──────────────────────────────────────────────────────────────
103
124
  {
@@ -216,8 +237,10 @@ export const GENERATE_SCRIPT_PRESETS: readonly FactoryPreset[] = [
216
237
  sceneCount: 4,
217
238
  targetLength: 30,
218
239
  structure: "freeform",
240
+ llmModel: "claude-opus-5.5",
241
+ reasoningEffort: "high",
219
242
  styleGuide:
220
- "Selfie-style UGC testimonial for {product}. Structure: Hook (stop the scroll) → Show (the product in real use) → Proof (a concrete result or detail) → Opinion (a genuine personal take). A real person sharing a real opinion — first-person, natural, unpolished. Short spoken sentences for clean lip-sync; no salesy voiceover.",
243
+ "Selfie-style UGC testimonial for {product}. Open on the payload: the first spoken line is a short claim the viewer can check — a number, a price, a time saved, a before and after — never a greeting or an introduction. Then Show (the product in real use) → Proof (a concrete result or detail) → Opinion (a genuine personal take). Sound said, not written: contractions, sentences of mixed length, one small aside; carry emphasis with word choice, never capitals or stage directions. Spell the brand exactly as given and write numbers as digits. Use only figures the user supplies, in what is said and what is shown, and take the Proof from the topic. Short spoken sentences for clean lip-sync; no salesy voiceover.",
221
244
  },
222
245
  },
223
246
 
@@ -1,4 +1,5 @@
1
1
  import type { FactoryPreset } from "./types.js"
2
+ import { REALISM_FINISH } from "../realism-wording.js"
2
3
 
3
4
  export const VIDEO_TO_VIDEO_PRESETS: readonly FactoryPreset[] = [
4
5
  {
@@ -43,6 +44,21 @@ export const VIDEO_TO_VIDEO_PRESETS: readonly FactoryPreset[] = [
43
44
  group: "Restyle Looks",
44
45
  data: { provider: "wan", promptPrefix: "Restyle as a flowing watercolor animation with soft washes, bleeding edges, and visible paper texture. Keep the original motion and composition.", negativePrompt: "photorealistic, harsh edges, blurry, flickering" },
45
46
  },
47
+ {
48
+ id: "video-to-video/phone-footage-look",
49
+ name: "Phone Footage Look",
50
+ description: "Everyday handheld look for b-roll — it re-renders faces, so not for talking clips.",
51
+ group: "Restyle Looks",
52
+ data: {
53
+ provider: "wan-videoedit",
54
+ v2vResolution: "720p",
55
+ videoEditDuration: "0",
56
+ audioSetting: "origin",
57
+ promptExtend: false,
58
+ promptPrefix: `Keep everything in this clip as it is — the same people, objects, colours and light — and only make it look like everyday handheld digital video, left unedited. ${REALISM_FINISH} The edges of the frame keep the light they have in the clip, and each moment keeps the brightness it has in the clip, from the first frame to the last. Preserve the original motion, composition and timing.`,
59
+ negativePrompt: "cinematic grade, shallow depth of field, glow, skin smoothing, flickering, light leaks, flare, coloured bands at the edges of the frame, colour cast, shifted colours, oversaturated colours, vintage film look, overexposure, pulsing brightness, heavy grain",
60
+ },
61
+ },
46
62
  ]
47
63
 
48
64
  export const ADD_CAPTIONS_PRESETS: readonly FactoryPreset[] = [
@@ -89,6 +89,8 @@ export const NODE_PROMPT_FIELDS: Readonly<Record<string, PromptFieldSpec>> = {
89
89
  "cinematic-avatar": { prompt: "prompt", media: "video", inline: true },
90
90
  "video-sfx": { prompt: "prompt", negative: "negativePrompt", promptLabel: "Sound prompt", media: "audio", inline: true },
91
91
  "video-retake": { prompt: "prompt", promptLabel: "Retake prompt", media: "video", inline: true },
92
+ // Video Mask (SAM 3) — the subject to track, like Generate Mask's "what to mask".
93
+ "video-mask": { prompt: "prompt", promptLabel: "What to mask", media: "video", inline: true },
92
94
  // ── Audio / music ──
93
95
  "generate-music": { prompt: "prompt", media: "audio", inline: true },
94
96
  "suno-generate": { prompt: "prompt", media: "audio", inline: true },