@nodaro/prompts 1.10.0 → 1.12.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.
Files changed (35) hide show
  1. package/dist/index.cjs +693 -53
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1084 -27
  4. package/dist/index.d.ts +1084 -27
  5. package/dist/index.js +664 -55
  6. package/dist/index.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/__tests__/__snapshots__/entity-convergence-image.test.ts.snap +19 -0
  9. package/src/__tests__/animal-getters-parity.test.ts +82 -0
  10. package/src/__tests__/assemble-image-input-cap.test.ts +212 -0
  11. package/src/__tests__/assemble-image-input.test.ts +93 -3
  12. package/src/__tests__/assemble-video-input-cap.test.ts +356 -0
  13. package/src/__tests__/assemble-video-input.test.ts +301 -0
  14. package/src/__tests__/direction-hint-token-safety.test.ts +113 -0
  15. package/src/__tests__/direction-registry.test.ts +393 -0
  16. package/src/__tests__/entity-convergence-image.test.ts +374 -0
  17. package/src/__tests__/image-convergence-image.test.ts +370 -0
  18. package/src/__tests__/location-convergence-image.test.ts +29 -1
  19. package/src/__tests__/location-default-role-image.test.ts +166 -0
  20. package/src/__tests__/mention-splice-spacing.test.ts +257 -0
  21. package/src/__tests__/read-node-direction.test.ts +154 -0
  22. package/src/__tests__/read-node-subject.test.ts +140 -0
  23. package/src/__tests__/subject-fold.test.ts +232 -0
  24. package/src/__tests__/subject-registry.test.ts +312 -0
  25. package/src/assemble-image-input.ts +160 -58
  26. package/src/assemble-video-input.ts +244 -0
  27. package/src/direction-registry.ts +371 -0
  28. package/src/hint-shedding.ts +68 -0
  29. package/src/index.ts +10 -2
  30. package/src/parameter-prompt-hint.ts +8 -7
  31. package/src/picker-catalogs.ts +14 -7
  32. package/src/prompt-builder.ts +728 -58
  33. package/src/prompt-hint-join.ts +30 -0
  34. package/src/read-node-direction.ts +233 -0
  35. package/src/subject-registry.ts +464 -0
package/dist/index.cjs CHANGED
@@ -9241,7 +9241,7 @@ function objectOptions(arr, phrase) {
9241
9241
  label: e.label,
9242
9242
  description: e.description,
9243
9243
  category: e.subcategory,
9244
- promptHint: phrase(e.label.toLowerCase(), e.description),
9244
+ promptHint: phrase(e),
9245
9245
  // Object entities have no `promptHint` field of their own (it is
9246
9246
  // synthesized above), so the term cannot come from `resolveTerm`'s
9247
9247
  // empty-hint rule: an authored `term` wins, and otherwise the label IS the
@@ -9573,7 +9573,7 @@ var SINGLE_CATALOGS = [
9573
9573
  defaultValue: "dog-golden-retriever",
9574
9574
  categoryOrder: shared.ANIMAL_SUBCATEGORY_ORDER,
9575
9575
  categoryLabels: shared.ANIMAL_SUBCATEGORY_LABELS,
9576
- options: objectOptions(shared.ANIMALS, (label, description) => `featuring a ${label}, ${description}`)
9576
+ options: objectOptions(shared.ANIMALS, (e) => shared.getAnimalPromptHint(e.id))
9577
9577
  },
9578
9578
  {
9579
9579
  nodeType: "vehicle",
@@ -9584,7 +9584,7 @@ var SINGLE_CATALOGS = [
9584
9584
  defaultValue: "sedan",
9585
9585
  categoryOrder: shared.VEHICLE_SUBCATEGORY_ORDER,
9586
9586
  categoryLabels: shared.VEHICLE_SUBCATEGORY_LABELS,
9587
- options: objectOptions(shared.VEHICLES, (label, description) => `featuring a ${label}, ${description}`)
9587
+ options: objectOptions(shared.VEHICLES, (e) => `featuring a ${e.label.toLowerCase()}, ${e.description}`)
9588
9588
  },
9589
9589
  {
9590
9590
  nodeType: "weapon",
@@ -9595,7 +9595,7 @@ var SINGLE_CATALOGS = [
9595
9595
  defaultValue: "katana",
9596
9596
  categoryOrder: shared.WEAPON_SUBCATEGORY_ORDER,
9597
9597
  categoryLabels: shared.WEAPON_SUBCATEGORY_LABELS,
9598
- options: objectOptions(shared.WEAPONS, (label, description) => `with a ${label}, ${description}`)
9598
+ options: objectOptions(shared.WEAPONS, (e) => `with a ${e.label.toLowerCase()}, ${e.description}`)
9599
9599
  },
9600
9600
  {
9601
9601
  nodeType: "furniture",
@@ -9606,7 +9606,7 @@ var SINGLE_CATALOGS = [
9606
9606
  defaultValue: "sofa",
9607
9607
  categoryOrder: shared.FURNITURE_SUBCATEGORY_ORDER,
9608
9608
  categoryLabels: shared.FURNITURE_SUBCATEGORY_LABELS,
9609
- options: objectOptions(shared.FURNITURE, (label, description) => `including a ${label}, ${description}`)
9609
+ options: objectOptions(shared.FURNITURE, (e) => `including a ${e.label.toLowerCase()}, ${e.description}`)
9610
9610
  },
9611
9611
  {
9612
9612
  nodeType: "held-prop",
@@ -9974,13 +9974,16 @@ function resolveBaseHint(type, data, mode = "full") {
9974
9974
  return withCustomText(data, byMode(mode, getLoopSubjectPromptHint, getLoopSubjectTerm)(asStr(data.loopSubject)));
9975
9975
  case "material":
9976
9976
  return withCustomText(data, buildMaterialHints(data.material, mode));
9977
- case "animal": {
9978
- const animal = shared.getAnimal(asStr(data.animal));
9977
+ // Animal is the one Object-entity catalog whose phrasing has a single
9978
+ // owner: `@nodaro/shared`'s `getAnimalPromptHint` / `getAnimalTerm`, which
9979
+ // the picker-catalog funnel calls too. Both getters already return "" on a
9980
+ // miss, so the entry lookup and the `animal ? … : ""` guard are the
9981
+ // getters' job now, not this switch's.
9982
+ case "animal":
9979
9983
  return withCustomText(
9980
9984
  data,
9981
- animal ? byMode(mode, `featuring a ${animal.label.toLowerCase()}, ${animal.description}`, objectEntityTerm(animal)) : ""
9985
+ byMode(mode, shared.getAnimalPromptHint, shared.getAnimalTerm)(asStr(data.animal))
9982
9986
  );
9983
- }
9984
9987
  case "vehicle": {
9985
9988
  const vehicle = shared.getVehicle(asStr(data.vehicle));
9986
9989
  return withCustomText(
@@ -10284,6 +10287,11 @@ ${resolvedPrompt}`;
10284
10287
  }
10285
10288
  return { prompt: resolvedPrompt, additionalUrls, mentionedCharacterSlugs };
10286
10289
  }
10290
+ function spliceMentionPhrase(prompt, offset, tokenLength, phrase) {
10291
+ const before = prompt.slice(0, offset).replace(/(?<=\S)[^\S\r\n]{2,}$/, " ");
10292
+ const after = prompt.slice(offset + tokenLength).replace(/^[^\S\r\n]{2,}/, " ");
10293
+ return before + phrase + after;
10294
+ }
10287
10295
  function resolveCharacterMentionsHybrid(prompt, tokens, refs, existingUrls) {
10288
10296
  const bySlug = /* @__PURE__ */ new Map();
10289
10297
  const byVariant = /* @__PURE__ */ new Map();
@@ -10322,7 +10330,7 @@ function resolveCharacterMentionsHybrid(prompt, tokens, refs, existingUrls) {
10322
10330
  let resolvedPrompt = prompt;
10323
10331
  for (const m of [...matched].sort((a, b) => b.offset - a.offset)) {
10324
10332
  const phrase = shared.roleToPhrase(m.role, bindingFor(m.url));
10325
- resolvedPrompt = resolvedPrompt.slice(0, m.offset) + phrase + resolvedPrompt.slice(m.offset + m.token.length);
10333
+ resolvedPrompt = spliceMentionPhrase(resolvedPrompt, m.offset, m.token.length, phrase);
10326
10334
  }
10327
10335
  const lockLines = [];
10328
10336
  const elementDirectives = [];
@@ -10353,16 +10361,21 @@ function locationModeDirective(mode) {
10353
10361
  }
10354
10362
  }
10355
10363
  function locationModeToRole(mode) {
10356
- switch (mode) {
10357
- case "identical":
10358
- return "background";
10359
- case "style":
10360
- return "style";
10361
- case "layout":
10362
- return "layout";
10363
- default:
10364
- return shared.defaultRoleForSource("wired-location");
10365
- }
10364
+ return roleBearingLocationMode(mode) ?? shared.defaultRoleForSource("wired-location");
10365
+ }
10366
+ function roleBearingLocationMode(mode) {
10367
+ const m = mode?.trim();
10368
+ if (!m) return null;
10369
+ return shared.REFERENCE_ROLE_PRESETS["wired-location"].includes(m) ? m : null;
10370
+ }
10371
+ function resolveLocationRole(tokenRole, tokenUsageMode, ref) {
10372
+ const explicitRole = tokenRole?.trim();
10373
+ if (explicitRole) return shared.normalizeRoleSlug(explicitRole);
10374
+ const modeRole = roleBearingLocationMode(tokenUsageMode);
10375
+ if (modeRole) return modeRole;
10376
+ const nodeRole = ref.defaultRole?.trim();
10377
+ if (nodeRole) return shared.normalizeRoleSlug(nodeRole);
10378
+ return locationModeToRole(ref.defaultUsageMode ?? shared.DEFAULT_LOCATION_USAGE_MODE);
10366
10379
  }
10367
10380
  function resolveLocationMentions(prompt, tokens, refs) {
10368
10381
  const bySlug = /* @__PURE__ */ new Map();
@@ -10444,8 +10457,7 @@ function resolveLocationMentionsHybrid(prompt, tokens, refs, existingUrls) {
10444
10457
  mentionedLocationSlugs.add(t.locationSlug);
10445
10458
  refByUrl.set(match.url, match);
10446
10459
  if (t.lock !== void 0) lockOverrideByUrl.set(match.url, t.lock);
10447
- const mode = t.usageMode ?? match.defaultUsageMode ?? shared.DEFAULT_LOCATION_USAGE_MODE;
10448
- const role = t.role ? shared.normalizeRoleSlug(t.role) : locationModeToRole(mode);
10460
+ const role = resolveLocationRole(t.role, t.usageMode, match);
10449
10461
  matched.push({ token: t.token, offset: t.offset, url: match.url, role });
10450
10462
  }
10451
10463
  const slotByUrl = /* @__PURE__ */ new Map();
@@ -10459,7 +10471,7 @@ function resolveLocationMentionsHybrid(prompt, tokens, refs, existingUrls) {
10459
10471
  let resolvedPrompt = prompt;
10460
10472
  for (const m of [...matched].sort((a, b) => b.offset - a.offset)) {
10461
10473
  const phrase = shared.roleToPhrase(m.role, bindingFor(m.url));
10462
- resolvedPrompt = resolvedPrompt.slice(0, m.offset) + phrase + resolvedPrompt.slice(m.offset + m.token.length);
10474
+ resolvedPrompt = spliceMentionPhrase(resolvedPrompt, m.offset, m.token.length, phrase);
10463
10475
  }
10464
10476
  const lockLines = [];
10465
10477
  const elementDirectives = [];
@@ -10477,6 +10489,109 @@ function resolveLocationMentionsHybrid(prompt, tokens, refs, existingUrls) {
10477
10489
  }
10478
10490
  return { prompt: resolvedPrompt, additionalUrls, mentionedLocationSlugs, lockLines, elementDirectives };
10479
10491
  }
10492
+ function resolveImageMentionsHybrid(prompt, tokens, refs, existingUrls) {
10493
+ const bySlug = /* @__PURE__ */ new Map();
10494
+ for (const r of refs) {
10495
+ const slug = shared.imageMentionSlugForRef(r);
10496
+ if (!slug) continue;
10497
+ if (!bySlug.has(slug)) bySlug.set(slug, r);
10498
+ }
10499
+ const additionalUrls = [];
10500
+ const refByUrl = /* @__PURE__ */ new Map();
10501
+ const lockOverrideByUrl = /* @__PURE__ */ new Map();
10502
+ const matched = [];
10503
+ for (const t of tokens) {
10504
+ const match = bySlug.get(t.imageSlug);
10505
+ if (!match || !match.url) continue;
10506
+ additionalUrls.push(match.url);
10507
+ refByUrl.set(match.url, match);
10508
+ if (t.lock !== void 0) lockOverrideByUrl.set(match.url, t.lock);
10509
+ const role = (t.role ?? "").trim() || shared.resolveDefaultRole(match.defaultRole, match.defaultUsageMode, match.source);
10510
+ matched.push({ token: t.token, offset: t.offset, url: match.url, role });
10511
+ }
10512
+ const slotByUrl = /* @__PURE__ */ new Map();
10513
+ for (const u of [...existingUrls, ...additionalUrls]) {
10514
+ if (!slotByUrl.has(u)) slotByUrl.set(u, slotByUrl.size + 1);
10515
+ }
10516
+ const bindingFor = (url2) => {
10517
+ const slot = slotByUrl.get(url2);
10518
+ return slot ? `reference image ${slotToLetter(slot)}` : "the reference image";
10519
+ };
10520
+ let resolvedPrompt = prompt;
10521
+ for (const m of [...matched].sort((a, b) => b.offset - a.offset)) {
10522
+ const phrase = shared.roleToPhrase(m.role, bindingFor(m.url));
10523
+ resolvedPrompt = spliceMentionPhrase(resolvedPrompt, m.offset, m.token.length, phrase);
10524
+ }
10525
+ const lockLines = [];
10526
+ const elementDirectives = [];
10527
+ const seenUrls = /* @__PURE__ */ new Set();
10528
+ for (const m of matched) {
10529
+ if (seenUrls.has(m.url)) continue;
10530
+ seenUrls.add(m.url);
10531
+ const ref = refByUrl.get(m.url);
10532
+ if (!ref) continue;
10533
+ const binding = bindingFor(m.url);
10534
+ const lock = buildIdentityLockLine(withForcedIdentityLock(ref, lockOverrideByUrl.get(m.url)), binding);
10535
+ if (lock) lockLines.push(lock);
10536
+ const inject = ref.elementInjection?.trim();
10537
+ if (inject) elementDirectives.push(inject);
10538
+ }
10539
+ return { prompt: resolvedPrompt, additionalUrls, lockLines, elementDirectives };
10540
+ }
10541
+ function resolveEntityMentionsHybrid(prompt, tokens, refs, existingUrls) {
10542
+ const bySlug = /* @__PURE__ */ new Map();
10543
+ for (const source of ["wired-creature", "wired-object"]) {
10544
+ for (const r of refs) {
10545
+ if (r.source !== source) continue;
10546
+ const slug = shared.entityMentionSlugForRef(r);
10547
+ if (!slug) continue;
10548
+ if (!bySlug.has(slug)) bySlug.set(slug, r);
10549
+ }
10550
+ }
10551
+ const additionalUrls = [];
10552
+ const mentionedUrls = /* @__PURE__ */ new Set();
10553
+ const refByUrl = /* @__PURE__ */ new Map();
10554
+ const lockOverrideByUrl = /* @__PURE__ */ new Map();
10555
+ const matched = [];
10556
+ for (const t of tokens) {
10557
+ const match = bySlug.get(t.entitySlug);
10558
+ if (!match || !match.url) continue;
10559
+ additionalUrls.push(match.url);
10560
+ mentionedUrls.add(match.url);
10561
+ refByUrl.set(match.url, match);
10562
+ if (t.lock !== void 0) lockOverrideByUrl.set(match.url, t.lock);
10563
+ const role = (t.role ?? "").trim() || shared.resolveDefaultRole(match.defaultRole, match.defaultUsageMode, match.source);
10564
+ matched.push({ token: t.token, offset: t.offset, url: match.url, role });
10565
+ }
10566
+ const slotByUrl = /* @__PURE__ */ new Map();
10567
+ for (const u of [...existingUrls, ...additionalUrls]) {
10568
+ if (!slotByUrl.has(u)) slotByUrl.set(u, slotByUrl.size + 1);
10569
+ }
10570
+ const bindingFor = (url2) => {
10571
+ const slot = slotByUrl.get(url2);
10572
+ return slot ? `reference image ${slotToLetter(slot)}` : "the reference image";
10573
+ };
10574
+ let resolvedPrompt = prompt;
10575
+ for (const m of [...matched].sort((a, b) => b.offset - a.offset)) {
10576
+ const phrase = shared.roleToPhrase(m.role, bindingFor(m.url));
10577
+ resolvedPrompt = spliceMentionPhrase(resolvedPrompt, m.offset, m.token.length, phrase);
10578
+ }
10579
+ const lockLines = [];
10580
+ const elementDirectives = [];
10581
+ const seenUrls = /* @__PURE__ */ new Set();
10582
+ for (const m of matched) {
10583
+ if (seenUrls.has(m.url)) continue;
10584
+ seenUrls.add(m.url);
10585
+ const ref = refByUrl.get(m.url);
10586
+ if (!ref) continue;
10587
+ const binding = bindingFor(m.url);
10588
+ const lock = buildIdentityLockLine(withForcedIdentityLock(ref, lockOverrideByUrl.get(m.url)), binding);
10589
+ if (lock) lockLines.push(lock);
10590
+ const inject = ref.elementInjection?.trim();
10591
+ if (inject) elementDirectives.push(inject);
10592
+ }
10593
+ return { prompt: resolvedPrompt, additionalUrls, mentionedUrls, lockLines, elementDirectives };
10594
+ }
10480
10595
  function buildCanonicalFallback(refs, mentionedSlugs, startIndex) {
10481
10596
  const directiveLines = [];
10482
10597
  const urls = [];
@@ -10635,7 +10750,7 @@ function renderLocationCanonicalHybrid(nonCharacterRefs, finalIndexByUrl, covere
10635
10750
  if (!slot) continue;
10636
10751
  seenUrls.add(r.url);
10637
10752
  const binding = `reference image ${slotToLetter(slot)}`;
10638
- phrases.push(shared.roleToPhrase(locationModeToRole(r.defaultUsageMode), binding));
10753
+ phrases.push(shared.roleToPhrase(resolveLocationRole(null, null, r), binding));
10639
10754
  const lock = buildIdentityLockLine(r, binding);
10640
10755
  if (lock) lockLines.push(lock);
10641
10756
  const inject = r.elementInjection?.trim();
@@ -10794,6 +10909,20 @@ function reconcileBodySegments(body, bodySegments) {
10794
10909
  function buildImagePrompt(config2) {
10795
10910
  return buildImagePromptInternal(config2);
10796
10911
  }
10912
+ function buildImagePromptWithOverflow(config2) {
10913
+ const marks = newAssemblyMarks();
10914
+ const result = buildImagePromptInternal(config2, marks);
10915
+ return { ...result, overflowChars: marks.overflowChars };
10916
+ }
10917
+ function newAssemblyMarks() {
10918
+ return {
10919
+ directivesPrefix: "",
10920
+ bodyBeforeSuffixes: "",
10921
+ styleSuffix: "",
10922
+ avoidSuffix: "",
10923
+ overflowChars: 0
10924
+ };
10925
+ }
10797
10926
  function buildImagePromptInternal(config2, marks) {
10798
10927
  let {
10799
10928
  provider,
@@ -10815,6 +10944,7 @@ function buildImagePromptInternal(config2, marks) {
10815
10944
  const suppressedCanonicalLocationIds = config2.suppressedCanonicalLocationIds ?? [];
10816
10945
  const isHybrid = config2.referenceFormat === "hybrid";
10817
10946
  let hybridBodyConverged = false;
10947
+ const mentionedEntityUrls = /* @__PURE__ */ new Set();
10818
10948
  if (config2.skipCharacterMentions) {
10819
10949
  config2 = {
10820
10950
  ...config2,
@@ -10859,7 +10989,11 @@ function buildImagePromptInternal(config2, marks) {
10859
10989
  connectedReferences.map((r) => r.locationSlug).filter((s) => typeof s === "string" && s.length > 0)
10860
10990
  )
10861
10991
  );
10862
- if (knownCharacterSlugs.length > 0 || hasExtraRefs || knownLocationSlugs.length > 0) {
10992
+ const knownImageSlugs = shared.knownImageSlugsFromRefs(connectedReferences);
10993
+ const hasImageMentionTokens = isHybrid && knownImageSlugs.length > 0 && shared.findImageMentionTokens(config2.prompt, knownImageSlugs).length > 0;
10994
+ const knownEntitySlugs = shared.knownEntitySlugsFromRefs(connectedReferences);
10995
+ const hasEntityMentionTokens = isHybrid && knownEntitySlugs.length > 0 && shared.findEntityMentionTokens(config2.prompt, knownEntitySlugs).length > 0;
10996
+ if (knownCharacterSlugs.length > 0 || hasExtraRefs || knownLocationSlugs.length > 0 || hasImageMentionTokens || hasEntityMentionTokens) {
10863
10997
  const mentionTokens = knownCharacterSlugs.length > 0 ? shared.findCharacterMentionTokens(config2.prompt, knownCharacterSlugs) : [];
10864
10998
  let hybridLockLines = [];
10865
10999
  let hybridElementDirectives = [];
@@ -10913,6 +11047,43 @@ function buildImagePromptInternal(config2, marks) {
10913
11047
  if (r.locationVariantBucket) return false;
10914
11048
  return !mentionedLocationSlugs.has(r.locationSlug);
10915
11049
  });
11050
+ let hybridImageLockLines = [];
11051
+ let hybridImageElementDirectives = [];
11052
+ if (hasImageMentionTokens) {
11053
+ const imageTokens = shared.findImageMentionTokens(resolved.prompt, knownImageSlugs);
11054
+ if (imageTokens.length > 0) {
11055
+ const hi = resolveImageMentionsHybrid(
11056
+ resolved.prompt,
11057
+ imageTokens,
11058
+ connectedReferences,
11059
+ [...referenceImageUrls || [], ...resolved.additionalUrls]
11060
+ );
11061
+ resolved.prompt = hi.prompt;
11062
+ resolved.additionalUrls = [...resolved.additionalUrls, ...hi.additionalUrls];
11063
+ hybridImageLockLines = hi.lockLines;
11064
+ hybridImageElementDirectives = hi.elementDirectives;
11065
+ if (hi.additionalUrls.length > 0) hybridBodyConverged = true;
11066
+ }
11067
+ }
11068
+ let hybridEntityLockLines = [];
11069
+ let hybridEntityElementDirectives = [];
11070
+ if (hasEntityMentionTokens) {
11071
+ const entityTokens = shared.findEntityMentionTokens(resolved.prompt, knownEntitySlugs);
11072
+ if (entityTokens.length > 0) {
11073
+ const he = resolveEntityMentionsHybrid(
11074
+ resolved.prompt,
11075
+ entityTokens,
11076
+ connectedReferences,
11077
+ [...referenceImageUrls || [], ...resolved.additionalUrls]
11078
+ );
11079
+ resolved.prompt = he.prompt;
11080
+ resolved.additionalUrls = [...resolved.additionalUrls, ...he.additionalUrls];
11081
+ hybridEntityLockLines = he.lockLines;
11082
+ hybridEntityElementDirectives = he.elementDirectives;
11083
+ for (const u of he.mentionedUrls) mentionedEntityUrls.add(u);
11084
+ if (he.additionalUrls.length > 0) hybridBodyConverged = true;
11085
+ }
11086
+ }
10916
11087
  const prefixDedupedLength = [
10917
11088
  ...referenceImageUrls || [],
10918
11089
  ...resolved.additionalUrls
@@ -10993,12 +11164,16 @@ ${promptForNext}`;
10993
11164
  const allLockLines = [.../* @__PURE__ */ new Set([
10994
11165
  ...hybridLockLines,
10995
11166
  ...hybridLocationLockLines,
11167
+ ...hybridImageLockLines,
11168
+ ...hybridEntityLockLines,
10996
11169
  ...canonical.lockLines,
10997
11170
  ...extrasRendered.lockLines
10998
11171
  ])];
10999
11172
  const trailingLines = [
11000
11173
  ...hybridElementDirectives,
11001
11174
  ...hybridLocationElementDirectives,
11175
+ ...hybridImageElementDirectives,
11176
+ ...hybridEntityElementDirectives,
11002
11177
  ...canonical.phrases,
11003
11178
  ...canonical.elementDirectives,
11004
11179
  ...extrasRendered.bodyLines,
@@ -11045,7 +11220,8 @@ ${trailingLines.join("\n")}` : "";
11045
11220
  if (coveredUrl) tokenCoveredUrls.add(coveredUrl);
11046
11221
  }
11047
11222
  const locCanon = renderLocationCanonicalHybrid(nonCharacterRefs, finalIndexByUrl, tokenCoveredUrls);
11048
- const objCanon = renderObjectCreatureCanonicalHybrid(nonCharacterRefs, finalIndexByUrl, tokenCoveredUrls);
11223
+ const objCoveredUrls = mentionedEntityUrls.size > 0 ? /* @__PURE__ */ new Set([...tokenCoveredUrls, ...mentionedEntityUrls]) : tokenCoveredUrls;
11224
+ const objCanon = renderObjectCreatureCanonicalHybrid(nonCharacterRefs, finalIndexByUrl, objCoveredUrls);
11049
11225
  const canonLockLines = [...locCanon.lockLines, ...objCanon.lockLines];
11050
11226
  const canonLockBlock = canonLockLines.length > 0 ? `${canonLockLines.join("\n")}
11051
11227
 
@@ -11104,11 +11280,13 @@ Avoid: ${negPrompt2}`;
11104
11280
  const maxLen2 = shared.getMaxImagePromptChars(provider);
11105
11281
  const reserved2 = styleSuffix2.length + avoidSuffix2.length;
11106
11282
  if (prompt2.length + reserved2 > maxLen2) {
11283
+ if (marks) marks.overflowChars += prompt2.length + reserved2 - maxLen2;
11107
11284
  prompt2 = prompt2.slice(0, Math.max(0, maxLen2 - reserved2 - 3)) + "...";
11108
11285
  }
11109
11286
  if (marks) marks.bodyBeforeSuffixes = prompt2.slice(marks.directivesPrefix.length);
11110
11287
  prompt2 = prompt2 + styleSuffix2 + avoidSuffix2;
11111
11288
  if (prompt2.length > maxLen2) {
11289
+ if (marks) marks.overflowChars += prompt2.length - maxLen2;
11112
11290
  prompt2 = prompt2.slice(0, maxLen2 - 3) + "...";
11113
11291
  }
11114
11292
  if (!isHybrid) {
@@ -11182,11 +11360,13 @@ Avoid: ${negPrompt}`;
11182
11360
  const maxLen = shared.getMaxImagePromptChars(provider);
11183
11361
  const reserved = styleSuffix.length + avoidSuffix.length;
11184
11362
  if (prompt.length + reserved > maxLen) {
11363
+ if (marks) marks.overflowChars += prompt.length + reserved - maxLen;
11185
11364
  prompt = prompt.slice(0, Math.max(0, maxLen - reserved - 3)) + "...";
11186
11365
  }
11187
11366
  if (marks) marks.bodyBeforeSuffixes = prompt;
11188
11367
  prompt = prompt + styleSuffix + avoidSuffix;
11189
11368
  if (prompt.length > maxLen) {
11369
+ if (marks) marks.overflowChars += prompt.length - maxLen;
11190
11370
  prompt = prompt.slice(0, maxLen - 3) + "...";
11191
11371
  }
11192
11372
  const allRefs = referenceImageUrls.length > 0 ? referenceImageUrls : ancestorRefs;
@@ -11196,12 +11376,7 @@ Avoid: ${negPrompt}`;
11196
11376
  return { prompt, nativeNegativePrompt, referenceImageUrls: refsToSend };
11197
11377
  }
11198
11378
  function buildImagePromptSegments(config2, bodySegments) {
11199
- const marks = {
11200
- directivesPrefix: "",
11201
- bodyBeforeSuffixes: "",
11202
- styleSuffix: "",
11203
- avoidSuffix: ""
11204
- };
11379
+ const marks = newAssemblyMarks();
11205
11380
  const result = buildImagePromptInternal(config2, marks);
11206
11381
  let segments = [
11207
11382
  ...marks.directivesPrefix ? [{ text: marks.directivesPrefix, origin: "mention" }] : [],
@@ -11391,13 +11566,24 @@ function hybridRolePhrase(label, letter) {
11391
11566
  return `the ${label} from reference image ${letter}`;
11392
11567
  }
11393
11568
  function expandImageRefTokensHybrid(prompt, refs, finalIndexByUrl) {
11394
- return prompt.replace(IMAGE_TOKEN_PATTERN, (match, num, label) => {
11395
- const n = parseInt(num, 10);
11569
+ const splices = [];
11570
+ for (const m of prompt.matchAll(IMAGE_TOKEN_PATTERN)) {
11571
+ const n = parseInt(m[1], 10);
11396
11572
  const ref = refs[n - 1];
11397
11573
  const finalIdx = ref?.url ? finalIndexByUrl.get(ref.url) : void 0;
11398
- if (!finalIdx) return match;
11399
- return hybridRolePhrase((label ?? "").trim(), slotToLetter(finalIdx));
11400
- });
11574
+ if (!finalIdx) continue;
11575
+ splices.push({
11576
+ offset: m.index ?? 0,
11577
+ length: m[0].length,
11578
+ phrase: hybridRolePhrase((m[2] ?? "").trim(), slotToLetter(finalIdx))
11579
+ });
11580
+ }
11581
+ let resolved = prompt;
11582
+ for (let i = splices.length - 1; i >= 0; i--) {
11583
+ const s = splices[i];
11584
+ resolved = spliceMentionPhrase(resolved, s.offset, s.length, s.phrase);
11585
+ }
11586
+ return resolved;
11401
11587
  }
11402
11588
  function capitalizeLineInitial(line) {
11403
11589
  return line.replace(/[a-zA-Z]/, (c) => c.toUpperCase());
@@ -11622,6 +11808,266 @@ function renderLens(l) {
11622
11808
  return bits.length > 0 ? `Lens: ${bits.join(", ")}.` : "";
11623
11809
  }
11624
11810
 
11811
+ // src/direction-registry.ts
11812
+ var perId = (full, compact) => (ids, mode) => {
11813
+ const out = [];
11814
+ for (const id of ids) {
11815
+ const frag = mode === "compact" ? compact(id) : full(id);
11816
+ if (frag.length > 0) out.push(frag);
11817
+ }
11818
+ return out;
11819
+ };
11820
+ var viaListBuilder = (build) => (ids, mode) => build(ids.length === 1 ? ids[0] : [...ids], mode).filter((s) => s.length > 0);
11821
+ var viaStringBuilder = (build) => (ids, mode) => {
11822
+ const s = build(ids.length === 1 ? ids[0] : [...ids], mode);
11823
+ return s.length > 0 ? [s] : [];
11824
+ };
11825
+ var viaMood = (ids, mode) => buildMoodHints({ mood: ids.length === 1 ? ids[0] : [...ids] }, mode).filter(
11826
+ (s) => s.length > 0
11827
+ );
11828
+ var framing = perId(getFramingPromptHint, getFramingTerm);
11829
+ var lighting = perId(getLightingPromptHint, getLightingTerm);
11830
+ var exposure = perId(getExposurePromptHint, getExposureTerm);
11831
+ var temporal = perId(getTemporalPromptHint, getTemporalTerm);
11832
+ var DIRECTION_FIELDS = [
11833
+ { key: "cameraMotion", surface: "video", family: "motion", maxPicks: 1, render: perId(getCameraMotionPromptHint, getCameraMotionTerm) },
11834
+ // Composition (the Framing catalog, one row per category).
11835
+ { key: "shotSize", surface: "both", family: "look", maxPicks: 1, render: framing },
11836
+ { key: "angle", surface: "both", family: "look", maxPicks: 1, render: framing },
11837
+ { key: "coverage", surface: "both", family: "look", maxPicks: 1, render: framing },
11838
+ { key: "composition", surface: "both", family: "look", maxPicks: 2, render: framing },
11839
+ { key: "vantage", surface: "both", family: "look", maxPicks: 1, render: framing },
11840
+ { key: "pose", surface: "both", family: "look", maxPicks: 1, render: perId(getPosePromptHint, getPoseTerm) },
11841
+ { key: "compositionEffect", surface: "both", family: "look", maxPicks: 1, render: perId(getCompositionEffectPromptHint, getCompositionEffectTerm) },
11842
+ // Camera.
11843
+ { key: "cameraFormat", surface: "both", family: "look", maxPicks: 1, render: perId(getCameraFormatPromptHint, getCameraFormatTerm) },
11844
+ { key: "lens", surface: "both", family: "look", maxPicks: 1, render: perId(getLensPromptHint, getLensTerm) },
11845
+ // Exposure (stills only — a video's exposure rides its own temporal levers).
11846
+ { key: "aperture", surface: "image", family: "look", maxPicks: 1, render: exposure },
11847
+ { key: "shutterSpeed", surface: "image", family: "look", maxPicks: 1, render: exposure },
11848
+ { key: "isoValue", surface: "image", family: "look", maxPicks: 1, render: exposure },
11849
+ // Light (the Lighting catalog, one row per category).
11850
+ { key: "timeOfDay", surface: "both", family: "look", maxPicks: 1, render: lighting },
11851
+ { key: "lightingStyle", surface: "both", family: "look", maxPicks: 2, render: lighting },
11852
+ { key: "lightingDirection", surface: "both", family: "look", maxPicks: 1, render: lighting },
11853
+ { key: "lightingRatio", surface: "both", family: "look", maxPicks: 1, render: lighting },
11854
+ { key: "colorTemperature", surface: "both", family: "look", maxPicks: 1, render: lighting },
11855
+ { key: "colorLook", surface: "both", family: "look", maxPicks: 1, render: perId(getColorLookPromptHint, getColorLookTerm) },
11856
+ { key: "atmosphere", surface: "both", family: "look", maxPicks: 2, render: viaListBuilder(buildAtmosphereHints) },
11857
+ { key: "postProcess", surface: "image", family: "look", maxPicks: 2, render: viaListBuilder(buildPostProcessHints) },
11858
+ // Style.
11859
+ { key: "style", surface: "both", family: "look", maxPicks: 1, render: perId(getStylePromptHint, getStyleTerm) },
11860
+ { key: "mood", surface: "both", family: "look", maxPicks: 2, render: viaMood },
11861
+ { key: "aesthetic", surface: "both", family: "look", maxPicks: 2, render: viaStringBuilder(buildAestheticHints) },
11862
+ { key: "photoGenre", surface: "image", family: "look", maxPicks: 1, render: perId(getPhotoGenrePromptHint, getPhotoGenreTerm) },
11863
+ { key: "photographer", surface: "image", family: "look", maxPicks: 2, render: viaStringBuilder(buildPhotographerHints) },
11864
+ { key: "renderQuality", surface: "image", family: "look", maxPicks: 1, render: perId(getRenderQualityPromptHint, getRenderQualityTerm) },
11865
+ // Scene.
11866
+ { key: "setting", surface: "both", family: "look", maxPicks: 1, render: perId(getSettingPromptHint, getSettingTerm) },
11867
+ { key: "era", surface: "both", family: "look", maxPicks: 1, render: perId(getEraPromptHint, getEraTerm) },
11868
+ { key: "backdrop", surface: "both", family: "look", maxPicks: 1, render: perId(getBackdropPromptHint, getBackdropTerm) },
11869
+ // Motion & time.
11870
+ { key: "actionFx", surface: "video", family: "motion", maxPicks: 2, render: viaListBuilder(buildActionFxHints) },
11871
+ { key: "temporalSpeed", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11872
+ { key: "temporalFreeze", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11873
+ { key: "temporalDirection", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11874
+ { key: "temporalShutter", surface: "video", family: "motion", maxPicks: 1, render: temporal },
11875
+ { key: "transition", surface: "video", family: "motion", maxPicks: 2, render: perId(getTransitionPromptHint, getTransitionTerm) },
11876
+ { key: "loopSubject", surface: "video", family: "motion", maxPicks: 1, render: perId(getLoopSubjectPromptHint, getLoopSubjectTerm) },
11877
+ // ── LEGACY BLOCK — see the table doc above. Placed LAST, in today's exact
11878
+ // `composePromptText` order, so every pre-registry caller is byte-identical.
11879
+ { key: "framingId", surface: "both", family: "look", maxPicks: 1, render: framing },
11880
+ { key: "framingAngleId", surface: "both", family: "look", maxPicks: 1, render: framing },
11881
+ { key: "lightingId", surface: "both", family: "look", maxPicks: 1, render: lighting },
11882
+ { key: "lensId", surface: "both", family: "look", maxPicks: 1, render: perId(getLensPromptHint, getLensTerm) },
11883
+ { key: "cameraFormatId", surface: "both", family: "look", maxPicks: 1, render: perId(getCameraFormatPromptHint, getCameraFormatTerm) }
11884
+ ];
11885
+ var DIRECTION_KEYS = DIRECTION_FIELDS.map((f) => f.key);
11886
+ var IMAGE_HINT_MODE_DEFAULT = "full";
11887
+ var VIDEO_HINT_MODE_DEFAULT = { look: "full", motion: "compact" };
11888
+ function modeForFamily(mode, family) {
11889
+ return typeof mode === "string" ? mode : family === "motion" ? mode.motion : mode.look;
11890
+ }
11891
+ var DIRECTION_ARRAY_CEILING = 8;
11892
+ var DIRECTION_ID_MAX_CHARS = 100;
11893
+ function normalizeDirectionIds(value, maxPicks) {
11894
+ const ids = [];
11895
+ if (typeof value === "string") {
11896
+ if (value) ids.push(value);
11897
+ } else if (Array.isArray(value)) {
11898
+ for (const v of value) {
11899
+ if (ids.length >= maxPicks) break;
11900
+ if (typeof v === "string" && v && !ids.includes(v)) ids.push(v);
11901
+ }
11902
+ }
11903
+ return ids.slice(0, maxPicks);
11904
+ }
11905
+ function directionFieldsForSurface(surface) {
11906
+ return DIRECTION_FIELDS.filter((f) => f.surface === "both" || f.surface === surface);
11907
+ }
11908
+ function renderDirectionHints(direction, opts) {
11909
+ if (!direction) return [];
11910
+ const mode = opts.mode ?? "full";
11911
+ const out = [];
11912
+ const seen = /* @__PURE__ */ new Set();
11913
+ for (const spec of DIRECTION_FIELDS) {
11914
+ if (spec.surface !== "both" && spec.surface !== opts.surface) continue;
11915
+ const ids = normalizeDirectionIds(
11916
+ direction[spec.key],
11917
+ spec.maxPicks
11918
+ );
11919
+ if (ids.length === 0) continue;
11920
+ for (const hint of spec.render(ids, modeForFamily(mode, spec.family))) {
11921
+ if (hint.length > 0 && !seen.has(hint)) {
11922
+ seen.add(hint);
11923
+ out.push(hint);
11924
+ }
11925
+ }
11926
+ }
11927
+ return out;
11928
+ }
11929
+ var viaFragmentBuilder = (build) => (subject, mode) => {
11930
+ const clause = build(subject, mode).filter((s) => s.length > 0).join(", ");
11931
+ return clause.length > 0 ? [clause] : [];
11932
+ };
11933
+ var viaListBuilder2 = (build) => (ids, mode) => build(ids.length === 1 ? ids[0] : [...ids], mode).filter((s) => s.length > 0);
11934
+ var viaStringBuilder2 = (build) => (ids, mode) => {
11935
+ const s = build(ids.length === 1 ? ids[0] : [...ids], mode);
11936
+ return s.length > 0 ? [s] : [];
11937
+ };
11938
+ var perId2 = (full, compact) => (ids, mode) => {
11939
+ const out = [];
11940
+ for (const id of ids) {
11941
+ const frag = mode === "compact" ? compact(id) : full(id);
11942
+ if (frag.length > 0) out.push(frag);
11943
+ }
11944
+ return out;
11945
+ };
11946
+ var SUBJECT_FIELDS = [
11947
+ { key: "person", kind: "group", surface: "both", render: viaFragmentBuilder(buildPersonHints) },
11948
+ { key: "styling", kind: "group", surface: "both", render: viaFragmentBuilder(buildStylingHints) },
11949
+ { key: "heldProp", kind: "ids", surface: "both", maxPicks: 2, render: viaListBuilder2(buildHeldPropHints) },
11950
+ { key: "material", kind: "ids", surface: "both", maxPicks: 2, render: viaStringBuilder2(buildMaterialHints) },
11951
+ { key: "animal", kind: "ids", surface: "both", maxPicks: 1, render: perId2(shared.getAnimalPromptHint, shared.getAnimalTerm) }
11952
+ ];
11953
+ var SUBJECT_FOLD_KEYS = SUBJECT_FIELDS.map((f) => f.key);
11954
+ var IDS_ROWS = SUBJECT_FIELDS.filter((f) => f.kind === "ids");
11955
+ var SUBJECT_CUSTOM_AGE_KEY = "customAge";
11956
+ var CUSTOM_AGE_MIN = 0;
11957
+ var CUSTOM_AGE_MAX = 120;
11958
+ var SUBJECT_KEYS = [
11959
+ ...PERSON_DIMENSION_ORDER.map((d) => PERSON_FIELD_BY_DIMENSION[d]),
11960
+ SUBJECT_CUSTOM_AGE_KEY,
11961
+ ...STYLING_DIMENSION_ORDER.map((d) => STYLING_FIELD_BY_DIMENSION[d]),
11962
+ ...IDS_ROWS.map((f) => f.key)
11963
+ ];
11964
+ function getRegisteredSubjectKeys() {
11965
+ const fieldByDimension = getRegisteredPersonFieldByDimension();
11966
+ const personFields = [];
11967
+ for (const dimension of getRegisteredPersonDimensionOrder()) {
11968
+ const field = fieldByDimension[dimension];
11969
+ if (field) personFields.push(field);
11970
+ }
11971
+ return [
11972
+ ...personFields,
11973
+ SUBJECT_CUSTOM_AGE_KEY,
11974
+ ...STYLING_DIMENSION_ORDER.map((d) => STYLING_FIELD_BY_DIMENSION[d]),
11975
+ ...IDS_ROWS.map((f) => f.key)
11976
+ ];
11977
+ }
11978
+ var SUBJECT_IMAGE_HINT_MODE_DEFAULT = "full";
11979
+ var SUBJECT_VIDEO_HINT_MODE_DEFAULT = "compact";
11980
+ var SUBJECT_ARRAY_CEILING = DIRECTION_ARRAY_CEILING;
11981
+ var SUBJECT_ID_MAX_CHARS = DIRECTION_ID_MAX_CHARS;
11982
+ var MAX_SUBJECT_KEYS = 128;
11983
+ var SUBJECT_KEY_MAX_CHARS = 64;
11984
+ function normalizeIds(value, maxPicks) {
11985
+ const ids = [];
11986
+ if (typeof value === "string") {
11987
+ if (value) ids.push(value);
11988
+ } else if (Array.isArray(value)) {
11989
+ for (const v of value) {
11990
+ if (ids.length >= maxPicks) break;
11991
+ if (typeof v === "string" && v && !ids.includes(v)) ids.push(v);
11992
+ }
11993
+ }
11994
+ return ids.slice(0, maxPicks);
11995
+ }
11996
+ function limitByField() {
11997
+ const out = /* @__PURE__ */ new Map();
11998
+ const personFieldByDimension = getRegisteredPersonFieldByDimension();
11999
+ for (const dimension of getRegisteredPersonDimensionOrder()) {
12000
+ const field = personFieldByDimension[dimension];
12001
+ if (field) out.set(field, getPersonDimensionLimit(dimension));
12002
+ }
12003
+ for (const dimension of STYLING_DIMENSION_ORDER) {
12004
+ out.set(
12005
+ STYLING_FIELD_BY_DIMENSION[dimension],
12006
+ getStylingDimensionLimit(dimension)
12007
+ );
12008
+ }
12009
+ for (const row of IDS_ROWS) out.set(row.key, row.maxPicks);
12010
+ return out;
12011
+ }
12012
+ function normalizeSubjectFields(subject) {
12013
+ if (!subject || typeof subject !== "object" || Array.isArray(subject)) return void 0;
12014
+ const src = subject;
12015
+ const limits = limitByField();
12016
+ const out = {};
12017
+ const rawAge = src[SUBJECT_CUSTOM_AGE_KEY];
12018
+ if (typeof rawAge === "number" && Number.isFinite(rawAge)) {
12019
+ out[SUBJECT_CUSTOM_AGE_KEY] = Math.max(
12020
+ CUSTOM_AGE_MIN,
12021
+ Math.min(CUSTOM_AGE_MAX, Math.round(rawAge))
12022
+ );
12023
+ }
12024
+ for (const [field, limit] of limits) {
12025
+ const ids = normalizeIds(src[field], limit);
12026
+ if (ids.length === 0) continue;
12027
+ out[field] = ids.length === 1 ? ids[0] : ids;
12028
+ }
12029
+ return Object.keys(out).length > 0 ? out : void 0;
12030
+ }
12031
+ function subjectFieldsForSurface(surface) {
12032
+ return SUBJECT_FIELDS.filter((f) => f.surface === "both" || f.surface === surface);
12033
+ }
12034
+ function renderSubjectHints(subject, opts) {
12035
+ const bag = normalizeSubjectFields(subject);
12036
+ if (!bag) return [];
12037
+ const mode = opts.mode ?? "full";
12038
+ const out = [];
12039
+ const seen = /* @__PURE__ */ new Set();
12040
+ for (const spec of SUBJECT_FIELDS) {
12041
+ if (spec.surface !== "both" && spec.surface !== opts.surface) continue;
12042
+ const hints = spec.kind === "group" ? spec.render(bag, mode) : spec.render(normalizeIds(bag[spec.key], spec.maxPicks), mode);
12043
+ for (const hint of hints) {
12044
+ if (hint.length > 0 && !seen.has(hint)) {
12045
+ seen.add(hint);
12046
+ out.push(hint);
12047
+ }
12048
+ }
12049
+ }
12050
+ return out;
12051
+ }
12052
+
12053
+ // src/prompt-hint-join.ts
12054
+ var PROMPT_HINT_SEPARATOR = ". ";
12055
+ function joinPromptHints(userPrompt, hints) {
12056
+ if (hints.length === 0) return userPrompt;
12057
+ return [userPrompt.trim(), ...hints].filter((p) => p.length > 0).join(PROMPT_HINT_SEPARATOR);
12058
+ }
12059
+
12060
+ // src/hint-shedding.ts
12061
+ function keepableDirectionHints(hintClauses, kept, deficit) {
12062
+ let remaining = deficit;
12063
+ let next = kept;
12064
+ while (next > 0 && remaining > 0) {
12065
+ next -= 1;
12066
+ remaining -= hintClauses[next].length + PROMPT_HINT_SEPARATOR.length;
12067
+ }
12068
+ return next;
12069
+ }
12070
+
11625
12071
  // src/ref-binding.ts
11626
12072
  function identityRefsSentence(firstOrdinal, lastOrdinal) {
11627
12073
  return firstOrdinal === lastOrdinal ? `${REF_BINDING.ordinal(firstOrdinal)} is an identity reference for this shot's subjects \u2014 match its subject's exact appearance; it is not a frame.` : `${REF_BINDING.ordinal(firstOrdinal)} through ${REF_BINDING.ordinal(lastOrdinal)} are identity references for this shot's subjects \u2014 match each subject's exact appearance; they are not frames.`;
@@ -12207,22 +12653,32 @@ function assembleSunoInput(input) {
12207
12653
  }
12208
12654
 
12209
12655
  // src/assemble-image-input.ts
12210
- function composePromptText(userPrompt, direction, structured) {
12656
+ function renderImageHintPieces(subject, direction, structured) {
12657
+ return {
12658
+ hintClauses: [
12659
+ ...renderSubjectHints(subject, {
12660
+ surface: "image",
12661
+ mode: SUBJECT_IMAGE_HINT_MODE_DEFAULT
12662
+ }),
12663
+ ...renderDirectionHints(direction, {
12664
+ surface: "image",
12665
+ mode: IMAGE_HINT_MODE_DEFAULT
12666
+ })
12667
+ ].filter((p) => p.length > 0),
12668
+ structuredFragment: structured ? renderStructuredFields(structured) : ""
12669
+ };
12670
+ }
12671
+ function composePromptText(userPrompt, pieces, keptHintClauses) {
12211
12672
  const hints = [
12212
- getFramingPromptHint(direction?.framingId),
12213
- getFramingPromptHint(direction?.framingAngleId),
12214
- getLightingPromptHint(direction?.lightingId),
12215
- getLensPromptHint(direction?.lensId),
12216
- getCameraFormatPromptHint(direction?.cameraFormatId),
12217
- structured ? renderStructuredFields(structured) : ""
12673
+ ...pieces.hintClauses.slice(0, keptHintClauses),
12674
+ pieces.structuredFragment
12218
12675
  ].filter((p) => p.length > 0);
12219
- if (hints.length === 0) return userPrompt;
12220
- return [userPrompt.trim(), ...hints].filter((p) => p.length > 0).join(". ");
12676
+ return joinPromptHints(userPrompt, hints);
12221
12677
  }
12222
12678
  function assembleImageInput(input) {
12223
- const prompt = composePromptText(input.userPrompt, input.direction, input.structured);
12224
- const result = buildImagePrompt({
12225
- prompt,
12679
+ const pieces = renderImageHintPieces(input.subject, input.direction, input.structured);
12680
+ const assembleWith = (keptHintClauses) => buildImagePromptWithOverflow({
12681
+ prompt: composePromptText(input.userPrompt, pieces, keptHintClauses),
12226
12682
  provider: input.provider,
12227
12683
  ...input.connectedReferences !== void 0 ? { connectedReferences: input.connectedReferences } : {},
12228
12684
  // Manual uploads / direct refs ride the builder's reference-URL channel so
@@ -12245,6 +12701,13 @@ function assembleImageInput(input) {
12245
12701
  ...input.sourceNodeIdById !== void 0 ? { sourceNodeIdById: input.sourceNodeIdById } : {},
12246
12702
  ...input.skipCharacterMentions !== void 0 ? { skipCharacterMentions: input.skipCharacterMentions } : {}
12247
12703
  });
12704
+ let kept = pieces.hintClauses.length;
12705
+ let fitted = assembleWith(kept);
12706
+ while (fitted.overflowChars > 0 && kept > 0) {
12707
+ kept = keepableDirectionHints(pieces.hintClauses, kept, fitted.overflowChars);
12708
+ fitted = assembleWith(kept);
12709
+ }
12710
+ const { overflowChars, ...result } = fitted;
12248
12711
  if (input.throwOnEmpty && !result.prompt.trim()) {
12249
12712
  throw new Error(
12250
12713
  "No prompt \u2014 type one, bind a character, or pick a cinematography direction"
@@ -12252,6 +12715,152 @@ function assembleImageInput(input) {
12252
12715
  }
12253
12716
  return result;
12254
12717
  }
12718
+
12719
+ // src/assemble-video-input.ts
12720
+ function composeVideoPromptText(userPrompt, direction, structured, opts) {
12721
+ const hintClauses = [
12722
+ ...renderSubjectHints(opts?.subject, {
12723
+ surface: "video",
12724
+ mode: opts?.subjectHintMode ?? SUBJECT_VIDEO_HINT_MODE_DEFAULT
12725
+ }),
12726
+ ...renderDirectionHints(direction, {
12727
+ surface: "video",
12728
+ mode: opts?.hintMode ?? VIDEO_HINT_MODE_DEFAULT
12729
+ })
12730
+ ].filter((p) => p.length > 0);
12731
+ const structuredFragment = structured ? renderStructuredFields(structured) : "";
12732
+ const composeWith = (kept2) => {
12733
+ const hints = [...hintClauses.slice(0, kept2), structuredFragment].filter(
12734
+ (p) => p.length > 0
12735
+ );
12736
+ if (hints.length === 0) return userPrompt;
12737
+ return joinPromptHints(userPrompt ?? "", hints);
12738
+ };
12739
+ const cap = opts?.cap;
12740
+ if (cap === void 0) return composeWith(hintClauses.length);
12741
+ const frame = opts?.frame ?? ((body2) => body2);
12742
+ let kept = hintClauses.length;
12743
+ let body = composeWith(kept);
12744
+ let framedLength = frame(body)?.length ?? 0;
12745
+ while (framedLength > cap && kept > 0) {
12746
+ kept = keepableDirectionHints(hintClauses, kept, framedLength - cap);
12747
+ body = composeWith(kept);
12748
+ framedLength = frame(body)?.length ?? 0;
12749
+ }
12750
+ return body;
12751
+ }
12752
+
12753
+ // src/read-node-direction.ts
12754
+ function readDirectionFields(value) {
12755
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
12756
+ const src = value;
12757
+ const out = {};
12758
+ for (const spec of DIRECTION_FIELDS) {
12759
+ const v = src[spec.key];
12760
+ if (typeof v === "string") {
12761
+ if (v.length > 0 && v.length <= DIRECTION_ID_MAX_CHARS) out[spec.key] = v;
12762
+ } else if (Array.isArray(v)) {
12763
+ const kept = [];
12764
+ for (const x of v) {
12765
+ if (kept.length >= DIRECTION_ARRAY_CEILING) break;
12766
+ if (typeof x === "string" && x.length > 0 && x.length <= DIRECTION_ID_MAX_CHARS) {
12767
+ kept.push(x);
12768
+ }
12769
+ }
12770
+ if (kept.length > 0) out[spec.key] = kept;
12771
+ }
12772
+ }
12773
+ return Object.keys(out).length > 0 ? out : void 0;
12774
+ }
12775
+ function readSubjectFields(value) {
12776
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
12777
+ const src = value;
12778
+ const out = {};
12779
+ for (const key of getRegisteredSubjectKeys()) {
12780
+ const v = src[key];
12781
+ if (key === SUBJECT_CUSTOM_AGE_KEY) {
12782
+ if (typeof v === "number" && Number.isFinite(v)) out[key] = v;
12783
+ continue;
12784
+ }
12785
+ if (typeof v === "string") {
12786
+ if (v.length > 0 && v.length <= SUBJECT_ID_MAX_CHARS) out[key] = v;
12787
+ } else if (Array.isArray(v)) {
12788
+ const kept = [];
12789
+ for (const x of v) {
12790
+ if (kept.length >= SUBJECT_ARRAY_CEILING) break;
12791
+ if (typeof x === "string" && x.length > 0 && x.length <= SUBJECT_ID_MAX_CHARS) {
12792
+ kept.push(x);
12793
+ }
12794
+ }
12795
+ if (kept.length > 0) out[key] = kept;
12796
+ }
12797
+ }
12798
+ return Object.keys(out).length > 0 ? out : void 0;
12799
+ }
12800
+ var GENDERS = ["man", "woman", "child", "non-binary"];
12801
+ var MAX_STRUCTURED_VALUE_CHARS = 200;
12802
+ var PERSON_FIELDS2 = {
12803
+ age: "number",
12804
+ gender: "gender",
12805
+ hair: "string",
12806
+ eyes: "string",
12807
+ expression: "string",
12808
+ profession: "string",
12809
+ warriorType: "string"
12810
+ };
12811
+ var STYLING_FIELDS2 = {
12812
+ mood: "string",
12813
+ lighting: "string",
12814
+ aesthetic: "string",
12815
+ colorLook: "string"
12816
+ };
12817
+ var SETTING_FIELDS = {
12818
+ era: "string",
12819
+ atmosphere: "string",
12820
+ backdrop: "string"
12821
+ };
12822
+ var CAMERA_FIELDS = {
12823
+ framing: "string",
12824
+ motion: "string",
12825
+ format: "string"
12826
+ };
12827
+ var LENS_FIELDS = { focalLength: "string", aperture: "string" };
12828
+ var STRUCTURED_GROUPS = {
12829
+ person: PERSON_FIELDS2,
12830
+ styling: STYLING_FIELDS2,
12831
+ setting: SETTING_FIELDS,
12832
+ camera: CAMERA_FIELDS,
12833
+ lens: LENS_FIELDS
12834
+ };
12835
+ function readField(v, kind) {
12836
+ if (kind === "number") return typeof v === "number" && Number.isFinite(v) ? v : void 0;
12837
+ if (typeof v !== "string" || v.length === 0 || v.length > MAX_STRUCTURED_VALUE_CHARS) {
12838
+ return void 0;
12839
+ }
12840
+ if (kind === "gender") return GENDERS.includes(v) ? v : void 0;
12841
+ return v;
12842
+ }
12843
+ function readStructuredFields(value) {
12844
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
12845
+ const src = value;
12846
+ const out = {};
12847
+ for (const group of Object.keys(STRUCTURED_GROUPS)) {
12848
+ const raw = src[group];
12849
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
12850
+ const rawRec = raw;
12851
+ const kept = {};
12852
+ for (const [field, kind] of Object.entries(
12853
+ STRUCTURED_GROUPS[group]
12854
+ )) {
12855
+ const v = readField(rawRec[field], kind);
12856
+ if (v !== void 0) kept[field] = v;
12857
+ }
12858
+ if (Object.keys(kept).length > 0) out[group] = kept;
12859
+ }
12860
+ const mood = readField(src.mood, "string");
12861
+ if (mood !== void 0) out.mood = mood;
12862
+ return Object.keys(out).length > 0 ? out : void 0;
12863
+ }
12255
12864
  var clean = (u) => {
12256
12865
  const t = u?.trim();
12257
12866
  return t && t.length > 0 ? t : void 0;
@@ -33268,8 +33877,8 @@ var SINGLE_PICKER_WIRING = [
33268
33877
  { kind: "single", nodeType: "furniture", label: "Furniture", valueField: "furniture", defaultValue: "sofa", catalogId: "furniture", entries: mapCat(shared.FURNITURE, "subcategory"), groupOrder: shared.FURNITURE_SUBCATEGORY_ORDER, groupLabels: shared.FURNITURE_SUBCATEGORY_LABELS },
33269
33878
  { kind: "single", nodeType: "held-prop", label: "Held Prop", valueField: "heldProp", defaultValue: "smartphone", catalogId: "held-prop", entries: mapCat(HELD_PROPS, "category"), groupOrder: HELD_PROP_CATEGORY_ORDER, groupLabels: HELD_PROP_CATEGORY_LABELS }
33270
33879
  ];
33271
- var STYLING_FIELDS2 = STYLING_DIMENSION_ORDER.map((d) => STYLING_FIELD_BY_DIMENSION[d]);
33272
- var PERSON_FIELDS2 = PERSON_DIMENSION_ORDER.map((d) => PERSON_FIELD_BY_DIMENSION[d]);
33880
+ var STYLING_FIELDS3 = STYLING_DIMENSION_ORDER.map((d) => STYLING_FIELD_BY_DIMENSION[d]);
33881
+ var PERSON_FIELDS3 = PERSON_DIMENSION_ORDER.map((d) => PERSON_FIELD_BY_DIMENSION[d]);
33273
33882
  var LIGHTING_FIELDS2 = LIGHTING_CATEGORY_ORDER.map((c) => LIGHTING_FIELD_BY_CATEGORY[c]);
33274
33883
  var TEMPORAL_FIELD_BY_CATEGORY2 = {
33275
33884
  speed: "temporalSpeed",
@@ -33305,7 +33914,7 @@ var MULTI_PICKER_WIRING = [
33305
33914
  kind: "multi",
33306
33915
  nodeType: "person",
33307
33916
  label: "Person",
33308
- fields: PERSON_FIELDS2,
33917
+ fields: PERSON_FIELDS3,
33309
33918
  catalogId: "person",
33310
33919
  catalogEntries: flatCat(PEOPLE),
33311
33920
  fieldOptions: optionsByDiscriminator(PEOPLE, (e) => e.dimension, PERSON_FIELD_BY_DIMENSION)
@@ -33314,7 +33923,7 @@ var MULTI_PICKER_WIRING = [
33314
33923
  kind: "multi",
33315
33924
  nodeType: "styling",
33316
33925
  label: "Styling",
33317
- fields: STYLING_FIELDS2,
33926
+ fields: STYLING_FIELDS3,
33318
33927
  catalogId: "styling",
33319
33928
  catalogEntries: flatCat(STYLINGS),
33320
33929
  fieldOptions: optionsByDiscriminator(STYLINGS, (e) => e.dimension, STYLING_FIELD_BY_DIMENSION)
@@ -33496,6 +34105,10 @@ exports.COMPOSITION_EFFECTS = COMPOSITION_EFFECTS;
33496
34105
  exports.COMPOSITION_EFFECT_IDS = COMPOSITION_EFFECT_IDS;
33497
34106
  exports.DEFAULT_IDENTITY_LOCK = DEFAULT_IDENTITY_LOCK;
33498
34107
  exports.DEFAULT_TEMPLATES = DEFAULT_TEMPLATES;
34108
+ exports.DIRECTION_ARRAY_CEILING = DIRECTION_ARRAY_CEILING;
34109
+ exports.DIRECTION_FIELDS = DIRECTION_FIELDS;
34110
+ exports.DIRECTION_ID_MAX_CHARS = DIRECTION_ID_MAX_CHARS;
34111
+ exports.DIRECTION_KEYS = DIRECTION_KEYS;
33499
34112
  exports.ERAS = ERAS;
33500
34113
  exports.ERA_CATEGORY_LABELS = ERA_CATEGORY_LABELS;
33501
34114
  exports.ERA_CATEGORY_ORDER = ERA_CATEGORY_ORDER;
@@ -33518,6 +34131,7 @@ exports.HELD_PROPS = HELD_PROPS;
33518
34131
  exports.HELD_PROP_CATEGORY_LABELS = HELD_PROP_CATEGORY_LABELS;
33519
34132
  exports.HELD_PROP_CATEGORY_ORDER = HELD_PROP_CATEGORY_ORDER;
33520
34133
  exports.HELD_PROP_IDS = HELD_PROP_IDS;
34134
+ exports.IMAGE_HINT_MODE_DEFAULT = IMAGE_HINT_MODE_DEFAULT;
33521
34135
  exports.IMAGE_REFERENCE_PROMPT_DOCTRINE = IMAGE_REFERENCE_PROMPT_DOCTRINE;
33522
34136
  exports.IMAGE_WIZARD_CATEGORIES = IMAGE_WIZARD_CATEGORIES;
33523
34137
  exports.INSTRUMENTATION_DEFAULT_DATA = INSTRUMENTATION_DEFAULT_DATA;
@@ -33542,6 +34156,7 @@ exports.MATERIAL_IDS = MATERIAL_IDS;
33542
34156
  exports.MAX_SELECTED_BY_DIMENSION = MAX_SELECTED_BY_DIMENSION;
33543
34157
  exports.MAX_SELECTED_BY_FRAMING_CATEGORY = MAX_SELECTED_BY_FRAMING_CATEGORY;
33544
34158
  exports.MAX_SELECTED_BY_STYLING_DIMENSION = MAX_SELECTED_BY_STYLING_DIMENSION;
34159
+ exports.MAX_SUBJECT_KEYS = MAX_SUBJECT_KEYS;
33545
34160
  exports.MOODS = MOODS;
33546
34161
  exports.MOOD_CATEGORY_LABELS = MOOD_CATEGORY_LABELS;
33547
34162
  exports.MOOD_CATEGORY_ORDER = MOOD_CATEGORY_ORDER;
@@ -33595,6 +34210,7 @@ exports.POST_PROCESS_EFFECT_IDS = POST_PROCESS_EFFECT_IDS;
33595
34210
  exports.PRODUCTION_STYLES = PRODUCTION_STYLES;
33596
34211
  exports.PROMPT_AFFIX_CORE_FIELD_OVERRIDES = PROMPT_AFFIX_CORE_FIELD_OVERRIDES;
33597
34212
  exports.PROMPT_AFFIX_NODE_TYPES = PROMPT_AFFIX_NODE_TYPES;
34213
+ exports.PROMPT_HINT_SEPARATOR = PROMPT_HINT_SEPARATOR;
33598
34214
  exports.PROVIDER_CAPABILITIES = PROVIDER_CAPABILITIES;
33599
34215
  exports.PROVIDER_PROMPT_DOCTRINES = PROVIDER_PROMPT_DOCTRINES;
33600
34216
  exports.REFERENCE_IMAGE_ROLES = REFERENCE_IMAGE_ROLES;
@@ -33620,6 +34236,15 @@ exports.STYLING_DIMENSION_LABELS = STYLING_DIMENSION_LABELS;
33620
34236
  exports.STYLING_DIMENSION_ORDER = STYLING_DIMENSION_ORDER;
33621
34237
  exports.STYLING_FIELD_BY_DIMENSION = STYLING_FIELD_BY_DIMENSION;
33622
34238
  exports.STYLING_IDS = STYLING_IDS;
34239
+ exports.SUBJECT_ARRAY_CEILING = SUBJECT_ARRAY_CEILING;
34240
+ exports.SUBJECT_CUSTOM_AGE_KEY = SUBJECT_CUSTOM_AGE_KEY;
34241
+ exports.SUBJECT_FIELDS = SUBJECT_FIELDS;
34242
+ exports.SUBJECT_FOLD_KEYS = SUBJECT_FOLD_KEYS;
34243
+ exports.SUBJECT_ID_MAX_CHARS = SUBJECT_ID_MAX_CHARS;
34244
+ exports.SUBJECT_IMAGE_HINT_MODE_DEFAULT = SUBJECT_IMAGE_HINT_MODE_DEFAULT;
34245
+ exports.SUBJECT_KEYS = SUBJECT_KEYS;
34246
+ exports.SUBJECT_KEY_MAX_CHARS = SUBJECT_KEY_MAX_CHARS;
34247
+ exports.SUBJECT_VIDEO_HINT_MODE_DEFAULT = SUBJECT_VIDEO_HINT_MODE_DEFAULT;
33623
34248
  exports.TEMPORALS = TEMPORALS;
33624
34249
  exports.TEMPORAL_CATEGORY_LABELS = TEMPORAL_CATEGORY_LABELS;
33625
34250
  exports.TEMPORAL_CATEGORY_ORDER = TEMPORAL_CATEGORY_ORDER;
@@ -33634,6 +34259,7 @@ exports.TRANSITION_DURATIONS = TRANSITION_DURATIONS;
33634
34259
  exports.TRANSITION_IDS = TRANSITION_IDS;
33635
34260
  exports.TRANSITION_INTENSITIES = TRANSITION_INTENSITIES;
33636
34261
  exports.TRANSITION_POSITIONS = TRANSITION_POSITIONS;
34262
+ exports.VIDEO_HINT_MODE_DEFAULT = VIDEO_HINT_MODE_DEFAULT;
33637
34263
  exports.VIDEO_WIZARD_CATEGORIES = VIDEO_WIZARD_CATEGORIES;
33638
34264
  exports.VOCAL_PRESENCE = VOCAL_PRESENCE;
33639
34265
  exports.VOCAL_PRESENCE_INSTRUMENTAL_ID = VOCAL_PRESENCE_INSTRUMENTAL_ID;
@@ -33679,6 +34305,7 @@ exports.buildIdentityDirectives = buildIdentityDirectives;
33679
34305
  exports.buildIdentityLockLine = buildIdentityLockLine;
33680
34306
  exports.buildImagePrompt = buildImagePrompt;
33681
34307
  exports.buildImagePromptSegments = buildImagePromptSegments;
34308
+ exports.buildImagePromptWithOverflow = buildImagePromptWithOverflow;
33682
34309
  exports.buildInstrumentationHints = buildInstrumentationHints;
33683
34310
  exports.buildInstrumentationTerms = buildInstrumentationTerms;
33684
34311
  exports.buildLightingHints = buildLightingHints;
@@ -33731,10 +34358,12 @@ exports.composeNegative = composeNegative;
33731
34358
  exports.composePickerCatalogs = composePickerCatalogs;
33732
34359
  exports.composeSoundHintFromConnections = composeSoundHintFromConnections;
33733
34360
  exports.composeTransitionHintFromConnections = composeTransitionHintFromConnections;
34361
+ exports.composeVideoPromptText = composeVideoPromptText;
33734
34362
  exports.computeLlmChatFields = computeLlmChatFields;
33735
34363
  exports.computeNodePrompt = computeNodePrompt;
33736
34364
  exports.computePackSidecarCoverage = computePackSidecarCoverage;
33737
34365
  exports.deriveTerm = deriveTerm;
34366
+ exports.directionFieldsForSurface = directionFieldsForSurface;
33738
34367
  exports.expandImagePositionRefs = expandImagePositionRefs;
33739
34368
  exports.expandImageRefTokens = expandImageRefTokens;
33740
34369
  exports.filmStillPrefix = filmStillPrefix;
@@ -33867,6 +34496,7 @@ exports.getRegisteredPersonDimensionLabels = getRegisteredPersonDimensionLabels;
33867
34496
  exports.getRegisteredPersonDimensionOrder = getRegisteredPersonDimensionOrder;
33868
34497
  exports.getRegisteredPersonFieldByDimension = getRegisteredPersonFieldByDimension;
33869
34498
  exports.getRegisteredPickerCatalogs = getRegisteredPickerCatalogs;
34499
+ exports.getRegisteredSubjectKeys = getRegisteredSubjectKeys;
33870
34500
  exports.getRenderQuality = getRenderQuality;
33871
34501
  exports.getRenderQualityLabel = getRenderQualityLabel;
33872
34502
  exports.getRenderQualityPromptHint = getRenderQualityPromptHint;
@@ -33925,12 +34555,16 @@ exports.isInstrumentalVocal = isInstrumentalVocal;
33925
34555
  exports.isSuspiciousDerivedTerm = isSuspiciousDerivedTerm;
33926
34556
  exports.isVantageFraming = isVantageFraming;
33927
34557
  exports.isWizardSupported = isWizardSupported;
34558
+ exports.joinPromptHints = joinPromptHints;
33928
34559
  exports.joinPromptParts = joinPromptParts;
34560
+ exports.keepableDirectionHints = keepableDirectionHints;
33929
34561
  exports.listPickerCatalogs = listPickerCatalogs;
33930
34562
  exports.migratePersonValue = migratePersonValue;
34563
+ exports.modeForFamily = modeForFamily;
33931
34564
  exports.nodeHasInlinePrompt = nodeHasInlinePrompt;
33932
34565
  exports.nodeHasPromptField = nodeHasPromptField;
33933
34566
  exports.nodeSupportsPromptAffixes = nodeSupportsPromptAffixes;
34567
+ exports.normalizeSubjectFields = normalizeSubjectFields;
33934
34568
  exports.personPacksVersion = personPacksVersion;
33935
34569
  exports.pickerFanoutTargets = pickerFanoutTargets;
33936
34570
  exports.projectAllCatalogs = projectAllCatalogs;
@@ -33939,10 +34573,15 @@ exports.promptAffixCoreField = promptAffixCoreField;
33939
34573
  exports.promptBindsFirstFrame = promptBindsFirstFrame;
33940
34574
  exports.promptFieldCarriesAffixes = promptFieldCarriesAffixes;
33941
34575
  exports.promptPartSeparator = promptPartSeparator;
34576
+ exports.readDirectionFields = readDirectionFields;
34577
+ exports.readStructuredFields = readStructuredFields;
34578
+ exports.readSubjectFields = readSubjectFields;
33942
34579
  exports.referenceRulesBlock = referenceRulesBlock;
33943
34580
  exports.registerCatalogPack = registerCatalogPack;
33944
34581
  exports.registerPersonPack = registerPersonPack;
34582
+ exports.renderDirectionHints = renderDirectionHints;
33945
34583
  exports.renderStructuredFields = renderStructuredFields;
34584
+ exports.renderSubjectHints = renderSubjectHints;
33946
34585
  exports.resetCatalogPacks = resetCatalogPacks;
33947
34586
  exports.resetPersonPacks = resetPersonPacks;
33948
34587
  exports.resolveBrandInput = resolveBrandInput;
@@ -33957,6 +34596,7 @@ exports.resolveTemplate = resolveTemplate;
33957
34596
  exports.resolveTerm = resolveTerm;
33958
34597
  exports.resolveVeoI2vInputs = resolveVeoI2vInputs;
33959
34598
  exports.resolveVideoReferenceCore = resolveVideoReferenceCore;
34599
+ exports.subjectFieldsForSurface = subjectFieldsForSurface;
33960
34600
  exports.summarizePickerCatalogs = summarizePickerCatalogs;
33961
34601
  exports.toIdentityLockMode = toIdentityLockMode;
33962
34602
  exports.truncateForField = truncateForField;