@nodaro/prompts 1.15.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/dist/index.cjs +243 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +149 -3
- package/dist/index.d.ts +149 -3
- package/dist/index.js +239 -70
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/assemble-image-input.test.ts +30 -0
- package/src/__tests__/described-references-video.test.ts +250 -0
- package/src/__tests__/described-references.test.ts +595 -0
- package/src/__tests__/doctrine-roster-completeness.test.ts +1 -0
- package/src/__tests__/fixtures/parameter-hint-golden.json +313 -35
- package/src/__tests__/minor-age-floor.test.ts +4 -4
- package/src/__tests__/person-body-axes.test.ts +2 -2
- package/src/__tests__/person-exposure-hints.test.ts +59 -4
- package/src/__tests__/retired-hint-strings.test.ts +68 -0
- package/src/__tests__/subject-registry.test.ts +1 -1
- package/src/__tests__/w1b-rephrase.test.ts +119 -0
- package/src/age-floor.ts +65 -2
- package/src/assemble-image-input.ts +11 -1
- package/src/described-references.ts +138 -0
- package/src/index.ts +1 -0
- package/src/person.ts +18 -17
- package/src/pose.ts +1 -1
- package/src/prompt-builder.ts +196 -19
- package/src/styling.ts +38 -2
- package/src/video-reference-resolver.ts +85 -10
package/src/person.ts
CHANGED
|
@@ -437,10 +437,10 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
437
437
|
{ id: "bust-small", label: "Small", dimension: "bust", description: "Small bust", promptHint: "small bust", term: "small bust" , adultOnly: true },
|
|
438
438
|
{ id: "bust-average", label: "Average", dimension: "bust", description: "Average bust", promptHint: "" },
|
|
439
439
|
{ id: "bust-full", label: "Full", dimension: "bust", description: "Full bust", promptHint: "full bust", term: "full bust" , adultOnly: true },
|
|
440
|
-
{ id: "bust-very-full", label: "Very Full", dimension: "bust", description: "Very full bust", promptHint: "
|
|
440
|
+
{ id: "bust-very-full", label: "Very Full", dimension: "bust", description: "Very full bust", promptHint: "a fuller bust", term: "fuller bust" , adultOnly: true },
|
|
441
441
|
|
|
442
442
|
// ----- Waist -----
|
|
443
|
-
{ id: "waist-defined", label: "Defined", dimension: "waist", description: "Defined, cinched waist", promptHint: "defined
|
|
443
|
+
{ id: "waist-defined", label: "Defined", dimension: "waist", description: "Defined, cinched waist", promptHint: "a defined waistline", term: "defined waistline" , adultOnly: true },
|
|
444
444
|
{ id: "waist-average", label: "Average", dimension: "waist", description: "Average waist", promptHint: "" },
|
|
445
445
|
{ id: "waist-straight", label: "Straight", dimension: "waist", description: "Straight, undefined waist", promptHint: "straight waistline", term: "straight waistline" , adultOnly: true },
|
|
446
446
|
|
|
@@ -450,7 +450,7 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
450
450
|
{ id: "hips-wide", label: "Wide", dimension: "hips", description: "Wide hips", promptHint: "wide hips", term: "wide hips" , adultOnly: true },
|
|
451
451
|
|
|
452
452
|
// ----- Silhouette (overall body shape; optional, no neutral) -----
|
|
453
|
-
{ id: "silhouette-hourglass", label: "Hourglass", dimension: "silhouette", description: "Balanced bust + hips, defined waist", promptHint: "hourglass
|
|
453
|
+
{ id: "silhouette-hourglass", label: "Hourglass", dimension: "silhouette", description: "Balanced bust + hips, defined waist", promptHint: "an hourglass figure", term: "hourglass figure" , adultOnly: true },
|
|
454
454
|
{ id: "silhouette-rectangular", label: "Rectangular", dimension: "silhouette", description: "Straight up-and-down silhouette", promptHint: "rectangular silhouette", term: "rectangular silhouette" },
|
|
455
455
|
{ id: "silhouette-pear", label: "Pear", dimension: "silhouette", description: "Hips wider than shoulders", promptHint: "pear-shaped silhouette", term: "pear-shaped silhouette" , adultOnly: true },
|
|
456
456
|
{ id: "silhouette-inverted", label: "Inverted Triangle", dimension: "silhouette", description: "Broad shoulders, narrow hips", promptHint: "inverted-triangle silhouette", term: "inverted-triangle silhouette" },
|
|
@@ -553,11 +553,11 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
553
553
|
|
|
554
554
|
// -------------------- Lip State (what the lips are doing / wearing) --------------------
|
|
555
555
|
{ id: "lip-state-chapped", label: "Chapped", dimension: "lip-state", description: "Cracked, dry, weather-worn lips", promptHint: "with chapped, cracked, weather-worn dry lips", term: "chapped dry lips" },
|
|
556
|
-
{ id: "lip-state-glossy", label: "Glossy", dimension: "lip-state", description: "High-shine, wet-look lips", promptHint: "with
|
|
556
|
+
{ id: "lip-state-glossy", label: "Glossy", dimension: "lip-state", description: "High-shine, wet-look lips", promptHint: "with a glossy lip finish", term: "glossy lips" , adultOnly: true },
|
|
557
557
|
{ id: "lip-state-bare", label: "Bare", dimension: "lip-state", description: "Natural, untreated, no makeup", promptHint: "with bare, natural, untreated lips", term: "bare natural lips" },
|
|
558
558
|
{ id: "lip-state-bold-red", label: "Bold Red", dimension: "lip-state", description: "Saturated red lipstick statement", promptHint: "with a bold, saturated red lipstick statement", term: "bold red lipstick" },
|
|
559
|
-
{ id: "lip-state-bitten", label: "Bitten", dimension: "lip-state", description: "Slight playful lip-bite, lower lip caught", promptHint: "
|
|
560
|
-
{ id: "lip-state-parted", label: "Parted", dimension: "lip-state", description: "Lips slightly parted, breath of air", promptHint: "with lips slightly
|
|
559
|
+
{ id: "lip-state-bitten", label: "Bitten", dimension: "lip-state", description: "Slight playful lip-bite, lower lip caught", promptHint: "lightly biting the lower lip", term: "biting lower lip" , adultOnly: true },
|
|
560
|
+
{ id: "lip-state-parted", label: "Parted", dimension: "lip-state", description: "Lips slightly parted, breath of air", promptHint: "with lips relaxed and slightly open, as if mid-sentence", term: "lips slightly open" , adultOnly: true },
|
|
561
561
|
{ id: "lip-state-pursed", label: "Pursed", dimension: "lip-state", description: "Lips pressed and pushed forward", promptHint: "with lips pursed, pressed and pushed forward", term: "pursed lips" },
|
|
562
562
|
{ id: "lip-state-bold-black", label: "Bold Black", dimension: "lip-state", description: "Saturated black lipstick statement (goth / avant-garde)", promptHint: "with a bold, saturated black lipstick statement, goth / avant-garde", term: "bold black lipstick" },
|
|
563
563
|
{ id: "lip-state-burgundy", label: "Burgundy", dimension: "lip-state", description: "Deep wine-red lipstick", promptHint: "with deep wine-red burgundy lipstick", term: "burgundy lipstick" },
|
|
@@ -676,9 +676,9 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
676
676
|
|
|
677
677
|
// -------------------- Eye State (what the eyes are doing / where they look) --------------------
|
|
678
678
|
{ id: "eye-state-closed", label: "Closed", dimension: "eye-state", description: "Eyes fully closed, peaceful", promptHint: "with eyes fully closed in a peaceful expression", term: "eyes fully closed" },
|
|
679
|
-
{ id: "eye-state-half-lidded", label: "Half-lidded", dimension: "eye-state", description: "Heavy-lidded sleepy gaze", promptHint: "with
|
|
679
|
+
{ id: "eye-state-half-lidded", label: "Half-lidded", dimension: "eye-state", description: "Heavy-lidded sleepy gaze", promptHint: "with drowsy, partly closed eyes, the lids sitting low over the iris", term: "drowsy, partly closed eyes" , adultOnly: true },
|
|
680
680
|
{ id: "eye-state-wide-eyed", label: "Wide-eyed", dimension: "eye-state", description: "Eyes wide open, alert / surprised", promptHint: "with eyes wide open, alert and surprised" },
|
|
681
|
-
{ id: "eye-state-staring-camera", label: "Staring at Camera", dimension: "eye-state", description: "Direct unbroken eye contact with the lens", promptHint: "
|
|
681
|
+
{ id: "eye-state-staring-camera", label: "Staring at Camera", dimension: "eye-state", description: "Direct unbroken eye contact with the lens", promptHint: "looking directly into the camera", term: "direct gaze to camera" },
|
|
682
682
|
{ id: "eye-state-gazing-away", label: "Gazing Away", dimension: "eye-state", description: "Looking off-camera, contemplative", promptHint: "gazing off-camera with a contemplative expression", term: "gazing off-camera" },
|
|
683
683
|
{ id: "eye-state-gazing-up", label: "Gazing Up", dimension: "eye-state", description: "Eyes raised toward something above", promptHint: "with eyes gazing upward toward something above" },
|
|
684
684
|
{ id: "eye-state-gazing-down", label: "Gazing Down", dimension: "eye-state", description: "Eyes downcast", promptHint: "with eyes downcast, gazing softly downward" },
|
|
@@ -697,8 +697,8 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
697
697
|
{ id: "texture-smooth", label: "Smooth", dimension: "skin-texture", description: "Flawless, silky smooth skin", promptHint: "with flawless, silky smooth skin", term: "flawless smooth skin" },
|
|
698
698
|
{ id: "texture-wrinkled", label: "Wrinkled", dimension: "skin-texture", description: "Aged, deeply lined skin", promptHint: "with deep wrinkles and aged skin texture", term: "deeply wrinkled aged skin" },
|
|
699
699
|
{ id: "texture-goosebumps", label: "Goosebumps", dimension: "skin-texture", description: "Raised goosebumps on skin", promptHint: "with goosebumps raised on the skin" },
|
|
700
|
-
{ id: "texture-dewy", label: "Dewy", dimension: "skin-texture", description: "Glowing, dewy fresh skin", promptHint: "with dewy,
|
|
701
|
-
{ id: "texture-glistening", label: "Glistening", dimension: "skin-texture", description: "Sweat or oil sheen", promptHint: "with
|
|
700
|
+
{ id: "texture-dewy", label: "Dewy", dimension: "skin-texture", description: "Glowing, dewy fresh skin", promptHint: "with dewy, luminous skin", term: "dewy skin" },
|
|
701
|
+
{ id: "texture-glistening", label: "Glistening", dimension: "skin-texture", description: "Sweat or oil sheen", promptHint: "with a light glistening sheen on the skin", term: "glistening sheen" , adultOnly: true },
|
|
702
702
|
{ id: "texture-weathered", label: "Weathered", dimension: "skin-texture", description: "Sun-aged rough skin", promptHint: "with weathered, sun-worn rough skin", term: "weathered sun-worn skin" },
|
|
703
703
|
{ id: "texture-porcelain", label: "Porcelain", dimension: "skin-texture", description: "Flawless near-translucent porcelain", promptHint: "with flawless, near-translucent porcelain skin", term: "porcelain skin" },
|
|
704
704
|
{ id: "texture-sun-kissed", label: "Sun-kissed", dimension: "skin-texture", description: "Warm tan with healthy glow", promptHint: "with sun-kissed skin, warmly tanned with a healthy glow", term: "sun-kissed skin" },
|
|
@@ -710,8 +710,8 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
710
710
|
{ id: "texture-oily", label: "Oily / Shiny", dimension: "skin-texture", description: "Slight oil sheen on T-zone", promptHint: "with a slight oily sheen across the T-zone", term: "oily shiny skin" , adultOnly: true },
|
|
711
711
|
{ id: "texture-matte", label: "Matte", dimension: "skin-texture", description: "Poreless matte finish", promptHint: "with a poreless, matte skin finish", term: "matte skin finish" },
|
|
712
712
|
{ id: "texture-blemished", label: "Blemished", dimension: "skin-texture", description: "Visible blemishes, real-skin imperfections", promptHint: "with visible blemishes and natural real-skin imperfections", term: "blemished skin" },
|
|
713
|
-
{ id: "texture-baby-soft", label: "Baby-soft", dimension: "skin-texture", description: "Smooth, fine-pored youthful skin", promptHint: "with
|
|
714
|
-
{ id: "texture-shower-fresh-wet", label: "Shower-Fresh Wet", dimension: "skin-texture", description: "Just-out-of-shower wet skin with water beads", promptHint: "with
|
|
713
|
+
{ id: "texture-baby-soft", label: "Baby-soft", dimension: "skin-texture", description: "Smooth, fine-pored youthful skin", promptHint: "with soft, fine-pored skin", term: "soft fine-pored skin" },
|
|
714
|
+
{ id: "texture-shower-fresh-wet", label: "Shower-Fresh Wet", dimension: "skin-texture", description: "Just-out-of-shower wet skin with water beads", promptHint: "with fresh, water-dappled skin as if just out of the shower", term: "water-dappled skin" , adultOnly: true },
|
|
715
715
|
{ id: "texture-acne-scarred", label: "Acne-scarred", dimension: "skin-texture", description: "Visible acne scarring (distinct from blemished — healed scar pattern)", promptHint: "with visible acne scarring, healed pitted-skin texture and uneven surface from past breakouts", term: "acne-scarred skin" },
|
|
716
716
|
|
|
717
717
|
// -------------------- Distinctive Features --------------------
|
|
@@ -741,9 +741,9 @@ export const PEOPLE: ReadonlyArray<Person> = [
|
|
|
741
741
|
{ id: "feature-ear-piercings", label: "Ear Piercings", dimension: "distinctive-features", description: "Multiple stacked ear piercings (cartilage, helix, conch)", promptHint: "with multiple stacked ear piercings — cartilage, helix, and conch", term: "multiple stacked ear piercings" },
|
|
742
742
|
{ id: "feature-lip-piercing", label: "Lip Piercing", dimension: "distinctive-features", description: "Lip ring / labret stud", promptHint: "with a lip piercing, a small ring or labret stud at the lip" },
|
|
743
743
|
{ id: "feature-nostril-piercing", label: "Nostril Piercing", dimension: "distinctive-features", description: "Single nostril stud or ring", promptHint: "with a single nostril piercing, a small stud or ring at the nostril" },
|
|
744
|
-
{ id: "feature-bare-shoulders", label: "Bare Shoulders", dimension: "distinctive-features", description: "Bare shoulders exposed", promptHint: "with
|
|
745
|
-
{ id: "feature-collarbone-visible", label: "Collarbone Visible", dimension: "distinctive-features", description: "Prominent collarbone catching light", promptHint: "with
|
|
746
|
-
{ id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "
|
|
744
|
+
{ id: "feature-bare-shoulders", label: "Bare Shoulders", dimension: "distinctive-features", description: "Bare shoulders exposed", promptHint: "with the shoulders uncovered", term: "shoulders uncovered" , adultOnly: true },
|
|
745
|
+
{ id: "feature-collarbone-visible", label: "Collarbone Visible", dimension: "distinctive-features", description: "Prominent collarbone catching light", promptHint: "with an open neckline that leaves the collarbones uncovered", term: "open neckline, collarbones uncovered" , adultOnly: true },
|
|
746
|
+
{ id: "feature-midriff-visible", label: "Midriff Visible", dimension: "distinctive-features", description: "Exposed midriff between top and bottom", promptHint: "with a cropped hemline", term: "cropped hemline" , adultOnly: true },
|
|
747
747
|
{ id: "feature-navel-visible", label: "Navel Visible", dimension: "distinctive-features", description: "Visible navel on bare stomach", promptHint: "with the navel visible", term: "visible navel" , adultOnly: true },
|
|
748
748
|
{ id: "feature-elongated-neck", label: "Elongated Neck", dimension: "distinctive-features", description: "Long swan-like neck", promptHint: "with an elongated, swan-like neck, long and gracefully extended", term: "elongated swan-like neck" },
|
|
749
749
|
{ id: "feature-under-eye-circles", label: "Under-Eye Circles", dimension: "distinctive-features", description: "Subtle dark circles under the eyes (distinct from puffy eye-bags)", promptHint: "with subtle dark circles under the eyes", term: "dark under-eye circles" },
|
|
@@ -1333,7 +1333,8 @@ function emitIndependentFragments(value: unknown, fragmentFor: PersonFragmentFor
|
|
|
1333
1333
|
// "midriff visible" + "navel visible" as separate clauses reads twice as
|
|
1334
1334
|
// exposed as it is, and stacked with glamour hints it tips borderline draws
|
|
1335
1335
|
// into content blocks (observed on kie-standard, 2026-07-18 app_reports).
|
|
1336
|
-
// When both are picked, fold them into ONE neutral clause
|
|
1336
|
+
// When both are picked, fold them into ONE neutral clause
|
|
1337
|
+
// ("with a cropped hemline and the navel showing").
|
|
1337
1338
|
const midriff = ids.includes("feature-midriff-visible")
|
|
1338
1339
|
const navel = ids.includes("feature-navel-visible")
|
|
1339
1340
|
for (const id of ids) {
|
|
@@ -1342,7 +1343,7 @@ function emitIndependentFragments(value: unknown, fragmentFor: PersonFragmentFor
|
|
|
1342
1343
|
// floored `fragmentFor` (see `emit` in collectPersonFragments) returns
|
|
1343
1344
|
// "" for a flagged id, so gate the fold on that instead of hardcoding
|
|
1344
1345
|
// the minor check here.
|
|
1345
|
-
if (id === "feature-midriff-visible" && fragmentFor(id) !== "") out.push("
|
|
1346
|
+
if (id === "feature-midriff-visible" && fragmentFor(id) !== "") out.push("with a cropped hemline and the navel showing")
|
|
1346
1347
|
continue
|
|
1347
1348
|
}
|
|
1348
1349
|
const fragment = fragmentFor(id)
|
package/src/pose.ts
CHANGED
|
@@ -105,7 +105,7 @@ export const POSES: ReadonlyArray<Pose> = [
|
|
|
105
105
|
{ id: "throwing", label: "Throwing", category: "action", description: "Mid-throw motion", promptHint: "caught mid-throw, body coiled and releasing", term: "caught mid-throw" },
|
|
106
106
|
{ id: "leaping", label: "Leaping", category: "action", description: "Leaping forward dynamically", promptHint: "leaping forward dynamically with body extended" },
|
|
107
107
|
{ id: "dramatic-action", label: "Dramatic Action", category: "action", description: "Exaggerated action pose", promptHint: "in a dramatic, exaggerated action pose full of motion", term: "dramatic exaggerated action pose" },
|
|
108
|
-
{ id: "biting-lip", label: "Biting Lip", category: "action", description: "Slight playful lip-bite", promptHint: "biting the lower lip
|
|
108
|
+
{ id: "biting-lip", label: "Biting Lip", category: "action", description: "Slight playful lip-bite", promptHint: "lightly biting the lower lip, a subtle playful expression", term: "biting lower lip" , adultOnly: true },
|
|
109
109
|
{ id: "mid-laugh", label: "Mid-Laugh", category: "action", description: "Caught mid-laugh, head back", promptHint: "caught mid-laugh with head tipped back, eyes crinkled" },
|
|
110
110
|
{ id: "pointing-at-camera", label: "Pointing at Camera", category: "action", description: "Pointing directly at camera", promptHint: "pointing one finger directly at the camera, arm extended", term: "pointing directly at the camera" },
|
|
111
111
|
{ id: "tongue-out", label: "Sticking Tongue Out", category: "action", description: "Playful tongue-out expression", promptHint: "sticking the tongue out playfully" },
|
package/src/prompt-builder.ts
CHANGED
|
@@ -21,7 +21,8 @@ import { buildIdentityLockLine, withForcedIdentityLock } from "./identity-lock.j
|
|
|
21
21
|
import { findLocationMentionTokens, DEFAULT_LOCATION_USAGE_MODE, type LocationMentionTokenInfo, type LocationUsageMode } from "@nodaro/shared"
|
|
22
22
|
import { findImageMentionTokens, imageMentionSlugForRef, knownImageSlugsFromRefs, type ImageMentionTokenInfo } from "@nodaro/shared"
|
|
23
23
|
import { findEntityMentionTokens, entityMentionSlugForRef, knownEntitySlugsFromRefs, type EntityMentionTokenInfo } from "@nodaro/shared"
|
|
24
|
-
import type { CharacterDef, ConnectedReference, IdentityFidelity, IdentityMeta, ReferenceSource, SceneData } from "@nodaro/shared"
|
|
24
|
+
import type { CharacterDef, ConnectedReference, DescribedReference, IdentityFidelity, IdentityMeta, ReferenceSource, SceneData } from "@nodaro/shared"
|
|
25
|
+
import { appendReferenceLines, referenceDescriptionLine, renderDescribedReferenceLines } from "./described-references.js"
|
|
25
26
|
import { locationReferencePhotoKindLabel, type LocationReferencePhotoKind } from "@nodaro/shared"
|
|
26
27
|
|
|
27
28
|
export interface ResolveCharacterMentionsResult {
|
|
@@ -62,6 +63,30 @@ function composeIdentityDescPart(
|
|
|
62
63
|
return parts.length > 0 ? `${subject} — ${parts.join(". ")}` : subject
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The trailing line a per-use `descriptionOverride` contributes in HYBRID
|
|
68
|
+
* format. The hybrid role phrase ("the person from reference image A") carries
|
|
69
|
+
* no description slot, so the override is surfaced as its own binding-subject
|
|
70
|
+
* line via the ONE phrasing helper in `described-references.ts`. It is pushed
|
|
71
|
+
* onto the reference's `elementDirectives` — the array every hybrid renderer
|
|
72
|
+
* already uses for "the trailing scene directives this reference contributes" —
|
|
73
|
+
* so it lands with that reference's other lines instead of threading a ninth
|
|
74
|
+
* channel through eight call sites.
|
|
75
|
+
*
|
|
76
|
+
* The hybrid EXTRAS path does not call this: it has a real `, <desc>` clause,
|
|
77
|
+
* which the override fills instead (so the model is never told twice). Nothing
|
|
78
|
+
* is pushed without an override, which is what keeps every existing hybrid
|
|
79
|
+
* output byte-identical.
|
|
80
|
+
*/
|
|
81
|
+
function pushOverrideDirective(
|
|
82
|
+
out: string[],
|
|
83
|
+
ref: Pick<ConnectedReference, "descriptionOverride">,
|
|
84
|
+
binding: string,
|
|
85
|
+
): void {
|
|
86
|
+
const line = referenceDescriptionLine(binding, ref.descriptionOverride)
|
|
87
|
+
if (line) out.push(line)
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
/**
|
|
66
91
|
* Resolve @-mention tokens in a prompt against connected references.
|
|
67
92
|
* Returns: augmented prompt (with directives prepended + tokens replaced
|
|
@@ -176,9 +201,15 @@ export function resolveCharacterMentions(
|
|
|
176
201
|
// (held-prop / styling / text) ride the bullet in every mode that emits
|
|
177
202
|
// one. Byte-identical to the old `${subject} — ${canonical}` form when no
|
|
178
203
|
// injection is present.
|
|
204
|
+
// A per-use `descriptionOverride` IS the caller describing this subject
|
|
205
|
+
// for this run, so it fills the identity slot ahead of the entity's stored
|
|
206
|
+
// canonical description — and rides every mode that emits a bullet at all
|
|
207
|
+
// (the mode gate exists to suppress STORED identity noise, not the
|
|
208
|
+
// caller's own words). "none" still emits nothing: it returned above.
|
|
179
209
|
const descPart = composeIdentityDescPart(
|
|
180
210
|
subject,
|
|
181
|
-
|
|
211
|
+
match.descriptionOverride?.trim()
|
|
212
|
+
|| (includeCanonicalDesc ? match.characterCanonicalDescription : undefined),
|
|
182
213
|
match.elementInjection,
|
|
183
214
|
)
|
|
184
215
|
// `directive` is non-null here because usageModeDirective only returns
|
|
@@ -407,6 +438,7 @@ function resolveCharacterMentionsHybrid(
|
|
|
407
438
|
const binding = bindingFor(m.url)
|
|
408
439
|
const lock = buildIdentityLockLine(withForcedIdentityLock(ref, lockOverrideByUrl.get(m.url)), binding)
|
|
409
440
|
if (lock) lockLines.push(lock)
|
|
441
|
+
pushOverrideDirective(elementDirectives, ref, binding)
|
|
410
442
|
const inject = ref.elementInjection?.trim()
|
|
411
443
|
if (inject) elementDirectives.push(inject)
|
|
412
444
|
}
|
|
@@ -670,9 +702,13 @@ export function resolveLocationMentions(
|
|
|
670
702
|
// description noise.
|
|
671
703
|
const includeCanonicalDesc = effectiveMode === "identical"
|
|
672
704
|
const canonicalDesc = match.locationCanonicalDescription?.trim()
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
705
|
+
// A per-use `descriptionOverride` IS the caller describing this place for
|
|
706
|
+
// this run, so it fills the identity slot ahead of the location's stored
|
|
707
|
+
// canonical description — and rides every mode that emits a bullet at all,
|
|
708
|
+
// the same rule the character resolvers apply.
|
|
709
|
+
const shownDesc = match.descriptionOverride?.trim()
|
|
710
|
+
|| (includeCanonicalDesc ? canonicalDesc : undefined)
|
|
711
|
+
const descPart = shownDesc ? `${subject} — ${shownDesc}` : subject
|
|
676
712
|
directiveLines.push(`- ${descPart}.${directive ? ` ${directive}` : ""}`)
|
|
677
713
|
|
|
678
714
|
// Variant display-name sub-line: only when the user pinned a specific
|
|
@@ -830,6 +866,7 @@ function resolveLocationMentionsHybrid(
|
|
|
830
866
|
const binding = bindingFor(m.url)
|
|
831
867
|
const lock = buildIdentityLockLine(withForcedIdentityLock(ref, lockOverrideByUrl.get(m.url)), binding)
|
|
832
868
|
if (lock) lockLines.push(lock)
|
|
869
|
+
pushOverrideDirective(elementDirectives, ref, binding)
|
|
833
870
|
const inject = ref.elementInjection?.trim()
|
|
834
871
|
if (inject) elementDirectives.push(inject)
|
|
835
872
|
}
|
|
@@ -848,6 +885,14 @@ interface ResolveImageMentionsHybridResult {
|
|
|
848
885
|
* mentioned refs out of `connectedReferences`, and this pass deliberately
|
|
849
886
|
* does no such filtering (see the caller's NOTE). Carrying the set anyway
|
|
850
887
|
* would advertise a filter that does not exist. */
|
|
888
|
+
/**
|
|
889
|
+
* The URLs a mention BOUND (deduped). This pass already surfaced each bound
|
|
890
|
+
* ref's per-use `descriptionOverride` as its own trailing line, so the
|
|
891
|
+
* caller's untold-override sweep skips them — the same by-URL suppression
|
|
892
|
+
* contract `resolveEntityMentionsHybrid` carries, for the same reason: told
|
|
893
|
+
* once, never twice.
|
|
894
|
+
*/
|
|
895
|
+
mentionedUrls: Set<string>
|
|
851
896
|
/** Per-reference identity-lock lines (deduped per URL). Caller prepends them
|
|
852
897
|
* as ONE block, merged with the character/location lock lines. */
|
|
853
898
|
lockLines: string[]
|
|
@@ -955,11 +1000,12 @@ function resolveImageMentionsHybrid(
|
|
|
955
1000
|
const binding = bindingFor(m.url)
|
|
956
1001
|
const lock = buildIdentityLockLine(withForcedIdentityLock(ref, lockOverrideByUrl.get(m.url)), binding)
|
|
957
1002
|
if (lock) lockLines.push(lock)
|
|
1003
|
+
pushOverrideDirective(elementDirectives, ref, binding)
|
|
958
1004
|
const inject = ref.elementInjection?.trim()
|
|
959
1005
|
if (inject) elementDirectives.push(inject)
|
|
960
1006
|
}
|
|
961
1007
|
|
|
962
|
-
return { prompt: resolvedPrompt, additionalUrls, lockLines, elementDirectives }
|
|
1008
|
+
return { prompt: resolvedPrompt, additionalUrls, mentionedUrls: seenUrls, lockLines, elementDirectives }
|
|
963
1009
|
}
|
|
964
1010
|
|
|
965
1011
|
interface ResolveEntityMentionsHybridResult {
|
|
@@ -1137,6 +1183,7 @@ function resolveEntityMentionsHybrid(
|
|
|
1137
1183
|
const binding = bindingFor(m.url)
|
|
1138
1184
|
const lock = buildIdentityLockLine(withForcedIdentityLock(ref, lockOverrideByUrl.get(m.url)), binding)
|
|
1139
1185
|
if (lock) lockLines.push(lock)
|
|
1186
|
+
pushOverrideDirective(elementDirectives, ref, binding)
|
|
1140
1187
|
const inject = ref.elementInjection?.trim()
|
|
1141
1188
|
if (inject) elementDirectives.push(inject)
|
|
1142
1189
|
}
|
|
@@ -1221,9 +1268,12 @@ function buildCanonicalFallback(
|
|
|
1221
1268
|
// the (mode-gated) canonical description. This is the reported path — a
|
|
1222
1269
|
// character wired with no @-mention — so a composed character surfaces its
|
|
1223
1270
|
// elements wherever it's used downstream.
|
|
1271
|
+
// Per-use override ahead of the stored canonical description, mode-gate
|
|
1272
|
+
// included — same rule as the mention path above.
|
|
1224
1273
|
const descPart = composeIdentityDescPart(
|
|
1225
1274
|
subject,
|
|
1226
|
-
|
|
1275
|
+
r.descriptionOverride?.trim()
|
|
1276
|
+
|| (includeCanonicalDesc ? r.characterCanonicalDescription : undefined),
|
|
1227
1277
|
r.elementInjection,
|
|
1228
1278
|
)
|
|
1229
1279
|
// `directive` is non-null here ("none"/"name" already short-circuited).
|
|
@@ -1283,7 +1333,12 @@ function buildExtraRefDirectives(
|
|
|
1283
1333
|
for (const r of refs) {
|
|
1284
1334
|
if (!r.isExtraRef) continue
|
|
1285
1335
|
if (!r.url) continue
|
|
1286
|
-
|
|
1336
|
+
// A per-use `descriptionOverride` fills THIS extra's description slot —
|
|
1337
|
+
// every branch below reads one `description`, so the override lands in the
|
|
1338
|
+
// pair-back tail, the name-mode bullet and the first-sight descriptor alike
|
|
1339
|
+
// (and, being the caller's own words, outranks the stored canonical too).
|
|
1340
|
+
const description = (r.descriptionOverride ?? "").trim()
|
|
1341
|
+
|| (r.description ?? r.variantDescription ?? "").trim()
|
|
1287
1342
|
// Character extra
|
|
1288
1343
|
if (r.source === "wired-character" && r.characterSlug) {
|
|
1289
1344
|
const effectiveMode: UsageMode = r.defaultUsageMode ?? DEFAULT_USAGE_MODE
|
|
@@ -1421,6 +1476,7 @@ function renderCanonicalFallbackHybrid(
|
|
|
1421
1476
|
phrases.push(roleToPhrase(resolveDefaultRole(r.defaultRole, r.defaultUsageMode, r.source), binding))
|
|
1422
1477
|
const lock = buildIdentityLockLine(r, binding)
|
|
1423
1478
|
if (lock) lockLines.push(lock)
|
|
1479
|
+
pushOverrideDirective(elementDirectives, r, binding)
|
|
1424
1480
|
const inject = r.elementInjection?.trim()
|
|
1425
1481
|
if (inject) elementDirectives.push(inject)
|
|
1426
1482
|
}
|
|
@@ -1464,7 +1520,10 @@ function renderExtraRefsHybrid(
|
|
|
1464
1520
|
if (!r.url) continue
|
|
1465
1521
|
const letter = letterForUrl(r.url)
|
|
1466
1522
|
const binding = `reference image ${letter}`
|
|
1467
|
-
|
|
1523
|
+
// Per-use override fills the extras' own `, <desc>` clause — the one
|
|
1524
|
+
// description slot the hybrid format has (see `described-references.ts`).
|
|
1525
|
+
const description = (r.descriptionOverride ?? "").trim()
|
|
1526
|
+
|| (r.description ?? r.variantDescription ?? "").trim()
|
|
1468
1527
|
if (r.source === "wired-character" && r.characterSlug) {
|
|
1469
1528
|
const earlier = firstLetterByChar.get(r.characterSlug)
|
|
1470
1529
|
if (earlier !== undefined && earlier !== letter) {
|
|
@@ -1523,12 +1582,16 @@ function renderExtraRefsHybrid(
|
|
|
1523
1582
|
* the call site). A location that is BOTH unmentioned AND `{image:N}`-token-
|
|
1524
1583
|
* referenced is rendered ONCE (inline, via the scene); we `continue` here so it
|
|
1525
1584
|
* is not ALSO emitted as a trailing canonical phrase (the C1 review Minor).
|
|
1585
|
+
*
|
|
1586
|
+
* `renderedUrls` reports the URLs that DID render here — and therefore already
|
|
1587
|
+
* carry their per-use `descriptionOverride` line — so the caller's untold-
|
|
1588
|
+
* override sweep skips them instead of re-deriving this loop's predicate.
|
|
1526
1589
|
*/
|
|
1527
1590
|
function renderLocationCanonicalHybrid(
|
|
1528
1591
|
nonCharacterRefs: readonly ConnectedReference[],
|
|
1529
1592
|
finalIndexByUrl: ReadonlyMap<string, number>,
|
|
1530
1593
|
coveredUrls: ReadonlySet<string>,
|
|
1531
|
-
): { phrases: string[]; lockLines: string[]; elementDirectives: string[] } {
|
|
1594
|
+
): { phrases: string[]; lockLines: string[]; elementDirectives: string[]; renderedUrls: Set<string> } {
|
|
1532
1595
|
const phrases: string[] = []
|
|
1533
1596
|
const lockLines: string[] = []
|
|
1534
1597
|
const elementDirectives: string[] = []
|
|
@@ -1546,10 +1609,11 @@ function renderLocationCanonicalHybrid(
|
|
|
1546
1609
|
phrases.push(roleToPhrase(resolveLocationRole(null, null, r), binding))
|
|
1547
1610
|
const lock = buildIdentityLockLine(r, binding)
|
|
1548
1611
|
if (lock) lockLines.push(lock)
|
|
1612
|
+
pushOverrideDirective(elementDirectives, r, binding)
|
|
1549
1613
|
const inject = r.elementInjection?.trim()
|
|
1550
1614
|
if (inject) elementDirectives.push(inject)
|
|
1551
1615
|
}
|
|
1552
|
-
return { phrases, lockLines, elementDirectives }
|
|
1616
|
+
return { phrases, lockLines, elementDirectives, renderedUrls: seenUrls }
|
|
1553
1617
|
}
|
|
1554
1618
|
|
|
1555
1619
|
/**
|
|
@@ -1576,13 +1640,15 @@ function renderLocationCanonicalHybrid(
|
|
|
1576
1640
|
* has to happen HERE, per URL. The mention pass emits the same lock line and
|
|
1577
1641
|
* `elementInjection` this loop would have, so nothing is lost by skipping it.
|
|
1578
1642
|
*
|
|
1579
|
-
* Deduped per URL (an object wired twice → one phrase).
|
|
1643
|
+
* Deduped per URL (an object wired twice → one phrase). `renderedUrls` reports
|
|
1644
|
+
* the URLs that DID render — already carrying their per-use
|
|
1645
|
+
* `descriptionOverride` line — for the caller's untold-override sweep.
|
|
1580
1646
|
*/
|
|
1581
1647
|
function renderObjectCreatureCanonicalHybrid(
|
|
1582
1648
|
nonCharacterRefs: readonly ConnectedReference[],
|
|
1583
1649
|
finalIndexByUrl: ReadonlyMap<string, number>,
|
|
1584
1650
|
coveredUrls: ReadonlySet<string>,
|
|
1585
|
-
): { phrases: string[]; lockLines: string[]; elementDirectives: string[] } {
|
|
1651
|
+
): { phrases: string[]; lockLines: string[]; elementDirectives: string[]; renderedUrls: Set<string> } {
|
|
1586
1652
|
const phrases: string[] = []
|
|
1587
1653
|
const lockLines: string[] = []
|
|
1588
1654
|
const elementDirectives: string[] = []
|
|
@@ -1597,10 +1663,49 @@ function renderObjectCreatureCanonicalHybrid(
|
|
|
1597
1663
|
phrases.push(roleToPhrase(defaultRoleForSource(r.source), binding))
|
|
1598
1664
|
const lock = buildIdentityLockLine(r, binding)
|
|
1599
1665
|
if (lock) lockLines.push(lock)
|
|
1666
|
+
pushOverrideDirective(elementDirectives, r, binding)
|
|
1600
1667
|
const inject = r.elementInjection?.trim()
|
|
1601
1668
|
if (inject) elementDirectives.push(inject)
|
|
1602
1669
|
}
|
|
1603
|
-
return { phrases, lockLines, elementDirectives }
|
|
1670
|
+
return { phrases, lockLines, elementDirectives, renderedUrls: seenUrls }
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
/**
|
|
1674
|
+
* The per-use `descriptionOverride` of every non-character reference the hybrid
|
|
1675
|
+
* format renders NO role phrase for — the last leg of "the override is honoured
|
|
1676
|
+
* wherever the reference ships".
|
|
1677
|
+
*
|
|
1678
|
+
* Three kinds land here: a ref a `{image:N:label}` token expanded INLINE (the
|
|
1679
|
+
* expansion carries no description slot, and the covered set suppresses both
|
|
1680
|
+
* canonical renders), an unmentioned `wired-image` / `manual` / `wired-face` ref
|
|
1681
|
+
* (no canonical render exists for those sources at all), and any other seated
|
|
1682
|
+
* ref the passes above left unspoken. Each gets ONE
|
|
1683
|
+
* `reference image <LETTER> — <override>.` line, from the same helper every
|
|
1684
|
+
* other site uses, numbered against `finalIndexByUrl`.
|
|
1685
|
+
*
|
|
1686
|
+
* `toldUrls` is every URL a pass above ALREADY gave an override line — the two
|
|
1687
|
+
* canonical renders' `renderedUrls` plus the image / entity mention passes'
|
|
1688
|
+
* bound URLs — so a reference that is both @-mentioned and token-covered is told
|
|
1689
|
+
* once, never twice. A URL is marked swept only when it actually emitted a line,
|
|
1690
|
+
* so two refs sharing one URL where only the later carries an override still
|
|
1691
|
+
* speak.
|
|
1692
|
+
*/
|
|
1693
|
+
function renderUntoldOverridesHybrid(
|
|
1694
|
+
nonCharacterRefs: readonly ConnectedReference[],
|
|
1695
|
+
finalIndexByUrl: ReadonlyMap<string, number>,
|
|
1696
|
+
toldUrls: ReadonlySet<string>,
|
|
1697
|
+
): string[] {
|
|
1698
|
+
const lines: string[] = []
|
|
1699
|
+
const sweptUrls = new Set<string>()
|
|
1700
|
+
for (const r of nonCharacterRefs) {
|
|
1701
|
+
if (!r.url || toldUrls.has(r.url) || sweptUrls.has(r.url)) continue
|
|
1702
|
+
const slot = finalIndexByUrl.get(r.url)
|
|
1703
|
+
if (!slot) continue
|
|
1704
|
+
const before = lines.length
|
|
1705
|
+
pushOverrideDirective(lines, r, `reference image ${slotToLetter(slot)}`)
|
|
1706
|
+
if (lines.length > before) sweptUrls.add(r.url)
|
|
1707
|
+
}
|
|
1708
|
+
return lines
|
|
1604
1709
|
}
|
|
1605
1710
|
|
|
1606
1711
|
/**
|
|
@@ -1874,6 +1979,16 @@ export interface BuildImagePromptConfig {
|
|
|
1874
1979
|
* in order, and tokens expand to "the {label} from image {N}".
|
|
1875
1980
|
*/
|
|
1876
1981
|
connectedReferences?: ConnectedReference[]
|
|
1982
|
+
/**
|
|
1983
|
+
* References the caller can NAME and DESCRIBE but has no media for (an
|
|
1984
|
+
* un-bound cast role, an analysis slot). They attach no URL and claim no
|
|
1985
|
+
* `Image N` / lettered slot — they reach the model as prose, rendered once by
|
|
1986
|
+
* `renderDescribedReferenceLines` and joined the way the active format joins
|
|
1987
|
+
* its trailing directives. Present with NO `connectedReferences` is the normal
|
|
1988
|
+
* case (a story landing before any entity exists), so they enter the
|
|
1989
|
+
* connected-reference path on their own.
|
|
1990
|
+
*/
|
|
1991
|
+
describedReferences?: readonly DescribedReference[]
|
|
1877
1992
|
/** Per-identity (imageIndex+label) user overrides for fidelity / custom text. */
|
|
1878
1993
|
identityMeta?: readonly IdentityMeta[]
|
|
1879
1994
|
/**
|
|
@@ -2141,6 +2256,13 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2141
2256
|
// reassignment, so computing this once up front is stable. Gates the hybrid
|
|
2142
2257
|
// character convergence (Phase 0) and the non-capitalizing scene render below.
|
|
2143
2258
|
const isHybrid = config.referenceFormat === "hybrid"
|
|
2259
|
+
// Described references (name + description, no media) — rendered ONCE, up
|
|
2260
|
+
// front, so the reassignments below can't lose them. They attach no URL, so
|
|
2261
|
+
// they take no part in numbering; they are joined onto the assembled prompt at
|
|
2262
|
+
// the single site in the connected-reference path below, ahead of the provider
|
|
2263
|
+
// cap so a shed sees them like any other body text. Empty for every caller
|
|
2264
|
+
// that doesn't send them → nothing downstream changes.
|
|
2265
|
+
const describedLines = renderDescribedReferenceLines(config.describedReferences)
|
|
2144
2266
|
// Set when Phase 0 converged character OR location @-mentions into inline
|
|
2145
2267
|
// hybrid role phrases (+ identity-lock + element directives). Tells the hybrid
|
|
2146
2268
|
// scene render to expand any remaining {image:N} tokens WITHOUT capitalizing
|
|
@@ -2155,6 +2277,12 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2155
2277
|
// every prompt without an entity mention, which is what keeps those outputs
|
|
2156
2278
|
// byte-identical.
|
|
2157
2279
|
const mentionedEntityUrls = new Set<string>()
|
|
2280
|
+
// URLs bound by a Phase-0 named-image (`@<image-name>`) mention. Bridges the
|
|
2281
|
+
// same two blocks for the OTHER suppression the New path owes: that pass
|
|
2282
|
+
// already emitted each bound ref's per-use `descriptionOverride` line, so the
|
|
2283
|
+
// untold-override sweep must not say it a second time. Empty for every prompt
|
|
2284
|
+
// without an image mention.
|
|
2285
|
+
const mentionedImageUrls = new Set<string>()
|
|
2158
2286
|
|
|
2159
2287
|
// Character LoRA inference path: trigger word + LoRA model carry identity,
|
|
2160
2288
|
// so we strip raw `@slug[:V[:variant]]` tokens from the prompt AND drop the
|
|
@@ -2410,6 +2538,10 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2410
2538
|
resolved.additionalUrls = [...resolved.additionalUrls, ...hi.additionalUrls]
|
|
2411
2539
|
hybridImageLockLines = hi.lockLines
|
|
2412
2540
|
hybridImageElementDirectives = hi.elementDirectives
|
|
2541
|
+
// The override handoff to the New path: a bound URL was already told
|
|
2542
|
+
// its per-use description here, inline with the mention, so the
|
|
2543
|
+
// untold-override sweep below skips it.
|
|
2544
|
+
for (const u of hi.mentionedUrls) mentionedImageUrls.add(u)
|
|
2413
2545
|
// Inline role phrases now live in the body → skip line-initial
|
|
2414
2546
|
// capitalization, which would otherwise corrupt a mid-sentence
|
|
2415
2547
|
// "the background from reference image C".
|
|
@@ -2646,9 +2778,25 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2646
2778
|
// wired-image, wired-face, wired-object, wired-location) still auto-
|
|
2647
2779
|
// attach so unchanged behavior for them.
|
|
2648
2780
|
// -------------------------------------------------------------------------
|
|
2649
|
-
|
|
2781
|
+
// Entered by `connectedReferences` OR by described references alone: a story
|
|
2782
|
+
// landing whose cast has no entities yet sends only names + descriptions, and
|
|
2783
|
+
// the legacy path below has no directive seam to join them onto.
|
|
2784
|
+
if (connectedReferences || describedLines.length > 0) {
|
|
2785
|
+
const structuredRefs = connectedReferences ?? []
|
|
2650
2786
|
let prompt = config.prompt
|
|
2651
2787
|
|
|
2788
|
+
// Described references, LEGACY half — joined BEFORE the directive assembly
|
|
2789
|
+
// below, because the block it creates is the block that assembly consolidates
|
|
2790
|
+
// into. Joined AFTER instead, a request whose references produce the
|
|
2791
|
+
// "Use these references for the output image:" wrap (any non-character ref,
|
|
2792
|
+
// no wired character) would get a SECOND "Use these characters:" header
|
|
2793
|
+
// prepended above it. Prepending here also keeps `marks.directivesPrefix`
|
|
2794
|
+
// honest: the prepend branch that records it is only reachable when the
|
|
2795
|
+
// prompt does NOT already open with the character block.
|
|
2796
|
+
if (!isHybrid && describedLines.length > 0) {
|
|
2797
|
+
prompt = appendReferenceLines(prompt, describedLines, "legacy")
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2652
2800
|
// Non-character refs are still emitted as per-identity directives + URLs.
|
|
2653
2801
|
// Character refs are filtered out here — Phase 0 has already added their
|
|
2654
2802
|
// URLs to `referenceImageUrls` (via mention resolution) and prepended the
|
|
@@ -2658,7 +2806,7 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2658
2806
|
// subject as Image M, …") and merged their URLs into `referenceImageUrls`.
|
|
2659
2807
|
// Letting them through here would double-emit the URLs (and append a
|
|
2660
2808
|
// second positional directive via the {image:N:label} path).
|
|
2661
|
-
const nonCharacterRefs =
|
|
2809
|
+
const nonCharacterRefs = structuredRefs.filter(
|
|
2662
2810
|
(r) => r.source !== "wired-character" && r.isExtraRef !== true,
|
|
2663
2811
|
)
|
|
2664
2812
|
|
|
@@ -2761,9 +2909,27 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2761
2909
|
const objCanon = renderObjectCreatureCanonicalHybrid(nonCharacterRefs, finalIndexByUrl, objCoveredUrls)
|
|
2762
2910
|
const canonLockLines = [...locCanon.lockLines, ...objCanon.lockLines]
|
|
2763
2911
|
const canonLockBlock = canonLockLines.length > 0 ? `${canonLockLines.join("\n")}\n\n` : ""
|
|
2912
|
+
// Per-use descriptions nothing above spoke for: a `{image:N}`-covered ref
|
|
2913
|
+
// (expanded inline, suppressed from both canonical renders) and a seated
|
|
2914
|
+
// `wired-image` / `manual` ref (which has no canonical render at all)
|
|
2915
|
+
// would otherwise drop their `descriptionOverride` silently — the one
|
|
2916
|
+
// place the hybrid format renders neither a description slot nor a role
|
|
2917
|
+
// phrase. Deduped against every URL a pass above already told.
|
|
2918
|
+
const toldOverrideUrls = new Set<string>([
|
|
2919
|
+
...locCanon.renderedUrls,
|
|
2920
|
+
...objCanon.renderedUrls,
|
|
2921
|
+
...mentionedEntityUrls,
|
|
2922
|
+
...mentionedImageUrls,
|
|
2923
|
+
])
|
|
2924
|
+
const untoldOverrides = renderUntoldOverridesHybrid(
|
|
2925
|
+
nonCharacterRefs,
|
|
2926
|
+
finalIndexByUrl,
|
|
2927
|
+
toldOverrideUrls,
|
|
2928
|
+
)
|
|
2764
2929
|
const canonTrailingLines = [
|
|
2765
2930
|
...locCanon.phrases, ...objCanon.phrases,
|
|
2766
2931
|
...locCanon.elementDirectives, ...objCanon.elementDirectives,
|
|
2932
|
+
...untoldOverrides,
|
|
2767
2933
|
]
|
|
2768
2934
|
// Role phrases and element injections are scene content → they extend the
|
|
2769
2935
|
// BODY, ahead of the `[style]` section (which has no terminator, so a flat
|
|
@@ -2811,6 +2977,14 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2811
2977
|
}
|
|
2812
2978
|
}
|
|
2813
2979
|
|
|
2980
|
+
// Described references, HYBRID half: trailing scene directives, so they land
|
|
2981
|
+
// behind the reference role phrases the branch above appended (the legacy
|
|
2982
|
+
// half ran before it — see the comment at that call). Still ahead of the
|
|
2983
|
+
// provider cap below, so the lines are body text like any other.
|
|
2984
|
+
if (isHybrid && describedLines.length > 0) {
|
|
2985
|
+
prompt = appendReferenceLines(prompt, describedLines, "hybrid")
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2814
2988
|
const styleText = style?.trim()
|
|
2815
2989
|
const styleLine = styleText && !isDeniedStyleId(styleText) ? `Style: ${getStylePromptHint(styleText) || styleText}` : ""
|
|
2816
2990
|
|
|
@@ -2883,7 +3057,7 @@ function buildImagePromptInternal(config: BuildImagePromptConfig, marks?: Assemb
|
|
|
2883
3057
|
const reordered = applyReferenceOrder(
|
|
2884
3058
|
assembledUrls,
|
|
2885
3059
|
prompt,
|
|
2886
|
-
|
|
3060
|
+
structuredRefs,
|
|
2887
3061
|
referenceOrder,
|
|
2888
3062
|
sourceNodeIdById,
|
|
2889
3063
|
)
|
|
@@ -3110,7 +3284,10 @@ function collectIdentities(
|
|
|
3110
3284
|
label,
|
|
3111
3285
|
fidelity: m?.fidelity ?? defaultFidelityForSource(ref?.source),
|
|
3112
3286
|
customText: m?.customText?.trim() || undefined,
|
|
3113
|
-
|
|
3287
|
+
// Per-use override ahead of the ref's own description; the directive
|
|
3288
|
+
// builder's location-canonical fallback only fires when BOTH are absent,
|
|
3289
|
+
// so an override outranks the stored location wording too.
|
|
3290
|
+
description: ref?.descriptionOverride?.trim() || ref?.description,
|
|
3114
3291
|
source: ref?.source,
|
|
3115
3292
|
locationCanonicalDescription: ref?.locationCanonicalDescription,
|
|
3116
3293
|
locationSlug: ref?.locationSlug,
|
|
@@ -3303,7 +3480,7 @@ function buildNonCharacterDirectives(
|
|
|
3303
3480
|
? "creature"
|
|
3304
3481
|
: "object",
|
|
3305
3482
|
fidelity: defaultFidelityForSource(ref.source),
|
|
3306
|
-
description: ref.description?.trim() || undefined,
|
|
3483
|
+
description: ref.descriptionOverride?.trim() || ref.description?.trim() || undefined,
|
|
3307
3484
|
source: ref.source,
|
|
3308
3485
|
locationCanonicalDescription: ref.locationCanonicalDescription,
|
|
3309
3486
|
locationSlug: ref.locationSlug,
|