@nodaro/prompts 1.16.0 → 1.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,13 @@ import { findCharacterMentionTokens, type CharacterMentionTokenInfo } from "@nod
32
32
  import { resolveCharacterMentions, applyReferenceOrderToVideo } from "./prompt-builder.js"
33
33
  import { roleToPhrase, REFERENCE_ROLE_PRESETS, resolveDefaultRole } from "@nodaro/shared"
34
34
  import { buildIdentityLockLine, withForcedIdentityLock } from "./identity-lock.js"
35
- import type { ConnectedReference } from "@nodaro/shared"
35
+ import type { ConnectedReference, DescribedReference } from "@nodaro/shared"
36
+ import {
37
+ appendReferenceLines,
38
+ referenceDescriptionLine,
39
+ renderDescribedReferenceLines,
40
+ renderReferenceCaptionLines,
41
+ } from "./described-references.js"
36
42
  import { REF_BINDING } from "./ref-binding.js"
37
43
  import { resolveRefIdTokens } from "./ref-id-tokens.js"
38
44
  import { insertBeforeStyleSection } from "./prompt-style-section.js"
@@ -152,6 +158,13 @@ export interface VideoExtraRef {
152
158
  * orchestrator extras leave this unset (they resolve via `CharacterMeta`).
153
159
  */
154
160
  defaultRole?: string
161
+ /**
162
+ * The `ConnectedReference.descriptionOverride` analogue — the PER-USE identity
163
+ * description for this extra. Fills the row's own description slot (the
164
+ * pair-back tail, the name-mode bullet, the first-sight descriptor), winning
165
+ * over `description`, which keeps its label semantics. Absent → unchanged.
166
+ */
167
+ descriptionOverride?: string
155
168
  }
156
169
 
157
170
  /**
@@ -230,6 +243,24 @@ export interface ResolveVideoReferenceCoreArgs {
230
243
  * The wired character refs' `defaultName`s are known without this.
231
244
  */
232
245
  refNamesById?: ReadonlyMap<string, string>
246
+ /**
247
+ * References the caller can NAME and DESCRIBE but has no media for — an
248
+ * un-bound cast role, an analysis slot. They attach no URL and claim no
249
+ * `@image_N` seat: they are rendered as prose by `renderDescribedReferenceLines`
250
+ * and joined the way this lane joins its trailing directives. The
251
+ * described-ONLY request (no wired characters, no extras) is the normal case,
252
+ * so the early return below carries the join too.
253
+ */
254
+ describedReferences?: readonly DescribedReference[]
255
+ /**
256
+ * Captions for the node's video / audio rail references, INDEX-ALIGNED with
257
+ * the caller's `referenceVideoUrls` / `referenceAudioUrls`. Rendered as
258
+ * `@video_N: <caption>.` / `@audio_N: <caption>.` and bounded by
259
+ * `videoRefCount` / `audioRefCount`, so a caption for a url the provider cap
260
+ * dropped never binds a slot the payload does not ship.
261
+ */
262
+ videoCaptions?: readonly string[]
263
+ audioCaptions?: readonly string[]
233
264
  }
234
265
 
235
266
  /** Result of the HYBRID mention pass — inline role phrases + surfaced opt-in
@@ -368,6 +399,11 @@ function resolveVideoCharacterMentionsHybrid(
368
399
  const binding = bindingFor(m.url)
369
400
  const lock = buildIdentityLockLine(withForcedIdentityLock(ref, lockOverrideByUrl.get(m.url)), binding)
370
401
  if (lock) lockLines.push(lock)
402
+ // A per-use `descriptionOverride` has no slot in a hybrid role phrase, so it
403
+ // rides the trailing-directive channel as its own binding-subject line —
404
+ // exactly as on the image lane (`pushOverrideDirective`).
405
+ const overrideLine = referenceDescriptionLine(binding, ref.descriptionOverride)
406
+ if (overrideLine) elementDirectives.push(overrideLine)
371
407
  const inject = ref.elementInjection?.trim()
372
408
  if (inject) elementDirectives.push(inject)
373
409
  }
@@ -429,6 +465,18 @@ export function resolveVideoReferenceCore(
429
465
  video: args.videoRefCount ?? 0,
430
466
  audio: args.audioRefCount ?? 0,
431
467
  })
468
+ // Described references (name + description, no media) and the video/audio rail
469
+ // captions — rendered ONCE, ahead of every exit, because both the early return
470
+ // below and the main assembly have to carry them. They attach no URL and claim
471
+ // no `@image_N` seat, so they take no part in the numbering walk. Empty for
472
+ // every caller that sends neither → every existing output is untouched.
473
+ const describedAndCaptionLines = [
474
+ ...renderDescribedReferenceLines(args.describedReferences),
475
+ ...renderReferenceCaptionLines(args.videoCaptions, args.audioCaptions, {
476
+ video: args.videoRefCount ?? 0,
477
+ audio: args.audioRefCount ?? 0,
478
+ }),
479
+ ]
432
480
  let wiredCharRefs = [...args.wiredCharRefs]
433
481
  const suppressedSlugs = new Set(args.suppressedCanonicalCharacterIds ?? [])
434
482
  if (suppressedSlugs.size > 0) {
@@ -469,14 +517,20 @@ export function resolveVideoReferenceCore(
469
517
  // leading refs were passed); the leading URLs are returned for the payload.
470
518
  // Nothing was seated, so every `{ref:}` degrades (label → name → "").
471
519
  const counts = tokenCounts(leadingRefUrls.length)
520
+ // tokenCounts(leadingRefUrls.length) → image count == offset (no assets here):
521
+ // leadingRefUrls mode counts the leading refs; ordinalOffset mode counts the
522
+ // caller-owned leading refs the offset stands in for.
523
+ const resolved = resolveReferenceTokens(
524
+ resolveRefIdTokens(args.prompt, { slotById: slotByRefId, nameById: nameByRefId, imageCount: counts.image }),
525
+ counts,
526
+ )
472
527
  return {
473
- // tokenCounts(leadingRefUrls.length) → image count == offset (no assets here):
474
- // leadingRefUrls mode counts the leading refs; ordinalOffset mode counts the
475
- // caller-owned leading refs the offset stands in for.
476
- prompt: resolveReferenceTokens(
477
- resolveRefIdTokens(args.prompt, { slotById: slotByRefId, nameById: nameByRefId, imageCount: counts.image }),
478
- counts,
479
- ),
528
+ // Described references and rail captions are the ONE thing this branch can
529
+ // still contribute: they need no seat, so a request that carries only them
530
+ // (a story landing before any entity exists) reaches the model here.
531
+ prompt: describedAndCaptionLines.length > 0
532
+ ? appendReferenceLines(resolved ?? "", describedAndCaptionLines, args.hybridRoles === true ? "hybrid" : "legacy")
533
+ : resolved,
480
534
  additionalUrls: [...leadingRefUrls],
481
535
  }
482
536
  }
@@ -579,6 +633,8 @@ export function resolveVideoReferenceCore(
579
633
  canonicalPhrases.push(roleToPhrase(resolveDefaultRole(r.defaultRole, r.defaultUsageMode, r.source), binding))
580
634
  const lock = buildIdentityLockLine(r, binding)
581
635
  if (lock) canonicalLockLines.push(lock)
636
+ const overrideLine = referenceDescriptionLine(binding, r.descriptionOverride)
637
+ if (overrideLine) canonicalElementDirectives.push(overrideLine)
582
638
  const inject = r.elementInjection?.trim()
583
639
  if (inject) canonicalElementDirectives.push(inject)
584
640
  continue
@@ -601,7 +657,13 @@ export function resolveVideoReferenceCore(
601
657
  // mirrors the shared image-side `composeIdentityDescPart`. The subject here
602
658
  // is the bare display name (video numbering is applied separately above).
603
659
  const descBodyParts: string[] = []
604
- if (includeCanonicalDesc && r.characterCanonicalDescription?.trim()) {
660
+ // The per-use override IS the caller describing this subject for this run,
661
+ // so it fills the identity slot ahead of the stored canonical description —
662
+ // and rides every mode that emits a bullet at all ("none" returned above).
663
+ const canonicalOverride = r.descriptionOverride?.trim()
664
+ if (canonicalOverride) {
665
+ descBodyParts.push(canonicalOverride)
666
+ } else if (includeCanonicalDesc && r.characterCanonicalDescription?.trim()) {
605
667
  descBodyParts.push(r.characterCanonicalDescription.trim())
606
668
  }
607
669
  if (r.elementInjection?.trim()) descBodyParts.push(r.elementInjection.trim())
@@ -637,7 +699,9 @@ export function resolveVideoReferenceCore(
637
699
  if (!ex.url) continue
638
700
  position += 1
639
701
  if (ex.id && !slotByRefId.has(ex.id)) slotByRefId.set(ex.id, position)
640
- const desc = (ex.description ?? "").trim()
702
+ // Per-use override fills this row's description slot; `description` keeps
703
+ // its own label semantics (mirrors the image extras).
704
+ const desc = (ex.descriptionOverride ?? "").trim() || (ex.description ?? "").trim()
641
705
  if (ex.characterSlug) {
642
706
  // First sight of this character via an extra. Resolution chain
643
707
  // matches the image side: per-ref override → upstream character
@@ -820,6 +884,17 @@ export function resolveVideoReferenceCore(
820
884
  if (u && !seen.has(u)) { seen.add(u); merged.push(u) }
821
885
  }
822
886
 
887
+ // Described references + rail captions — joined the way this format joins its
888
+ // own trailing directives (hybrid: ahead of the `[style]` section; legacy:
889
+ // bullets consolidated into the "Use these characters:" block the branch above
890
+ // may just have created). Ahead of the reorder, like every other assembled
891
+ // line; they carry no `@image_N` binding for the renumber to move.
892
+ finalPrompt = appendReferenceLines(
893
+ finalPrompt,
894
+ describedAndCaptionLines,
895
+ hybrid ? "hybrid" : "legacy",
896
+ )
897
+
823
898
  // `{ref:<id>}` tokens resolve HERE — after the walk has seated every reference
824
899
  // (the slot map is complete) and BEFORE the user reorder below, so the
825
900
  // reorder's `@image_N` renumber pass carries the freshly emitted binding to