@nodaro/shared 2.16.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/dist/index.cjs +120 -22
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +261 -18
- package/dist/index.d.ts +261 -18
- package/dist/index.js +113 -23
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/apply-video-negative-prompt.test.ts +38 -1
- package/src/__tests__/entity-mention-slug.test.ts +380 -0
- package/src/__tests__/producer-types.test.ts +22 -0
- package/src/animals.ts +57 -0
- package/src/entity-mention-slug.ts +203 -0
- package/src/image-mention-slug.ts +47 -96
- package/src/index.ts +19 -0
- package/src/mention-token-grammar.ts +167 -0
- package/src/model-catalog.ts +2 -2
- package/src/model-constants.ts +35 -1
- package/src/producer-types.ts +6 -0
- package/src/types.ts +11 -5
- package/src/workflow-export.ts +34 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wired-entity `@-mention` parser — `@<name-slug>:<index>[:<role>][~lock|~nolock]`
|
|
3
|
+
* for `wired-creature` and `wired-object` references.
|
|
4
|
+
*
|
|
5
|
+
* THE BUG THIS KILLS. Before this leg, creatures and objects were the only wired
|
|
6
|
+
* sources with NO mention grammar. A user writing "Nessie rises from the lake"
|
|
7
|
+
* with a creature node wired in got the creature's NAME as plain prose while its
|
|
8
|
+
* binding dangled as a trailing "the creature from reference image D" line after
|
|
9
|
+
* the style hints — two disconnected halves of one intent, which is exactly the
|
|
10
|
+
* failure mode mentions exist to remove. With a mention, the binding renders
|
|
11
|
+
* INLINE at the typed position and the trailing canonical fallback for that ref
|
|
12
|
+
* is suppressed.
|
|
13
|
+
*
|
|
14
|
+
* @nessie:4 — bare mention; the source-default role phrase
|
|
15
|
+
* ("the creature from reference image D")
|
|
16
|
+
* @nessie:4:markings — role phrase ("the markings from reference image D")
|
|
17
|
+
* @chair:2:material — objects use the `wired-object` presets
|
|
18
|
+
* @nessie:4:my-custom-role — custom roles pass through verbatim
|
|
19
|
+
* @nessie:4~lock — additive identity-lock sentinel (also `~nolock`)
|
|
20
|
+
* @nessie:4:a:b — NULL. A 4-part token is never an entity mention.
|
|
21
|
+
*
|
|
22
|
+
* GRAMMAR CORE. Identical grammar to the named-image mention, so the slug shape,
|
|
23
|
+
* the parser and the finder (with BOTH collision guards — the 4-part trailing
|
|
24
|
+
* reject and the location slash guard) are the SHARED `mention-token-grammar.ts`,
|
|
25
|
+
* not a second hand-copied edition. This module is the ENTITY view of that core:
|
|
26
|
+
* the field names, and the part that genuinely differs — WHICH refs contribute a
|
|
27
|
+
* slug.
|
|
28
|
+
*
|
|
29
|
+
* PRECEDENCE, across all five kinds:
|
|
30
|
+
*
|
|
31
|
+
* character → location → image → creature → object
|
|
32
|
+
*
|
|
33
|
+
* Enforced by RESOLUTION ORDER in `buildImagePrompt`'s Phase 0, not by anything
|
|
34
|
+
* in this file: each pass splices its matched tokens out of the prompt before the
|
|
35
|
+
* next pass runs its finder, so a slug claimed by an earlier kind never reaches a
|
|
36
|
+
* later pass. A name shared by a character and a creature resolves as the
|
|
37
|
+
* CHARACTER, and the creature token never fires. The creature-before-object half
|
|
38
|
+
* of the tail is enforced inside the single entity pass, whose slug → ref map is
|
|
39
|
+
* built creature-first (see `resolveEntityMentionsHybrid`).
|
|
40
|
+
*
|
|
41
|
+
* NO WIRE FIELD, matching the image grammar and unlike `characterSlug` /
|
|
42
|
+
* `locationSlug`: the slug is DERIVED from `defaultName` at resolve time
|
|
43
|
+
* (`knownEntitySlugsFromRefs`), so a client cannot drift from the grammar and the
|
|
44
|
+
* reference schema is untouched.
|
|
45
|
+
*
|
|
46
|
+
* NO LEGACY RESOLVER — the image-grammar precedent. Only the hybrid reference
|
|
47
|
+
* format resolves these tokens; under the legacy format an `@name:N` token stays
|
|
48
|
+
* literal text and the entity auto-attaches with its trailing canonical phrase
|
|
49
|
+
* exactly as it does today. Legacy assembly has no inline role-phrase machinery
|
|
50
|
+
* at all (its object/creature rendering is the numbered-directive block), so
|
|
51
|
+
* there is no clean seam to add one and no consumer asking for it.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import type { ConnectedReference } from "./types.js"
|
|
55
|
+
import {
|
|
56
|
+
MENTION_SLUG_PATTERN,
|
|
57
|
+
findMentionTokens,
|
|
58
|
+
mentionNameSlug,
|
|
59
|
+
parseMentionToken,
|
|
60
|
+
} from "./mention-token-grammar.js"
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Slugify a wired entity's display name for `@`-mention tokens. Byte-identical
|
|
64
|
+
* algorithm to `characterMentionSlug` / `imageMentionSlug`; kept as a separate
|
|
65
|
+
* export to make the call site's intent explicit.
|
|
66
|
+
*/
|
|
67
|
+
export function entityMentionSlug(name: string): string {
|
|
68
|
+
return mentionNameSlug(name)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface EntityMentionTokenInfo {
|
|
72
|
+
/** The matched token text, verbatim — spliced out of the prompt at resolve time. */
|
|
73
|
+
readonly token: string
|
|
74
|
+
readonly entitySlug: string
|
|
75
|
+
/**
|
|
76
|
+
* 1-based correlation index assigned at insertion by the autocomplete
|
|
77
|
+
* (`nextMentionIndex` = max(existing N) + 1, unified across every mention
|
|
78
|
+
* kind). The hybrid resolver binds by its own numbering walk, so the index is
|
|
79
|
+
* CORRELATION ONLY — it is never echoed into the prompt.
|
|
80
|
+
*/
|
|
81
|
+
readonly entityIndex: number
|
|
82
|
+
/**
|
|
83
|
+
* Per-mention ROLE from the 3rd segment (`@nessie:4:markings`) — curated
|
|
84
|
+
* (`REFERENCE_ROLE_PRESETS["wired-creature"]` / `["wired-object"]`) or custom,
|
|
85
|
+
* stored VERBATIM. Both preset lists are entirely single-word, so this never
|
|
86
|
+
* needs `normalizeRoleSlug` (the location-only remapping for multi-word
|
|
87
|
+
* presets). OMITTED (undefined, never null) for 2-part tokens.
|
|
88
|
+
*/
|
|
89
|
+
readonly role?: string
|
|
90
|
+
/**
|
|
91
|
+
* Additive `~lock` / `~nolock` sentinel. Tri-state: `true` (force ON) |
|
|
92
|
+
* `false` (force OFF, suppressing a ref-level `identityLock.enabled`) |
|
|
93
|
+
* ABSENT/undefined (inherit the ref default). Honored only by the hybrid
|
|
94
|
+
* resolver — there is no legacy entity resolver to make it inert on.
|
|
95
|
+
*/
|
|
96
|
+
readonly lock?: boolean
|
|
97
|
+
/** Byte offset into the source prompt — used to splice the token out. */
|
|
98
|
+
readonly offset: number
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Parse a single `@<name-slug>:<index>[:<role>]` token. Returns null when the
|
|
103
|
+
* token doesn't match a supported shape (the caller falls back to literal text).
|
|
104
|
+
*
|
|
105
|
+
* Segment count is 2 or 3 — NOT 2–4 like the character/location parsers. A wired
|
|
106
|
+
* creature or object has no variant/bucket slot, so there is nothing for a 4th
|
|
107
|
+
* segment to mean, and claiming one would let this parser swallow a character
|
|
108
|
+
* token.
|
|
109
|
+
*/
|
|
110
|
+
export function parseEntityMentionToken(text: string): {
|
|
111
|
+
entitySlug: string
|
|
112
|
+
entityIndex: number
|
|
113
|
+
/** Present ONLY for a 3-part token; omitted otherwise (shape rule). */
|
|
114
|
+
role?: string
|
|
115
|
+
/** Present ONLY when a sentinel was found; omitted otherwise (shape rule). */
|
|
116
|
+
lock?: boolean
|
|
117
|
+
} | null {
|
|
118
|
+
const parsed = parseMentionToken(text)
|
|
119
|
+
if (!parsed) return null
|
|
120
|
+
return {
|
|
121
|
+
entitySlug: parsed.slug,
|
|
122
|
+
entityIndex: parsed.index,
|
|
123
|
+
...(parsed.role !== undefined ? { role: parsed.role } : {}),
|
|
124
|
+
...(parsed.lock !== undefined ? { lock: parsed.lock } : {}),
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Find every entity `@-mention` in a prompt whose slug is a known entity slug.
|
|
130
|
+
*
|
|
131
|
+
* `knownEntitySlugs` (from `knownEntitySlugsFromRefs`) is what keeps this parser
|
|
132
|
+
* off the other grammars' tokens — every finder matches the same `@slug:N…`
|
|
133
|
+
* surface and only the known-slug set separates them. Cross-kind precedence is
|
|
134
|
+
* additionally enforced by pass ORDER at the resolver (see the module header).
|
|
135
|
+
*/
|
|
136
|
+
export function findEntityMentionTokens(
|
|
137
|
+
prompt: string,
|
|
138
|
+
knownEntitySlugs: readonly string[],
|
|
139
|
+
): EntityMentionTokenInfo[] {
|
|
140
|
+
return findMentionTokens(prompt, knownEntitySlugs).map((t) => ({
|
|
141
|
+
token: t.token,
|
|
142
|
+
entitySlug: t.slug,
|
|
143
|
+
entityIndex: t.index,
|
|
144
|
+
...(t.role !== undefined ? { role: t.role } : {}),
|
|
145
|
+
...(t.lock !== undefined ? { lock: t.lock } : {}),
|
|
146
|
+
offset: t.offset,
|
|
147
|
+
}))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The mention slug a single reference contributes, or `null` when the ref cannot
|
|
152
|
+
* carry an entity mention at all — the SINGLE gate, so every view of "which refs
|
|
153
|
+
* are entity-mentionable" is the same view.
|
|
154
|
+
*
|
|
155
|
+
* Shared by `knownEntitySlugsFromRefs` (the finder's known-slug set) and the
|
|
156
|
+
* prompt-builder's hybrid resolver (its slug → ref lookup map). Those two must
|
|
157
|
+
* admit exactly the same refs: a slug the finder accepts but the resolver drops
|
|
158
|
+
* would splice a token with nothing to bind, and a ref the resolver keys under a
|
|
159
|
+
* slug no token can match is dead weight. Emptiness is NOT the gate (see
|
|
160
|
+
* `MENTION_SLUG_PATTERN`) — a creature named "3-Eyed Raven" slugs to the truthy
|
|
161
|
+
* but unparseable `"3-eyed-raven"`.
|
|
162
|
+
*
|
|
163
|
+
* `isExtraRef` refs are EXCLUDED, mirroring `imageMentionSlugForRef`: an extra
|
|
164
|
+
* renders through the extras path with its own body line, so letting a mention
|
|
165
|
+
* also bind one would double-emit prose.
|
|
166
|
+
*/
|
|
167
|
+
export function entityMentionSlugForRef(r: ConnectedReference): string | null {
|
|
168
|
+
if (r.source !== "wired-creature" && r.source !== "wired-object") return null
|
|
169
|
+
if (r.isExtraRef === true) return null
|
|
170
|
+
if (!r.url || !r.defaultName) return null
|
|
171
|
+
const slug = entityMentionSlug(r.defaultName)
|
|
172
|
+
return MENTION_SLUG_PATTERN.test(slug) ? slug : null
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The known-entity-slug set for a reference list — the SINGLE source of truth for
|
|
177
|
+
* the derivation, shared by `buildImagePrompt`'s Phase 0 and the backend
|
|
178
|
+
* orchestrator's structured-branch gate so the two can never disagree about
|
|
179
|
+
* whether a prompt carries a resolvable entity mention.
|
|
180
|
+
*
|
|
181
|
+
* UNFILTERED, exactly like `knownImageSlugsFromRefs` and the character/location
|
|
182
|
+
* slug sets: every mentionable creature/object contributes its slug regardless of
|
|
183
|
+
* what any OTHER kind may also claim. Cross-kind precedence is a property of the
|
|
184
|
+
* resolver's pass order (character → location → image → creature → object), NOT
|
|
185
|
+
* of this set — subtracting the earlier kinds' slugs here would put the
|
|
186
|
+
* precedence rule in two places and let them drift.
|
|
187
|
+
*
|
|
188
|
+
* Creature and object share ONE set (and one resolver pass): the grammar, the
|
|
189
|
+
* gate and the rendering are identical, and `defaultRoleForSource(r.source)`
|
|
190
|
+
* already tells the two apart at phrase time. Their relative precedence is
|
|
191
|
+
* settled where a tie can actually occur — the resolver's slug → ref map, built
|
|
192
|
+
* creature-first.
|
|
193
|
+
*/
|
|
194
|
+
export function knownEntitySlugsFromRefs(
|
|
195
|
+
refs: readonly ConnectedReference[],
|
|
196
|
+
): string[] {
|
|
197
|
+
const out = new Set<string>()
|
|
198
|
+
for (const r of refs) {
|
|
199
|
+
const slug = entityMentionSlugForRef(r)
|
|
200
|
+
if (slug) out.add(slug)
|
|
201
|
+
}
|
|
202
|
+
return [...out]
|
|
203
|
+
}
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
* The media analog of `character-mention-slug.ts` / `location-mention-slug.ts`,
|
|
5
5
|
* for a wired image (`wired-image` / `manual` reference) addressed by the slug of
|
|
6
6
|
* its NAME: an upload node's label on the canvas, or the name a thin client puts
|
|
7
|
-
* on the reference. The grammar is the
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* on the reference. The grammar is the SHORT one — 2 or 3 segments, no buckets, no
|
|
8
|
+
* variants, no usage-mode enum — because a media reference has no variant array to
|
|
9
|
+
* select from and no identity mode to override. Every valid 3rd segment is a ROLE.
|
|
10
|
+
* It is not unique to media: `entity-mention-slug.ts` (creatures/objects) speaks
|
|
11
|
+
* the identical grammar off the shared core noted below.
|
|
11
12
|
*
|
|
12
13
|
* @town:3 — bare mention; renders the reference's binding
|
|
13
14
|
* ("reference image C") at the typed position
|
|
@@ -16,6 +17,13 @@
|
|
|
16
17
|
* @town:3~lock — additive identity-lock sentinel (also `~nolock`)
|
|
17
18
|
* @town:1:a:b — NULL. A 4-part token is never an image mention.
|
|
18
19
|
*
|
|
20
|
+
* GRAMMAR CORE. The slug shape, the parser and the finder (including BOTH
|
|
21
|
+
* collision guards) live in `mention-token-grammar.ts` and are shared verbatim
|
|
22
|
+
* with `entity-mention-slug.ts` (creatures/objects), which speaks the identical
|
|
23
|
+
* 2-or-3-segment grammar. This module is the MEDIA view of that core: the
|
|
24
|
+
* per-kind field names, and the part that genuinely differs — WHICH refs
|
|
25
|
+
* contribute a slug.
|
|
26
|
+
*
|
|
19
27
|
* NO WIRE FIELD. Unlike `characterSlug` / `locationSlug`, there is no `imageSlug`
|
|
20
28
|
* on `ConnectedReference`: the slug is DERIVED from `defaultName` at resolve time
|
|
21
29
|
* (`knownImageSlugsFromRefs`), so a client cannot drift from the grammar and the
|
|
@@ -27,30 +35,20 @@
|
|
|
27
35
|
*/
|
|
28
36
|
|
|
29
37
|
import type { ConnectedReference } from "./types.js"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
* non-empty yet UNPARSEABLE (a leading digit), so a ref named "3D Render" must be
|
|
37
|
-
* dropped from the known-slug set even though its slug is truthy. This pattern is
|
|
38
|
-
* what drops it.
|
|
39
|
-
*/
|
|
40
|
-
const IMAGE_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/
|
|
38
|
+
import {
|
|
39
|
+
MENTION_SLUG_PATTERN,
|
|
40
|
+
findMentionTokens,
|
|
41
|
+
mentionNameSlug,
|
|
42
|
+
parseMentionToken,
|
|
43
|
+
} from "./mention-token-grammar.js"
|
|
41
44
|
|
|
42
45
|
/**
|
|
43
46
|
* Slugify an image reference's display name for `@`-mention tokens. Byte-
|
|
44
47
|
* identical algorithm to `characterMentionSlug` / `locationMentionSlug`; kept as
|
|
45
|
-
* a separate export to make the call site's intent explicit
|
|
46
|
-
* divergence.
|
|
48
|
+
* a separate export to make the call site's intent explicit.
|
|
47
49
|
*/
|
|
48
50
|
export function imageMentionSlug(name: string): string {
|
|
49
|
-
return name
|
|
50
|
-
.toLowerCase()
|
|
51
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
52
|
-
.replace(/-+/g, "-")
|
|
53
|
-
.replace(/^-|-$/g, "")
|
|
51
|
+
return mentionNameSlug(name)
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
export interface ImageMentionTokenInfo {
|
|
@@ -91,6 +89,11 @@ export interface ImageMentionTokenInfo {
|
|
|
91
89
|
* Segment count is 2 or 3 — NOT 2–4 like the character/location parsers. A media
|
|
92
90
|
* reference has no variant/bucket slot, so there is nothing for a 4th segment to
|
|
93
91
|
* mean, and claiming one would let this parser swallow a character token.
|
|
92
|
+
*
|
|
93
|
+
* Delegates to the shared `parseMentionToken` and renames its kind-neutral
|
|
94
|
+
* `slug` / `index` to this module's `imageSlug` / `imageIndex`. The optional
|
|
95
|
+
* `role` / `lock` keys are re-emitted CONDITIONALLY so the documented shape rule
|
|
96
|
+
* survives the rename: a 2-part token has no `role` key at all.
|
|
94
97
|
*/
|
|
95
98
|
export function parseImageMentionToken(text: string): {
|
|
96
99
|
imageSlug: string
|
|
@@ -100,87 +103,35 @@ export function parseImageMentionToken(text: string): {
|
|
|
100
103
|
/** Present ONLY when a sentinel was found; omitted otherwise (shape rule). */
|
|
101
104
|
lock?: boolean
|
|
102
105
|
} | null {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
// gains NO `lock` key.
|
|
111
|
-
let lockField: { lock?: boolean } = {}
|
|
112
|
-
if (rest.endsWith("~nolock")) {
|
|
113
|
-
rest = rest.slice(0, -"~nolock".length)
|
|
114
|
-
lockField = { lock: false }
|
|
115
|
-
} else if (rest.endsWith("~lock")) {
|
|
116
|
-
rest = rest.slice(0, -"~lock".length)
|
|
117
|
-
lockField = { lock: true }
|
|
106
|
+
const parsed = parseMentionToken(text)
|
|
107
|
+
if (!parsed) return null
|
|
108
|
+
return {
|
|
109
|
+
imageSlug: parsed.slug,
|
|
110
|
+
imageIndex: parsed.index,
|
|
111
|
+
...(parsed.role !== undefined ? { role: parsed.role } : {}),
|
|
112
|
+
...(parsed.lock !== undefined ? { lock: parsed.lock } : {}),
|
|
118
113
|
}
|
|
119
|
-
|
|
120
|
-
const parts = rest.split(":")
|
|
121
|
-
if (parts.length < 2 || parts.length > 3) return null
|
|
122
|
-
|
|
123
|
-
const [imageSlug, indexStr, third] = parts
|
|
124
|
-
if (!IMAGE_SLUG_PATTERN.test(imageSlug)) return null
|
|
125
|
-
if (!/^\d+$/.test(indexStr)) return null
|
|
126
|
-
const imageIndex = parseInt(indexStr, 10)
|
|
127
|
-
if (!Number.isInteger(imageIndex) || imageIndex < 1) return null
|
|
128
|
-
|
|
129
|
-
if (parts.length === 2) return { imageSlug, imageIndex, ...lockField }
|
|
130
|
-
if (!IMAGE_SLUG_PATTERN.test(third)) return null
|
|
131
|
-
return { imageSlug, imageIndex, role: third, ...lockField }
|
|
132
114
|
}
|
|
133
115
|
|
|
134
116
|
/**
|
|
135
117
|
* Find every image `@-mention` in a prompt whose slug is a known image slug.
|
|
136
118
|
*
|
|
137
119
|
* `knownImageSlugs` (from `knownImageSlugsFromRefs`) is what keeps this parser
|
|
138
|
-
* off the other
|
|
139
|
-
*
|
|
120
|
+
* off the other grammars' tokens — every finder matches the same `@slug:N…`
|
|
121
|
+
* surface and only the known-slug set separates them.
|
|
140
122
|
*/
|
|
141
123
|
export function findImageMentionTokens(
|
|
142
124
|
prompt: string,
|
|
143
125
|
knownImageSlugs: readonly string[],
|
|
144
126
|
): ImageMentionTokenInfo[] {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// image mention. `~lock` still matches (`~` is outside the class), and its own
|
|
154
|
-
// `(?![a-z0-9-])` keeps `~locked` / `~nolockx` literal.
|
|
155
|
-
//
|
|
156
|
-
// Linear-scan shape (a fixed prefix then bounded optional groups, no nested
|
|
157
|
-
// quantifiers) — matching the sibling finders, and ReDoS-free.
|
|
158
|
-
const regex =
|
|
159
|
-
/(?:^|[^a-zA-Z0-9])(@[a-z][a-z0-9-]*:\d+(?::[a-z][a-z0-9-]*)?(?:~(?:no)?lock(?![a-z0-9-]))?)(?![:a-z0-9-])/g
|
|
160
|
-
const knownSet = new Set(knownImageSlugs)
|
|
161
|
-
for (const match of prompt.matchAll(regex)) {
|
|
162
|
-
const token = match[1]
|
|
163
|
-
const offset = (match.index ?? 0) + (match[0].length - token.length)
|
|
164
|
-
// SLASH GUARD — the second half of the collision guard, and the reason it
|
|
165
|
-
// is a post-match check instead of another lookahead in the regex. `/` is
|
|
166
|
-
// the LOCATION grammar's bucket/variant separator (`@lib:1:weather/rain`),
|
|
167
|
-
// so a token immediately followed by `/<segment>` is a sibling-grammar
|
|
168
|
-
// token, never an image mention. A lookahead cannot express this: the
|
|
169
|
-
// engine would just BACKTRACK to a shorter prefix (`@lib:1:weather` →
|
|
170
|
-
// `@lib:1`, or `@town:1~lock` → `@town:1`) and splice THAT, which is the
|
|
171
|
-
// very corruption being prevented. Rejecting the whole match here leaves
|
|
172
|
-
// the token literal, exactly as the character/location finders do.
|
|
173
|
-
//
|
|
174
|
-
// `/` alone is NOT the signal — `@town:1/@barn:2` (two mentions separated
|
|
175
|
-
// by a slash) must keep matching, and a location segment always starts
|
|
176
|
-
// `[a-z]`. So the guard is `/` + a segment start.
|
|
177
|
-
if (/^\/[a-z]/.test(prompt.slice(offset + token.length))) continue
|
|
178
|
-
const parsed = parseImageMentionToken(token)
|
|
179
|
-
if (parsed && knownSet.has(parsed.imageSlug)) {
|
|
180
|
-
tokens.push({ token, ...parsed, offset })
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return tokens
|
|
127
|
+
return findMentionTokens(prompt, knownImageSlugs).map((t) => ({
|
|
128
|
+
token: t.token,
|
|
129
|
+
imageSlug: t.slug,
|
|
130
|
+
imageIndex: t.index,
|
|
131
|
+
...(t.role !== undefined ? { role: t.role } : {}),
|
|
132
|
+
...(t.lock !== undefined ? { lock: t.lock } : {}),
|
|
133
|
+
offset: t.offset,
|
|
134
|
+
}))
|
|
184
135
|
}
|
|
185
136
|
|
|
186
137
|
/**
|
|
@@ -193,14 +144,14 @@ export function findImageMentionTokens(
|
|
|
193
144
|
* admit exactly the same refs: a slug the finder accepts but the resolver drops
|
|
194
145
|
* would splice a token with nothing to bind, and a ref the resolver keys under
|
|
195
146
|
* a slug no token can match is dead weight. Emptiness is NOT the gate (see
|
|
196
|
-
* `
|
|
147
|
+
* `MENTION_SLUG_PATTERN`).
|
|
197
148
|
*/
|
|
198
149
|
export function imageMentionSlugForRef(r: ConnectedReference): string | null {
|
|
199
150
|
if (r.source !== "wired-image" && r.source !== "manual") return null
|
|
200
151
|
if (r.isExtraRef === true) return null
|
|
201
152
|
if (!r.url || !r.defaultName) return null
|
|
202
153
|
const slug = imageMentionSlug(r.defaultName)
|
|
203
|
-
return
|
|
154
|
+
return MENTION_SLUG_PATTERN.test(slug) ? slug : null
|
|
204
155
|
}
|
|
205
156
|
|
|
206
157
|
/**
|
|
@@ -210,13 +161,13 @@ export function imageMentionSlugForRef(r: ConnectedReference): string | null {
|
|
|
210
161
|
* whether a prompt carries a resolvable image mention.
|
|
211
162
|
*
|
|
212
163
|
* Only MEDIA refs (`wired-image` / `manual`) with a URL participate — the other
|
|
213
|
-
* sources have their own mention grammars
|
|
214
|
-
*
|
|
164
|
+
* sources have their own mention grammars: characters, locations, and — since the
|
|
165
|
+
* creature/object leg — wired entities via `knownEntitySlugsFromRefs`.
|
|
215
166
|
*
|
|
216
167
|
* `isExtraRef` refs are EXCLUDED: an extra renders through the extras path with
|
|
217
168
|
* its own body line, so letting a mention also bind one would double-emit prose.
|
|
218
169
|
*
|
|
219
|
-
* Grammar-invalid slugs are DROPPED (see `
|
|
170
|
+
* Grammar-invalid slugs are DROPPED (see `MENTION_SLUG_PATTERN`) — a ref named
|
|
220
171
|
* "3D Render" slugs to the non-empty but unparseable `"3d-render"`, and admitting
|
|
221
172
|
* it would put a slug in the set that no token can ever match.
|
|
222
173
|
*
|
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,
|
|
@@ -637,6 +638,8 @@ export {
|
|
|
637
638
|
ANIMAL_SUBCATEGORY_ORDER,
|
|
638
639
|
getAnimal,
|
|
639
640
|
getAnimalLabel,
|
|
641
|
+
getAnimalPromptHint,
|
|
642
|
+
getAnimalTerm,
|
|
640
643
|
} from "./animals.js"
|
|
641
644
|
export type { Animal, AnimalSubcategory } from "./animals.js"
|
|
642
645
|
|
|
@@ -768,6 +771,8 @@ export type {
|
|
|
768
771
|
WorkflowMediaRef,
|
|
769
772
|
WorkflowPortability,
|
|
770
773
|
WorkflowImportReport,
|
|
774
|
+
WorkflowImportSkippedAsset,
|
|
775
|
+
WorkflowAssetKind,
|
|
771
776
|
} from "./workflow-export.js"
|
|
772
777
|
export { stripExportContent } from "./workflow-export.js"
|
|
773
778
|
|
|
@@ -818,6 +823,20 @@ export {
|
|
|
818
823
|
} from "./image-mention-slug.js"
|
|
819
824
|
export type { ImageMentionTokenInfo } from "./image-mention-slug.js"
|
|
820
825
|
|
|
826
|
+
// Wired-creature / wired-object mentions — the SAME 2-or-3-segment grammar as
|
|
827
|
+
// the named-image mention (both are views of `mention-token-grammar.ts`), with
|
|
828
|
+
// their own ref gate. The grammar core itself stays internal to the package: it
|
|
829
|
+
// has no standalone consumer, and a third public `@slug:N` surface would invite
|
|
830
|
+
// call sites that bypass a kind's gate.
|
|
831
|
+
export {
|
|
832
|
+
entityMentionSlug,
|
|
833
|
+
parseEntityMentionToken,
|
|
834
|
+
findEntityMentionTokens,
|
|
835
|
+
knownEntitySlugsFromRefs,
|
|
836
|
+
entityMentionSlugForRef,
|
|
837
|
+
} from "./entity-mention-slug.js"
|
|
838
|
+
export type { EntityMentionTokenInfo } from "./entity-mention-slug.js"
|
|
839
|
+
|
|
821
840
|
export {
|
|
822
841
|
toConnectedReference,
|
|
823
842
|
toConnectedReferences,
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The SHORT `@-mention` grammar core — `@<name-slug>:<index>[:<role>][~lock|~nolock]`.
|
|
3
|
+
*
|
|
4
|
+
* ONE parser, ONE finder, ONE pair of collision guards, shared by every mention
|
|
5
|
+
* kind whose token has NO variant/bucket slot:
|
|
6
|
+
*
|
|
7
|
+
* - `image-mention-slug.ts` — wired media (`wired-image` / `manual`)
|
|
8
|
+
* - `entity-mention-slug.ts` — wired entities (`wired-creature` / `wired-object`)
|
|
9
|
+
*
|
|
10
|
+
* WHY EXTRACTED (and why the 5-line slugify precedent does NOT apply here). The
|
|
11
|
+
* character/location/image modules each keep their own copy of the trivial
|
|
12
|
+
* `characterMentionSlug` algorithm — duplication that is cheap because the
|
|
13
|
+
* function is five obvious lines. What is shared HERE is the opposite: the
|
|
14
|
+
* two-part collision guard (`(?![:a-z0-9-])`, which stops a 4-part CHARACTER
|
|
15
|
+
* token being claimed as a 3-part one, and the post-match slash guard, which
|
|
16
|
+
* stops a LOCATION bucket token being spliced as its own truncated prefix).
|
|
17
|
+
* Those guards exist precisely to prevent prompt corruption, and a second
|
|
18
|
+
* hand-copied edition of them is a drift surface with a corruption payload. So
|
|
19
|
+
* the media and entity grammars converge on this module and the per-kind files
|
|
20
|
+
* keep only what genuinely differs: WHICH refs contribute a slug.
|
|
21
|
+
*
|
|
22
|
+
* The character and location grammars do NOT use this core — their tokens carry
|
|
23
|
+
* 2–4 segments with a variant/bucket/usage-mode slot, a materially different
|
|
24
|
+
* shape, and their finders deliberately have NO trailing-reject lookahead.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Grammar-valid slug shape — the exact shape `findMentionTokens`' regex can
|
|
29
|
+
* produce, and therefore the gate on BOTH sides of the match.
|
|
30
|
+
*
|
|
31
|
+
* Emptiness is NOT the gate: `mentionNameSlug("3D Render")` → `"3d-render"` is
|
|
32
|
+
* non-empty yet UNPARSEABLE (a leading digit), so a ref named "3D Render" must
|
|
33
|
+
* be dropped from a known-slug set even though its slug is truthy. This pattern
|
|
34
|
+
* is what drops it.
|
|
35
|
+
*/
|
|
36
|
+
export const MENTION_SLUG_PATTERN = /^[a-z][a-z0-9-]*$/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Slugify a reference's display name for `@`-mention tokens. Byte-identical
|
|
40
|
+
* algorithm to `characterMentionSlug` / `locationMentionSlug`; the per-kind
|
|
41
|
+
* modules re-export it under their own name so each call site's intent stays
|
|
42
|
+
* explicit.
|
|
43
|
+
*/
|
|
44
|
+
export function mentionNameSlug(name: string): string {
|
|
45
|
+
return name
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
48
|
+
.replace(/-+/g, "-")
|
|
49
|
+
.replace(/^-|-$/g, "")
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Kind-neutral parse result. The per-kind modules rename `slug` / `index`. */
|
|
53
|
+
export interface ParsedMentionToken {
|
|
54
|
+
readonly slug: string
|
|
55
|
+
readonly index: number
|
|
56
|
+
/** Present ONLY for a 3-part token; omitted otherwise (shape rule). */
|
|
57
|
+
readonly role?: string
|
|
58
|
+
/** Present ONLY when a sentinel was found; omitted otherwise (shape rule). */
|
|
59
|
+
readonly lock?: boolean
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Kind-neutral finder result — a `ParsedMentionToken` plus its splice site. */
|
|
63
|
+
export interface FoundMentionToken extends ParsedMentionToken {
|
|
64
|
+
/** The matched token text, verbatim — spliced out of the prompt at resolve time. */
|
|
65
|
+
readonly token: string
|
|
66
|
+
/** Byte offset into the source prompt — used to splice the token out. */
|
|
67
|
+
readonly offset: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Parse a single `@<name-slug>:<index>[:<role>]` token. Returns null when the
|
|
72
|
+
* token doesn't match a supported shape (the caller falls back to literal text).
|
|
73
|
+
*
|
|
74
|
+
* Segment count is 2 or 3 — NOT 2–4 like the character/location parsers. Neither
|
|
75
|
+
* a media reference nor a wired entity has a variant/bucket slot, so there is
|
|
76
|
+
* nothing for a 4th segment to mean, and claiming one would let this parser
|
|
77
|
+
* swallow a character token.
|
|
78
|
+
*/
|
|
79
|
+
export function parseMentionToken(text: string): ParsedMentionToken | null {
|
|
80
|
+
if (!text.startsWith("@")) return null
|
|
81
|
+
let rest = text.slice(1)
|
|
82
|
+
if (rest.length === 0 || !/^[a-z]/.test(rest)) return null
|
|
83
|
+
|
|
84
|
+
// Strip a trailing `~nolock` (force OFF) or `~lock` (force ON) BEFORE splitting
|
|
85
|
+
// so the segment grammar is untouched (a `~` never appears inside a segment).
|
|
86
|
+
// Check `~nolock` FIRST — `~lock` is its suffix. A token with NEITHER sentinel
|
|
87
|
+
// gains NO `lock` key.
|
|
88
|
+
let lockField: { lock?: boolean } = {}
|
|
89
|
+
if (rest.endsWith("~nolock")) {
|
|
90
|
+
rest = rest.slice(0, -"~nolock".length)
|
|
91
|
+
lockField = { lock: false }
|
|
92
|
+
} else if (rest.endsWith("~lock")) {
|
|
93
|
+
rest = rest.slice(0, -"~lock".length)
|
|
94
|
+
lockField = { lock: true }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parts = rest.split(":")
|
|
98
|
+
if (parts.length < 2 || parts.length > 3) return null
|
|
99
|
+
|
|
100
|
+
const [slug, indexStr, third] = parts
|
|
101
|
+
if (!MENTION_SLUG_PATTERN.test(slug)) return null
|
|
102
|
+
if (!/^\d+$/.test(indexStr)) return null
|
|
103
|
+
const index = parseInt(indexStr, 10)
|
|
104
|
+
if (!Number.isInteger(index) || index < 1) return null
|
|
105
|
+
|
|
106
|
+
if (parts.length === 2) return { slug, index, ...lockField }
|
|
107
|
+
if (!MENTION_SLUG_PATTERN.test(third)) return null
|
|
108
|
+
return { slug, index, role: third, ...lockField }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ONE optional segment (the role) — media refs and wired entities have no
|
|
112
|
+
// variant/bucket slot.
|
|
113
|
+
//
|
|
114
|
+
// The trailing `(?![:a-z0-9-])` is the DELIBERATE divergence from the character
|
|
115
|
+
// and location finders. Without it, a 4-part CHARACTER token that the character
|
|
116
|
+
// pass failed to resolve (`@kira:1:smile:face`) would be captured here as the
|
|
117
|
+
// 3-part `@kira:1:smile`, leaving `:face` dangling in the prompt. The lookahead
|
|
118
|
+
// makes the regex backtrack and match nothing, so a 4-part token is NEVER a
|
|
119
|
+
// short-grammar mention. `~lock` still matches (`~` is outside the class), and
|
|
120
|
+
// its own `(?![a-z0-9-])` keeps `~locked` / `~nolockx` literal.
|
|
121
|
+
//
|
|
122
|
+
// Linear-scan shape (a fixed prefix then bounded optional groups, no nested
|
|
123
|
+
// quantifiers) — matching the sibling finders, and ReDoS-free.
|
|
124
|
+
const MENTION_TOKEN_REGEX =
|
|
125
|
+
/(?:^|[^a-zA-Z0-9])(@[a-z][a-z0-9-]*:\d+(?::[a-z][a-z0-9-]*)?(?:~(?:no)?lock(?![a-z0-9-]))?)(?![:a-z0-9-])/g
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Find every short-grammar `@-mention` in a prompt whose slug is in
|
|
129
|
+
* `knownSlugs`.
|
|
130
|
+
*
|
|
131
|
+
* `knownSlugs` is what keeps one kind's parser off another kind's tokens — every
|
|
132
|
+
* finder matches the same `@slug:N…` surface and only the known-slug set
|
|
133
|
+
* separates them.
|
|
134
|
+
*/
|
|
135
|
+
export function findMentionTokens(
|
|
136
|
+
prompt: string,
|
|
137
|
+
knownSlugs: readonly string[],
|
|
138
|
+
): FoundMentionToken[] {
|
|
139
|
+
const tokens: FoundMentionToken[] = []
|
|
140
|
+
// A module-level `g` regex carries `lastIndex` state; `matchAll` requires the
|
|
141
|
+
// `g` flag but resets nothing, so re-create the scanner per call.
|
|
142
|
+
const regex = new RegExp(MENTION_TOKEN_REGEX.source, "g")
|
|
143
|
+
const knownSet = new Set(knownSlugs)
|
|
144
|
+
for (const match of prompt.matchAll(regex)) {
|
|
145
|
+
const token = match[1]
|
|
146
|
+
const offset = (match.index ?? 0) + (match[0].length - token.length)
|
|
147
|
+
// SLASH GUARD — the second half of the collision guard, and the reason it
|
|
148
|
+
// is a post-match check instead of another lookahead in the regex. `/` is
|
|
149
|
+
// the LOCATION grammar's bucket/variant separator (`@lib:1:weather/rain`),
|
|
150
|
+
// so a token immediately followed by `/<segment>` is a sibling-grammar
|
|
151
|
+
// token, never a short-grammar mention. A lookahead cannot express this:
|
|
152
|
+
// the engine would just BACKTRACK to a shorter prefix (`@lib:1:weather` →
|
|
153
|
+
// `@lib:1`, or `@town:1~lock` → `@town:1`) and splice THAT, which is the
|
|
154
|
+
// very corruption being prevented. Rejecting the whole match here leaves
|
|
155
|
+
// the token literal, exactly as the character/location finders do.
|
|
156
|
+
//
|
|
157
|
+
// `/` alone is NOT the signal — `@town:1/@barn:2` (two mentions separated
|
|
158
|
+
// by a slash) must keep matching, and a location segment always starts
|
|
159
|
+
// `[a-z]`. So the guard is `/` + a segment start.
|
|
160
|
+
if (/^\/[a-z]/.test(prompt.slice(offset + token.length))) continue
|
|
161
|
+
const parsed = parseMentionToken(token)
|
|
162
|
+
if (parsed && knownSet.has(parsed.slug)) {
|
|
163
|
+
tokens.push({ token, ...parsed, offset })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return tokens
|
|
167
|
+
}
|
package/src/model-catalog.ts
CHANGED
|
@@ -2223,9 +2223,9 @@ const AUDIO_MODELS: Record<string, ModelCatalogEntry> = {
|
|
|
2223
2223
|
family: "ElevenLabs",
|
|
2224
2224
|
label: "ElevenLabs Dubbing",
|
|
2225
2225
|
series: "ElevenLabs",
|
|
2226
|
-
description: "Translate + dub a video into a new language. Async.",
|
|
2226
|
+
description: "Translate + dub audio or a whole video into a new language — video in, dubbed video out. Async.",
|
|
2227
2227
|
useCases: ["dubbing", "multilingual"],
|
|
2228
|
-
pricing: [{ identifier: "elevenlabs-dubbing", credits:
|
|
2228
|
+
pricing: [{ identifier: "elevenlabs-dubbing", credits: 40, note: "per minute of the dubbed span (min 1)" }],
|
|
2229
2229
|
},
|
|
2230
2230
|
"elevenlabs-forced-alignment": {
|
|
2231
2231
|
id: "elevenlabs-forced-alignment",
|