@nodaro/prompts 1.3.0 → 1.4.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 +18 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +18 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/person-exposure-hints.test.ts +61 -0
- package/src/__tests__/seedance-2-inputs.test.ts +22 -0
- package/src/person.ts +13 -2
- package/src/seedance-2-inputs.ts +20 -1
- package/src/styling.ts +12 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodaro/prompts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { buildPersonHints } from "../person.js"
|
|
3
|
+
import { buildStylingHints } from "../styling.js"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Content-rejection hardening (app_reports batch, 2026-07-18): skin-exposure
|
|
7
|
+
* and lipstick clauses COMPOUND in providers' output-safety classifiers when
|
|
8
|
+
* the analyzer fills several overlapping dimensions from one photo. These
|
|
9
|
+
* tests pin the two de-stacking rules.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
describe("exposure hint collapsing", () => {
|
|
13
|
+
it("midriff + navel together fold into ONE neutral clause", () => {
|
|
14
|
+
const hints = buildPersonHints({
|
|
15
|
+
distinctiveFeature: ["feature-midriff-visible", "feature-navel-visible"],
|
|
16
|
+
} as never)
|
|
17
|
+
const exposure = hints.filter((h) => /midriff|navel|stomach/i.test(h))
|
|
18
|
+
expect(exposure).toEqual(["wearing a cropped style, midriff and navel visible"])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it("each alone keeps its own (softened, garment-language) hint", () => {
|
|
22
|
+
const midriffOnly = buildPersonHints({
|
|
23
|
+
distinctiveFeature: ["feature-midriff-visible"],
|
|
24
|
+
} as never)
|
|
25
|
+
expect(midriffOnly).toContain("wearing a cropped style with the midriff visible")
|
|
26
|
+
expect(midriffOnly.join(" ")).not.toMatch(/bare stomach/i)
|
|
27
|
+
|
|
28
|
+
const navelOnly = buildPersonHints({
|
|
29
|
+
distinctiveFeature: ["feature-navel-visible"],
|
|
30
|
+
} as never)
|
|
31
|
+
expect(navelOnly).toContain("with the navel visible")
|
|
32
|
+
expect(navelOnly.join(" ")).not.toMatch(/bare stomach/i)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("other distinctive features still emit alongside the collapsed pair", () => {
|
|
36
|
+
const hints = buildPersonHints({
|
|
37
|
+
distinctiveFeature: [
|
|
38
|
+
"feature-midriff-visible",
|
|
39
|
+
"feature-navel-visible",
|
|
40
|
+
"feature-freckles",
|
|
41
|
+
],
|
|
42
|
+
} as never)
|
|
43
|
+
expect(hints.filter((h) => /midriff/i.test(h))).toHaveLength(1)
|
|
44
|
+
expect(hints.length).toBeGreaterThan(1)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe("bold-lips cross-catalog dedupe", () => {
|
|
49
|
+
it("skips makeup-bold-lips when the shared map already carries lip-state-bold-red", () => {
|
|
50
|
+
const hints = buildStylingHints({
|
|
51
|
+
makeup: "makeup-bold-lips",
|
|
52
|
+
lipState: ["lip-state-bold-red"],
|
|
53
|
+
} as never)
|
|
54
|
+
expect(hints.join(" ")).not.toMatch(/bold lips/i)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it("keeps makeup-bold-lips without the person clause (separate-node consumers)", () => {
|
|
58
|
+
const hints = buildStylingHints({ makeup: "makeup-bold-lips" } as never)
|
|
59
|
+
expect(hints.join(" ")).toMatch(/bold lips/i)
|
|
60
|
+
})
|
|
61
|
+
})
|
|
@@ -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
|
+
})
|
package/src/person.ts
CHANGED
|
@@ -708,8 +708,8 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
708
708
|
{ id: "feature-nostril-piercing", label: "Nostril Piercing", dimension: "distinctive-features", description: "Single nostril stud or ring", promptHint: "with a single nostril piercing, a small stud or ring at the nostril" },
|
|
709
709
|
{ id: "feature-bare-shoulders", label: "Bare Shoulders", dimension: "distinctive-features", description: "Bare shoulders exposed", promptHint: "with bare shoulders exposed, the line of the collarbone and shoulder muscles uncovered" },
|
|
710
710
|
{ id: "feature-collarbone-visible", label: "Collarbone Visible", dimension: "distinctive-features", description: "Prominent collarbone catching light", promptHint: "with a prominent collarbone clearly defined and catching the light" },
|
|
711
|
-
{ id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "
|
|
712
|
-
{ id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with
|
|
711
|
+
{ id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "wearing a cropped style with the midriff visible" },
|
|
712
|
+
{ id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with the navel visible" },
|
|
713
713
|
{ id: "feature-elongated-neck", label: "Elongated Neck", dimension: "distinctive-features", description: "Long swan-like neck", promptHint: "with an elongated, swan-like neck, long and gracefully extended" },
|
|
714
714
|
{ id: "feature-under-eye-circles", label: "Under-Eye Circles", dimension: "distinctive-features", description: "Subtle dark circles under the eyes (distinct from puffy eye-bags)", promptHint: "with subtle dark circles under the eyes" },
|
|
715
715
|
{ id: "feature-fangs", label: "Fangs", dimension: "distinctive-features", description: "Visible fangs (vampire / character archetype)", promptHint: "with visible fangs at the canines, character / vampire archetype" },
|
|
@@ -1242,7 +1242,18 @@ export function buildAgeHint(
|
|
|
1242
1242
|
function emitIndependentHints(value: unknown): string[] {
|
|
1243
1243
|
const ids = normalizePickIds(value)
|
|
1244
1244
|
const out: string[] = []
|
|
1245
|
+
// Skin-exposure clauses COMPOUND in providers' output-safety classifiers:
|
|
1246
|
+
// "midriff visible" + "navel visible" as separate clauses reads twice as
|
|
1247
|
+
// exposed as it is, and stacked with glamour hints it tips borderline draws
|
|
1248
|
+
// into content blocks (observed on kie-standard, 2026-07-18 app_reports).
|
|
1249
|
+
// When both are picked, fold them into ONE neutral clause.
|
|
1250
|
+
const midriff = ids.includes("feature-midriff-visible")
|
|
1251
|
+
const navel = ids.includes("feature-navel-visible")
|
|
1245
1252
|
for (const id of ids) {
|
|
1253
|
+
if (midriff && navel && (id === "feature-midriff-visible" || id === "feature-navel-visible")) {
|
|
1254
|
+
if (id === "feature-midriff-visible") out.push("wearing a cropped style, midriff and navel visible")
|
|
1255
|
+
continue
|
|
1256
|
+
}
|
|
1246
1257
|
const hint = getPersonPromptHint(id)
|
|
1247
1258
|
if (hint) out.push(hint)
|
|
1248
1259
|
}
|
package/src/seedance-2-inputs.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
package/src/styling.ts
CHANGED
|
@@ -568,9 +568,21 @@ export function buildStylingHints(
|
|
|
568
568
|
const pre = typeof data.preText === "string" ? data.preText.trim() : ""
|
|
569
569
|
if (pre) hints.push(pre)
|
|
570
570
|
|
|
571
|
+
// The Person catalog's `lip-state-bold-red` and this catalog's
|
|
572
|
+
// `makeup-bold-lips` say the same thing; consumers that fold BOTH pickers
|
|
573
|
+
// off one shared value map (person.nodaro.ai's subject fold) double the
|
|
574
|
+
// lipstick clause. When the shared map shows the person clause already
|
|
575
|
+
// covers it, skip the makeup twin. Separate-node consumers (platform
|
|
576
|
+
// workflows) don't carry `lipState` here, so this no-ops for them.
|
|
577
|
+
const lipStateRaw = (data as Record<string, unknown>).lipState
|
|
578
|
+
const boldRedAlready = Array.isArray(lipStateRaw)
|
|
579
|
+
? lipStateRaw.includes("lip-state-bold-red")
|
|
580
|
+
: lipStateRaw === "lip-state-bold-red"
|
|
581
|
+
|
|
571
582
|
for (const dimension of STYLING_DIMENSION_ORDER) {
|
|
572
583
|
const field = STYLING_FIELD_BY_DIMENSION[dimension]
|
|
573
584
|
const raw = data[field]
|
|
585
|
+
if (dimension === "makeup" && boldRedAlready && raw === "makeup-bold-lips") continue
|
|
574
586
|
// jewelry / wardrobe-state / hair-state are multi-pick (string | string[]);
|
|
575
587
|
// emit each id's hint independently and let the comma-join compose.
|
|
576
588
|
if (typeof raw === "string" && raw.length > 0) {
|