@nodaro/prompts 1.0.1 → 1.1.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/dist/index.cjs +14245 -3776
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -46
- package/dist/index.d.ts +8 -46
- package/dist/index.js +14245 -3776
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/ref-only-references.test.ts +63 -0
- package/src/__tests__/variant-role-separation.test.ts +92 -0
- package/src/__tests__/video-reference-resolver.test.ts +5 -4
- package/src/picker-analyzer-registry.ts +4 -8
- package/src/prompt-builder.ts +11 -4
- package/src/prompt-wizard-categories.ts +3 -3
- package/src/video-reference-resolver.ts +12 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nodaro/prompts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
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,63 @@
|
|
|
1
|
+
// End-to-end coverage for the "ref-only" reference role: a mention or node
|
|
2
|
+
// default of `ref-only` injects ONLY the bare reference pointer — `reference
|
|
3
|
+
// image A` on image nodes, `@image_1` on video nodes — with no "the {role}
|
|
4
|
+
// from …" wrapper. Character is preset-gated (ref-only must be a curated
|
|
5
|
+
// preset); location honors the token role verbatim. Both route through the one
|
|
6
|
+
// `roleToPhrase` chokepoint.
|
|
7
|
+
import { describe, it, expect } from "vitest"
|
|
8
|
+
import { buildImagePrompt } from "../prompt-builder.js"
|
|
9
|
+
import { resolveVideoReferenceCore } from "../video-reference-resolver.js"
|
|
10
|
+
import type { ConnectedReference } from "@nodaro/shared"
|
|
11
|
+
|
|
12
|
+
const kira = (over: Partial<ConnectedReference> = {}): ConnectedReference => ({
|
|
13
|
+
id: "k", defaultName: "Kira", source: "wired-character", url: "u-kira", characterSlug: "kira", ...over,
|
|
14
|
+
})
|
|
15
|
+
const library: ConnectedReference = {
|
|
16
|
+
id: "l", defaultName: "Old Library", source: "wired-location",
|
|
17
|
+
url: "https://cdn/library.png", locationSlug: "old-library",
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("ref-only character (image)", () => {
|
|
21
|
+
it("mention '@kira:1:ref-only' → bare 'reference image A'", () => {
|
|
22
|
+
const out = buildImagePrompt({
|
|
23
|
+
provider: "nano-banana-pro", prompt: "@kira:1:ref-only in the rain",
|
|
24
|
+
connectedReferences: [kira()], referenceFormat: "hybrid",
|
|
25
|
+
})
|
|
26
|
+
expect(out.prompt).toContain("reference image A in the rain")
|
|
27
|
+
expect(out.prompt).not.toContain("the person from reference image A")
|
|
28
|
+
})
|
|
29
|
+
it("node defaultRole 'ref-only' (unmentioned canonical) → no 'the person from …'", () => {
|
|
30
|
+
const out = buildImagePrompt({
|
|
31
|
+
provider: "nano-banana-pro", prompt: "a portrait",
|
|
32
|
+
connectedReferences: [kira({ defaultRole: "ref-only" })], referenceFormat: "hybrid",
|
|
33
|
+
})
|
|
34
|
+
expect(out.prompt).not.toContain("the person from reference image A")
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
describe("ref-only character (video)", () => {
|
|
39
|
+
it("mention '@kira:1:ref-only' → bare '@image_1'", () => {
|
|
40
|
+
const out = resolveVideoReferenceCore({
|
|
41
|
+
prompt: "@kira:1:ref-only walks in", wiredCharRefs: [kira()], hybridRoles: true,
|
|
42
|
+
})
|
|
43
|
+
expect(out.prompt).toContain("@image_1 walks in")
|
|
44
|
+
expect(out.prompt).not.toContain("the person from @image_1")
|
|
45
|
+
})
|
|
46
|
+
it("node defaultRole 'ref-only' (unmentioned canonical) → no 'the person from @image_1'", () => {
|
|
47
|
+
const out = resolveVideoReferenceCore({
|
|
48
|
+
prompt: "a slow dolly", wiredCharRefs: [kira({ defaultRole: "ref-only" })], hybridRoles: true,
|
|
49
|
+
})
|
|
50
|
+
expect(out.prompt).not.toContain("the person from @image_1")
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
describe("ref-only location (image)", () => {
|
|
55
|
+
it("mention '@old-library:1:ref-only' → bare 'reference image A'", () => {
|
|
56
|
+
const out = buildImagePrompt({
|
|
57
|
+
provider: "nano-banana-pro", prompt: "@old-library:1:ref-only at night",
|
|
58
|
+
connectedReferences: [library], referenceFormat: "hybrid",
|
|
59
|
+
})
|
|
60
|
+
expect(out.prompt).toContain("reference image A at night")
|
|
61
|
+
expect(out.prompt).not.toContain("the background from reference image A")
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// Variant + Role Separation: a mention's VARIANT picks the image, its ROLE
|
|
2
|
+
// picks the phrase — independently (`@kira:1:walking:clothes` → the walking
|
|
3
|
+
// image attached, "the clothes from …" injected). Covers the character
|
|
4
|
+
// resolvers on BOTH bindings (image letters, video @image_N), the location
|
|
5
|
+
// image resolver, ref-only composition, and the mode-in-seg4 back-compat.
|
|
6
|
+
import { describe, it, expect } from "vitest"
|
|
7
|
+
import { buildImagePrompt } from "../prompt-builder.js"
|
|
8
|
+
import { resolveVideoReferenceCore } from "../video-reference-resolver.js"
|
|
9
|
+
import type { ConnectedReference } from "@nodaro/shared"
|
|
10
|
+
|
|
11
|
+
const kira = (over: Partial<ConnectedReference> = {}): ConnectedReference => ({
|
|
12
|
+
id: "k", defaultName: "Kira", source: "wired-character", url: "u-kira", characterSlug: "kira", ...over,
|
|
13
|
+
})
|
|
14
|
+
const kiraWalking = kira({ id: "kw", variantSlug: "walking", url: "u-walk" })
|
|
15
|
+
|
|
16
|
+
const library: ConnectedReference = {
|
|
17
|
+
id: "l", defaultName: "Old Library", source: "wired-location",
|
|
18
|
+
url: "u-lib", locationSlug: "oldlibrary",
|
|
19
|
+
}
|
|
20
|
+
const libraryRain: ConnectedReference = {
|
|
21
|
+
...library, id: "lr", url: "u-rain",
|
|
22
|
+
locationVariantBucket: "weather", locationVariantSlug: "rain",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("character variant + role (image)", () => {
|
|
26
|
+
it("@kira:1:walking:clothes → walking image attached, 'the clothes from reference image A'", () => {
|
|
27
|
+
const out = buildImagePrompt({
|
|
28
|
+
provider: "nano-banana-pro", prompt: "@kira:1:walking:clothes in the rain",
|
|
29
|
+
connectedReferences: [kira(), kiraWalking], referenceFormat: "hybrid",
|
|
30
|
+
})
|
|
31
|
+
expect(out.prompt).toContain("the clothes from reference image A in the rain")
|
|
32
|
+
expect(out.referenceImageUrls).toContain("u-walk")
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("@kira:1:walking:ref-only → walking image attached, bare 'reference image A'", () => {
|
|
36
|
+
const out = buildImagePrompt({
|
|
37
|
+
provider: "nano-banana-pro", prompt: "@kira:1:walking:ref-only in the rain",
|
|
38
|
+
connectedReferences: [kira(), kiraWalking], referenceFormat: "hybrid",
|
|
39
|
+
})
|
|
40
|
+
expect(out.prompt).toContain("reference image A in the rain")
|
|
41
|
+
expect(out.prompt).not.toContain("from reference image A")
|
|
42
|
+
expect(out.referenceImageUrls).toContain("u-walk")
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("a CUSTOM 4th-segment role survives verbatim", () => {
|
|
46
|
+
const out = buildImagePrompt({
|
|
47
|
+
provider: "nano-banana-pro", prompt: "@kira:1:walking:earrings at dawn",
|
|
48
|
+
connectedReferences: [kira(), kiraWalking], referenceFormat: "hybrid",
|
|
49
|
+
})
|
|
50
|
+
expect(out.prompt).toContain("the earrings from reference image A at dawn")
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("back-compat: @kira:1:walking:pose (mode in seg4) is unchanged", () => {
|
|
54
|
+
const out = buildImagePrompt({
|
|
55
|
+
provider: "nano-banana-pro", prompt: "@kira:1:walking:pose at dawn",
|
|
56
|
+
connectedReferences: [kira(), kiraWalking], referenceFormat: "hybrid",
|
|
57
|
+
})
|
|
58
|
+
expect(out.prompt).toContain("the pose from reference image A at dawn")
|
|
59
|
+
expect(out.referenceImageUrls).toContain("u-walk")
|
|
60
|
+
})
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
describe("character variant + role (video)", () => {
|
|
64
|
+
it("@kira:1:walking:clothes → 'the clothes from @image_1', walking image attached", () => {
|
|
65
|
+
const out = resolveVideoReferenceCore({
|
|
66
|
+
prompt: "@kira:1:walking:clothes walks in",
|
|
67
|
+
wiredCharRefs: [kira(), kiraWalking], hybridRoles: true,
|
|
68
|
+
})
|
|
69
|
+
expect(out.prompt).toContain("the clothes from @image_1 walks in")
|
|
70
|
+
expect(out.additionalUrls).toContain("u-walk")
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("@kira:1:walking:ref-only → bare '@image_1'", () => {
|
|
74
|
+
const out = resolveVideoReferenceCore({
|
|
75
|
+
prompt: "@kira:1:walking:ref-only walks in",
|
|
76
|
+
wiredCharRefs: [kira(), kiraWalking], hybridRoles: true,
|
|
77
|
+
})
|
|
78
|
+
expect(out.prompt).toContain("@image_1 walks in")
|
|
79
|
+
expect(out.prompt).not.toContain("from @image_1")
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe("location variant + role (image)", () => {
|
|
84
|
+
it("@oldlibrary:1:weather/rain:lighting → rain image attached, 'the lighting from reference image A'", () => {
|
|
85
|
+
const out = buildImagePrompt({
|
|
86
|
+
provider: "nano-banana-pro", prompt: "@oldlibrary:1:weather/rain:lighting a chase",
|
|
87
|
+
connectedReferences: [library, libraryRain], referenceFormat: "hybrid",
|
|
88
|
+
})
|
|
89
|
+
expect(out.prompt).toContain("the lighting from reference image A a chase")
|
|
90
|
+
expect(out.referenceImageUrls).toContain("u-rain")
|
|
91
|
+
})
|
|
92
|
+
})
|
|
@@ -156,8 +156,7 @@ describe("REF_BINDING", () => {
|
|
|
156
156
|
// into `@image_N`-style subject bindings (Task 2.3). Positional, 1-based against
|
|
157
157
|
// the corresponding count. Out-of-range / missing count → drop to the bare label
|
|
158
158
|
// (legacy `stripVideoImageTokens` strip behavior). Label-less in-range token →
|
|
159
|
-
//
|
|
160
|
-
// defined + exported + tested here, NOT yet called by the core (that is Task 2.4).
|
|
159
|
+
// the bare "@kind_N" (the ref-only default) so the binding lands with no wrapper.
|
|
161
160
|
describe("resolveReferenceTokens", () => {
|
|
162
161
|
const counts = { image: 4, video: 0, audio: 0 }
|
|
163
162
|
it("resolves repeated labels distinctly by slot", () => {
|
|
@@ -168,8 +167,10 @@ describe("resolveReferenceTokens", () => {
|
|
|
168
167
|
it("drops out-of-range tokens to bare label", () => {
|
|
169
168
|
expect(resolveReferenceTokens("a {image:9:ghost} here", counts)).toBe("a ghost here")
|
|
170
169
|
})
|
|
171
|
-
it("
|
|
172
|
-
expect(resolveReferenceTokens("{image:1}", counts)).toBe("
|
|
170
|
+
it("resolves a label-less token to the bare kind ordinal (ref-only)", () => {
|
|
171
|
+
expect(resolveReferenceTokens("{image:1}", counts)).toBe("@image_1")
|
|
172
|
+
expect(resolveReferenceTokens("{video:1}", { image: 0, video: 1, audio: 0 })).toBe("@video_1")
|
|
173
|
+
expect(resolveReferenceTokens("{audio:1}", { image: 0, video: 0, audio: 1 })).toBe("@audio_1")
|
|
173
174
|
})
|
|
174
175
|
it("resolves video + audio tokens against their own counts", () => {
|
|
175
176
|
expect(resolveReferenceTokens("dance like {video:1:clip} to {audio:1:song}", { image: 0, video: 1, audio: 1 }))
|
|
@@ -188,7 +188,7 @@ export function buildPickerAnalyzerSpec(pickerType: PickerType): PickerAnalyzerS
|
|
|
188
188
|
* (multi). `.strict()` blocks unknown keys. Mirrors the field cardinality. */
|
|
189
189
|
export function buildPickerZodSchema(
|
|
190
190
|
spec: PickerAnalyzerSpec,
|
|
191
|
-
): z.ZodType<Record<string, string | string[]>,
|
|
191
|
+
): z.ZodType<Record<string, string | string[]>, unknown> {
|
|
192
192
|
const shape: Record<string, z.ZodTypeAny> = {}
|
|
193
193
|
for (const d of spec.dimensions) {
|
|
194
194
|
const ids = d.entryIds as unknown as [string, ...string[]]
|
|
@@ -196,16 +196,12 @@ export function buildPickerZodSchema(
|
|
|
196
196
|
shape[d.dimension] = d.limit > 1 ? z.array(enumZ).max(d.limit).optional() : enumZ.optional()
|
|
197
197
|
}
|
|
198
198
|
// dynamic shape → Zod can't infer the narrowed output type; runtime-validated by picker-analyzer-registry.test.ts
|
|
199
|
-
return z.object(shape).strict() as unknown as z.ZodType<
|
|
200
|
-
Record<string, string | string[]>,
|
|
201
|
-
z.ZodTypeDef,
|
|
202
|
-
unknown
|
|
203
|
-
>
|
|
199
|
+
return z.object(shape).strict() as unknown as z.ZodType<Record<string, string | string[]>, unknown>
|
|
204
200
|
}
|
|
205
201
|
|
|
206
202
|
export interface PickerAnalyzer {
|
|
207
203
|
readonly spec: PickerAnalyzerSpec
|
|
208
|
-
readonly schema: z.ZodType<Record<string, string | string[]>,
|
|
204
|
+
readonly schema: z.ZodType<Record<string, string | string[]>, unknown>
|
|
209
205
|
readonly legend: string
|
|
210
206
|
}
|
|
211
207
|
|
|
@@ -265,7 +261,7 @@ export interface PickerGaps {
|
|
|
265
261
|
}
|
|
266
262
|
|
|
267
263
|
export interface MultiPickerAnalyzerSpec {
|
|
268
|
-
readonly schema: z.ZodType<Record<string, unknown>,
|
|
264
|
+
readonly schema: z.ZodType<Record<string, unknown>, unknown>
|
|
269
265
|
readonly toolName: string
|
|
270
266
|
readonly legend: string
|
|
271
267
|
}
|
package/src/prompt-builder.ts
CHANGED
|
@@ -289,6 +289,12 @@ function resolveCharacterMentionsHybrid(
|
|
|
289
289
|
mentionedCharacterSlugs.add(t.characterSlug)
|
|
290
290
|
refByUrl.set(match.url, match)
|
|
291
291
|
if (t.lock !== undefined) lockOverrideByUrl.set(match.url, t.lock)
|
|
292
|
+
// Variant + Role Separation: an explicit 4th-segment ROLE
|
|
293
|
+
// (`@kira:1:walking:clothes`) wins outright and coexists with the variant —
|
|
294
|
+
// the variant picked the image (variantMatch above), the role picks the
|
|
295
|
+
// phrase. Used VERBATIM (curated or custom), mirroring the seg3 custom-role
|
|
296
|
+
// relaxation below.
|
|
297
|
+
const explicitRole = (t.role ?? "").trim()
|
|
292
298
|
const segment = (t.usageMode ?? t.variantSlug ?? "").trim()
|
|
293
299
|
// Custom roles survive VERBATIM (Unified Reference Roles, Phase D). A
|
|
294
300
|
// non-empty segment is the role when it is a curated preset (face/pose/style
|
|
@@ -299,10 +305,11 @@ function resolveCharacterMentionsHybrid(
|
|
|
299
305
|
// face-pose/emotion/name/none) fall back to the NODE default: the character
|
|
300
306
|
// node's `defaultRole` (hybrid dropdown pick, verbatim) → its
|
|
301
307
|
// `defaultUsageMode`-derived role → the source default ("person") — via
|
|
302
|
-
// `resolveDefaultRole` (Character Node Role+Lock). Precedence:
|
|
303
|
-
// token role → node defaultRole → defaultUsageMode-derived → source default.
|
|
304
|
-
const role =
|
|
305
|
-
|
|
308
|
+
// `resolveDefaultRole` (Character Node Role+Lock). Precedence: seg4 role →
|
|
309
|
+
// seg3 token role → node defaultRole → defaultUsageMode-derived → source default.
|
|
310
|
+
const role = explicitRole
|
|
311
|
+
? explicitRole
|
|
312
|
+
: segment && (presets.includes(segment) || (t.usageMode == null && !variantMatch))
|
|
306
313
|
? segment
|
|
307
314
|
: resolveDefaultRole(match.defaultRole, match.defaultUsageMode, "wired-character")
|
|
308
315
|
matched.push({ token: t.token, offset: t.offset, url: match.url, role })
|
|
@@ -245,7 +245,7 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
245
245
|
"bytedance-pro": "Higher quality ByteDance",
|
|
246
246
|
"runway-kie": "Runway via KIE, strong cinematic quality",
|
|
247
247
|
"wan-2.7-t2v": "Wan 2.7 T2V — 2–15s, 720p/1080p",
|
|
248
|
-
"happyhorse": "HappyHorse T2V — 3–15s, 720p/1080p",
|
|
248
|
+
"happyhorse": "HappyHorse 1.1 T2V — 3–15s, 720p/1080p, 9 aspect ratios incl. 21:9/9:21",
|
|
249
249
|
"ltx-2.3-pro": "Lightricks LTX 2.3 Pro — text/image/audio→video, 6–10s, up to 4K",
|
|
250
250
|
"ltx-2.3-fast": "Lightricks LTX 2.3 Fast — text/image→video, 6–20s, up to 4K",
|
|
251
251
|
"gemini-omni-video": "Google Gemini Omni — multimodal video with native audio, 4–10s, up to 4K.",
|
|
@@ -275,8 +275,8 @@ export const PROVIDER_CAPABILITIES: Record<string, Record<string, string>> = {
|
|
|
275
275
|
"grok-i2v": "General purpose animation",
|
|
276
276
|
"runway-kie": "Cinematic image animation",
|
|
277
277
|
"wan-2.7-i2v": "Wan 2.7 I2V — 2–15s, 720p/1080p, start+end frame",
|
|
278
|
-
"happyhorse-i2v": "HappyHorse I2V — 3–15s, 720p/1080p",
|
|
279
|
-
"happyhorse-ref2v": "HappyHorse Ref2V — multi-ref image to video, 3–15s",
|
|
278
|
+
"happyhorse-i2v": "HappyHorse 1.1 I2V — 3–15s, 720p/1080p",
|
|
279
|
+
"happyhorse-ref2v": "HappyHorse 1.1 Ref2V — multi-ref image to video (1–9 refs), 3–15s",
|
|
280
280
|
"ltx-2.3-pro": "Lightricks LTX 2.3 Pro — start/end frame i2v + audio→video, 6–10s, up to 4K",
|
|
281
281
|
"ltx-2.3-fast": "Lightricks LTX 2.3 Fast — start/end frame i2v, 6–20s, up to 4K",
|
|
282
282
|
"gemini-omni-video": "Google Gemini Omni — multimodal video with native audio, 4–10s, up to 4K.",
|
|
@@ -88,8 +88,8 @@ const REFERENCE_TOKEN_RE = /\{(image|video|audio):(\d+)(?::([a-zA-Z0-9_ -]+))?\}
|
|
|
88
88
|
* `stripVideoImageTokens` strip behavior: drop the token, keep the label text.
|
|
89
89
|
* - in range, label present → `REF_BINDING[kind](label, N)`
|
|
90
90
|
* (e.g. `the person from @image_2`).
|
|
91
|
-
* - in range, no label →
|
|
92
|
-
* lands
|
|
91
|
+
* - in range, no label → the bare `@${kind}_${N}` (the ref-only default): the
|
|
92
|
+
* binding lands with no descriptive wrapper.
|
|
93
93
|
*
|
|
94
94
|
* Runs of 2+ HORIZONTAL whitespace (left behind by a dropped label-less token)
|
|
95
95
|
* collapse to one space, the result is trimmed, and an empty result becomes
|
|
@@ -115,7 +115,7 @@ export function resolveReferenceTokens(
|
|
|
115
115
|
const n = parseInt(nStr, 10)
|
|
116
116
|
if (n < 1 || n > counts[kind]) return label ?? ""
|
|
117
117
|
if (label) return REF_BINDING[kind](label, n)
|
|
118
|
-
return
|
|
118
|
+
return `@${kind}_${n}`
|
|
119
119
|
})
|
|
120
120
|
// Horizontal whitespace only — preserve `\n` / `\n\n` block separators.
|
|
121
121
|
.replace(/[^\S\r\n]{2,}/g, " ")
|
|
@@ -309,6 +309,10 @@ function resolveVideoCharacterMentionsHybrid(
|
|
|
309
309
|
mentionedCharacterSlugs.add(t.characterSlug)
|
|
310
310
|
refByUrl.set(match.url, match)
|
|
311
311
|
if (t.lock !== undefined) lockOverrideByUrl.set(match.url, t.lock)
|
|
312
|
+
// Variant + Role Separation: an explicit 4th-segment ROLE
|
|
313
|
+
// (`@kira:1:walking:clothes`) wins outright and coexists with the variant —
|
|
314
|
+
// used VERBATIM, in lock-step with the image-side resolver.
|
|
315
|
+
const explicitRole = (t.role ?? "").trim()
|
|
312
316
|
const segment = (t.usageMode ?? t.variantSlug ?? "").trim()
|
|
313
317
|
// Custom roles survive VERBATIM — the SAME relaxation as the image-side
|
|
314
318
|
// `resolveCharacterMentionsHybrid` (Unified Reference Roles, Phase D), kept
|
|
@@ -317,9 +321,11 @@ function resolveVideoCharacterMentionsHybrid(
|
|
|
317
321
|
// a real variant URL → role; a real variant slug or a directive-only usage
|
|
318
322
|
// mode → the NODE default (`defaultRole` verbatim → `defaultUsageMode`-derived
|
|
319
323
|
// → "person", via `resolveDefaultRole` — Character Node Role+Lock), mirroring
|
|
320
|
-
// the image-side mention fallback.
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
// the image-side mention fallback. Precedence: seg4 role → seg3 token role →
|
|
325
|
+
// node default chain.
|
|
326
|
+
const role = explicitRole
|
|
327
|
+
? explicitRole
|
|
328
|
+
: segment && (presets.includes(segment) || (t.usageMode == null && !variantMatch))
|
|
323
329
|
? segment
|
|
324
330
|
: resolveDefaultRole(match.defaultRole, match.defaultUsageMode, "wired-character")
|
|
325
331
|
matched.push({ token: t.token, offset: t.offset, url: match.url, role })
|