@nodaro/prompts 1.0.1
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/LICENSE +105 -0
- package/README.md +27 -0
- package/dist/index.cjs +21240 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3599 -0
- package/dist/index.d.ts +3599 -0
- package/dist/index.js +20854 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/prompt-builder-segments.test.ts.snap +65 -0
- package/src/__tests__/action-fx.test.ts +155 -0
- package/src/__tests__/apply-picker-json.test.ts +50 -0
- package/src/__tests__/assemble-image-input.test.ts +248 -0
- package/src/__tests__/assemble-suno-input.test.ts +283 -0
- package/src/__tests__/brand-tokens.test.ts +81 -0
- package/src/__tests__/build-image-prompt-element-injection.test.ts +118 -0
- package/src/__tests__/build-image-prompt-hybrid-format.test.ts +131 -0
- package/src/__tests__/build-image-prompt-mentions.test.ts +809 -0
- package/src/__tests__/build-image-prompt-reference-cap.test.ts +57 -0
- package/src/__tests__/build-image-prompt-reference-numbering.test.ts +237 -0
- package/src/__tests__/build-image-prompt-reference-order.test.ts +313 -0
- package/src/__tests__/camera-motions-from-connections.test.ts +71 -0
- package/src/__tests__/catalog-gapfill.test.ts +53 -0
- package/src/__tests__/character-convergence-image.test.ts +217 -0
- package/src/__tests__/character-default-role-image.test.ts +167 -0
- package/src/__tests__/character-default-role-video.test.ts +168 -0
- package/src/__tests__/character-default-role.test.ts +70 -0
- package/src/__tests__/character-fx.test.ts +200 -0
- package/src/__tests__/entity-prompts-location.test.ts +97 -0
- package/src/__tests__/entity-prompts.test.ts +240 -0
- package/src/__tests__/expand-extra-refs-role.test.ts +50 -0
- package/src/__tests__/factory-presets.test.ts +1045 -0
- package/src/__tests__/factory-snippets.test.ts +68 -0
- package/src/__tests__/framing-multi.test.ts +71 -0
- package/src/__tests__/framing-vantage.test.ts +39 -0
- package/src/__tests__/i18n-entry-completeness.test.ts +269 -0
- package/src/__tests__/identity-lock.test.ts +46 -0
- package/src/__tests__/instrumentation.test.ts +68 -0
- package/src/__tests__/lighting-multi.test.ts +68 -0
- package/src/__tests__/location-convergence-image.test.ts +124 -0
- package/src/__tests__/mention-lock-flag.test.ts +499 -0
- package/src/__tests__/multi-picker-spec.test.ts +71 -0
- package/src/__tests__/music-genre.test.ts +103 -0
- package/src/__tests__/music-mood.test.ts +87 -0
- package/src/__tests__/object-creature-convergence-image.test.ts +105 -0
- package/src/__tests__/parameter-prompt-hint.test.ts +270 -0
- package/src/__tests__/parameter-registry-sync.test.ts +243 -0
- package/src/__tests__/person-age.test.ts +80 -0
- package/src/__tests__/person-analyzer-invariants.test.ts +47 -0
- package/src/__tests__/person-body-axes.test.ts +67 -0
- package/src/__tests__/person-facial-geometry.test.ts +156 -0
- package/src/__tests__/person-regional-aesthetic.test.ts +161 -0
- package/src/__tests__/person-sections.test.ts +18 -0
- package/src/__tests__/picker-analyzer-registry.test.ts +108 -0
- package/src/__tests__/picker-catalogs-project.test.ts +85 -0
- package/src/__tests__/picker-catalogs.test.ts +53 -0
- package/src/__tests__/picker-limits.test.ts +20 -0
- package/src/__tests__/prompt-builder-segments.test.ts +183 -0
- package/src/__tests__/prompt-builder-structured-fields.test.ts +40 -0
- package/src/__tests__/prompt-builder.test.ts +1773 -0
- package/src/__tests__/prompt-wizard-categories.test.ts +31 -0
- package/src/__tests__/provider-prompt-doctrine.test.ts +49 -0
- package/src/__tests__/resolve-prompt-append.test.ts +58 -0
- package/src/__tests__/resolve-prompt.test.ts +44 -0
- package/src/__tests__/role-picker-shared.test.ts +208 -0
- package/src/__tests__/seedance-2-inputs.test.ts +173 -0
- package/src/__tests__/seedance-extend.test.ts +44 -0
- package/src/__tests__/sound-aggregator.test.ts +350 -0
- package/src/__tests__/style-presets.test.ts +34 -0
- package/src/__tests__/temporal-multi.test.ts +74 -0
- package/src/__tests__/transitions.test.ts +213 -0
- package/src/__tests__/video-reference-features.test.ts +41 -0
- package/src/__tests__/video-reference-leading-refs.test.ts +90 -0
- package/src/__tests__/video-reference-resolver.test.ts +233 -0
- package/src/__tests__/video-reference-roles.test.ts +96 -0
- package/src/__tests__/voice-character.test.ts +48 -0
- package/src/__tests__/voice-delivery.test.ts +41 -0
- package/src/__tests__/wardrobe.test.ts +25 -0
- package/src/action-fx.ts +255 -0
- package/src/aesthetic.ts +435 -0
- package/src/assemble-image-input.ts +236 -0
- package/src/assemble-suno-input.ts +147 -0
- package/src/atmosphere.ts +104 -0
- package/src/backdrop.ts +131 -0
- package/src/brand-tokens.ts +154 -0
- package/src/camera-format.ts +76 -0
- package/src/camera-motions.ts +615 -0
- package/src/character-fx.ts +274 -0
- package/src/color-look.ts +103 -0
- package/src/composition-effects.ts +67 -0
- package/src/entity-prompts.ts +231 -0
- package/src/era.ts +292 -0
- package/src/exposure-settings.ts +142 -0
- package/src/factory-presets/generate-image.ts +1644 -0
- package/src/factory-presets/generate-video.ts +1116 -0
- package/src/factory-presets/index.ts +46 -0
- package/src/factory-presets/lottie-overlay.ts +166 -0
- package/src/factory-presets/motion-graphics.ts +350 -0
- package/src/factory-presets/music.ts +734 -0
- package/src/factory-presets/sfx.ts +136 -0
- package/src/factory-presets/shared-image.ts +207 -0
- package/src/factory-presets/switchx.ts +65 -0
- package/src/factory-presets/text.ts +292 -0
- package/src/factory-presets/types.ts +51 -0
- package/src/factory-presets/video-edit.ts +136 -0
- package/src/factory-presets/voice.ts +172 -0
- package/src/factory-presets.ts +2 -0
- package/src/factory-snippets/catalog.ts +105 -0
- package/src/factory-snippets/index.ts +16 -0
- package/src/factory-snippets/types.ts +33 -0
- package/src/framing.ts +634 -0
- package/src/held-prop.ts +187 -0
- package/src/identity-lock.ts +213 -0
- package/src/index.ts +66 -0
- package/src/instrumentation.ts +337 -0
- package/src/lens.ts +59 -0
- package/src/lighting.ts +229 -0
- package/src/loop-subject.ts +276 -0
- package/src/materials.ts +184 -0
- package/src/mood.ts +186 -0
- package/src/music-genre.ts +662 -0
- package/src/music-mood.ts +137 -0
- package/src/object-asset-presets.ts +81 -0
- package/src/parameter-prompt-hint.ts +281 -0
- package/src/person.ts +1368 -0
- package/src/photo-genre.ts +151 -0
- package/src/photographer.ts +612 -0
- package/src/picker-analyzer-registry.ts +374 -0
- package/src/picker-catalogs.ts +858 -0
- package/src/pose.ts +246 -0
- package/src/post-process-effects.ts +94 -0
- package/src/prompt-builder-structured-fields.ts +116 -0
- package/src/prompt-builder.ts +2964 -0
- package/src/prompt-templates.ts +50 -0
- package/src/prompt-wizard-categories.ts +334 -0
- package/src/provider-prompt-doctrine.ts +85 -0
- package/src/render-quality.ts +89 -0
- package/src/resolve-prompt.ts +120 -0
- package/src/seedance-2-inputs.ts +100 -0
- package/src/setting.ts +130 -0
- package/src/sound-aggregator.ts +241 -0
- package/src/style-presets.ts +162 -0
- package/src/style.ts +99 -0
- package/src/styling.ts +585 -0
- package/src/temporal.ts +151 -0
- package/src/transitions.ts +333 -0
- package/src/video-reference-resolver.ts +823 -0
- package/src/voice-character.ts +237 -0
- package/src/voice-delivery.ts +142 -0
- package/src/wardrobe.ts +179 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest"
|
|
2
|
+
import { buildImagePrompt } from "../prompt-builder.js"
|
|
3
|
+
import type { ConnectedReference } from "@nodaro/shared"
|
|
4
|
+
|
|
5
|
+
// Location token format is `@slug:N(:bucket/variant)?(:mode)?` per
|
|
6
|
+
// `findLocationMentionTokens` — the canonical mention is just `@slug:N`.
|
|
7
|
+
const library: ConnectedReference = {
|
|
8
|
+
id: "l", defaultName: "Old Library", source: "wired-location",
|
|
9
|
+
url: "https://cdn/library.png", locationSlug: "old-library",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("location reference converges onto the image hybrid form", () => {
|
|
13
|
+
it("wired location (canonical) → 'the background from reference image A', no legacy block", () => {
|
|
14
|
+
const out = buildImagePrompt({
|
|
15
|
+
prompt: "a detective at her desk", connectedReferences: [library],
|
|
16
|
+
provider: "nano-banana-pro", referenceFormat: "hybrid",
|
|
17
|
+
})
|
|
18
|
+
expect(out.prompt).toContain("the background from reference image A")
|
|
19
|
+
expect(out.prompt).not.toContain("Use these locations:")
|
|
20
|
+
expect(out.referenceImageUrls).toContain("https://cdn/library.png")
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("@-mention with style mode (location node default) → 'the style from reference image A'", () => {
|
|
24
|
+
const out = buildImagePrompt({
|
|
25
|
+
prompt: "@old-library:1 a chase scene",
|
|
26
|
+
connectedReferences: [{ ...library, defaultUsageMode: "style" } as ConnectedReference],
|
|
27
|
+
provider: "nano-banana-pro", referenceFormat: "hybrid",
|
|
28
|
+
})
|
|
29
|
+
expect(out.prompt).toContain("the style from reference image A")
|
|
30
|
+
expect(out.prompt).not.toContain("Use these locations:")
|
|
31
|
+
expect(out.referenceImageUrls).toContain("https://cdn/library.png")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("@-mention with explicit :layout mode → 'the layout from reference image A'", () => {
|
|
35
|
+
const out = buildImagePrompt({
|
|
36
|
+
prompt: "@old-library:1:layout a chase scene",
|
|
37
|
+
connectedReferences: [library],
|
|
38
|
+
provider: "nano-banana-pro", referenceFormat: "hybrid",
|
|
39
|
+
})
|
|
40
|
+
expect(out.prompt).toContain("the layout from reference image A")
|
|
41
|
+
expect(out.prompt).not.toContain("Use these locations:")
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("opt-in identity lock surfaces a lock line; default OFF emits none", () => {
|
|
45
|
+
const locked = buildImagePrompt({
|
|
46
|
+
prompt: "@old-library:1 a chase scene",
|
|
47
|
+
connectedReferences: [{
|
|
48
|
+
...library,
|
|
49
|
+
identityLock: { enabled: true, text: "Lock the exact setting in {ref}." },
|
|
50
|
+
} as ConnectedReference],
|
|
51
|
+
provider: "nano-banana-pro", referenceFormat: "hybrid",
|
|
52
|
+
})
|
|
53
|
+
expect(locked.prompt).toContain("the background from reference image A")
|
|
54
|
+
expect(locked.prompt).toContain("Lock the exact setting in reference image A.")
|
|
55
|
+
|
|
56
|
+
const unlocked = buildImagePrompt({
|
|
57
|
+
prompt: "@old-library:1 a chase scene", connectedReferences: [library],
|
|
58
|
+
provider: "nano-banana-pro", referenceFormat: "hybrid",
|
|
59
|
+
})
|
|
60
|
+
expect(unlocked.prompt).not.toContain("Lock the exact setting")
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it("LEGACY (no referenceFormat) unchanged → still the block, not the hybrid phrase", () => {
|
|
64
|
+
const out = buildImagePrompt({
|
|
65
|
+
prompt: "@old-library:1 scene", connectedReferences: [library], provider: "nano-banana-pro",
|
|
66
|
+
})
|
|
67
|
+
// legacy still uses the location block / legacy directive form
|
|
68
|
+
expect(out.prompt).not.toContain("the background from reference image A")
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Phase D restored-legacy guard (the review blocker). Pre-Phase-D the bare-slug
|
|
74
|
+
* ROLE tokens — `@old-library:1:background` / `:atmosphere` / `:as-is` /
|
|
75
|
+
* `:empty-background` / `:lighting` — did NOT parse, so they stayed literal text
|
|
76
|
+
* on the prod-default LEGACY path. The additive Phase-D parser now PARSES them
|
|
77
|
+
* (with `role` set), but `resolveLocationMentions` (legacy) must STILL leave them
|
|
78
|
+
* untouched so legacy stays byte-identical. The HYBRID resolver SHOULD resolve
|
|
79
|
+
* them to the inline role phrase — pinned here so the two paths can't silently
|
|
80
|
+
* converge again. (`layout`/`style` were already usage modes → unaffected.)
|
|
81
|
+
*/
|
|
82
|
+
describe("Phase D legacy guard: bare-slug ROLE location tokens", () => {
|
|
83
|
+
it("LEGACY: @old-library:1:background stays literal — no bullet, no inline swap, no hybrid phrase", () => {
|
|
84
|
+
const out = buildImagePrompt({
|
|
85
|
+
prompt: "@old-library:1:background a scene",
|
|
86
|
+
connectedReferences: [library],
|
|
87
|
+
provider: "nano-banana-pro",
|
|
88
|
+
// no referenceFormat → legacy (the prod default)
|
|
89
|
+
})
|
|
90
|
+
// The role token is left verbatim (literal text exactly as pre-Phase-D).
|
|
91
|
+
expect(out.prompt).toContain("@old-library:1:background")
|
|
92
|
+
// No legacy mention bullet claimed by the role token.
|
|
93
|
+
expect(out.prompt).not.toContain("Use these locations:")
|
|
94
|
+
// No hybrid role phrase leaking onto the legacy path.
|
|
95
|
+
expect(out.prompt).not.toContain("the background from reference image")
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it("LEGACY: the other four role slugs (:atmosphere/:as-is/:empty-background/:lighting) also stay literal", () => {
|
|
99
|
+
for (const slug of ["atmosphere", "as-is", "empty-background", "lighting"]) {
|
|
100
|
+
const token = `@old-library:1:${slug}`
|
|
101
|
+
const out = buildImagePrompt({
|
|
102
|
+
prompt: `${token} a scene`,
|
|
103
|
+
connectedReferences: [library],
|
|
104
|
+
provider: "nano-banana-pro",
|
|
105
|
+
})
|
|
106
|
+
expect(out.prompt).toContain(token)
|
|
107
|
+
expect(out.prompt).not.toContain("Use these locations:")
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it("HYBRID counterpart still resolves the role → 'the background from reference image A'", () => {
|
|
112
|
+
const out = buildImagePrompt({
|
|
113
|
+
prompt: "@old-library:1:background a scene",
|
|
114
|
+
connectedReferences: [library],
|
|
115
|
+
provider: "nano-banana-pro",
|
|
116
|
+
referenceFormat: "hybrid",
|
|
117
|
+
})
|
|
118
|
+
expect(out.prompt).toContain("the background from reference image A")
|
|
119
|
+
// Token consumed by the inline phrase, not left raw.
|
|
120
|
+
expect(out.prompt).not.toContain("@old-library:1:background")
|
|
121
|
+
expect(out.prompt).not.toContain("Use these locations:")
|
|
122
|
+
expect(out.referenceImageUrls).toContain("https://cdn/library.png")
|
|
123
|
+
})
|
|
124
|
+
})
|
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-mention identity-lock toggle (Unified Reference Roles, Task 4).
|
|
3
|
+
*
|
|
4
|
+
* An ADDITIVE, trailing `~lock` sentinel on a character/location mention token
|
|
5
|
+
* (`@kira:1:face~lock`, `@old-library:1:background~lock`) parses to a
|
|
6
|
+
* `lock: true` token-info field. The HYBRID resolvers force
|
|
7
|
+
* `identityLock.enabled = true` for that mention's reference before
|
|
8
|
+
* `buildIdentityLockLine`, so a per-mention lock line appears — regardless of
|
|
9
|
+
* the ref's default (default-off) lock.
|
|
10
|
+
*
|
|
11
|
+
* CRITICAL INVARIANTS pinned here:
|
|
12
|
+
* - A `~lock`-LESS token parses + resolves BYTE-IDENTICALLY to pre-Task-4
|
|
13
|
+
* (the parser must NOT gain a `lock` key for a lock-less token).
|
|
14
|
+
* - LEGACY is inert: the `~lock` flag never produces a lock line and never
|
|
15
|
+
* changes lock resolution (the legacy resolvers ignore it).
|
|
16
|
+
* - Hybrid character (image + video) + hybrid location honor the flag.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { describe, it, expect } from "vitest"
|
|
20
|
+
import { parseCharacterMentionToken, findCharacterMentionTokens } from "@nodaro/shared"
|
|
21
|
+
import { parseLocationMentionToken, findLocationMentionTokens } from "@nodaro/shared"
|
|
22
|
+
import { buildIdentityLockLine } from "../identity-lock.js"
|
|
23
|
+
import { buildImagePrompt } from "../prompt-builder.js"
|
|
24
|
+
import { resolveVideoReferenceCore } from "../video-reference-resolver.js"
|
|
25
|
+
import type { ConnectedReference } from "@nodaro/shared"
|
|
26
|
+
|
|
27
|
+
const CHAR_LOCK_LINE =
|
|
28
|
+
"Lock the exact identity of the person in reference image A — face, bone structure, skin tone, and all unique features."
|
|
29
|
+
const LOC_LOCK_LINE =
|
|
30
|
+
"Lock the exact look of reference image A — match the location's architecture, layout, and lighting."
|
|
31
|
+
|
|
32
|
+
// ─── Parser: additive `~lock` (character) ────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
describe("parseCharacterMentionToken — additive ~lock", () => {
|
|
35
|
+
it("parses a trailing ~lock on a mode token → lock:true", () => {
|
|
36
|
+
expect(parseCharacterMentionToken("@kira:1:face~lock")).toEqual({
|
|
37
|
+
characterSlug: "kira",
|
|
38
|
+
imageIndex: 1,
|
|
39
|
+
variantSlug: null,
|
|
40
|
+
usageMode: "face",
|
|
41
|
+
lock: true,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("parses ~lock on a bare 2-part token", () => {
|
|
46
|
+
expect(parseCharacterMentionToken("@kira:1~lock")).toEqual({
|
|
47
|
+
characterSlug: "kira",
|
|
48
|
+
imageIndex: 1,
|
|
49
|
+
variantSlug: null,
|
|
50
|
+
usageMode: null,
|
|
51
|
+
lock: true,
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it("parses ~lock on a variant token", () => {
|
|
56
|
+
expect(parseCharacterMentionToken("@kira:1:smile~lock")).toEqual({
|
|
57
|
+
characterSlug: "kira",
|
|
58
|
+
imageIndex: 1,
|
|
59
|
+
variantSlug: "smile",
|
|
60
|
+
usageMode: null,
|
|
61
|
+
lock: true,
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it("parses ~lock on a 4-part variant+mode token", () => {
|
|
66
|
+
expect(parseCharacterMentionToken("@kira:1:smile:face~lock")).toEqual({
|
|
67
|
+
characterSlug: "kira",
|
|
68
|
+
imageIndex: 1,
|
|
69
|
+
variantSlug: "smile",
|
|
70
|
+
usageMode: "face",
|
|
71
|
+
lock: true,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("BYTE-IDENTICAL: a ~lock-less token gains NO lock key", () => {
|
|
76
|
+
const parsed = parseCharacterMentionToken("@kira:1:face")
|
|
77
|
+
expect(parsed).toEqual({
|
|
78
|
+
characterSlug: "kira",
|
|
79
|
+
imageIndex: 1,
|
|
80
|
+
variantSlug: null,
|
|
81
|
+
usageMode: "face",
|
|
82
|
+
})
|
|
83
|
+
expect(parsed).not.toHaveProperty("lock")
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe("findCharacterMentionTokens — additive ~lock", () => {
|
|
88
|
+
it("captures the full ~lock token + lock:true at the right offset", () => {
|
|
89
|
+
const tokens = findCharacterMentionTokens("Hi @kira:1:face~lock waves", ["kira"])
|
|
90
|
+
expect(tokens).toEqual([
|
|
91
|
+
{
|
|
92
|
+
token: "@kira:1:face~lock",
|
|
93
|
+
characterSlug: "kira",
|
|
94
|
+
imageIndex: 1,
|
|
95
|
+
variantSlug: null,
|
|
96
|
+
usageMode: "face",
|
|
97
|
+
lock: true,
|
|
98
|
+
offset: 3,
|
|
99
|
+
},
|
|
100
|
+
])
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("BYTE-IDENTICAL: lock-less tokens are unchanged (no lock key)", () => {
|
|
104
|
+
expect(findCharacterMentionTokens("@kira:1:face waves", ["kira"])).toEqual([
|
|
105
|
+
{ token: "@kira:1:face", characterSlug: "kira", imageIndex: 1, variantSlug: null, usageMode: "face", offset: 0 },
|
|
106
|
+
])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("WORD-BOUNDARY: ~lock is NOT consumed inside a longer word (~locked)", () => {
|
|
110
|
+
// `~lock(?![a-z0-9-])` — a trailing `~locked` is ordinary text, so the token
|
|
111
|
+
// stays `@kira:1` (no lock) and `~locked` is left literal, byte-identical.
|
|
112
|
+
const tokens = findCharacterMentionTokens("@kira:1~locked in", ["kira"])
|
|
113
|
+
expect(tokens).toEqual([
|
|
114
|
+
{ token: "@kira:1", characterSlug: "kira", imageIndex: 1, variantSlug: null, usageMode: null, offset: 0 },
|
|
115
|
+
])
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// ─── Parser: additive `~lock` (location) ─────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe("parseLocationMentionToken — additive ~lock", () => {
|
|
122
|
+
it("parses ~lock on a role token → role + lock:true", () => {
|
|
123
|
+
expect(parseLocationMentionToken("@old-library:1:background~lock")).toEqual({
|
|
124
|
+
locationSlug: "old-library",
|
|
125
|
+
imageIndex: 1,
|
|
126
|
+
bucket: null,
|
|
127
|
+
variant: null,
|
|
128
|
+
usageMode: null,
|
|
129
|
+
role: "background",
|
|
130
|
+
lock: true,
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it("parses ~lock on a bare canonical token", () => {
|
|
135
|
+
expect(parseLocationMentionToken("@old-library:1~lock")).toEqual({
|
|
136
|
+
locationSlug: "old-library",
|
|
137
|
+
imageIndex: 1,
|
|
138
|
+
bucket: null,
|
|
139
|
+
variant: null,
|
|
140
|
+
usageMode: null,
|
|
141
|
+
lock: true,
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it("parses ~lock on a bucket/variant + mode token", () => {
|
|
146
|
+
expect(parseLocationMentionToken("@old-library:1:weather/rain:style~lock")).toEqual({
|
|
147
|
+
locationSlug: "old-library",
|
|
148
|
+
imageIndex: 1,
|
|
149
|
+
bucket: "weather",
|
|
150
|
+
variant: "rain",
|
|
151
|
+
usageMode: "style",
|
|
152
|
+
lock: true,
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it("BYTE-IDENTICAL: a ~lock-less role token gains NO lock key", () => {
|
|
157
|
+
const parsed = parseLocationMentionToken("@old-library:1:background")
|
|
158
|
+
expect(parsed).toEqual({
|
|
159
|
+
locationSlug: "old-library",
|
|
160
|
+
imageIndex: 1,
|
|
161
|
+
bucket: null,
|
|
162
|
+
variant: null,
|
|
163
|
+
usageMode: null,
|
|
164
|
+
role: "background",
|
|
165
|
+
})
|
|
166
|
+
expect(parsed).not.toHaveProperty("lock")
|
|
167
|
+
})
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
describe("findLocationMentionTokens — additive ~lock", () => {
|
|
171
|
+
it("captures the full ~lock token + lock:true", () => {
|
|
172
|
+
const tokens = findLocationMentionTokens("at @old-library:1:background~lock now", ["old-library"])
|
|
173
|
+
expect(tokens).toEqual([
|
|
174
|
+
{
|
|
175
|
+
token: "@old-library:1:background~lock",
|
|
176
|
+
locationSlug: "old-library",
|
|
177
|
+
imageIndex: 1,
|
|
178
|
+
bucket: null,
|
|
179
|
+
variant: null,
|
|
180
|
+
usageMode: null,
|
|
181
|
+
role: "background",
|
|
182
|
+
lock: true,
|
|
183
|
+
offset: 3,
|
|
184
|
+
},
|
|
185
|
+
])
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
// ─── DEFAULT_LOCK_TEXT gains a wired-location wording ─────────────────────────
|
|
190
|
+
|
|
191
|
+
describe("buildIdentityLockLine — wired-location default wording", () => {
|
|
192
|
+
it("enabled:true + no custom text → the built-in location lock line", () => {
|
|
193
|
+
expect(
|
|
194
|
+
buildIdentityLockLine(
|
|
195
|
+
{ id: "l", defaultName: "Old Library", source: "wired-location", url: "u", identityLock: { enabled: true } },
|
|
196
|
+
"reference image A",
|
|
197
|
+
),
|
|
198
|
+
).toBe(LOC_LOCK_LINE)
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it("still default-OFF for a location with no flag", () => {
|
|
202
|
+
expect(
|
|
203
|
+
buildIdentityLockLine({ id: "l", defaultName: "Old Library", source: "wired-location", url: "u" }, "reference image A"),
|
|
204
|
+
).toBeNull()
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// ─── Hybrid resolver override — image (character) ─────────────────────────────
|
|
209
|
+
|
|
210
|
+
const victoria: ConnectedReference = {
|
|
211
|
+
id: "v", defaultName: "Victoria Hayes", source: "wired-character",
|
|
212
|
+
url: "https://cdn/victoria.png", characterSlug: "victoria-hayes",
|
|
213
|
+
}
|
|
214
|
+
const library: ConnectedReference = {
|
|
215
|
+
id: "l", defaultName: "Old Library", source: "wired-location",
|
|
216
|
+
url: "https://cdn/library.png", locationSlug: "old-library",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
describe("hybrid image — per-mention ~lock forces the lock line", () => {
|
|
220
|
+
it("@victoria-hayes:1:face~lock → phrase + character lock line (ref default off)", () => {
|
|
221
|
+
const out = buildImagePrompt({
|
|
222
|
+
provider: "nano-banana-pro",
|
|
223
|
+
prompt: "@victoria-hayes:1:face~lock on a rooftop",
|
|
224
|
+
connectedReferences: [victoria],
|
|
225
|
+
referenceFormat: "hybrid",
|
|
226
|
+
})
|
|
227
|
+
expect(out.prompt).toContain("the face from reference image A on a rooftop")
|
|
228
|
+
expect(out.prompt).toContain(CHAR_LOCK_LINE)
|
|
229
|
+
expect(out.prompt).not.toContain("@victoria-hayes")
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it("@victoria-hayes:1:face (no flag) → NO lock line (unchanged default-off)", () => {
|
|
233
|
+
const out = buildImagePrompt({
|
|
234
|
+
provider: "nano-banana-pro",
|
|
235
|
+
prompt: "@victoria-hayes:1:face on a rooftop",
|
|
236
|
+
connectedReferences: [victoria],
|
|
237
|
+
referenceFormat: "hybrid",
|
|
238
|
+
})
|
|
239
|
+
expect(out.prompt).toContain("the face from reference image A on a rooftop")
|
|
240
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("@old-library:1:background~lock → phrase + location lock line", () => {
|
|
244
|
+
const out = buildImagePrompt({
|
|
245
|
+
provider: "nano-banana-pro",
|
|
246
|
+
prompt: "@old-library:1:background~lock a chase scene",
|
|
247
|
+
connectedReferences: [library],
|
|
248
|
+
referenceFormat: "hybrid",
|
|
249
|
+
})
|
|
250
|
+
expect(out.prompt).toContain("the background from reference image A")
|
|
251
|
+
expect(out.prompt).toContain(LOC_LOCK_LINE)
|
|
252
|
+
expect(out.prompt).not.toContain("@old-library")
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it("@old-library:1:background (no flag) → NO lock line", () => {
|
|
256
|
+
const out = buildImagePrompt({
|
|
257
|
+
provider: "nano-banana-pro",
|
|
258
|
+
prompt: "@old-library:1:background a chase scene",
|
|
259
|
+
connectedReferences: [library],
|
|
260
|
+
referenceFormat: "hybrid",
|
|
261
|
+
})
|
|
262
|
+
expect(out.prompt).toContain("the background from reference image A")
|
|
263
|
+
expect(out.prompt).not.toContain("Lock the exact look")
|
|
264
|
+
})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
// ─── Legacy inertness ────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
describe("legacy — ~lock is inert (no lock, no different lock resolution)", () => {
|
|
270
|
+
it("LEGACY character @…:face~lock → no lock line", () => {
|
|
271
|
+
const out = buildImagePrompt({
|
|
272
|
+
provider: "nano-banana-pro",
|
|
273
|
+
prompt: "@victoria-hayes:1:face~lock on a rooftop",
|
|
274
|
+
connectedReferences: [victoria],
|
|
275
|
+
// no referenceFormat → legacy (prod default kill-switch)
|
|
276
|
+
})
|
|
277
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
278
|
+
expect(out.prompt).not.toContain("the face from reference image")
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it("LEGACY location role @…:background~lock stays literal, no lock, no hybrid phrase", () => {
|
|
282
|
+
const out = buildImagePrompt({
|
|
283
|
+
provider: "nano-banana-pro",
|
|
284
|
+
prompt: "@old-library:1:background~lock a scene",
|
|
285
|
+
connectedReferences: [library],
|
|
286
|
+
})
|
|
287
|
+
// Role token (incl ~lock) is left verbatim, exactly like the role-only case.
|
|
288
|
+
expect(out.prompt).toContain("@old-library:1:background~lock")
|
|
289
|
+
expect(out.prompt).not.toContain("Lock the exact look")
|
|
290
|
+
expect(out.prompt).not.toContain("the background from reference image")
|
|
291
|
+
expect(out.prompt).not.toContain("Use these locations:")
|
|
292
|
+
})
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
// ─── Video parity (character hybrid) ─────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
const kira = (over: Partial<ConnectedReference> = {}): ConnectedReference => ({
|
|
298
|
+
id: "k", defaultName: "Kira", source: "wired-character", url: "u-kira", characterSlug: "kira", ...over,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
describe("video hybrid — per-mention ~lock forces the lock line", () => {
|
|
302
|
+
it("@kira:1:face~lock → 'the face from @image_1' + lock line", () => {
|
|
303
|
+
const out = resolveVideoReferenceCore({
|
|
304
|
+
prompt: "@kira:1:face~lock runs", wiredCharRefs: [kira()], hybridRoles: true,
|
|
305
|
+
})
|
|
306
|
+
expect(out.prompt).toContain("the face from @image_1 runs")
|
|
307
|
+
expect(out.prompt).toContain("Lock the exact identity of the person in @image_1")
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
it("@kira:1:face (no flag) → NO lock line (unchanged)", () => {
|
|
311
|
+
const out = resolveVideoReferenceCore({
|
|
312
|
+
prompt: "@kira:1:face runs", wiredCharRefs: [kira()], hybridRoles: true,
|
|
313
|
+
})
|
|
314
|
+
expect(out.prompt).toContain("the face from @image_1 runs")
|
|
315
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
316
|
+
})
|
|
317
|
+
|
|
318
|
+
it("LEGACY video @kira:1:face~lock → no lock line", () => {
|
|
319
|
+
const out = resolveVideoReferenceCore({ prompt: "@kira:1:face~lock runs", wiredCharRefs: [kira()] })
|
|
320
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
325
|
+
// Task F4 — symmetric per-mention lock FORCE-OFF (`~nolock`) + tri-state
|
|
326
|
+
// ══════════════════════════════════════════════════════════════════════════
|
|
327
|
+
//
|
|
328
|
+
// `~nolock` is the mirror of `~lock`: it FORCES the per-reference identity lock
|
|
329
|
+
// OFF, suppressing a ref-level `identityLock.enabled = true`. Tri-state:
|
|
330
|
+
// `~lock` → lock:true (force ON)
|
|
331
|
+
// `~nolock` → lock:false (force OFF)
|
|
332
|
+
// neither → lock ABSENT (undefined — inherit the ref/source default)
|
|
333
|
+
// Additive + hybrid-gated; legacy byte-identical + inert.
|
|
334
|
+
|
|
335
|
+
const victoriaLocked: ConnectedReference = { ...victoria, identityLock: { enabled: true } }
|
|
336
|
+
const libraryLocked: ConnectedReference = { ...library, identityLock: { enabled: true } }
|
|
337
|
+
|
|
338
|
+
describe("parseCharacterMentionToken — additive ~nolock (force off)", () => {
|
|
339
|
+
it("parses a trailing ~nolock on a mode token → lock:false", () => {
|
|
340
|
+
expect(parseCharacterMentionToken("@kira:1:face~nolock")).toEqual({
|
|
341
|
+
characterSlug: "kira",
|
|
342
|
+
imageIndex: 1,
|
|
343
|
+
variantSlug: null,
|
|
344
|
+
usageMode: "face",
|
|
345
|
+
lock: false,
|
|
346
|
+
})
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it("parses ~nolock on a bare 2-part token → lock:false", () => {
|
|
350
|
+
expect(parseCharacterMentionToken("@kira:1~nolock")).toEqual({
|
|
351
|
+
characterSlug: "kira",
|
|
352
|
+
imageIndex: 1,
|
|
353
|
+
variantSlug: null,
|
|
354
|
+
usageMode: null,
|
|
355
|
+
lock: false,
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it("~nolock is NOT mis-parsed as ~lock (endsWith('~lock') is false)", () => {
|
|
360
|
+
const parsed = parseCharacterMentionToken("@kira:1:face~nolock")
|
|
361
|
+
expect(parsed?.lock).toBe(false)
|
|
362
|
+
expect(parsed?.usageMode).toBe("face")
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
describe("findCharacterMentionTokens — additive ~nolock", () => {
|
|
367
|
+
it("captures the full ~nolock token + lock:false at the right offset", () => {
|
|
368
|
+
expect(findCharacterMentionTokens("Hi @kira:1:face~nolock waves", ["kira"])).toEqual([
|
|
369
|
+
{
|
|
370
|
+
token: "@kira:1:face~nolock",
|
|
371
|
+
characterSlug: "kira",
|
|
372
|
+
imageIndex: 1,
|
|
373
|
+
variantSlug: null,
|
|
374
|
+
usageMode: "face",
|
|
375
|
+
lock: false,
|
|
376
|
+
offset: 3,
|
|
377
|
+
},
|
|
378
|
+
])
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it("WORD-BOUNDARY: ~nolockx stays literal (token is @kira:1, no lock)", () => {
|
|
382
|
+
expect(findCharacterMentionTokens("@kira:1~nolockx in", ["kira"])).toEqual([
|
|
383
|
+
{ token: "@kira:1", characterSlug: "kira", imageIndex: 1, variantSlug: null, usageMode: null, offset: 0 },
|
|
384
|
+
])
|
|
385
|
+
})
|
|
386
|
+
})
|
|
387
|
+
|
|
388
|
+
describe("parseLocationMentionToken — additive ~nolock (force off)", () => {
|
|
389
|
+
it("parses ~nolock on a role token → role + lock:false", () => {
|
|
390
|
+
expect(parseLocationMentionToken("@old-library:1:background~nolock")).toEqual({
|
|
391
|
+
locationSlug: "old-library",
|
|
392
|
+
imageIndex: 1,
|
|
393
|
+
bucket: null,
|
|
394
|
+
variant: null,
|
|
395
|
+
usageMode: null,
|
|
396
|
+
role: "background",
|
|
397
|
+
lock: false,
|
|
398
|
+
})
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it("parses ~nolock on a bucket/variant + mode token → lock:false", () => {
|
|
402
|
+
expect(parseLocationMentionToken("@old-library:1:weather/rain:style~nolock")).toEqual({
|
|
403
|
+
locationSlug: "old-library",
|
|
404
|
+
imageIndex: 1,
|
|
405
|
+
bucket: "weather",
|
|
406
|
+
variant: "rain",
|
|
407
|
+
usageMode: "style",
|
|
408
|
+
lock: false,
|
|
409
|
+
})
|
|
410
|
+
})
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
describe("hybrid image — per-mention ~nolock suppresses a ref-level enabled lock", () => {
|
|
414
|
+
it("@victoria-hayes:1:face~nolock (ref enabled:true) → phrase but NO lock line", () => {
|
|
415
|
+
const out = buildImagePrompt({
|
|
416
|
+
provider: "nano-banana-pro",
|
|
417
|
+
prompt: "@victoria-hayes:1:face~nolock on a rooftop",
|
|
418
|
+
connectedReferences: [victoriaLocked],
|
|
419
|
+
referenceFormat: "hybrid",
|
|
420
|
+
})
|
|
421
|
+
expect(out.prompt).toContain("the face from reference image A on a rooftop")
|
|
422
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
423
|
+
expect(out.prompt).not.toContain("@victoria-hayes")
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
it("@victoria-hayes:1:face (inherit, ref enabled:true) → lock line PRESENT", () => {
|
|
427
|
+
const out = buildImagePrompt({
|
|
428
|
+
provider: "nano-banana-pro",
|
|
429
|
+
prompt: "@victoria-hayes:1:face on a rooftop",
|
|
430
|
+
connectedReferences: [victoriaLocked],
|
|
431
|
+
referenceFormat: "hybrid",
|
|
432
|
+
})
|
|
433
|
+
expect(out.prompt).toContain(CHAR_LOCK_LINE)
|
|
434
|
+
})
|
|
435
|
+
|
|
436
|
+
it("@victoria-hayes:1:face~lock (ref enabled:true) → lock line still PRESENT", () => {
|
|
437
|
+
const out = buildImagePrompt({
|
|
438
|
+
provider: "nano-banana-pro",
|
|
439
|
+
prompt: "@victoria-hayes:1:face~lock on a rooftop",
|
|
440
|
+
connectedReferences: [victoriaLocked],
|
|
441
|
+
referenceFormat: "hybrid",
|
|
442
|
+
})
|
|
443
|
+
expect(out.prompt).toContain(CHAR_LOCK_LINE)
|
|
444
|
+
})
|
|
445
|
+
|
|
446
|
+
it("location @old-library:1:background~nolock (ref enabled:true) → NO lock line", () => {
|
|
447
|
+
const out = buildImagePrompt({
|
|
448
|
+
provider: "nano-banana-pro",
|
|
449
|
+
prompt: "@old-library:1:background~nolock a scene",
|
|
450
|
+
connectedReferences: [libraryLocked],
|
|
451
|
+
referenceFormat: "hybrid",
|
|
452
|
+
})
|
|
453
|
+
expect(out.prompt).toContain("the background from reference image A")
|
|
454
|
+
expect(out.prompt).not.toContain("Lock the exact look")
|
|
455
|
+
})
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
describe("legacy — ~nolock is inert (like ~lock)", () => {
|
|
459
|
+
it("LEGACY character @…:face~nolock → no hybrid phrase, no lock line", () => {
|
|
460
|
+
const out = buildImagePrompt({
|
|
461
|
+
provider: "nano-banana-pro",
|
|
462
|
+
prompt: "@victoria-hayes:1:face~nolock on a rooftop",
|
|
463
|
+
connectedReferences: [victoria],
|
|
464
|
+
})
|
|
465
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
466
|
+
expect(out.prompt).not.toContain("the face from reference image")
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
it("LEGACY location role @…:background~nolock stays literal", () => {
|
|
470
|
+
const out = buildImagePrompt({
|
|
471
|
+
provider: "nano-banana-pro",
|
|
472
|
+
prompt: "@old-library:1:background~nolock a scene",
|
|
473
|
+
connectedReferences: [library],
|
|
474
|
+
})
|
|
475
|
+
expect(out.prompt).toContain("@old-library:1:background~nolock")
|
|
476
|
+
expect(out.prompt).not.toContain("the background from reference image")
|
|
477
|
+
})
|
|
478
|
+
})
|
|
479
|
+
|
|
480
|
+
describe("video hybrid — per-mention ~nolock suppresses the lock line", () => {
|
|
481
|
+
it("@kira:1:face~nolock (ref enabled:true) → phrase but NO lock line", () => {
|
|
482
|
+
const out = resolveVideoReferenceCore({
|
|
483
|
+
prompt: "@kira:1:face~nolock runs",
|
|
484
|
+
wiredCharRefs: [kira({ identityLock: { enabled: true } })],
|
|
485
|
+
hybridRoles: true,
|
|
486
|
+
})
|
|
487
|
+
expect(out.prompt).toContain("the face from @image_1 runs")
|
|
488
|
+
expect(out.prompt).not.toContain("Lock the exact identity")
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
it("@kira:1:face (inherit, ref enabled:true) → lock line PRESENT", () => {
|
|
492
|
+
const out = resolveVideoReferenceCore({
|
|
493
|
+
prompt: "@kira:1:face runs",
|
|
494
|
+
wiredCharRefs: [kira({ identityLock: { enabled: true } })],
|
|
495
|
+
hybridRoles: true,
|
|
496
|
+
})
|
|
497
|
+
expect(out.prompt).toContain("Lock the exact identity of the person in @image_1")
|
|
498
|
+
})
|
|
499
|
+
})
|