@sogni-ai/sogni-intelligence-client 2.4.3 → 3.0.0-alpha.10

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 (200) hide show
  1. package/dist/agent/index.d.ts +5 -0
  2. package/dist/agent/index.d.ts.map +1 -0
  3. package/dist/agent/index.js +38 -0
  4. package/dist/agent/index.js.map +1 -0
  5. package/dist/agent/intentInput.d.ts +106 -0
  6. package/dist/agent/intentInput.d.ts.map +1 -0
  7. package/dist/agent/intentInput.js +410 -0
  8. package/dist/agent/intentInput.js.map +1 -0
  9. package/dist/agent/toolMetadata.d.ts +37 -0
  10. package/dist/agent/toolMetadata.d.ts.map +1 -0
  11. package/dist/agent/toolMetadata.js +146 -0
  12. package/dist/agent/toolMetadata.js.map +1 -0
  13. package/dist/agent/turnAnalysis.d.ts +34 -0
  14. package/dist/agent/turnAnalysis.d.ts.map +1 -0
  15. package/dist/agent/turnAnalysis.js +155 -0
  16. package/dist/agent/turnAnalysis.js.map +1 -0
  17. package/dist/agent/turnPlan.d.ts +40 -0
  18. package/dist/agent/turnPlan.d.ts.map +1 -0
  19. package/dist/agent/turnPlan.js +187 -0
  20. package/dist/agent/turnPlan.js.map +1 -0
  21. package/dist/artifacts/graph.d.ts +92 -0
  22. package/dist/artifacts/graph.d.ts.map +1 -0
  23. package/dist/artifacts/graph.js +484 -0
  24. package/dist/artifacts/graph.js.map +1 -0
  25. package/dist/artifacts/index.d.ts +3 -0
  26. package/dist/artifacts/index.d.ts.map +1 -0
  27. package/dist/artifacts/index.js +28 -0
  28. package/dist/artifacts/index.js.map +1 -0
  29. package/dist/artifacts/queries.d.ts +7 -0
  30. package/dist/artifacts/queries.d.ts.map +1 -0
  31. package/dist/artifacts/queries.js +85 -0
  32. package/dist/artifacts/queries.js.map +1 -0
  33. package/dist/billing/index.d.ts +3 -0
  34. package/dist/billing/index.d.ts.map +1 -0
  35. package/dist/billing/index.js +27 -0
  36. package/dist/billing/index.js.map +1 -0
  37. package/dist/billing/spendGate.d.ts +72 -0
  38. package/dist/billing/spendGate.d.ts.map +1 -0
  39. package/dist/billing/spendGate.js +344 -0
  40. package/dist/billing/spendGate.js.map +1 -0
  41. package/dist/billing/workflowAuthorization.d.ts +56 -0
  42. package/dist/billing/workflowAuthorization.d.ts.map +1 -0
  43. package/dist/billing/workflowAuthorization.js +270 -0
  44. package/dist/billing/workflowAuthorization.js.map +1 -0
  45. package/dist/contracts/data/promptContracts.d.ts.map +1 -1
  46. package/dist/contracts/data/promptContracts.js +48 -36
  47. package/dist/contracts/data/promptContracts.js.map +1 -1
  48. package/dist/contracts/data/toolPermissions.d.ts +2 -0
  49. package/dist/contracts/data/toolPermissions.d.ts.map +1 -1
  50. package/dist/contracts/data/toolPermissions.js +2 -13
  51. package/dist/contracts/data/toolPermissions.js.map +1 -1
  52. package/dist/contracts/index.d.ts +2 -2
  53. package/dist/contracts/index.d.ts.map +1 -1
  54. package/dist/contracts/index.js +5 -3
  55. package/dist/contracts/index.js.map +1 -1
  56. package/dist/contracts/toolGatingPolicy.d.ts.map +1 -1
  57. package/dist/contracts/toolGatingPolicy.js +6 -2
  58. package/dist/contracts/toolGatingPolicy.js.map +1 -1
  59. package/dist/contracts/toolSurface.d.ts +1 -0
  60. package/dist/contracts/toolSurface.d.ts.map +1 -1
  61. package/dist/contracts/toolSurface.js +12 -5
  62. package/dist/contracts/toolSurface.js.map +1 -1
  63. package/dist/contracts/turnPolicy.d.ts +2 -1
  64. package/dist/contracts/turnPolicy.d.ts.map +1 -1
  65. package/dist/contracts/turnPolicy.js +14 -0
  66. package/dist/contracts/turnPolicy.js.map +1 -1
  67. package/dist/events/index.d.ts +2 -0
  68. package/dist/events/index.d.ts.map +1 -0
  69. package/dist/events/index.js +12 -0
  70. package/dist/events/index.js.map +1 -0
  71. package/dist/events/runEvent.d.ts +31 -0
  72. package/dist/events/runEvent.d.ts.map +1 -0
  73. package/dist/events/runEvent.js +161 -0
  74. package/dist/events/runEvent.js.map +1 -0
  75. package/dist/index.d.ts +6 -0
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.js +26 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/public-skill-runtime/index.d.ts +1 -1
  80. package/dist/public-skill-runtime/index.d.ts.map +1 -1
  81. package/dist/public-skill-runtime/index.js +229 -119
  82. package/dist/public-skill-runtime/index.js.map +1 -1
  83. package/dist/schemas/agent/intent-input.schema.json +128 -0
  84. package/dist/schemas/agent/turn-analysis.schema.json +75 -0
  85. package/dist/schemas/artifacts/artifact-graph.schema.json +42 -0
  86. package/dist/schemas/artifacts/artifact-node.schema.json +137 -0
  87. package/dist/schemas/billing/spend-gate.schema.json +130 -0
  88. package/dist/schemas/billing/workflow-authorization.schema.json +83 -0
  89. package/dist/schemas/events/run-event.schema.json +111 -0
  90. package/dist/schemas/tools/tool-metadata.schema.json +78 -0
  91. package/dist/tools/definitions/animate-photo/definition.js +5 -5
  92. package/dist/tools/definitions/animate-photo/definition.js.map +1 -1
  93. package/dist/tools/definitions/edit-image/definition.js +3 -3
  94. package/dist/tools/definitions/edit-image/definition.js.map +1 -1
  95. package/dist/tools/definitions/generate-image/definition.js +4 -4
  96. package/dist/tools/definitions/generate-image/definition.js.map +1 -1
  97. package/dist/tools/definitions/generate-music/definition.js +2 -2
  98. package/dist/tools/definitions/generate-music/definition.js.map +1 -1
  99. package/dist/tools/definitions/generate-video/definition.js +2 -2
  100. package/dist/tools/definitions/generate-video/definition.js.map +1 -1
  101. package/dist/tools/definitions/sound-to-video/definition.js +2 -2
  102. package/dist/tools/definitions/sound-to-video/definition.js.map +1 -1
  103. package/dist/tools/index.d.ts +1 -1
  104. package/dist/tools/index.d.ts.map +1 -1
  105. package/dist/tools/index.js +3 -2
  106. package/dist/tools/index.js.map +1 -1
  107. package/dist/tools/normalizeArgs.d.ts +1 -0
  108. package/dist/tools/normalizeArgs.d.ts.map +1 -1
  109. package/dist/tools/normalizeArgs.js +83 -8
  110. package/dist/tools/normalizeArgs.js.map +1 -1
  111. package/dist/utils/helpers.d.ts.map +1 -1
  112. package/dist/utils/helpers.js +4 -2
  113. package/dist/utils/helpers.js.map +1 -1
  114. package/dist/workflows/index.d.ts +2 -0
  115. package/dist/workflows/index.d.ts.map +1 -1
  116. package/dist/workflows/index.js +7 -1
  117. package/dist/workflows/index.js.map +1 -1
  118. package/dist/workflows/primitives/index.d.ts +1 -0
  119. package/dist/workflows/primitives/index.d.ts.map +1 -1
  120. package/dist/workflows/primitives/index.js +1 -0
  121. package/dist/workflows/primitives/index.js.map +1 -1
  122. package/dist/workflows/primitives/sanitizer.d.ts +22 -0
  123. package/dist/workflows/primitives/sanitizer.d.ts.map +1 -0
  124. package/dist/workflows/primitives/sanitizer.js +96 -0
  125. package/dist/workflows/primitives/sanitizer.js.map +1 -0
  126. package/dist-esm/agent/index.js +5 -0
  127. package/dist-esm/agent/index.js.map +1 -0
  128. package/dist-esm/agent/intentInput.js +397 -0
  129. package/dist-esm/agent/intentInput.js.map +1 -0
  130. package/dist-esm/agent/toolMetadata.js +136 -0
  131. package/dist-esm/agent/toolMetadata.js.map +1 -0
  132. package/dist-esm/agent/turnAnalysis.js +147 -0
  133. package/dist-esm/agent/turnAnalysis.js.map +1 -0
  134. package/dist-esm/agent/turnPlan.js +180 -0
  135. package/dist-esm/agent/turnPlan.js.map +1 -0
  136. package/dist-esm/artifacts/graph.js +465 -0
  137. package/dist-esm/artifacts/graph.js.map +1 -0
  138. package/dist-esm/artifacts/index.js +3 -0
  139. package/dist-esm/artifacts/index.js.map +1 -0
  140. package/dist-esm/artifacts/queries.js +78 -0
  141. package/dist-esm/artifacts/queries.js.map +1 -0
  142. package/dist-esm/billing/index.js +3 -0
  143. package/dist-esm/billing/index.js.map +1 -0
  144. package/dist-esm/billing/spendGate.js +329 -0
  145. package/dist-esm/billing/spendGate.js.map +1 -0
  146. package/dist-esm/billing/workflowAuthorization.js +260 -0
  147. package/dist-esm/billing/workflowAuthorization.js.map +1 -0
  148. package/dist-esm/contracts/data/promptContracts.js +48 -36
  149. package/dist-esm/contracts/data/promptContracts.js.map +1 -1
  150. package/dist-esm/contracts/data/toolPermissions.js +2 -13
  151. package/dist-esm/contracts/data/toolPermissions.js.map +1 -1
  152. package/dist-esm/contracts/index.js +2 -2
  153. package/dist-esm/contracts/index.js.map +1 -1
  154. package/dist-esm/contracts/toolGatingPolicy.js +6 -2
  155. package/dist-esm/contracts/toolGatingPolicy.js.map +1 -1
  156. package/dist-esm/contracts/toolSurface.js +11 -4
  157. package/dist-esm/contracts/toolSurface.js.map +1 -1
  158. package/dist-esm/contracts/turnPolicy.js +13 -0
  159. package/dist-esm/contracts/turnPolicy.js.map +1 -1
  160. package/dist-esm/events/index.js +2 -0
  161. package/dist-esm/events/index.js.map +1 -0
  162. package/dist-esm/events/runEvent.js +152 -0
  163. package/dist-esm/events/runEvent.js.map +1 -0
  164. package/dist-esm/index.js +5 -0
  165. package/dist-esm/index.js.map +1 -1
  166. package/dist-esm/public-skill-runtime/index.js +229 -119
  167. package/dist-esm/public-skill-runtime/index.js.map +1 -1
  168. package/dist-esm/schemas/agent/intent-input.schema.json +128 -0
  169. package/dist-esm/schemas/agent/turn-analysis.schema.json +75 -0
  170. package/dist-esm/schemas/artifacts/artifact-graph.schema.json +42 -0
  171. package/dist-esm/schemas/artifacts/artifact-node.schema.json +137 -0
  172. package/dist-esm/schemas/billing/spend-gate.schema.json +130 -0
  173. package/dist-esm/schemas/billing/workflow-authorization.schema.json +83 -0
  174. package/dist-esm/schemas/events/run-event.schema.json +111 -0
  175. package/dist-esm/schemas/tools/tool-metadata.schema.json +78 -0
  176. package/dist-esm/tools/definitions/animate-photo/definition.js +5 -5
  177. package/dist-esm/tools/definitions/animate-photo/definition.js.map +1 -1
  178. package/dist-esm/tools/definitions/edit-image/definition.js +3 -3
  179. package/dist-esm/tools/definitions/edit-image/definition.js.map +1 -1
  180. package/dist-esm/tools/definitions/generate-image/definition.js +4 -4
  181. package/dist-esm/tools/definitions/generate-image/definition.js.map +1 -1
  182. package/dist-esm/tools/definitions/generate-music/definition.js +2 -2
  183. package/dist-esm/tools/definitions/generate-music/definition.js.map +1 -1
  184. package/dist-esm/tools/definitions/generate-video/definition.js +2 -2
  185. package/dist-esm/tools/definitions/generate-video/definition.js.map +1 -1
  186. package/dist-esm/tools/definitions/sound-to-video/definition.js +2 -2
  187. package/dist-esm/tools/definitions/sound-to-video/definition.js.map +1 -1
  188. package/dist-esm/tools/index.js +1 -1
  189. package/dist-esm/tools/index.js.map +1 -1
  190. package/dist-esm/tools/normalizeArgs.js +82 -8
  191. package/dist-esm/tools/normalizeArgs.js.map +1 -1
  192. package/dist-esm/utils/helpers.js +4 -2
  193. package/dist-esm/utils/helpers.js.map +1 -1
  194. package/dist-esm/workflows/index.js +1 -0
  195. package/dist-esm/workflows/index.js.map +1 -1
  196. package/dist-esm/workflows/primitives/index.js +1 -0
  197. package/dist-esm/workflows/primitives/index.js.map +1 -1
  198. package/dist-esm/workflows/primitives/sanitizer.js +89 -0
  199. package/dist-esm/workflows/primitives/sanitizer.js.map +1 -0
  200. package/package.json +30 -3
@@ -99,6 +99,7 @@ exports.inferDefaultVideoSteps = inferDefaultVideoSteps;
99
99
  exports.resolveVideoSteps = resolveVideoSteps;
100
100
  exports.sanitizeBatchPrompt = sanitizeBatchPrompt;
101
101
  const index_js_1 = require("../contracts/data/index.js");
102
+ const turnPolicy_js_1 = require("../contracts/turnPolicy.js");
102
103
  function isLtxWorkflow(workflow) {
103
104
  return workflow === 't2v' || workflow === 'i2v' || workflow === 'ia2v' || workflow === 'a2v' || workflow === 'v2v';
104
105
  }
@@ -185,9 +186,18 @@ function createPublicSkillDefaultContractRuntime(input = {}) {
185
186
  ],
186
187
  });
187
188
  }
189
+ const PUBLIC_SKILL_ADVISORY_SIGNAL_SOURCES = new Set(['regex', 'fact_extractor']);
190
+ function normalizePublicSkillSignal(signal) {
191
+ const source = (0, turnPolicy_js_1.normalizeSignalSource)(String(signal.source));
192
+ if (source === signal.source)
193
+ return signal;
194
+ return { ...signal, source };
195
+ }
188
196
  function publicSkillSignalSourcesByKind(signals) {
189
197
  const sourcesByKind = new Map();
190
198
  for (const signal of signals) {
199
+ if (PUBLIC_SKILL_ADVISORY_SIGNAL_SOURCES.has(signal.source))
200
+ continue;
191
201
  const sources = sourcesByKind.get(signal.kind) ?? new Set();
192
202
  sources.add(signal.source);
193
203
  sourcesByKind.set(signal.kind, sources);
@@ -226,7 +236,7 @@ function classifyPublicSkillTurn(input) {
226
236
  const signals = [
227
237
  ...(input.signals ?? []),
228
238
  ...publicSkillSessionSignals(input.sessionState),
229
- ];
239
+ ].map(normalizePublicSkillSignal);
230
240
  const sourcesByKind = publicSkillSignalSourcesByKind(signals);
231
241
  const forbidden = new Set();
232
242
  const required = new Set();
@@ -654,10 +664,10 @@ exports.IMAGE_GENERATION_SKILL = {
654
664
  exports.IMAGE_EDITING_SKILL = {
655
665
  id: 'image_editing',
656
666
  name: 'Image editing',
657
- description: 'Edit, restore, restyle, refine, or change the camera angle of an existing image. Includes persona-conditioned edits — persona images must always be produced with edit_image and reference photos, never via text-to-image.',
667
+ description: 'Edit, restore, restyle, refine, or change the camera angle of an existing image. Includes persona-conditioned edits — persona images should use edit_image and reference photos rather than text-to-image.',
658
668
  toolNames: ['edit_image', 'restore_photo', 'apply_style', 'change_angle', 'refine_result'],
659
669
  constraints: [
660
- 'Persona images must always be produced with edit_image and a reference photo — never invoke generate_image for persona output.',
670
+ 'For persona image output, use edit_image with a reference photo rather than generate_image.',
661
671
  'refine_result acts on a prior generation in the session; do not call it before any image has been produced or uploaded.',
662
672
  ],
663
673
  };
@@ -667,7 +677,7 @@ exports.VIDEO_GENERATION_SKILL = {
667
677
  description: 'Text-to-video synthesis (LTX-2). Use when the user wants a new video clip generated from a prompt with no source image, audio, or clip.',
668
678
  toolNames: ['generate_video'],
669
679
  constraints: [
670
- 'Persona-driven video requests must always go through image_editing first to produce a conditioned image; never go straight to text-to-video for personas.',
680
+ 'For My Personas video requests, default to image_editing first to produce a conditioned scene image before animation. Use direct video only when the user explicitly asks to animate an existing persona image/reference or no source image is available for a voice-only request.',
671
681
  ],
672
682
  };
673
683
  exports.VIDEO_EDITING_SKILL = {
@@ -687,7 +697,7 @@ exports.VIDEO_EDITING_SKILL = {
687
697
  'add_subtitles',
688
698
  ],
689
699
  constraints: [
690
- 'Per-clip retry and the batch progress contract are sacred — never collapse a multi-clip render down to a single waterfall call.',
700
+ 'Preserve per-clip retry and batch progress semantics. Use one Dynamic Prompt project for prompt-only fan-out, and avoid serial waterfall calls for independent clips.',
691
701
  'animate_photo errors with all_failed must surface to the user; do not auto-retry from inside the chat loop.',
692
702
  ],
693
703
  };
@@ -709,7 +719,7 @@ exports.PERSONA_MANAGEMENT_SKILL = {
709
719
  description: "Resolve named personas to their reference photos and read/write the user's long-term creative memory (preferences, named subjects, ongoing projects).",
710
720
  toolNames: ['resolve_personas', 'manage_memory'],
711
721
  constraints: [
712
- 'A persona-driven request must call resolve_personas before any image_editing or video_editing tool — never assume a name resolves on its own.',
722
+ 'For clear My Personas references, call resolve_personas before persona-conditioned image or video tools; do not rely on name text alone for identity.',
713
723
  ],
714
724
  };
715
725
  exports.APP_SETTINGS_SKILL = {
@@ -787,33 +797,48 @@ function compileStoryboardImagePromptFromProject(project) {
787
797
  : `Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}.`;
788
798
  return [
789
799
  'CREATE:',
790
- `Create exactly ${frameCount} sequential video storyboard frames as one composite storyboard image.`,
791
- `Project title: ${project.title}.`,
792
- project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
793
- '',
794
- ...compileStoryboardCountContractSection(project, layout),
800
+ `Create exactly ${frameCount} sequential video storyboard frames as one production storyboard sheet.`,
795
801
  '',
796
- ...compileStoryboardReferenceSection(project),
802
+ 'PROJECT:',
803
+ `Title: ${project.title}.`,
804
+ `Format: ${layout.targetVideoAspectRatio} video storyboard.`,
805
+ project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
797
806
  '',
798
- 'CANVAS / LAYOUT:',
807
+ 'LAYOUT CONTRACT:',
808
+ `Create exactly ${frameCount} numbered storyboard panels; do not render fewer or more panels.`,
809
+ `Arrange panels in reading order, left-to-right then top-to-bottom: ${Array.from({ length: frameCount }, (_, index) => {
810
+ const scene = project.scenes[index];
811
+ return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
812
+ }).join(', ')}.`,
799
813
  boardSizeLine,
800
814
  `Individual scene-cell/frame aspect ratio: ${layout.cellAspectRatio}.`,
801
815
  `Target final video aspect ratio: ${layout.targetVideoAspectRatio}.`,
802
816
  `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}.`,
817
+ `Each panel must contain one distinct ${layout.cellAspectRatio} cinematic video-frame rectangle with compact notes outside the frame.`,
818
+ 'Keep scene numbers, timecodes, titles, dialogue/VO, audio notes, and production notes outside the video-frame rectangles.',
819
+ 'Do not merge panels, create inset thumbnails, make panels square, or overlay storyboard metadata inside the artwork frames.',
803
820
  '',
804
- ...compileStoryboardStoryContinuitySection(project),
821
+ ...compileStoryboardReferenceSection(project),
822
+ '',
823
+ 'STYLE:',
824
+ `${project.creativeBrief.visualQualityBar} with cinematic shot language, coherent art direction, readable labels, and consistent reference usage.`,
825
+ 'Reference-driven personality: before drawing, infer concrete visual and behavioral cues from uploaded/reference images, including character attitude, materials, props, palette, brand tone, typography style, and implied world. Let those cues make this storyboard specific to the supplied subject instead of a generic reusable template.',
826
+ 'Vary composition within the required grid: keep the exact scene count, layout, and cell geometry, and make each panel intentionally staged with distinct shot scale, pose/action, camera angle, lighting beat, transition idea, and character-specific detail.',
805
827
  '',
806
- 'GLOBAL STYLE:',
807
- project.creativeBrief.visualQualityBar,
828
+ ...compileStoryboardStoryContinuitySection(project),
808
829
  '',
809
830
  'CRITICAL REQUIREMENTS:',
810
831
  ...compileStoryboardCriticalRequirements().map((item, index) => `${index + 1}. ${item}`),
832
+ ...project.references
833
+ .filter(ref => ref.preservePriority === 'critical')
834
+ .map((ref, index) => `${compileStoryboardCriticalRequirements().length + index + 1}. Critical reference lock: ${ref.id} (${ref.kind}) must remain bound to its assigned usage scope: ${ref.usageScope}.`),
811
835
  '',
812
836
  ...compileStoryboardScenesSection(project),
813
- 'TEXT RENDERING:',
814
- 'Keep production labels outside video-frame artwork. Only user-required diegetic or brand text belongs inside a frame.',
837
+ 'TEXT RULES:',
838
+ 'Place scene number, timing, scene title, beat title, and compact production labels outside each video frame in a clearly associated header, footer strip, side rail, or table. Do not overlay scene numbers, timecodes, production notes, Dialogue/VO labels, Audio/SFX text, or SFX/action callout words such as Whoosh!, Impact!, Boom!, Thud!, Slash!, Crack!, or Pop! on top of the video-frame artwork. Also do not overlay scene/beat titles on top of the video-frame artwork. Project titles are metadata, not in-frame text. Only listed diegetic or brand text belongs inside a frame. Quote and spell any required visible text exactly.',
815
839
  'Visible text listed on a scene belongs only in that scene; repeat visible text only on scenes that list it.',
816
840
  ...storyboardRequiredVisibleText(project).map(formatStoryboardRequiredVisibleTextLine),
841
+ project.endCard.logoUsage ? `Logo usage: ${project.endCard.logoUsage}` : '',
817
842
  '',
818
843
  'NEGATIVE / AVOID:',
819
844
  ...compileStoryboardAvoidSection(project.creativeBrief.concept).map(item => `- ${item}`),
@@ -2748,8 +2773,8 @@ const STORYBOARD_COUNT_WORDS = {
2748
2773
  };
2749
2774
  const DEFAULT_STORYBOARD_TIMING_RULES = {
2750
2775
  normalWordsPerSecondMin: 2.0,
2751
- normalWordsPerSecondMax: 3.3,
2752
- fastWordsPerSecondMax: 4.0,
2776
+ normalWordsPerSecondMax: 2.5,
2777
+ fastWordsPerSecondMax: 3.0,
2753
2778
  minEndCardHoldSec: 2.0,
2754
2779
  minPunchlineSec: 0.5,
2755
2780
  toleranceSec: 0.25,
@@ -2942,10 +2967,13 @@ function describePortraitLetterboxCellArrangement(frameCount, cellAspectRatio) {
2942
2967
  }
2943
2968
  const columns = frameCount <= 8 ? 2 : frameCount <= 15 ? 3 : 4;
2944
2969
  const rows = Math.ceil(frameCount / columns);
2970
+ const unusedSlotNote = columns * rows > frameCount
2971
+ ? ['use unused grid slots as margin/notes space only']
2972
+ : [];
2945
2973
  return [
2946
2974
  `${frameCount} numbered scene slots arranged as a ${columns}-column x ${rows}-row grid inside a portrait sheet`,
2947
2975
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
2948
- 'use unused grid slots as margin/notes space only',
2976
+ ...unusedSlotNote,
2949
2977
  ].join('; ');
2950
2978
  }
2951
2979
  function describeLandscapePortraitCellArrangement(frameCount, cellAspectRatio) {
@@ -2955,10 +2983,13 @@ function describeLandscapePortraitCellArrangement(frameCount, cellAspectRatio) {
2955
2983
  }
2956
2984
  const rows = frameCount <= 8 ? 2 : frameCount <= 15 ? 3 : 4;
2957
2985
  const columns = Math.ceil(frameCount / rows);
2986
+ const unusedSlotNote = columns * rows > frameCount
2987
+ ? ['use unused grid slots as margin/notes space only']
2988
+ : [];
2958
2989
  return [
2959
2990
  `${frameCount} numbered scene slots arranged as a ${rows}-row x ${columns}-column grid inside a landscape board`,
2960
2991
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
2961
- 'use unused grid slots as margin/notes space only',
2992
+ ...unusedSlotNote,
2962
2993
  ].join('; ');
2963
2994
  }
2964
2995
  function parseAspectRatioPair(aspectRatio) {
@@ -3073,13 +3104,16 @@ function describeSingleOrientationStoryboardArrangement(boardAspectRatio, cellAs
3073
3104
  const slotGrid = pickBalancedSlotGrid(frameCount);
3074
3105
  const cols = cellOrientation === 'landscape' ? slotGrid.minor : slotGrid.major;
3075
3106
  const rows = cellOrientation === 'landscape' ? slotGrid.major : slotGrid.minor;
3107
+ const unusedSlotNote = cols * rows > frameCount
3108
+ ? ['use unused grid slots as margin/notes space only']
3109
+ : [];
3076
3110
  if (boardOrientation === 'portrait') {
3077
3111
  return {
3078
3112
  layoutKind: 'portrait_grid',
3079
3113
  layoutDescription: [
3080
3114
  `${frameCount} numbered scene slots arranged as a ${cols}-column x ${rows}-row grid inside a portrait storyboard sheet`,
3081
3115
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
3082
- 'use unused grid slots as margin/notes space only',
3116
+ ...unusedSlotNote,
3083
3117
  ].join('; '),
3084
3118
  };
3085
3119
  }
@@ -3089,7 +3123,7 @@ function describeSingleOrientationStoryboardArrangement(boardAspectRatio, cellAs
3089
3123
  layoutDescription: [
3090
3124
  `${frameCount} numbered scene slots arranged as a ${rows}-row x ${cols}-column grid inside a ${boardLabel}`,
3091
3125
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
3092
- 'use unused grid slots as margin/notes space only',
3126
+ ...unusedSlotNote,
3093
3127
  ].join('; '),
3094
3128
  };
3095
3129
  }
@@ -3473,6 +3507,16 @@ function cleanStoryboardReferenceSubjectHint(context, index) {
3473
3507
  const explicitSubject = cleanExplicitStoryboardReferenceSubject(source, index);
3474
3508
  if (explicitSubject)
3475
3509
  return explicitSubject;
3510
+ const inlineReference = new RegExp(String.raw `\b(?:uploaded|attached|provided|reference|source|input)?\s*(?:image|photo|picture|asset)\s*(?:#|number\s*)?${index}\b\s*(?:is|=|:|-)?\s*([^.;,\n]{2,120})`, 'i').exec(source);
3511
+ if (inlineReference?.[1]) {
3512
+ const inlineSubject = compactStoryboardLine(stripStoryboardMarkup(inlineReference[1]))
3513
+ .replace(/\b(?:use|usage|preserve|reference|asset)\b[\s\S]*$/i, '')
3514
+ .replace(/[.\s]+$/g, '')
3515
+ .trim();
3516
+ if (inlineSubject && !/^(?:logo|brand|character|mascot|asset|reference|image|photo|picture)$/i.test(inlineSubject)) {
3517
+ return inlineSubject.slice(0, 180).replace(/\s+\S*$/, match => inlineSubject.length > 180 ? '' : match).trim();
3518
+ }
3519
+ }
3476
3520
  const cleaned = source
3477
3521
  .replace(new RegExp(String.raw `^(?:image|photo|picture|asset)\s*(?:#|number\s*)?${index}\s*(?:\([^)]*\))?\s*:?\s*`, 'i'), '')
3478
3522
  .replace(new RegExp(String.raw `^(?:uploaded|attached|provided|reference|source|input)\s+(?:image|photo|picture|asset)\s*(?:#|number\s*)?${index}\s*:?\s*`, 'i'), '')
@@ -3580,6 +3624,51 @@ function buildStoryboardReferenceAssets(userIntentText, prompt) {
3580
3624
  preservePriority: storyboardPreservePriorityFromRole(role),
3581
3625
  }));
3582
3626
  }
3627
+ function storyboardReferenceIndexFromText(value) {
3628
+ const normalized = value.trim();
3629
+ if (!normalized)
3630
+ return null;
3631
+ const match = normalized.match(/^(?:@?\s*)?(?:image|photo|picture|asset|uploaded\s+asset|uploaded\s+image)[\s_#-]*(\d{1,2})$/i)
3632
+ ?? normalized.match(/^image_(\d{1,2})$/i);
3633
+ if (!match)
3634
+ return null;
3635
+ const index = Number(match[1]);
3636
+ return Number.isInteger(index) && index > 0 ? index : null;
3637
+ }
3638
+ function looksLikeUnknownIndexedStoryboardReference(value) {
3639
+ return /^(?:@?\s*)?(?:image|photo|picture|asset|uploaded\s+asset|uploaded\s+image)[\s_#-]*\d{1,2}$/i.test(value.trim())
3640
+ || /^image_\d{1,2}$/i.test(value.trim());
3641
+ }
3642
+ function normalizeStoryboardSceneReferenceUsage(referenceUsage, references) {
3643
+ if (referenceUsage.length === 0 || references.length === 0)
3644
+ return [];
3645
+ const knownById = new Map(references.map(ref => [ref.id.toLowerCase(), ref.id]));
3646
+ const knownByIndex = new Map(references
3647
+ .filter(ref => typeof ref.index === 'number')
3648
+ .map(ref => [ref.index, ref.id]));
3649
+ const normalized = [];
3650
+ for (const raw of referenceUsage) {
3651
+ const value = raw.trim();
3652
+ if (!value)
3653
+ continue;
3654
+ const knownId = knownById.get(value.toLowerCase());
3655
+ if (knownId) {
3656
+ normalized.push(knownId);
3657
+ continue;
3658
+ }
3659
+ const index = storyboardReferenceIndexFromText(value);
3660
+ if (index !== null) {
3661
+ const id = knownByIndex.get(index);
3662
+ if (id)
3663
+ normalized.push(id);
3664
+ continue;
3665
+ }
3666
+ if (looksLikeUnknownIndexedStoryboardReference(value))
3667
+ continue;
3668
+ normalized.push(value);
3669
+ }
3670
+ return uniqueStoryboardStrings(normalized);
3671
+ }
3583
3672
  function compileStoryboardReferenceSection(project) {
3584
3673
  const refs = project.references;
3585
3674
  if (refs.length <= 0) {
@@ -3637,6 +3726,12 @@ function extractStoryboardRequiredText(text) {
3637
3726
  const isProductionDirectionField = /\b(?:action|motion|camera|transition|audio|sfx|fx|foley|sound|music|dialogue|vo|voiceover|voice-over|narration|speech|lighting|style|performance|beat)\b/i.test(fieldLabel)
3638
3727
  && !visibleTextContextPattern.test(fieldLabel);
3639
3728
  const hasExplicitVisibleTextContext = visibleTextContextPattern.test(line);
3729
+ const precedingCue = compactStoryboardLine([
3730
+ precedingText ?? '',
3731
+ line.slice(0, Math.max(0, matchIndex - lineStart)),
3732
+ ].join(' ')).slice(-180);
3733
+ const spokenTextCue = /\b(?:dialogue|vo|v\.o\.|voiceover|voice-over|voice\s*over|narration|speech|spoken(?:\s+line)?|audio\s*\/\s*dialogue)\b/i.test(precedingCue)
3734
+ || /\b(?:narrator|speaker|actor|character|person|subject|host|mascot|performer|he|she|they)\b[^"“`]{0,80}\b(?:says?|speaks?|whispers?|shouts?|asks?|replies?)\b/i.test(precedingCue);
3640
3735
  const isRestrictionOnlyTextInstruction = visibleTextContextPattern.test(fieldLabel || line)
3641
3736
  && visibleTextRestrictionPattern.test(value)
3642
3737
  && !/["“”`]/.test(value);
@@ -3668,6 +3763,9 @@ function extractStoryboardRequiredText(text) {
3668
3763
  if ((isProductionDirectionField || looksLikeActionOrSfxCallout) && !hasExplicitVisibleTextContext) {
3669
3764
  return true;
3670
3765
  }
3766
+ if (spokenTextCue && !/\b(?:visible|on[-\s]?screen|in[-\s]?frame|text|copy|cta|tagline|headline|title\s+card|caption|subtitle|super|wordmark|slogan|sign|poster|banner|label|placard|sticker|screen)\b/i.test(precedingCue)) {
3767
+ return true;
3768
+ }
3671
3769
  if (looksLikeAssetHandle
3672
3770
  && /\b(?:asset|reference|image|photo|upload|file|filename|logo|brand)\b/i.test(line)) {
3673
3771
  return true;
@@ -3743,7 +3841,7 @@ function extractStoryboardRequiredText(text) {
3743
3841
  || /^(?:powered|made|built|created|generated|rendered|presented)\s+by\b/i.test(value);
3744
3842
  return !productionDirection || copyOverridesProduction;
3745
3843
  };
3746
- const endCardLabelPattern = /^\s*(?:[-*+]\s*)?(?:end|final|closing)\s+(?:scene|card|frame|shot|cta|logo(?:\s+lockup)?)\s*:\s*([^\n]*)$/gim;
3844
+ const endCardLabelPattern = /^\s*(?:[-*+]\s*)?(?:(?:end|final|closing)\s+(?:scene|card|frame|shot|cta|logo(?:\s+lockup)?)|(?:scene|shot|beat|panel|frame)\s*\d{1,2}\s+(?:visible\s+text|on[-\s]?screen\s+text|onscreen\s+text|text|copy|cta)(?:\s+must\s+be\s+exactly)?)\s*:\s*([^\n]*)$/gim;
3747
3845
  for (const match of text.matchAll(endCardLabelPattern)) {
3748
3846
  const matchIndex = match.index ?? 0;
3749
3847
  const lineEnd = text.indexOf('\n', matchIndex);
@@ -3782,9 +3880,10 @@ function extractStoryboardRequiredText(text) {
3782
3880
  for (const match of text.matchAll(exactTextPattern)) {
3783
3881
  const fullMatch = match[0];
3784
3882
  const quoteOpenInMatch = fullMatch.search(/["“`]/);
3785
- const precedingInMatch = quoteOpenInMatch >= 0 ? fullMatch.slice(0, quoteOpenInMatch) : fullMatch;
3883
+ const quoteAbsIndex = (match.index ?? 0) + Math.max(0, quoteOpenInMatch);
3884
+ const lineStart = text.lastIndexOf('\n', quoteAbsIndex) + 1;
3786
3885
  addRequiredText(match[1] ?? match[2] ?? match[3], match.index ?? 0, {
3787
- precedingText: precedingInMatch,
3886
+ precedingText: text.slice(lineStart, quoteAbsIndex),
3788
3887
  });
3789
3888
  }
3790
3889
  const labeledTextPattern = /\b(?:on[-\s]?screen\s+text|visible\s+text|text\s+only|text|end\s+card\s+text|final\s+text|tagline|cta|headline|title\s+card|copy)\s*:\s*([^\n]{1,260})/gi;
@@ -4243,6 +4342,26 @@ function storyboardTableRowLooksLikeCountedBeat(cells, headers) {
4243
4342
  const candidate = compactStoryboardLine(cells[Math.max(0, headerIndex)] || cells[0] || '');
4244
4343
  return /^(?:beat|scene|shot|panel|frame)?\s*(?:#\s*)?0?\d{1,2}\b/i.test(candidate);
4245
4344
  }
4345
+ function normalizeStoryboardTableCellsToHeaders(cells, headers) {
4346
+ if (!headers || cells.length <= headers.length)
4347
+ return cells;
4348
+ const overflow = cells.length - headers.length;
4349
+ const combinedAudioDialogueIndex = headers.findIndex(header => /\baudio\b/i.test(header)
4350
+ && /\b(?:dialogue|vo|v\.o\.|voiceover|speech|narration)\b/i.test(header));
4351
+ if (combinedAudioDialogueIndex < 0)
4352
+ return cells;
4353
+ const mergeEnd = combinedAudioDialogueIndex + overflow;
4354
+ if (mergeEnd >= cells.length)
4355
+ return cells;
4356
+ const overflowCells = cells.slice(combinedAudioDialogueIndex, mergeEnd + 1);
4357
+ if (!overflowCells.some(cell => storyboardTableCellLooksLikeUnlabeledSpeech(cell)))
4358
+ return cells;
4359
+ return [
4360
+ ...cells.slice(0, combinedAudioDialogueIndex),
4361
+ overflowCells.join('\n'),
4362
+ ...cells.slice(mergeEnd + 1),
4363
+ ];
4364
+ }
4246
4365
  function storyboardTableCellWithoutDialogue(cell, dialogue) {
4247
4366
  const normalizedDialogue = compactStoryboardLine(dialogue).toLowerCase();
4248
4367
  return cell
@@ -4300,13 +4419,14 @@ function splitStoryboardTableSections(text) {
4300
4419
  const sections = [];
4301
4420
  let headers = null;
4302
4421
  for (const line of text.split(/\r?\n/)) {
4303
- const cells = storyboardMarkdownTableCells(line);
4304
- if (cells.length < 3)
4422
+ const rawCells = storyboardMarkdownTableCells(line);
4423
+ if (rawCells.length < 3)
4305
4424
  continue;
4306
- if (looksLikeStoryboardTableHeader(cells)) {
4307
- headers = cells;
4425
+ if (looksLikeStoryboardTableHeader(rawCells)) {
4426
+ headers = rawCells;
4308
4427
  continue;
4309
4428
  }
4429
+ const cells = normalizeStoryboardTableCellsToHeaders(rawCells, headers);
4310
4430
  const timingCellIndex = cells.findIndex(cell => extractStoryboardTimingMarker(cell) !== null);
4311
4431
  if (timingCellIndex < 0)
4312
4432
  continue;
@@ -5047,10 +5167,10 @@ function alignAssistantStoryboardDialogueWithUserSource(scenes, userIntentText,
5047
5167
  return { scenes, shouldRetime: false };
5048
5168
  return { scenes: repaired, shouldRetime: true };
5049
5169
  }
5050
- function minimumStoryboardSceneDurationForDialogue(scene, rules) {
5170
+ function minimumStoryboardSceneDurationForDialogue(scene, rules, wordsPerSecondMax = rules.normalWordsPerSecondMax) {
5051
5171
  const dialogueWords = countWords(scene.dialogue);
5052
5172
  let minDuration = dialogueWords > 0
5053
- ? dialogueWords / rules.normalWordsPerSecondMax
5173
+ ? dialogueWords / wordsPerSecondMax
5054
5174
  : 0.5;
5055
5175
  const sceneContext = `${scene.title}\n${scene.visual}\n${scene.textInImage.join(' ')}`;
5056
5176
  const hasReadableText = scene.textInImage.length > 0;
@@ -5073,13 +5193,22 @@ function retimeStoryboardScenesForDialogue(scenes, targetDurationSec, rules = DE
5073
5193
  const needsRetiming = scenes.some((scene, index) => (scene.durationSec ?? 0) + rules.toleranceSec < minimumDurations[index]);
5074
5194
  if (!needsRetiming)
5075
5195
  return scenes;
5076
- const minimumTotal = Math.round(minimumDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5196
+ let requiredDurations = minimumDurations;
5197
+ let minimumTotal = Math.round(requiredDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5198
+ if (minimumTotal > targetDurationSec + rules.toleranceSec && rules.fastWordsPerSecondMax > rules.normalWordsPerSecondMax) {
5199
+ const fastDurations = scenes.map(scene => minimumStoryboardSceneDurationForDialogue(scene, rules, rules.fastWordsPerSecondMax));
5200
+ const fastTotal = Math.round(fastDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5201
+ if (fastTotal <= targetDurationSec + rules.toleranceSec) {
5202
+ requiredDurations = fastDurations;
5203
+ minimumTotal = fastTotal;
5204
+ }
5205
+ }
5077
5206
  if (minimumTotal > targetDurationSec + rules.toleranceSec)
5078
5207
  return scenes;
5079
5208
  const extraDuration = Math.max(0, targetDurationSec - minimumTotal);
5080
5209
  const originalDurations = scenes.map(scene => scene.durationSec ?? 0);
5081
5210
  const weightTotal = originalDurations.reduce((sum, duration) => sum + Math.max(0.01, duration), 0);
5082
- const unroundedDurations = minimumDurations.map((minimum, index) => (minimum + (extraDuration * Math.max(0.01, originalDurations[index]) / weightTotal)));
5211
+ const unroundedDurations = requiredDurations.map((minimum, index) => (minimum + (extraDuration * Math.max(0.01, originalDurations[index]) / weightTotal)));
5083
5212
  const timelineStartSec = scenes[0].startSec ?? 0;
5084
5213
  const timelineEndSec = Math.round((timelineStartSec + targetDurationSec) * 100) / 100;
5085
5214
  let cursor = timelineStartSec;
@@ -5251,6 +5380,27 @@ function finalStoryboardSceneVisibleText(scenes) {
5251
5380
  }
5252
5381
  return [];
5253
5382
  }
5383
+ function applyStoryboardEndCardTextToScenes(scenes, endCardText) {
5384
+ if (scenes.length === 0 || endCardText.length === 0)
5385
+ return scenes;
5386
+ const finalSceneIndex = scenes.length - 1;
5387
+ const explicitEndCardIndex = (() => {
5388
+ for (let index = scenes.length - 1; index >= 0; index -= 1) {
5389
+ if (storyboardSceneLooksLikeEndCard(scenes[index]))
5390
+ return index;
5391
+ }
5392
+ return finalSceneIndex;
5393
+ })();
5394
+ return scenes.map((scene, index) => index === explicitEndCardIndex
5395
+ ? {
5396
+ ...scene,
5397
+ textInImage: uniqueStoryboardStrings([
5398
+ ...scene.textInImage,
5399
+ ...endCardText,
5400
+ ]),
5401
+ }
5402
+ : scene);
5403
+ }
5254
5404
  function storyboardRequiredTextKey(value) {
5255
5405
  return compactStoryboardLine(value)
5256
5406
  .toLowerCase()
@@ -5301,6 +5451,7 @@ function buildStoryboardProject(options) {
5301
5451
  const userIntentText = canonicalStoryboardScriptContext(rawUserIntentText) || rawUserIntentText;
5302
5452
  const narrativeUserIntentText = cleanStoryboardNarrativeSourceText(userIntentText);
5303
5453
  const approvedScriptContext = cleanStoryboardNarrativeSourceText(canonicalStoryboardScriptContext(options.approvedScriptContext));
5454
+ const referencePromptContext = cleanStoryboardNarrativeSourceText(canonicalStoryboardScriptContext(prompt) || prompt);
5304
5455
  const primarySourceBrief = selectStoryboardSourceBriefForCompile(options, userIntentText);
5305
5456
  const sourceText = stripGenericStoryboardVisibleTextMetadata(sanitizeStoryboardExternalAudioReferences([
5306
5457
  cleanStoryboardNarrativeSourceText(primarySourceBrief),
@@ -5324,7 +5475,7 @@ function buildStoryboardProject(options) {
5324
5475
  const requestedDurationSec = inferRequestedTotalVideoDurationSeconds(userIntentText);
5325
5476
  const durationSec = requestedDurationSec ?? inferRequestedTotalVideoDurationSeconds(sourceText);
5326
5477
  const references = buildStoryboardReferenceAssets(userIntentText, [
5327
- prompt,
5478
+ referencePromptContext,
5328
5479
  approvedScriptContext,
5329
5480
  ].filter(Boolean).join('\n\n'));
5330
5481
  const approvedSections = approvedScriptContext
@@ -5355,21 +5506,25 @@ function buildStoryboardProject(options) {
5355
5506
  return buildSceneFromSection(section, references, null, storyboardScenePlanningContractForIndex(options.planningContract, section.number));
5356
5507
  })
5357
5508
  : [];
5358
- const scenes = parsedScenes;
5509
+ const scenes = parsedScenes.map(scene => ({
5510
+ ...scene,
5511
+ referenceUsage: normalizeStoryboardSceneReferenceUsage(scene.referenceUsage, references),
5512
+ }));
5359
5513
  const timingNormalizedScenes = normalizeAssistantStoryboardSceneTiming(scenes, durationSec, options.promptAuthorship);
5360
5514
  const dialogueAlignment = alignAssistantStoryboardDialogueWithUserSource(timingNormalizedScenes, userIntentText, options.promptAuthorship);
5361
- const normalizedScenes = dialogueAlignment.shouldRetime
5515
+ const dialogueTimedScenes = (options.promptAuthorship === 'assistant' || dialogueAlignment.shouldRetime)
5362
5516
  ? retimeStoryboardScenesForDialogue(dialogueAlignment.scenes, durationSec)
5363
5517
  : dialogueAlignment.scenes;
5364
5518
  const userConstraintSource = buildStoryboardUserConstraintSource(narrativeUserIntentText, cleanStoryboardNarrativeSourceText(primarySourceBrief), options);
5365
- const { mustIncludeText, endCardText } = storyboardRequiredTextForProject(options, userConstraintSource, normalizedScenes);
5519
+ const { mustIncludeText, endCardText } = storyboardRequiredTextForProject(options, userConstraintSource, dialogueTimedScenes);
5520
+ const normalizedScenes = retimeStoryboardScenesForDialogue(applyStoryboardEndCardTextToScenes(dialogueTimedScenes, endCardText), durationSec);
5366
5521
  const voiceLines = assignVoiceLinesToScenes(normalizedScenes, sourceText);
5367
5522
  const storySpineFallback = approvedScriptContext
5368
5523
  || (options.promptAuthorship === 'assistant' ? narrativeUserIntentText : cleanStoryboardNarrativeSourceText(primarySourceBrief))
5369
5524
  || prompt
5370
5525
  || narrativeUserIntentText;
5371
5526
  const storySpine = inferStoryboardStorySpine(allText, storySpineFallback);
5372
- const productFeatureMap = inferStoryboardProductFeatureMap(allText, scenes);
5527
+ const productFeatureMap = inferStoryboardProductFeatureMap(allText, normalizedScenes);
5373
5528
  const beatMarkerScripts = [approvedScriptContext, sourceText];
5374
5529
  let directBeatMarkerCount = 0;
5375
5530
  for (const text of beatMarkerScripts) {
@@ -5599,67 +5754,17 @@ function compileStoryboardStoryContinuitySection(project) {
5599
5754
  'Keep product/feature/brand meaning in the associated notes. Do not force long product explanations or the whole script into tiny in-frame text.',
5600
5755
  ];
5601
5756
  }
5602
- function compileStoryboardCountContractSection(project, layout, expectedFrameCount = project.scenes.length) {
5603
- if (expectedFrameCount <= 0)
5604
- return [];
5605
- const sceneSlots = Array.from({ length: expectedFrameCount }, (_, index) => {
5606
- const scene = project.scenes[index];
5607
- return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
5608
- })
5609
- .join(', ');
5610
- return [
5611
- 'COUNT / GRID CONTRACT:',
5612
- `Required scene count: exactly ${expectedFrameCount} numbered storyboard scene slots; do not render fewer slots and do not add extra scene slots.`,
5613
- `Allocate the full ${expectedFrameCount}-slot storyboard grid before drawing details. Fill slots in reading order, left-to-right then top-to-bottom: ${sceneSlots}.`,
5614
- `Each allocated scene slot gets one distinct ${layout.cellAspectRatio} cinematic video-frame rectangle plus its own compact notes outside that rectangle. Do not merge adjacent scenes, combine two beats into one slot, duplicate slots, or place thumbnail/inset panels inside a slot.`,
5615
- `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}. The final sheet should be visibly countable as ${expectedFrameCount} numbered scene slots at a glance.`,
5616
- ];
5617
- }
5618
- function compileStoryboardFrameGeometrySection(layout) {
5619
- const cellOrientation = parseAspectRatioOrientation(layout.cellAspectRatio);
5620
- if (cellOrientation === 'portrait') {
5621
- return [
5622
- 'PORTRAIT FRAME GEOMETRY:',
5623
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5624
- `Inside every numbered scene slot, draw one identical upright ${layout.cellAspectRatio} video-frame rectangle whose height is visibly greater than its width.`,
5625
- `Keep all scene numbers, timing, titles, notes, headers, footers, and production labels outside the ${layout.cellAspectRatio} video-frame rectangles.`,
5626
- `Do not make the numbered scene slots, frame artwork areas, or thumbnails square. Square cells violate the requested ${layout.cellAspectRatio} final video format.`,
5627
- 'Unused grid slots must remain blank margin/notes space only; do not stretch or square off the portrait frame rectangles to fill the grid.',
5628
- ];
5629
- }
5630
- if (cellOrientation === 'landscape') {
5631
- return [
5632
- 'LANDSCAPE FRAME GEOMETRY:',
5633
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5634
- `Inside every numbered scene slot, draw one identical wide ${layout.cellAspectRatio} video-frame rectangle whose width is visibly greater than its height.`,
5635
- `Keep all scene numbers, timing, titles, notes, headers, footers, and production labels outside the ${layout.cellAspectRatio} video-frame rectangles.`,
5636
- `Do not make the numbered scene slots, frame artwork areas, or thumbnails square. Square cells violate the requested ${layout.cellAspectRatio} final video format.`,
5637
- 'Unused grid slots must remain blank margin/notes space only; do not stretch or square off the landscape frame rectangles to fill the grid.',
5638
- ];
5639
- }
5640
- if (cellOrientation === 'square') {
5641
- return [
5642
- 'SQUARE FRAME GEOMETRY:',
5643
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5644
- `Inside every numbered scene slot, draw one identical square ${layout.cellAspectRatio} video-frame area plus compact notes outside that square.`,
5645
- `Do not stretch square frame artwork areas into portrait, landscape, full-board, or any aspect ratio other than ${layout.cellAspectRatio}.`,
5646
- ];
5647
- }
5648
- return [
5649
- 'FRAME GEOMETRY:',
5650
- `Inside every numbered scene slot, draw one identical ${layout.cellAspectRatio} video-frame rectangle plus compact notes outside that rectangle.`,
5651
- `Do not let frame artwork areas drift to square, full-board, or any aspect ratio other than ${layout.cellAspectRatio}.`,
5652
- ];
5653
- }
5654
5757
  function promptReferenceUsageForScene(referenceUsage, references, modelId) {
5655
5758
  if (referenceUsage.length === 0)
5656
5759
  return '';
5657
5760
  const referenceById = new Map(references.map((ref, index) => [ref.id, { ref, index }]));
5658
- return uniqueStoryboardStrings(referenceUsage.map(value => {
5761
+ return uniqueStoryboardStrings(referenceUsage.flatMap(value => {
5659
5762
  const found = referenceById.get(value);
5660
- if (!found)
5661
- return value;
5662
- return formatModelRef(modelId, found.ref.index ?? found.index + 1, 'image');
5763
+ if (found)
5764
+ return [formatModelRef(modelId, found.ref.index ?? found.index + 1, 'image')];
5765
+ if (looksLikeUnknownIndexedStoryboardReference(value))
5766
+ return [];
5767
+ return [value];
5663
5768
  })).join(', ');
5664
5769
  }
5665
5770
  function storyboardMetadataLabelAliases(labels) {
@@ -5794,32 +5899,36 @@ function compileVideoStoryboardImagePrompt(options) {
5794
5899
  : `Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}.`;
5795
5900
  return [
5796
5901
  'CREATE:',
5797
- `Create exactly ${compiledFrameCount} sequential video storyboard frames as one composite storyboard image.`,
5798
- `Project title: ${project.title}.`,
5799
- project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
5800
- '',
5801
- ...compileStoryboardCountContractSection(project, layout, compiledFrameCount),
5902
+ `Create exactly ${compiledFrameCount} sequential video storyboard frames as one production storyboard sheet.`,
5802
5903
  '',
5803
- ...compileStoryboardReferenceSection(project),
5904
+ 'PROJECT:',
5905
+ `Title: ${project.title}.`,
5906
+ `Format: ${layout.targetVideoAspectRatio} video storyboard.`,
5907
+ project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
5804
5908
  '',
5805
- 'CANVAS / LAYOUT:',
5909
+ 'LAYOUT CONTRACT:',
5910
+ `Create exactly ${compiledFrameCount} numbered storyboard panels; do not render fewer or more panels.`,
5911
+ `Arrange panels in reading order, left-to-right then top-to-bottom: ${Array.from({ length: compiledFrameCount }, (_, index) => {
5912
+ const scene = project.scenes[index];
5913
+ return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
5914
+ }).join(', ')}.`,
5806
5915
  boardSizeLine,
5807
5916
  `Individual scene-cell/frame aspect ratio: ${layout.cellAspectRatio}.`,
5808
5917
  `Target final video aspect ratio: ${layout.targetVideoAspectRatio}.`,
5809
5918
  `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}.`,
5810
- `Every cinematic frame artwork area inside every scene cell must preserve ${layout.cellAspectRatio}; do not let any individual frame drift to square, full-board, or a different portrait/landscape ratio.`,
5811
- 'Keep margins, gutters, outside-frame numbering, note strips, and typography consistent across the full board.',
5812
- 'Each scene cell must make the frame-to-notes relationship clear while keeping production labels outside the actual video-frame artwork.',
5919
+ `Each panel must contain one distinct ${layout.cellAspectRatio} cinematic video-frame rectangle with compact notes outside the frame.`,
5920
+ 'Keep scene numbers, timecodes, titles, dialogue/VO, audio notes, and production notes outside the video-frame rectangles.',
5921
+ 'Do not merge panels, create inset thumbnails, make panels square, or overlay storyboard metadata inside the artwork frames.',
5813
5922
  '',
5814
- ...compileStoryboardFrameGeometrySection(layout),
5815
- '',
5816
- ...compileStoryboardStoryContinuitySection(project),
5923
+ ...compileStoryboardReferenceSection(project),
5817
5924
  '',
5818
- 'GLOBAL STYLE:',
5925
+ 'STYLE:',
5819
5926
  `${project.creativeBrief.visualQualityBar} with cinematic shot language, coherent art direction, readable labels, and consistent reference usage.`,
5820
5927
  'Reference-driven personality: before drawing, infer concrete visual and behavioral cues from uploaded/reference images, including character attitude, materials, props, palette, brand tone, typography style, and implied world. Let those cues make this storyboard specific to the supplied subject instead of a generic reusable template.',
5821
5928
  'Vary composition within the required grid: keep the exact scene count, layout, and cell geometry, and make each panel intentionally staged with distinct shot scale, pose/action, camera angle, lighting beat, transition idea, and character-specific detail.',
5822
5929
  '',
5930
+ ...compileStoryboardStoryContinuitySection(project),
5931
+ '',
5823
5932
  'CRITICAL REQUIREMENTS:',
5824
5933
  ...compileStoryboardCriticalRequirements().map((item, index) => `${index + 1}. ${item}`),
5825
5934
  ...project.references
@@ -5827,7 +5936,7 @@ function compileVideoStoryboardImagePrompt(options) {
5827
5936
  .map((ref, index) => `${compileStoryboardCriticalRequirements().length + index + 1}. Critical reference lock: ${ref.id} (${ref.kind}) must remain bound to its assigned usage scope: ${ref.usageScope}.`),
5828
5937
  '',
5829
5938
  ...compileStoryboardScenesSection(project),
5830
- 'TEXT RENDERING:',
5939
+ 'TEXT RULES:',
5831
5940
  'Place scene number, timing, scene title, beat title, and compact production labels outside each video frame in a clearly associated header, footer strip, side rail, or table. Do not overlay scene numbers, timecodes, production notes, Dialogue/VO labels, Audio/SFX text, or SFX/action callout words such as Whoosh!, Impact!, Boom!, Thud!, Slash!, Crack!, or Pop! on top of the video-frame artwork. Also do not overlay scene/beat titles on top of the video-frame artwork. Project titles are metadata, not in-frame text. Only listed diegetic or brand text belongs inside a frame. Quote and spell any required visible text exactly.',
5832
5941
  'Visible text listed on a scene belongs only in that scene; repeat visible text only on scenes that list it.',
5833
5942
  ...storyboardRequiredVisibleText(project).map(formatStoryboardRequiredVisibleTextLine),
@@ -5846,27 +5955,28 @@ function ensureCompiledVideoStoryboardPromptPreservesSourceBrief(compiledPrompt,
5846
5955
  function lintStoryboardImagePrompt(prompt, layout, project) {
5847
5956
  const errors = [];
5848
5957
  const warnings = [];
5958
+ const hasConsolidatedLayoutContract = /LAYOUT CONTRACT:/i.test(prompt);
5849
5959
  if (!/CREATE:/i.test(prompt))
5850
5960
  errors.push('missing CREATE section');
5851
- if (!/COUNT \/ GRID CONTRACT:/i.test(prompt))
5961
+ if (!hasConsolidatedLayoutContract && !/COUNT \/ GRID CONTRACT:/i.test(prompt))
5852
5962
  errors.push('missing COUNT / GRID CONTRACT section');
5853
5963
  if (!/REFERENCE IMAGES:/i.test(prompt))
5854
5964
  errors.push('missing REFERENCE IMAGES section');
5855
- if (!/CANVAS \/ LAYOUT:/i.test(prompt))
5965
+ if (!hasConsolidatedLayoutContract && !/CANVAS \/ LAYOUT:/i.test(prompt))
5856
5966
  errors.push('missing CANVAS / LAYOUT section');
5857
- if (!/FRAME GEOMETRY:/i.test(prompt))
5967
+ if (!hasConsolidatedLayoutContract && !/FRAME GEOMETRY:/i.test(prompt))
5858
5968
  errors.push('missing FRAME GEOMETRY section');
5859
5969
  const cellOrientation = parseAspectRatioOrientation(layout.cellAspectRatio);
5860
- if (cellOrientation === 'portrait' && !/PORTRAIT FRAME GEOMETRY:/i.test(prompt)) {
5970
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'portrait' && !/PORTRAIT FRAME GEOMETRY:/i.test(prompt)) {
5861
5971
  errors.push('missing PORTRAIT FRAME GEOMETRY section');
5862
5972
  }
5863
- if (cellOrientation === 'landscape' && !/LANDSCAPE FRAME GEOMETRY:/i.test(prompt)) {
5973
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'landscape' && !/LANDSCAPE FRAME GEOMETRY:/i.test(prompt)) {
5864
5974
  errors.push('missing LANDSCAPE FRAME GEOMETRY section');
5865
5975
  }
5866
- if (cellOrientation === 'square' && !/SQUARE FRAME GEOMETRY:/i.test(prompt)) {
5976
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'square' && !/SQUARE FRAME GEOMETRY:/i.test(prompt)) {
5867
5977
  errors.push('missing SQUARE FRAME GEOMETRY section');
5868
5978
  }
5869
- if (!/TEXT RENDERING:/i.test(prompt))
5979
+ if (!/(?:TEXT RENDERING|TEXT RULES):/i.test(prompt))
5870
5980
  errors.push('missing TEXT RENDERING section');
5871
5981
  if (!prompt.includes(`Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}`)
5872
5982
  && !prompt.includes(`(${layout.boardAspectRatio})`)) {
@@ -5937,7 +6047,7 @@ function inferCompiledStoryboardPromptAspectRatio(prompt, labelPattern) {
5937
6047
  return normalizeAspectRatio(match?.[1]);
5938
6048
  }
5939
6049
  function extractCompiledStoryboardScenesBlock(prompt) {
5940
- const match = prompt.match(/\nSCENES:\s*\n([\s\S]*?)(?:\nTEXT RENDERING:|\nSOURCE BRIEF TO FOLLOW:|\nNEGATIVE \/ AVOID:|$)/i);
6050
+ const match = prompt.match(/\nSCENES:\s*\n([\s\S]*?)(?:\nTEXT RENDERING:|\nTEXT RULES:|\nSOURCE BRIEF TO FOLLOW:|\nNEGATIVE \/ AVOID:|$)/i);
5941
6051
  return match?.[1] ?? '';
5942
6052
  }
5943
6053
  function parseCompiledStoryboardHeadingTiming(heading) {