@nodaro/shared 2.17.0 → 2.18.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/shared",
3
- "version": "2.17.0",
3
+ "version": "2.18.0",
4
4
  "description": "Shared types, model catalog, wire contracts, and structural vocabularies for the Nodaro platform and SDK.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -1,5 +1,37 @@
1
1
  import { describe, it, expect } from "vitest"
2
- import { applyVideoNegativePrompt, NATIVE_NEGATIVE_VIDEO_PROVIDERS } from "../model-constants.js"
2
+ import {
3
+ applyVideoNegativePrompt,
4
+ videoNegativeSuffix,
5
+ NATIVE_NEGATIVE_VIDEO_PROVIDERS,
6
+ } from "../model-constants.js"
7
+
8
+ /**
9
+ * A composed prompt that ends with the `[style]` section — what
10
+ * `@nodaro/prompts` emits for any run that picks a look dimension. The section
11
+ * has no terminator, so a `"\n"`-joined `Avoid:` reads as one more look clause
12
+ * under its header.
13
+ */
14
+ const SECTIONED = "a knight rides\n\n[style]:\nanime style\nwide shot"
15
+
16
+ describe("videoNegativeSuffix", () => {
17
+ it("joins on one newline for an ordinary prompt", () => {
18
+ expect(videoNegativeSuffix("blurry", "a knight rides")).toBe("\nAvoid: blurry")
19
+ })
20
+
21
+ it("closes an open `[style]` section with a blank line", () => {
22
+ expect(videoNegativeSuffix("blurry", SECTIONED)).toBe("\n\nAvoid: blurry")
23
+ })
24
+
25
+ it("joins on one newline once something else closed the section", () => {
26
+ expect(videoNegativeSuffix("blurry", `${SECTIONED}\n\nthe person from @image_1`))
27
+ .toBe("\nAvoid: blurry")
28
+ })
29
+
30
+ it("is the WIDEST form with no base — what a caller reserving room must budget", () => {
31
+ // `effectiveVideoPromptCeiling` reserves before the prompt exists.
32
+ expect(videoNegativeSuffix("blurry")).toBe("\n\nAvoid: blurry")
33
+ })
34
+ })
3
35
 
4
36
  describe("applyVideoNegativePrompt", () => {
5
37
  it("returns prompt unchanged + nativeNegativePrompt when provider is native", () => {
@@ -14,6 +46,11 @@ describe("applyVideoNegativePrompt", () => {
14
46
  expect(result.nativeNegativePrompt).toBeUndefined()
15
47
  })
16
48
 
49
+ it("separates 'Avoid: …' from an unterminated `[style]` section", () => {
50
+ const result = applyVideoNegativePrompt(SECTIONED, "blurry", "wan-animate-move")
51
+ expect(result.prompt).toBe(`${SECTIONED}\n\nAvoid: blurry`)
52
+ })
53
+
17
54
  it("returns 'Avoid: …' alone when prompt is empty and provider is non-native", () => {
18
55
  const result = applyVideoNegativePrompt("", "blurry", "veo3")
19
56
  expect(result.prompt).toBe("Avoid: blurry")
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ export {
38
38
  NATIVE_NEGATIVE_PROMPT_MODELS,
39
39
  NATIVE_NEGATIVE_VIDEO_PROVIDERS,
40
40
  applyVideoNegativePrompt,
41
+ videoNegativeSuffix,
41
42
  MODELS_WITH_REFERENCE_IMAGE_SUPPORT,
42
43
  T2I_TO_I2I_VARIANT,
43
44
  REF_IMAGE_MAX_LIMITS,
@@ -295,6 +295,35 @@ export const NATIVE_NEGATIVE_VIDEO_PROVIDERS = new Set<string>([
295
295
  "wan-s2v",
296
296
  ])
297
297
 
298
+ /**
299
+ * The `[style]` section header, verbatim. A composed prompt that ends with that
300
+ * section has NO terminator (`@nodaro/prompts`' `prompt-style-section.ts` owns
301
+ * the shape and the reasoning), so a `"\n"`-joined `Avoid:` would read as one
302
+ * more look clause under its header. Duplicated rather than imported because
303
+ * `@nodaro/prompts` depends on THIS package and never the reverse; the two are
304
+ * pinned together behaviourally by that package's
305
+ * `__tests__/style-section-boundary.test.ts`, which runs the real composer
306
+ * through this clamp.
307
+ */
308
+ const STYLE_SECTION_HEADER_LINE = "[style]:\n"
309
+
310
+ /**
311
+ * The `Avoid: …` suffix `applyVideoNegativePrompt` folds into a prompt, with the
312
+ * separator the base prompt calls for: a blank line when the base ends INSIDE an
313
+ * unterminated `[style]` section, a single newline otherwise.
314
+ *
315
+ * `base` omitted = the WIDEST form, which is what a caller reserving room before
316
+ * the prompt exists must budget for (`effectiveVideoPromptCeiling`).
317
+ */
318
+ export function videoNegativeSuffix(negativePrompt: string, base?: string): string {
319
+ const gapAt = base?.lastIndexOf("\n\n") ?? -1
320
+ const lastBlock = base === undefined ? undefined : (gapAt >= 0 ? base.slice(gapAt + 2) : base)
321
+ const separator = lastBlock === undefined || lastBlock.startsWith(STYLE_SECTION_HEADER_LINE)
322
+ ? "\n\n"
323
+ : "\n"
324
+ return `${separator}Avoid: ${negativePrompt}`
325
+ }
326
+
298
327
  /**
299
328
  * Apply the negative prompt to a video-provider request.
300
329
  *
@@ -306,6 +335,8 @@ export const NATIVE_NEGATIVE_VIDEO_PROVIDERS = new Set<string>([
306
335
  * Otherwise, the helper appends `Avoid: <negativePrompt>` to the prompt
307
336
  * (consistent with `buildImagePrompt` in `prompt-builder.ts`) so the model
308
337
  * still sees the negative intent, and returns `nativeNegativePrompt = undefined`.
338
+ * The line is separated by {@link videoNegativeSuffix}, which closes an open
339
+ * `[style]` section with a blank line rather than reading as a look clause.
309
340
  *
310
341
  * Empty / missing negative is a no-op.
311
342
  *
@@ -338,7 +369,10 @@ export function applyVideoNegativePrompt(
338
369
  // assembler), then clamp the whole to the model cap.
339
370
  const base = prompt && prompt.trim().length > 0 ? prompt : ""
340
371
  if (!base) return { prompt: `Avoid: ${neg}`.slice(0, promptMax), nativeNegativePrompt: undefined }
341
- const avoid = `\nAvoid: ${neg}`
372
+ // The separator is decided on the WHOLE base: the clamp below can only cut the
373
+ // tail, and a cut either leaves the `[style]` section behind or lands inside
374
+ // it, so the suffix never needs to grow after the reservation.
375
+ const avoid = videoNegativeSuffix(neg, base)
342
376
  const room = Math.max(0, promptMax - avoid.length)
343
377
  const injected = `${base.slice(0, room)}${avoid}`.slice(0, promptMax)
344
378
  return { prompt: injected, nativeNegativePrompt: undefined }