@sogni-ai/sogni-intelligence-client 3.0.0-alpha.9 → 3.0.2

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 (36) hide show
  1. package/dist/agent/intentInput.js +1 -1
  2. package/dist/agent/intentInput.js.map +1 -1
  3. package/dist/contracts/data/promptContracts.d.ts.map +1 -1
  4. package/dist/contracts/data/promptContracts.js +19 -7
  5. package/dist/contracts/data/promptContracts.js.map +1 -1
  6. package/dist/public-skill-runtime/index.d.ts.map +1 -1
  7. package/dist/public-skill-runtime/index.js +212 -108
  8. package/dist/public-skill-runtime/index.js.map +1 -1
  9. package/dist/tools/definitions/animate-photo/definition.js +1 -1
  10. package/dist/tools/definitions/animate-photo/definition.js.map +1 -1
  11. package/dist/workflows/executor.d.ts +5 -3
  12. package/dist/workflows/executor.d.ts.map +1 -1
  13. package/dist/workflows/executor.js +82 -29
  14. package/dist/workflows/executor.js.map +1 -1
  15. package/dist/workflows/primitives/retryUntilCondition.d.ts.map +1 -1
  16. package/dist/workflows/primitives/retryUntilCondition.js.map +1 -1
  17. package/dist/workflows/types.d.ts +14 -0
  18. package/dist/workflows/types.d.ts.map +1 -1
  19. package/dist/workflows/validation.d.ts +4 -1
  20. package/dist/workflows/validation.d.ts.map +1 -1
  21. package/dist/workflows/validation.js +12 -3
  22. package/dist/workflows/validation.js.map +1 -1
  23. package/dist-esm/agent/intentInput.js +1 -1
  24. package/dist-esm/agent/intentInput.js.map +1 -1
  25. package/dist-esm/contracts/data/promptContracts.js +19 -7
  26. package/dist-esm/contracts/data/promptContracts.js.map +1 -1
  27. package/dist-esm/public-skill-runtime/index.js +212 -108
  28. package/dist-esm/public-skill-runtime/index.js.map +1 -1
  29. package/dist-esm/tools/definitions/animate-photo/definition.js +1 -1
  30. package/dist-esm/tools/definitions/animate-photo/definition.js.map +1 -1
  31. package/dist-esm/workflows/executor.js +82 -29
  32. package/dist-esm/workflows/executor.js.map +1 -1
  33. package/dist-esm/workflows/primitives/retryUntilCondition.js.map +1 -1
  34. package/dist-esm/workflows/validation.js +12 -3
  35. package/dist-esm/workflows/validation.js.map +1 -1
  36. package/package.json +2 -2
@@ -797,33 +797,48 @@ function compileStoryboardImagePromptFromProject(project) {
797
797
  : `Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}.`;
798
798
  return [
799
799
  'CREATE:',
800
- `Create exactly ${frameCount} sequential video storyboard frames as one composite storyboard image.`,
801
- `Project title: ${project.title}.`,
802
- project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
803
- '',
804
- ...compileStoryboardCountContractSection(project, layout),
800
+ `Create exactly ${frameCount} sequential video storyboard frames as one production storyboard sheet.`,
805
801
  '',
806
- ...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.',
807
806
  '',
808
- '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(', ')}.`,
809
813
  boardSizeLine,
810
814
  `Individual scene-cell/frame aspect ratio: ${layout.cellAspectRatio}.`,
811
815
  `Target final video aspect ratio: ${layout.targetVideoAspectRatio}.`,
812
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.',
813
820
  '',
814
- ...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.',
815
827
  '',
816
- 'GLOBAL STYLE:',
817
- project.creativeBrief.visualQualityBar,
828
+ ...compileStoryboardStoryContinuitySection(project),
818
829
  '',
819
830
  'CRITICAL REQUIREMENTS:',
820
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}.`),
821
835
  '',
822
836
  ...compileStoryboardScenesSection(project),
823
- 'TEXT RENDERING:',
824
- '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.',
825
839
  'Visible text listed on a scene belongs only in that scene; repeat visible text only on scenes that list it.',
826
840
  ...storyboardRequiredVisibleText(project).map(formatStoryboardRequiredVisibleTextLine),
841
+ project.endCard.logoUsage ? `Logo usage: ${project.endCard.logoUsage}` : '',
827
842
  '',
828
843
  'NEGATIVE / AVOID:',
829
844
  ...compileStoryboardAvoidSection(project.creativeBrief.concept).map(item => `- ${item}`),
@@ -1660,7 +1675,7 @@ function suggestedDurationForDialogue(prompt, currentDuration) {
1660
1675
  const baseDuration = currentDuration ?? 0;
1661
1676
  if (words <= 0)
1662
1677
  return baseDuration;
1663
- const speechSeconds = Math.ceil(words / 2.5) + 2;
1678
+ const speechSeconds = Math.ceil(words / 3) + 2;
1664
1679
  return Math.max(baseDuration, Math.min(20, speechSeconds));
1665
1680
  }
1666
1681
  function formatAudioIdPrompt(prompt, voiceName) {
@@ -2758,8 +2773,8 @@ const STORYBOARD_COUNT_WORDS = {
2758
2773
  };
2759
2774
  const DEFAULT_STORYBOARD_TIMING_RULES = {
2760
2775
  normalWordsPerSecondMin: 2.0,
2761
- normalWordsPerSecondMax: 3.3,
2762
- fastWordsPerSecondMax: 4.0,
2776
+ normalWordsPerSecondMax: 3.0,
2777
+ fastWordsPerSecondMax: 3.75,
2763
2778
  minEndCardHoldSec: 2.0,
2764
2779
  minPunchlineSec: 0.5,
2765
2780
  toleranceSec: 0.25,
@@ -2952,10 +2967,13 @@ function describePortraitLetterboxCellArrangement(frameCount, cellAspectRatio) {
2952
2967
  }
2953
2968
  const columns = frameCount <= 8 ? 2 : frameCount <= 15 ? 3 : 4;
2954
2969
  const rows = Math.ceil(frameCount / columns);
2970
+ const unusedSlotNote = columns * rows > frameCount
2971
+ ? ['use unused grid slots as margin/notes space only']
2972
+ : [];
2955
2973
  return [
2956
2974
  `${frameCount} numbered scene slots arranged as a ${columns}-column x ${rows}-row grid inside a portrait sheet`,
2957
2975
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
2958
- 'use unused grid slots as margin/notes space only',
2976
+ ...unusedSlotNote,
2959
2977
  ].join('; ');
2960
2978
  }
2961
2979
  function describeLandscapePortraitCellArrangement(frameCount, cellAspectRatio) {
@@ -2965,10 +2983,13 @@ function describeLandscapePortraitCellArrangement(frameCount, cellAspectRatio) {
2965
2983
  }
2966
2984
  const rows = frameCount <= 8 ? 2 : frameCount <= 15 ? 3 : 4;
2967
2985
  const columns = Math.ceil(frameCount / rows);
2986
+ const unusedSlotNote = columns * rows > frameCount
2987
+ ? ['use unused grid slots as margin/notes space only']
2988
+ : [];
2968
2989
  return [
2969
2990
  `${frameCount} numbered scene slots arranged as a ${rows}-row x ${columns}-column grid inside a landscape board`,
2970
2991
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
2971
- 'use unused grid slots as margin/notes space only',
2992
+ ...unusedSlotNote,
2972
2993
  ].join('; ');
2973
2994
  }
2974
2995
  function parseAspectRatioPair(aspectRatio) {
@@ -3083,13 +3104,16 @@ function describeSingleOrientationStoryboardArrangement(boardAspectRatio, cellAs
3083
3104
  const slotGrid = pickBalancedSlotGrid(frameCount);
3084
3105
  const cols = cellOrientation === 'landscape' ? slotGrid.minor : slotGrid.major;
3085
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
+ : [];
3086
3110
  if (boardOrientation === 'portrait') {
3087
3111
  return {
3088
3112
  layoutKind: 'portrait_grid',
3089
3113
  layoutDescription: [
3090
3114
  `${frameCount} numbered scene slots arranged as a ${cols}-column x ${rows}-row grid inside a portrait storyboard sheet`,
3091
3115
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
3092
- 'use unused grid slots as margin/notes space only',
3116
+ ...unusedSlotNote,
3093
3117
  ].join('; '),
3094
3118
  };
3095
3119
  }
@@ -3099,7 +3123,7 @@ function describeSingleOrientationStoryboardArrangement(boardAspectRatio, cellAs
3099
3123
  layoutDescription: [
3100
3124
  `${frameCount} numbered scene slots arranged as a ${rows}-row x ${cols}-column grid inside a ${boardLabel}`,
3101
3125
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
3102
- 'use unused grid slots as margin/notes space only',
3126
+ ...unusedSlotNote,
3103
3127
  ].join('; '),
3104
3128
  };
3105
3129
  }
@@ -3483,6 +3507,16 @@ function cleanStoryboardReferenceSubjectHint(context, index) {
3483
3507
  const explicitSubject = cleanExplicitStoryboardReferenceSubject(source, index);
3484
3508
  if (explicitSubject)
3485
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
+ }
3486
3520
  const cleaned = source
3487
3521
  .replace(new RegExp(String.raw `^(?:image|photo|picture|asset)\s*(?:#|number\s*)?${index}\s*(?:\([^)]*\))?\s*:?\s*`, 'i'), '')
3488
3522
  .replace(new RegExp(String.raw `^(?:uploaded|attached|provided|reference|source|input)\s+(?:image|photo|picture|asset)\s*(?:#|number\s*)?${index}\s*:?\s*`, 'i'), '')
@@ -3590,6 +3624,51 @@ function buildStoryboardReferenceAssets(userIntentText, prompt) {
3590
3624
  preservePriority: storyboardPreservePriorityFromRole(role),
3591
3625
  }));
3592
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
+ }
3593
3672
  function compileStoryboardReferenceSection(project) {
3594
3673
  const refs = project.references;
3595
3674
  if (refs.length <= 0) {
@@ -3637,6 +3716,31 @@ function extractStoryboardRequiredText(text) {
3637
3716
  const visibleTextContextPattern = /\b(?:visible|on[-\s]?screen|in[-\s]?frame|text|copy|cta|tagline|headline|title\s+card|caption|subtitle|super|wordmark|spell(?:ed)?|read(?:s)?|slogan)\b/i;
3638
3717
  const sceneHeadingPattern = /^\s*(?:[-*+]\s*)?(?:#{1,6}\s*)?(?:[*_]{1,3})?\s*(?:Scene|Shot|Beat|Panel|Frame)[\s_#-]*\d{1,2}\b/i;
3639
3718
  const visibleTextRestrictionPattern = /\b(?:only\s+(?:text|copy|words?)|no\s+(?:captions?|subtitles?|overlays?|watermarks?|added\s+logos?|extra\s+logos?|logos?|text|copy)|without\s+(?:captions?|subtitles?|overlays?|watermarks?|added\s+logos?|extra\s+logos?|logos?|text|copy)|do\s+not\s+(?:add|include|render|show|write)\s+(?:captions?|subtitles?|overlays?|watermarks?|logos?|text|copy|words?))\b/i;
3719
+ const storyboardTextCandidateLooksLikeConversationInstruction = (candidate) => {
3720
+ const raw = compactStoryboardLine(stripStoryboardMarkup(candidate))
3721
+ .replace(/^["“”'`]+|["“”'`]+$/g, '')
3722
+ .trim();
3723
+ if (!raw)
3724
+ return false;
3725
+ if (/^\[(?:uploaded|attached|provided)\s+(?:images?|files?|videos?|audio)\s*:/i.test(raw))
3726
+ return true;
3727
+ if (/^(?:yes|no|approved?|looks?\s+good|ok(?:ay)?|sure)[.!?]?$/i.test(raw))
3728
+ return true;
3729
+ const value = raw.toLowerCase();
3730
+ const workflowTerm = /\b(?:script|prompt|story\s*board|storyboard|take|version|again|redo|retry|regenerate|re-generate|revise|revision|approve|approval|proceed|generate|render|run|do\s+it|best\s+judg(?:e)?ment)\b/i;
3731
+ if (/^use\s+your\s+best\s+judg(?:e)?ment\b/i.test(raw))
3732
+ return true;
3733
+ if (/^(?:try\s+another|another\s+take|regenerate|re-generate|redo|retry|revise)\b/i.test(raw))
3734
+ return true;
3735
+ if (/^(?:go\s+ahead|can\s+you|please)\b/i.test(raw) && workflowTerm.test(value))
3736
+ return true;
3737
+ if (/^let'?s\b|^lets\b/i.test(raw)) {
3738
+ return /\b(?:script|prompt|story\s*board|storyboard|take|version|again|redo|retry|regenerate|re-generate|revise|revision)\b/i.test(value)
3739
+ || /\b(?:generate|render|create|do|run)\s+it\b/i.test(value)
3740
+ || /\buse\s+your\s+best\s+judg(?:e)?ment\b/i.test(value);
3741
+ }
3742
+ return false;
3743
+ };
3640
3744
  const shouldIgnoreRequiredText = (value, matchIndex, precedingText) => {
3641
3745
  const lineStart = text.lastIndexOf('\n', matchIndex) + 1;
3642
3746
  const nextLineBreak = text.indexOf('\n', matchIndex);
@@ -3672,6 +3776,9 @@ function extractStoryboardRequiredText(text) {
3672
3776
  if (storyboardTextCandidateLooksLikeGenericProductionLabel(value)) {
3673
3777
  return true;
3674
3778
  }
3779
+ if (storyboardTextCandidateLooksLikeConversationInstruction(value)) {
3780
+ return true;
3781
+ }
3675
3782
  if (isNestedSceneHeadingField) {
3676
3783
  return true;
3677
3784
  }
@@ -3746,6 +3853,8 @@ function extractStoryboardRequiredText(text) {
3746
3853
  return false;
3747
3854
  if (storyboardTextCandidateLooksLikeGenericProductionLabel(value))
3748
3855
  return false;
3856
+ if (storyboardTextCandidateLooksLikeConversationInstruction(value))
3857
+ return false;
3749
3858
  const contextNamesText = /\b(?:slogans?|taglines?|cta|copy|text|words?|wordmark|brand|logo|end\s+card|final\s+card|end\s+scene|final\s+scene|closing\s+card)\b/i
3750
3859
  .test(context);
3751
3860
  if (!contextNamesText)
@@ -3762,7 +3871,10 @@ function extractStoryboardRequiredText(text) {
3762
3871
  || /^(?:powered|made|built|created|generated|rendered|presented)\s+by\b/i.test(value);
3763
3872
  return !productionDirection || copyOverridesProduction;
3764
3873
  };
3765
- const endCardLabelPattern = /^\s*(?:[-*+]\s*)?(?:end|final|closing)\s+(?:scene|card|frame|shot|cta|logo(?:\s+lockup)?)\s*:\s*([^\n]*)$/gim;
3874
+ const endCardFollowingLineLooksLikeContinuationInstruction = (line) => {
3875
+ return storyboardTextCandidateLooksLikeConversationInstruction(line);
3876
+ };
3877
+ 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;
3766
3878
  for (const match of text.matchAll(endCardLabelPattern)) {
3767
3879
  const matchIndex = match.index ?? 0;
3768
3880
  const lineEnd = text.indexOf('\n', matchIndex);
@@ -3774,6 +3886,8 @@ function extractStoryboardRequiredText(text) {
3774
3886
  break;
3775
3887
  if (/^(?:#{1,6}\s+|\|\s*|(?:Scene|Shot|Beat|Panel|Frame)\s*\d{1,2}\b)/i.test(trimmed))
3776
3888
  break;
3889
+ if (endCardFollowingLineLooksLikeContinuationInstruction(trimmed))
3890
+ break;
3777
3891
  if (followingLines.length >= 5)
3778
3892
  break;
3779
3893
  followingLines.push(trimmed);
@@ -5088,10 +5202,10 @@ function alignAssistantStoryboardDialogueWithUserSource(scenes, userIntentText,
5088
5202
  return { scenes, shouldRetime: false };
5089
5203
  return { scenes: repaired, shouldRetime: true };
5090
5204
  }
5091
- function minimumStoryboardSceneDurationForDialogue(scene, rules) {
5205
+ function minimumStoryboardSceneDurationForDialogue(scene, rules, wordsPerSecondMax = rules.normalWordsPerSecondMax) {
5092
5206
  const dialogueWords = countWords(scene.dialogue);
5093
5207
  let minDuration = dialogueWords > 0
5094
- ? dialogueWords / rules.normalWordsPerSecondMax
5208
+ ? dialogueWords / wordsPerSecondMax
5095
5209
  : 0.5;
5096
5210
  const sceneContext = `${scene.title}\n${scene.visual}\n${scene.textInImage.join(' ')}`;
5097
5211
  const hasReadableText = scene.textInImage.length > 0;
@@ -5114,13 +5228,22 @@ function retimeStoryboardScenesForDialogue(scenes, targetDurationSec, rules = DE
5114
5228
  const needsRetiming = scenes.some((scene, index) => (scene.durationSec ?? 0) + rules.toleranceSec < minimumDurations[index]);
5115
5229
  if (!needsRetiming)
5116
5230
  return scenes;
5117
- const minimumTotal = Math.round(minimumDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5231
+ let requiredDurations = minimumDurations;
5232
+ let minimumTotal = Math.round(requiredDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5233
+ if (minimumTotal > targetDurationSec + rules.toleranceSec && rules.fastWordsPerSecondMax > rules.normalWordsPerSecondMax) {
5234
+ const fastDurations = scenes.map(scene => minimumStoryboardSceneDurationForDialogue(scene, rules, rules.fastWordsPerSecondMax));
5235
+ const fastTotal = Math.round(fastDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5236
+ if (fastTotal <= targetDurationSec + rules.toleranceSec) {
5237
+ requiredDurations = fastDurations;
5238
+ minimumTotal = fastTotal;
5239
+ }
5240
+ }
5118
5241
  if (minimumTotal > targetDurationSec + rules.toleranceSec)
5119
5242
  return scenes;
5120
5243
  const extraDuration = Math.max(0, targetDurationSec - minimumTotal);
5121
5244
  const originalDurations = scenes.map(scene => scene.durationSec ?? 0);
5122
5245
  const weightTotal = originalDurations.reduce((sum, duration) => sum + Math.max(0.01, duration), 0);
5123
- const unroundedDurations = minimumDurations.map((minimum, index) => (minimum + (extraDuration * Math.max(0.01, originalDurations[index]) / weightTotal)));
5246
+ const unroundedDurations = requiredDurations.map((minimum, index) => (minimum + (extraDuration * Math.max(0.01, originalDurations[index]) / weightTotal)));
5124
5247
  const timelineStartSec = scenes[0].startSec ?? 0;
5125
5248
  const timelineEndSec = Math.round((timelineStartSec + targetDurationSec) * 100) / 100;
5126
5249
  let cursor = timelineStartSec;
@@ -5292,6 +5415,27 @@ function finalStoryboardSceneVisibleText(scenes) {
5292
5415
  }
5293
5416
  return [];
5294
5417
  }
5418
+ function applyStoryboardEndCardTextToScenes(scenes, endCardText) {
5419
+ if (scenes.length === 0 || endCardText.length === 0)
5420
+ return scenes;
5421
+ const finalSceneIndex = scenes.length - 1;
5422
+ const explicitEndCardIndex = (() => {
5423
+ for (let index = scenes.length - 1; index >= 0; index -= 1) {
5424
+ if (storyboardSceneLooksLikeEndCard(scenes[index]))
5425
+ return index;
5426
+ }
5427
+ return finalSceneIndex;
5428
+ })();
5429
+ return scenes.map((scene, index) => index === explicitEndCardIndex
5430
+ ? {
5431
+ ...scene,
5432
+ textInImage: uniqueStoryboardStrings([
5433
+ ...scene.textInImage,
5434
+ ...endCardText,
5435
+ ]),
5436
+ }
5437
+ : scene);
5438
+ }
5295
5439
  function storyboardRequiredTextKey(value) {
5296
5440
  return compactStoryboardLine(value)
5297
5441
  .toLowerCase()
@@ -5342,6 +5486,7 @@ function buildStoryboardProject(options) {
5342
5486
  const userIntentText = canonicalStoryboardScriptContext(rawUserIntentText) || rawUserIntentText;
5343
5487
  const narrativeUserIntentText = cleanStoryboardNarrativeSourceText(userIntentText);
5344
5488
  const approvedScriptContext = cleanStoryboardNarrativeSourceText(canonicalStoryboardScriptContext(options.approvedScriptContext));
5489
+ const referencePromptContext = cleanStoryboardNarrativeSourceText(canonicalStoryboardScriptContext(prompt) || prompt);
5345
5490
  const primarySourceBrief = selectStoryboardSourceBriefForCompile(options, userIntentText);
5346
5491
  const sourceText = stripGenericStoryboardVisibleTextMetadata(sanitizeStoryboardExternalAudioReferences([
5347
5492
  cleanStoryboardNarrativeSourceText(primarySourceBrief),
@@ -5365,7 +5510,7 @@ function buildStoryboardProject(options) {
5365
5510
  const requestedDurationSec = inferRequestedTotalVideoDurationSeconds(userIntentText);
5366
5511
  const durationSec = requestedDurationSec ?? inferRequestedTotalVideoDurationSeconds(sourceText);
5367
5512
  const references = buildStoryboardReferenceAssets(userIntentText, [
5368
- prompt,
5513
+ referencePromptContext,
5369
5514
  approvedScriptContext,
5370
5515
  ].filter(Boolean).join('\n\n'));
5371
5516
  const approvedSections = approvedScriptContext
@@ -5396,21 +5541,25 @@ function buildStoryboardProject(options) {
5396
5541
  return buildSceneFromSection(section, references, null, storyboardScenePlanningContractForIndex(options.planningContract, section.number));
5397
5542
  })
5398
5543
  : [];
5399
- const scenes = parsedScenes;
5544
+ const scenes = parsedScenes.map(scene => ({
5545
+ ...scene,
5546
+ referenceUsage: normalizeStoryboardSceneReferenceUsage(scene.referenceUsage, references),
5547
+ }));
5400
5548
  const timingNormalizedScenes = normalizeAssistantStoryboardSceneTiming(scenes, durationSec, options.promptAuthorship);
5401
5549
  const dialogueAlignment = alignAssistantStoryboardDialogueWithUserSource(timingNormalizedScenes, userIntentText, options.promptAuthorship);
5402
- const normalizedScenes = dialogueAlignment.shouldRetime
5550
+ const dialogueTimedScenes = (options.promptAuthorship === 'assistant' || dialogueAlignment.shouldRetime)
5403
5551
  ? retimeStoryboardScenesForDialogue(dialogueAlignment.scenes, durationSec)
5404
5552
  : dialogueAlignment.scenes;
5405
5553
  const userConstraintSource = buildStoryboardUserConstraintSource(narrativeUserIntentText, cleanStoryboardNarrativeSourceText(primarySourceBrief), options);
5406
- const { mustIncludeText, endCardText } = storyboardRequiredTextForProject(options, userConstraintSource, normalizedScenes);
5554
+ const { mustIncludeText, endCardText } = storyboardRequiredTextForProject(options, userConstraintSource, dialogueTimedScenes);
5555
+ const normalizedScenes = retimeStoryboardScenesForDialogue(applyStoryboardEndCardTextToScenes(dialogueTimedScenes, endCardText), durationSec);
5407
5556
  const voiceLines = assignVoiceLinesToScenes(normalizedScenes, sourceText);
5408
5557
  const storySpineFallback = approvedScriptContext
5409
5558
  || (options.promptAuthorship === 'assistant' ? narrativeUserIntentText : cleanStoryboardNarrativeSourceText(primarySourceBrief))
5410
5559
  || prompt
5411
5560
  || narrativeUserIntentText;
5412
5561
  const storySpine = inferStoryboardStorySpine(allText, storySpineFallback);
5413
- const productFeatureMap = inferStoryboardProductFeatureMap(allText, scenes);
5562
+ const productFeatureMap = inferStoryboardProductFeatureMap(allText, normalizedScenes);
5414
5563
  const beatMarkerScripts = [approvedScriptContext, sourceText];
5415
5564
  let directBeatMarkerCount = 0;
5416
5565
  for (const text of beatMarkerScripts) {
@@ -5640,67 +5789,17 @@ function compileStoryboardStoryContinuitySection(project) {
5640
5789
  'Keep product/feature/brand meaning in the associated notes. Do not force long product explanations or the whole script into tiny in-frame text.',
5641
5790
  ];
5642
5791
  }
5643
- function compileStoryboardCountContractSection(project, layout, expectedFrameCount = project.scenes.length) {
5644
- if (expectedFrameCount <= 0)
5645
- return [];
5646
- const sceneSlots = Array.from({ length: expectedFrameCount }, (_, index) => {
5647
- const scene = project.scenes[index];
5648
- return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
5649
- })
5650
- .join(', ');
5651
- return [
5652
- 'COUNT / GRID CONTRACT:',
5653
- `Required scene count: exactly ${expectedFrameCount} numbered storyboard scene slots; do not render fewer slots and do not add extra scene slots.`,
5654
- `Allocate the full ${expectedFrameCount}-slot storyboard grid before drawing details. Fill slots in reading order, left-to-right then top-to-bottom: ${sceneSlots}.`,
5655
- `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.`,
5656
- `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}. The final sheet should be visibly countable as ${expectedFrameCount} numbered scene slots at a glance.`,
5657
- ];
5658
- }
5659
- function compileStoryboardFrameGeometrySection(layout) {
5660
- const cellOrientation = parseAspectRatioOrientation(layout.cellAspectRatio);
5661
- if (cellOrientation === 'portrait') {
5662
- return [
5663
- 'PORTRAIT FRAME GEOMETRY:',
5664
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5665
- `Inside every numbered scene slot, draw one identical upright ${layout.cellAspectRatio} video-frame rectangle whose height is visibly greater than its width.`,
5666
- `Keep all scene numbers, timing, titles, notes, headers, footers, and production labels outside the ${layout.cellAspectRatio} video-frame rectangles.`,
5667
- `Do not make the numbered scene slots, frame artwork areas, or thumbnails square. Square cells violate the requested ${layout.cellAspectRatio} final video format.`,
5668
- 'Unused grid slots must remain blank margin/notes space only; do not stretch or square off the portrait frame rectangles to fill the grid.',
5669
- ];
5670
- }
5671
- if (cellOrientation === 'landscape') {
5672
- return [
5673
- 'LANDSCAPE FRAME GEOMETRY:',
5674
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5675
- `Inside every numbered scene slot, draw one identical wide ${layout.cellAspectRatio} video-frame rectangle whose width is visibly greater than its height.`,
5676
- `Keep all scene numbers, timing, titles, notes, headers, footers, and production labels outside the ${layout.cellAspectRatio} video-frame rectangles.`,
5677
- `Do not make the numbered scene slots, frame artwork areas, or thumbnails square. Square cells violate the requested ${layout.cellAspectRatio} final video format.`,
5678
- 'Unused grid slots must remain blank margin/notes space only; do not stretch or square off the landscape frame rectangles to fill the grid.',
5679
- ];
5680
- }
5681
- if (cellOrientation === 'square') {
5682
- return [
5683
- 'SQUARE FRAME GEOMETRY:',
5684
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5685
- `Inside every numbered scene slot, draw one identical square ${layout.cellAspectRatio} video-frame area plus compact notes outside that square.`,
5686
- `Do not stretch square frame artwork areas into portrait, landscape, full-board, or any aspect ratio other than ${layout.cellAspectRatio}.`,
5687
- ];
5688
- }
5689
- return [
5690
- 'FRAME GEOMETRY:',
5691
- `Inside every numbered scene slot, draw one identical ${layout.cellAspectRatio} video-frame rectangle plus compact notes outside that rectangle.`,
5692
- `Do not let frame artwork areas drift to square, full-board, or any aspect ratio other than ${layout.cellAspectRatio}.`,
5693
- ];
5694
- }
5695
5792
  function promptReferenceUsageForScene(referenceUsage, references, modelId) {
5696
5793
  if (referenceUsage.length === 0)
5697
5794
  return '';
5698
5795
  const referenceById = new Map(references.map((ref, index) => [ref.id, { ref, index }]));
5699
- return uniqueStoryboardStrings(referenceUsage.map(value => {
5796
+ return uniqueStoryboardStrings(referenceUsage.flatMap(value => {
5700
5797
  const found = referenceById.get(value);
5701
- if (!found)
5702
- return value;
5703
- return formatModelRef(modelId, found.ref.index ?? found.index + 1, 'image');
5798
+ if (found)
5799
+ return [formatModelRef(modelId, found.ref.index ?? found.index + 1, 'image')];
5800
+ if (looksLikeUnknownIndexedStoryboardReference(value))
5801
+ return [];
5802
+ return [value];
5704
5803
  })).join(', ');
5705
5804
  }
5706
5805
  function storyboardMetadataLabelAliases(labels) {
@@ -5835,32 +5934,36 @@ function compileVideoStoryboardImagePrompt(options) {
5835
5934
  : `Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}.`;
5836
5935
  return [
5837
5936
  'CREATE:',
5838
- `Create exactly ${compiledFrameCount} sequential video storyboard frames as one composite storyboard image.`,
5839
- `Project title: ${project.title}.`,
5840
- project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
5841
- '',
5842
- ...compileStoryboardCountContractSection(project, layout, compiledFrameCount),
5937
+ `Create exactly ${compiledFrameCount} sequential video storyboard frames as one production storyboard sheet.`,
5843
5938
  '',
5844
- ...compileStoryboardReferenceSection(project),
5939
+ 'PROJECT:',
5940
+ `Title: ${project.title}.`,
5941
+ `Format: ${layout.targetVideoAspectRatio} video storyboard.`,
5942
+ project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
5845
5943
  '',
5846
- 'CANVAS / LAYOUT:',
5944
+ 'LAYOUT CONTRACT:',
5945
+ `Create exactly ${compiledFrameCount} numbered storyboard panels; do not render fewer or more panels.`,
5946
+ `Arrange panels in reading order, left-to-right then top-to-bottom: ${Array.from({ length: compiledFrameCount }, (_, index) => {
5947
+ const scene = project.scenes[index];
5948
+ return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
5949
+ }).join(', ')}.`,
5847
5950
  boardSizeLine,
5848
5951
  `Individual scene-cell/frame aspect ratio: ${layout.cellAspectRatio}.`,
5849
5952
  `Target final video aspect ratio: ${layout.targetVideoAspectRatio}.`,
5850
5953
  `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}.`,
5851
- `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.`,
5852
- 'Keep margins, gutters, outside-frame numbering, note strips, and typography consistent across the full board.',
5853
- 'Each scene cell must make the frame-to-notes relationship clear while keeping production labels outside the actual video-frame artwork.',
5954
+ `Each panel must contain one distinct ${layout.cellAspectRatio} cinematic video-frame rectangle with compact notes outside the frame.`,
5955
+ 'Keep scene numbers, timecodes, titles, dialogue/VO, audio notes, and production notes outside the video-frame rectangles.',
5956
+ 'Do not merge panels, create inset thumbnails, make panels square, or overlay storyboard metadata inside the artwork frames.',
5854
5957
  '',
5855
- ...compileStoryboardFrameGeometrySection(layout),
5856
- '',
5857
- ...compileStoryboardStoryContinuitySection(project),
5958
+ ...compileStoryboardReferenceSection(project),
5858
5959
  '',
5859
- 'GLOBAL STYLE:',
5960
+ 'STYLE:',
5860
5961
  `${project.creativeBrief.visualQualityBar} with cinematic shot language, coherent art direction, readable labels, and consistent reference usage.`,
5861
5962
  '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.',
5862
5963
  '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.',
5863
5964
  '',
5965
+ ...compileStoryboardStoryContinuitySection(project),
5966
+ '',
5864
5967
  'CRITICAL REQUIREMENTS:',
5865
5968
  ...compileStoryboardCriticalRequirements().map((item, index) => `${index + 1}. ${item}`),
5866
5969
  ...project.references
@@ -5868,7 +5971,7 @@ function compileVideoStoryboardImagePrompt(options) {
5868
5971
  .map((ref, index) => `${compileStoryboardCriticalRequirements().length + index + 1}. Critical reference lock: ${ref.id} (${ref.kind}) must remain bound to its assigned usage scope: ${ref.usageScope}.`),
5869
5972
  '',
5870
5973
  ...compileStoryboardScenesSection(project),
5871
- 'TEXT RENDERING:',
5974
+ 'TEXT RULES:',
5872
5975
  '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.',
5873
5976
  'Visible text listed on a scene belongs only in that scene; repeat visible text only on scenes that list it.',
5874
5977
  ...storyboardRequiredVisibleText(project).map(formatStoryboardRequiredVisibleTextLine),
@@ -5887,27 +5990,28 @@ function ensureCompiledVideoStoryboardPromptPreservesSourceBrief(compiledPrompt,
5887
5990
  function lintStoryboardImagePrompt(prompt, layout, project) {
5888
5991
  const errors = [];
5889
5992
  const warnings = [];
5993
+ const hasConsolidatedLayoutContract = /LAYOUT CONTRACT:/i.test(prompt);
5890
5994
  if (!/CREATE:/i.test(prompt))
5891
5995
  errors.push('missing CREATE section');
5892
- if (!/COUNT \/ GRID CONTRACT:/i.test(prompt))
5996
+ if (!hasConsolidatedLayoutContract && !/COUNT \/ GRID CONTRACT:/i.test(prompt))
5893
5997
  errors.push('missing COUNT / GRID CONTRACT section');
5894
5998
  if (!/REFERENCE IMAGES:/i.test(prompt))
5895
5999
  errors.push('missing REFERENCE IMAGES section');
5896
- if (!/CANVAS \/ LAYOUT:/i.test(prompt))
6000
+ if (!hasConsolidatedLayoutContract && !/CANVAS \/ LAYOUT:/i.test(prompt))
5897
6001
  errors.push('missing CANVAS / LAYOUT section');
5898
- if (!/FRAME GEOMETRY:/i.test(prompt))
6002
+ if (!hasConsolidatedLayoutContract && !/FRAME GEOMETRY:/i.test(prompt))
5899
6003
  errors.push('missing FRAME GEOMETRY section');
5900
6004
  const cellOrientation = parseAspectRatioOrientation(layout.cellAspectRatio);
5901
- if (cellOrientation === 'portrait' && !/PORTRAIT FRAME GEOMETRY:/i.test(prompt)) {
6005
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'portrait' && !/PORTRAIT FRAME GEOMETRY:/i.test(prompt)) {
5902
6006
  errors.push('missing PORTRAIT FRAME GEOMETRY section');
5903
6007
  }
5904
- if (cellOrientation === 'landscape' && !/LANDSCAPE FRAME GEOMETRY:/i.test(prompt)) {
6008
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'landscape' && !/LANDSCAPE FRAME GEOMETRY:/i.test(prompt)) {
5905
6009
  errors.push('missing LANDSCAPE FRAME GEOMETRY section');
5906
6010
  }
5907
- if (cellOrientation === 'square' && !/SQUARE FRAME GEOMETRY:/i.test(prompt)) {
6011
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'square' && !/SQUARE FRAME GEOMETRY:/i.test(prompt)) {
5908
6012
  errors.push('missing SQUARE FRAME GEOMETRY section');
5909
6013
  }
5910
- if (!/TEXT RENDERING:/i.test(prompt))
6014
+ if (!/(?:TEXT RENDERING|TEXT RULES):/i.test(prompt))
5911
6015
  errors.push('missing TEXT RENDERING section');
5912
6016
  if (!prompt.includes(`Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}`)
5913
6017
  && !prompt.includes(`(${layout.boardAspectRatio})`)) {
@@ -5978,7 +6082,7 @@ function inferCompiledStoryboardPromptAspectRatio(prompt, labelPattern) {
5978
6082
  return normalizeAspectRatio(match?.[1]);
5979
6083
  }
5980
6084
  function extractCompiledStoryboardScenesBlock(prompt) {
5981
- const match = prompt.match(/\nSCENES:\s*\n([\s\S]*?)(?:\nTEXT RENDERING:|\nSOURCE BRIEF TO FOLLOW:|\nNEGATIVE \/ AVOID:|$)/i);
6085
+ const match = prompt.match(/\nSCENES:\s*\n([\s\S]*?)(?:\nTEXT RENDERING:|\nTEXT RULES:|\nSOURCE BRIEF TO FOLLOW:|\nNEGATIVE \/ AVOID:|$)/i);
5982
6086
  return match?.[1] ?? '';
5983
6087
  }
5984
6088
  function parseCompiledStoryboardHeadingTiming(heading) {