@nodaro/prompts 1.3.1 → 1.5.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.3.1",
3
+ "version": "1.5.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",
@@ -171,3 +171,25 @@ describe("resolveSeedance2Inputs", () => {
171
171
  }
172
172
  })
173
173
  })
174
+
175
+ describe("promptBindsFirstFrame suffix suppression (overlap colon-position finding)", () => {
176
+ it("suppresses the trailing sentence when the prompt already binds the first frame", () => {
177
+ const r = resolveSeedance2Inputs({
178
+ prompt: "extend @video_1 as follows:use @image_1 as the first frame, it is the last keyframe of @video_1\nbeats",
179
+ firstFrameUrl: "https://r2/anchor.jpg",
180
+ refVideoUrls: ["https://r2/ref.mp4"],
181
+ })
182
+ expect(r.promptSuffix).toBe("")
183
+ expect(r.referenceImageUrls).toEqual(["https://r2/anchor.jpg"]) // image still rides
184
+ })
185
+
186
+ it("keeps the sentence for plain prompts and for first+last frame pairs", () => {
187
+ const plain = resolveSeedance2Inputs({ prompt: "a cat", firstFrameUrl: "https://r2/a.jpg", refVideoUrls: ["https://r2/v.mp4"] })
188
+ expect(plain.promptSuffix).toContain("opening (first) frame")
189
+ const both = resolveSeedance2Inputs({
190
+ prompt: "use @image_1 as the first frame, it is the last keyframe of @video_1",
191
+ firstFrameUrl: "https://r2/a.jpg", lastFrameUrl: "https://r2/b.jpg", refVideoUrls: ["https://r2/v.mp4"],
192
+ })
193
+ expect(both.promptSuffix).toContain("closing (last) frame") // pair sentence kept — last frame has no in-prompt binding
194
+ })
195
+ })
@@ -231,6 +231,54 @@ export const GENERATE_IMAGE_PRESETS: readonly FactoryPreset[] = [
231
231
  "leftover callout labels, visible text labels, text boxes, leader lines, garbled text, restyled scene, recolored unrelated elements, redrawn image, cartoon, illustration, watermark, blurry",
232
232
  },
233
233
  },
234
+ // ── Face Privacy (connect the photo → {image:1}) ─────────────────────────
235
+ // gpt-image-2 + aspectRatio "auto" so the output keeps the input photo's
236
+ // shape. NOTE: KIE's gpt-image-2 spec forces resolution 1K when aspect is
237
+ // "auto" (the config panel's fail-safe snaps any other value back), so
238
+ // these pin 1K explicitly rather than shipping a self-contradicting 2K.
239
+ {
240
+ id: "generate-image/faceless-3d-head",
241
+ name: "Faceless · 3D Blank Head",
242
+ description: "Persons' faces → smooth generic white 3D head shape; everything else untouched.",
243
+ group: "Face Privacy",
244
+ data: {
245
+ provider: "gpt-image-2",
246
+ aspectRatio: "auto",
247
+ resolution: "1K",
248
+ prompt:
249
+ "make the persons in {image:1} faceless by making it 3d white generic face shape, keep everything else the same",
250
+ negativePrompt:
251
+ "visible facial features, eyes, nose, mouth, skin texture on the face, changing the background, altering clothing, hair or body, restyling the scene, relighting, changing pose or framing, cartoon look, distorted anatomy, watermark, blurry",
252
+ },
253
+ },
254
+ {
255
+ id: "generate-image/remove-faces",
256
+ name: "Remove Faces",
257
+ description: "Erase persons' faces entirely; everything else untouched.",
258
+ group: "Face Privacy",
259
+ data: {
260
+ provider: "gpt-image-2",
261
+ aspectRatio: "auto",
262
+ resolution: "1K",
263
+ prompt: "remove persons faces from {image:1}, keep everything else the same",
264
+ negativePrompt:
265
+ "visible facial features, eyes, nose, mouth, leftover face fragments, changing the background, altering clothing, hair or body, restyling the scene, relighting, changing pose or framing, horror or gore look, distorted anatomy, watermark, blurry",
266
+ },
267
+ },
268
+ {
269
+ id: "generate-image/transparent-faces",
270
+ name: "Transparent Faces",
271
+ description: "Persons' faces become transparent; everything else untouched.",
272
+ group: "Face Privacy",
273
+ data: {
274
+ provider: "gpt-image-2",
275
+ aspectRatio: "auto",
276
+ resolution: "1K",
277
+ prompt: "make faces transparent in {image:1}, keep everything else the same",
278
+ negativePrompt:
279
+ "opaque faces, visible facial features, eyes, nose, mouth, changing the background, altering clothing, hair or body, restyling the scene, relighting, changing pose or framing, distorted anatomy, watermark, blurry",
280
+ },
281
+ },
234
282
  // ── Photography & Cinematic ──────────────────────────────────────────────
235
283
  {
236
284
  id: "generate-image/cinematic-portrait",
@@ -4,6 +4,9 @@ import { REF_BINDING } from "./video-reference-resolver.js"
4
4
  export type Seedance2Mode = "first-frame" | "first-last-frame" | "reference"
5
5
 
6
6
  export interface Seedance2InputsArgs {
7
+ /** The generation prompt — used ONLY to detect an existing first-frame
8
+ * binding and suppress the duplicate trailing sentence. */
9
+ prompt?: string
7
10
  firstFrameUrl?: string
8
11
  lastFrameUrl?: string
9
12
  refImageUrls?: readonly string[]
@@ -37,6 +40,19 @@ const cleanList = (xs?: readonly string[]): string[] =>
37
40
  * reference_image_urls (after the user's own images, so their @Image ordinals do
38
41
  * not shift) and named in a doctrine-compliant prompt suffix.
39
42
  */
43
+ /** True when the prompt ALREADY binds an image as the first/opening frame —
44
+ * e.g. the gvp overlap transport's colon-position directive ("use @image_1
45
+ * as the first frame, it is the last keyframe of @video_1"). Field finding
46
+ * (2026-07-20): the first-frame directive only works adjacent to the extend
47
+ * colon; a duplicate sentence at the END of the prompt (this resolver's
48
+ * default position) dilutes it back into coin-flip behavior — so when the
49
+ * prompt carries its own binding, the suffix is suppressed (the frame image
50
+ * itself still rides the reference list). */
51
+ export function promptBindsFirstFrame(prompt: string | undefined): boolean {
52
+ if (!prompt) return false
53
+ return /@image_\d+\s+as\s+the\s+(first|opening)\s*(\(first\))?\s*frame/i.test(prompt)
54
+ }
55
+
40
56
  export function resolveSeedance2Inputs(args: Seedance2InputsArgs): Seedance2InputsResult {
41
57
  const firstFrameUrl = clean(args.firstFrameUrl)
42
58
  const lastFrameUrl = clean(args.lastFrameUrl)
@@ -76,7 +92,10 @@ export function resolveSeedance2Inputs(args: Seedance2InputsArgs): Seedance2Inpu
76
92
  if (lastFrameUrl) { referenceImageUrls.push(lastFrameUrl); lastOrdinal = referenceImageUrls.length }
77
93
 
78
94
  let promptSuffix = ""
79
- if (firstOrdinal > 0 && lastOrdinal > 0) {
95
+ const suppressFrameSentence = promptBindsFirstFrame(args.prompt)
96
+ if (suppressFrameSentence && firstOrdinal > 0 && lastOrdinal === 0) {
97
+ // The prompt already binds the first frame at its own (working) position.
98
+ } else if (firstOrdinal > 0 && lastOrdinal > 0) {
80
99
  // Combined sentence — REF_BINDING.frame() emits a single-frame sentence, so use
81
100
  // REF_BINDING.ordinal() for each ordinal inline to keep the combined wording AND
82
101
  // route both ordinals through the single swap-point.