@nodaro/prompts 1.16.0 → 1.17.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.16.0",
3
+ "version": "1.17.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",
@@ -20,7 +20,7 @@
20
20
  "test": "vitest run"
21
21
  },
22
22
  "dependencies": {
23
- "@nodaro/shared": "^2.21.0"
23
+ "@nodaro/shared": "^2.23.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "tsup": "^8.5.0",
@@ -417,3 +417,33 @@ describe("assembleImageInput — platform-caller parity (no direction)", () => {
417
417
  })
418
418
  }
419
419
  })
420
+
421
+ // ---------------------------------------------------------------------------
422
+ // Described references — forwarded verbatim to `buildImagePrompt`, including
423
+ // when they are the ONLY reference channel the caller sends (a story landing
424
+ // whose cast has no entities yet).
425
+ // ---------------------------------------------------------------------------
426
+ describe("assembleImageInput — describedReferences", () => {
427
+ it("forwards them with no connected references", () => {
428
+ const described = [{ name: "Natalie", description: "a tall woman in a red coat" }]
429
+ expect(
430
+ assembleImageInput({
431
+ userPrompt: "Natalie walks down the pier.",
432
+ provider: "nano-banana-pro",
433
+ describedReferences: described,
434
+ }),
435
+ ).toEqual(
436
+ buildImagePrompt({
437
+ prompt: "Natalie walks down the pier.",
438
+ provider: "nano-banana-pro",
439
+ describedReferences: described,
440
+ }),
441
+ )
442
+ })
443
+
444
+ it("omits the field entirely when absent (byte-identical to today)", () => {
445
+ expect(
446
+ assembleImageInput({ userPrompt: "A woman walks.", provider: "nano-banana-pro" }),
447
+ ).toEqual(buildImagePrompt({ prompt: "A woman walks.", provider: "nano-banana-pro" }))
448
+ })
449
+ })
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Described references, per-use `descriptionOverride` and the video/audio rail
3
+ * CAPTIONS, on the VIDEO lane, in BOTH reference formats.
4
+ *
5
+ * The video core has THREE ways out — the "no wired characters and no extras"
6
+ * early return (the described-only case: a story landing before any entity
7
+ * exists), the reorder return and the plain return — so the join is pinned on
8
+ * each. Phrasing itself comes from `described-references.ts`; there is no second
9
+ * spelling here.
10
+ */
11
+
12
+ import { describe, it, expect } from "vitest"
13
+ import type { ConnectedReference } from "@nodaro/shared"
14
+ import { resolveVideoReferenceCore } from "../video-reference-resolver.js"
15
+
16
+ const charRef = (over: Partial<ConnectedReference> = {}): ConnectedReference => ({
17
+ id: "n1",
18
+ defaultName: "Kira",
19
+ source: "wired-character",
20
+ url: "https://r2/kira.png",
21
+ characterSlug: "kira",
22
+ characterCanonicalDescription: "a woman with short black hair",
23
+ ...over,
24
+ })
25
+
26
+ const described = [{ name: "Natalie", description: "a tall woman in a red coat" }]
27
+
28
+ describe("video core — described references with nothing wired", () => {
29
+ it("bullets them into a legacy block", () => {
30
+ const out = resolveVideoReferenceCore({
31
+ prompt: "Natalie walks down the pier.",
32
+ wiredCharRefs: [],
33
+ describedReferences: described,
34
+ })
35
+ expect(out.prompt).toBe(
36
+ "Use these characters:\n- Natalie — a tall woman in a red coat.\n\nNatalie walks down the pier.",
37
+ )
38
+ expect(out.additionalUrls).toEqual([])
39
+ })
40
+
41
+ it("appends them as hybrid trailing directives", () => {
42
+ const out = resolveVideoReferenceCore({
43
+ prompt: "Natalie walks down the pier.",
44
+ wiredCharRefs: [],
45
+ describedReferences: described,
46
+ hybridRoles: true,
47
+ })
48
+ expect(out.prompt).toBe(
49
+ "Natalie walks down the pier.\nNatalie — a tall woman in a red coat.",
50
+ )
51
+ })
52
+
53
+ it("still returns an undefined prompt when there is nothing at all to say", () => {
54
+ const out = resolveVideoReferenceCore({ prompt: undefined, wiredCharRefs: [] })
55
+ expect(out.prompt).toBeUndefined()
56
+ })
57
+ })
58
+
59
+ describe("video core — described references alongside a wired character", () => {
60
+ it("consolidates into the one legacy character block", () => {
61
+ const out = resolveVideoReferenceCore({
62
+ prompt: "Kira meets Natalie.",
63
+ wiredCharRefs: [charRef()],
64
+ describedReferences: described,
65
+ })
66
+ expect(out.prompt).toContain("Use these characters:\n- Kira — a woman with short black hair.")
67
+ expect(out.prompt).toContain("- Natalie — a tall woman in a red coat.")
68
+ // ONE block, not two.
69
+ expect(out.prompt?.match(/Use these characters:/g)).toHaveLength(1)
70
+ })
71
+
72
+ it("lands after the hybrid role phrases", () => {
73
+ const out = resolveVideoReferenceCore({
74
+ prompt: "Kira meets Natalie.",
75
+ wiredCharRefs: [charRef()],
76
+ describedReferences: described,
77
+ hybridRoles: true,
78
+ })
79
+ expect(out.prompt).toBe(
80
+ "Kira meets Natalie.\n"
81
+ + "the person from @image_1\n"
82
+ + "Natalie — a tall woman in a red coat.",
83
+ )
84
+ })
85
+
86
+ it("rides the reorder exit unrenumbered — it carries no @image_N binding", () => {
87
+ const wiredCharRefs = [
88
+ charRef(),
89
+ charRef({
90
+ id: "n2",
91
+ defaultName: "Milo",
92
+ url: "https://r2/milo.png",
93
+ characterSlug: "milo",
94
+ characterCanonicalDescription: "a boy with a red scarf",
95
+ }),
96
+ ]
97
+ const out = resolveVideoReferenceCore({
98
+ prompt: "Kira and Milo meet Natalie.",
99
+ wiredCharRefs,
100
+ describedReferences: described,
101
+ hybridRoles: true,
102
+ // Swap the two canonical seats — the tile ids the reorder matches on.
103
+ referenceOrder: ["char-canonical:milo", "char-canonical:kira"],
104
+ })
105
+ // Non-vacuous: the reorder branch really ran (assets swapped) and its
106
+ // `@image_N` renumber pass rewrote both role phrases…
107
+ expect(out.additionalUrls).toEqual(["https://r2/milo.png", "https://r2/kira.png"])
108
+ expect(out.prompt).toBe(
109
+ "Kira and Milo meet Natalie.\n"
110
+ + "the person from @image_2\n"
111
+ + "the person from @image_1\n"
112
+ // …while the described line, which binds no seat, is byte-identical to
113
+ // the un-reordered run's.
114
+ + "Natalie — a tall woman in a red coat.",
115
+ )
116
+ })
117
+ })
118
+
119
+ describe("video core — rail captions", () => {
120
+ it("renders index-aligned video and audio captions in legacy", () => {
121
+ const out = resolveVideoReferenceCore({
122
+ prompt: "A cut between two shots.",
123
+ wiredCharRefs: [],
124
+ videoRefCount: 2,
125
+ audioRefCount: 1,
126
+ videoCaptions: ["the establishing drone shot", "the close-up"],
127
+ audioCaptions: ["the score"],
128
+ })
129
+ expect(out.prompt).toBe(
130
+ "Use these characters:\n"
131
+ + "- @video_1: the establishing drone shot.\n"
132
+ + "- @video_2: the close-up.\n"
133
+ + "- @audio_1: the score.\n\n"
134
+ + "A cut between two shots.",
135
+ )
136
+ })
137
+
138
+ it("never binds a caption to a rail slot the payload does not ship", () => {
139
+ const out = resolveVideoReferenceCore({
140
+ prompt: "A single shot.",
141
+ wiredCharRefs: [],
142
+ videoRefCount: 1,
143
+ videoCaptions: ["the establishing drone shot", "dropped by the provider cap"],
144
+ hybridRoles: true,
145
+ })
146
+ expect(out.prompt).toBe(
147
+ "A single shot.\n@video_1: the establishing drone shot.",
148
+ )
149
+ })
150
+
151
+ it("reaches the main path too, alongside a wired character", () => {
152
+ const out = resolveVideoReferenceCore({
153
+ prompt: "Kira reacts.",
154
+ wiredCharRefs: [charRef()],
155
+ videoRefCount: 1,
156
+ videoCaptions: ["the establishing drone shot"],
157
+ hybridRoles: true,
158
+ })
159
+ expect(out.prompt).toContain("@video_1: the establishing drone shot.")
160
+ })
161
+ })
162
+
163
+ describe("video core — descriptionOverride", () => {
164
+ const overridden = charRef({ descriptionOverride: "a woman with a shaved head and a scar" })
165
+
166
+ it("replaces the canonical description on the legacy canonical fallback", () => {
167
+ const out = resolveVideoReferenceCore({
168
+ prompt: "Kira walks.",
169
+ wiredCharRefs: [overridden],
170
+ })
171
+ expect(out.prompt).toContain("- Kira — a woman with a shaved head and a scar.")
172
+ expect(out.prompt).not.toContain("short black hair")
173
+ })
174
+
175
+ it("is honored in a mode that suppresses the canonical description", () => {
176
+ const out = resolveVideoReferenceCore({
177
+ prompt: "Kira walks.",
178
+ wiredCharRefs: [charRef({ ...overridden, defaultUsageMode: "emotion" })],
179
+ })
180
+ expect(out.prompt).toContain("- Kira — a woman with a shaved head and a scar.")
181
+ })
182
+
183
+ it("adds one binding-subject line on the hybrid canonical fallback", () => {
184
+ const out = resolveVideoReferenceCore({
185
+ prompt: "Kira walks.",
186
+ wiredCharRefs: [overridden],
187
+ hybridRoles: true,
188
+ })
189
+ expect(out.prompt).toBe(
190
+ "Kira walks.\n"
191
+ + "the person from @image_1\n"
192
+ + "@image_1 — a woman with a shaved head and a scar.",
193
+ )
194
+ })
195
+
196
+ it("adds one binding-subject line on a hybrid @-mention", () => {
197
+ const out = resolveVideoReferenceCore({
198
+ prompt: "@kira:1 walks.",
199
+ wiredCharRefs: [overridden],
200
+ hybridRoles: true,
201
+ })
202
+ expect(out.prompt).toBe(
203
+ "the person from @image_1 walks.\n@image_1 — a woman with a shaved head and a scar.",
204
+ )
205
+ })
206
+
207
+ it("replaces the canonical description on the legacy @-mention bullet", () => {
208
+ const out = resolveVideoReferenceCore({
209
+ prompt: "@kira:1 walks.",
210
+ wiredCharRefs: [overridden],
211
+ })
212
+ // The shared `resolveCharacterMentions` bullet — `Image N (Name)` subject,
213
+ // not the canonical fallback's bare name (pinned above).
214
+ expect(out.prompt).toContain("- Image 1 (Kira) — a woman with a shaved head and a scar.")
215
+ expect(out.prompt).not.toContain("short black hair")
216
+ })
217
+
218
+ it("fills an extra-ref's own description slot", () => {
219
+ const out = resolveVideoReferenceCore({
220
+ prompt: "A still life.",
221
+ wiredCharRefs: [],
222
+ extraRefs: [
223
+ {
224
+ url: "https://r2/vase.png",
225
+ description: "a blue vase",
226
+ descriptionOverride: "a cracked terracotta urn",
227
+ },
228
+ ],
229
+ })
230
+ expect(out.prompt).toContain("- @image_1 (reference): a cracked terracotta urn.")
231
+ expect(out.prompt).not.toContain("blue vase")
232
+ })
233
+
234
+ it("fills an extra-ref's own description slot in hybrid too", () => {
235
+ const out = resolveVideoReferenceCore({
236
+ prompt: "A still life.",
237
+ wiredCharRefs: [],
238
+ hybridRoles: true,
239
+ extraRefs: [
240
+ {
241
+ url: "https://r2/vase.png",
242
+ description: "a blue vase",
243
+ descriptionOverride: "a cracked terracotta urn",
244
+ },
245
+ ],
246
+ })
247
+ expect(out.prompt).toBe("A still life.\na cracked terracotta urn (@image_1).")
248
+ expect(out.prompt).not.toContain("blue vase")
249
+ })
250
+ })