@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
@@ -696,33 +696,48 @@ function compileStoryboardImagePromptFromProject(project) {
696
696
  : `Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}.`;
697
697
  return [
698
698
  'CREATE:',
699
- `Create exactly ${frameCount} sequential video storyboard frames as one composite storyboard image.`,
700
- `Project title: ${project.title}.`,
701
- project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
702
- '',
703
- ...compileStoryboardCountContractSection(project, layout),
699
+ `Create exactly ${frameCount} sequential video storyboard frames as one production storyboard sheet.`,
704
700
  '',
705
- ...compileStoryboardReferenceSection(project),
701
+ 'PROJECT:',
702
+ `Title: ${project.title}.`,
703
+ `Format: ${layout.targetVideoAspectRatio} video storyboard.`,
704
+ project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
706
705
  '',
707
- 'CANVAS / LAYOUT:',
706
+ 'LAYOUT CONTRACT:',
707
+ `Create exactly ${frameCount} numbered storyboard panels; do not render fewer or more panels.`,
708
+ `Arrange panels in reading order, left-to-right then top-to-bottom: ${Array.from({ length: frameCount }, (_, index) => {
709
+ const scene = project.scenes[index];
710
+ return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
711
+ }).join(', ')}.`,
708
712
  boardSizeLine,
709
713
  `Individual scene-cell/frame aspect ratio: ${layout.cellAspectRatio}.`,
710
714
  `Target final video aspect ratio: ${layout.targetVideoAspectRatio}.`,
711
715
  `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}.`,
716
+ `Each panel must contain one distinct ${layout.cellAspectRatio} cinematic video-frame rectangle with compact notes outside the frame.`,
717
+ 'Keep scene numbers, timecodes, titles, dialogue/VO, audio notes, and production notes outside the video-frame rectangles.',
718
+ 'Do not merge panels, create inset thumbnails, make panels square, or overlay storyboard metadata inside the artwork frames.',
712
719
  '',
713
- ...compileStoryboardStoryContinuitySection(project),
720
+ ...compileStoryboardReferenceSection(project),
721
+ '',
722
+ 'STYLE:',
723
+ `${project.creativeBrief.visualQualityBar} with cinematic shot language, coherent art direction, readable labels, and consistent reference usage.`,
724
+ '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.',
725
+ '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.',
714
726
  '',
715
- 'GLOBAL STYLE:',
716
- project.creativeBrief.visualQualityBar,
727
+ ...compileStoryboardStoryContinuitySection(project),
717
728
  '',
718
729
  'CRITICAL REQUIREMENTS:',
719
730
  ...compileStoryboardCriticalRequirements().map((item, index) => `${index + 1}. ${item}`),
731
+ ...project.references
732
+ .filter(ref => ref.preservePriority === 'critical')
733
+ .map((ref, index) => `${compileStoryboardCriticalRequirements().length + index + 1}. Critical reference lock: ${ref.id} (${ref.kind}) must remain bound to its assigned usage scope: ${ref.usageScope}.`),
720
734
  '',
721
735
  ...compileStoryboardScenesSection(project),
722
- 'TEXT RENDERING:',
723
- 'Keep production labels outside video-frame artwork. Only user-required diegetic or brand text belongs inside a frame.',
736
+ 'TEXT RULES:',
737
+ '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.',
724
738
  'Visible text listed on a scene belongs only in that scene; repeat visible text only on scenes that list it.',
725
739
  ...storyboardRequiredVisibleText(project).map(formatStoryboardRequiredVisibleTextLine),
740
+ project.endCard.logoUsage ? `Logo usage: ${project.endCard.logoUsage}` : '',
726
741
  '',
727
742
  'NEGATIVE / AVOID:',
728
743
  ...compileStoryboardAvoidSection(project.creativeBrief.concept).map(item => `- ${item}`),
@@ -1558,7 +1573,7 @@ export function suggestedDurationForDialogue(prompt, currentDuration) {
1558
1573
  const baseDuration = currentDuration ?? 0;
1559
1574
  if (words <= 0)
1560
1575
  return baseDuration;
1561
- const speechSeconds = Math.ceil(words / 2.5) + 2;
1576
+ const speechSeconds = Math.ceil(words / 3) + 2;
1562
1577
  return Math.max(baseDuration, Math.min(20, speechSeconds));
1563
1578
  }
1564
1579
  export function formatAudioIdPrompt(prompt, voiceName) {
@@ -2656,8 +2671,8 @@ const STORYBOARD_COUNT_WORDS = {
2656
2671
  };
2657
2672
  const DEFAULT_STORYBOARD_TIMING_RULES = {
2658
2673
  normalWordsPerSecondMin: 2.0,
2659
- normalWordsPerSecondMax: 3.3,
2660
- fastWordsPerSecondMax: 4.0,
2674
+ normalWordsPerSecondMax: 3.0,
2675
+ fastWordsPerSecondMax: 3.75,
2661
2676
  minEndCardHoldSec: 2.0,
2662
2677
  minPunchlineSec: 0.5,
2663
2678
  toleranceSec: 0.25,
@@ -2850,10 +2865,13 @@ function describePortraitLetterboxCellArrangement(frameCount, cellAspectRatio) {
2850
2865
  }
2851
2866
  const columns = frameCount <= 8 ? 2 : frameCount <= 15 ? 3 : 4;
2852
2867
  const rows = Math.ceil(frameCount / columns);
2868
+ const unusedSlotNote = columns * rows > frameCount
2869
+ ? ['use unused grid slots as margin/notes space only']
2870
+ : [];
2853
2871
  return [
2854
2872
  `${frameCount} numbered scene slots arranged as a ${columns}-column x ${rows}-row grid inside a portrait sheet`,
2855
2873
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
2856
- 'use unused grid slots as margin/notes space only',
2874
+ ...unusedSlotNote,
2857
2875
  ].join('; ');
2858
2876
  }
2859
2877
  function describeLandscapePortraitCellArrangement(frameCount, cellAspectRatio) {
@@ -2863,10 +2881,13 @@ function describeLandscapePortraitCellArrangement(frameCount, cellAspectRatio) {
2863
2881
  }
2864
2882
  const rows = frameCount <= 8 ? 2 : frameCount <= 15 ? 3 : 4;
2865
2883
  const columns = Math.ceil(frameCount / rows);
2884
+ const unusedSlotNote = columns * rows > frameCount
2885
+ ? ['use unused grid slots as margin/notes space only']
2886
+ : [];
2866
2887
  return [
2867
2888
  `${frameCount} numbered scene slots arranged as a ${rows}-row x ${columns}-column grid inside a landscape board`,
2868
2889
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
2869
- 'use unused grid slots as margin/notes space only',
2890
+ ...unusedSlotNote,
2870
2891
  ].join('; ');
2871
2892
  }
2872
2893
  function parseAspectRatioPair(aspectRatio) {
@@ -2981,13 +3002,16 @@ function describeSingleOrientationStoryboardArrangement(boardAspectRatio, cellAs
2981
3002
  const slotGrid = pickBalancedSlotGrid(frameCount);
2982
3003
  const cols = cellOrientation === 'landscape' ? slotGrid.minor : slotGrid.major;
2983
3004
  const rows = cellOrientation === 'landscape' ? slotGrid.major : slotGrid.minor;
3005
+ const unusedSlotNote = cols * rows > frameCount
3006
+ ? ['use unused grid slots as margin/notes space only']
3007
+ : [];
2984
3008
  if (boardOrientation === 'portrait') {
2985
3009
  return {
2986
3010
  layoutKind: 'portrait_grid',
2987
3011
  layoutDescription: [
2988
3012
  `${frameCount} numbered scene slots arranged as a ${cols}-column x ${rows}-row grid inside a portrait storyboard sheet`,
2989
3013
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
2990
- 'use unused grid slots as margin/notes space only',
3014
+ ...unusedSlotNote,
2991
3015
  ].join('; '),
2992
3016
  };
2993
3017
  }
@@ -2997,7 +3021,7 @@ function describeSingleOrientationStoryboardArrangement(boardAspectRatio, cellAs
2997
3021
  layoutDescription: [
2998
3022
  `${frameCount} numbered scene slots arranged as a ${rows}-row x ${cols}-column grid inside a ${boardLabel}`,
2999
3023
  `each slot contains one ${frameShape} with compact labels outside the rectangle`,
3000
- 'use unused grid slots as margin/notes space only',
3024
+ ...unusedSlotNote,
3001
3025
  ].join('; '),
3002
3026
  };
3003
3027
  }
@@ -3381,6 +3405,16 @@ function cleanStoryboardReferenceSubjectHint(context, index) {
3381
3405
  const explicitSubject = cleanExplicitStoryboardReferenceSubject(source, index);
3382
3406
  if (explicitSubject)
3383
3407
  return explicitSubject;
3408
+ 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);
3409
+ if (inlineReference?.[1]) {
3410
+ const inlineSubject = compactStoryboardLine(stripStoryboardMarkup(inlineReference[1]))
3411
+ .replace(/\b(?:use|usage|preserve|reference|asset)\b[\s\S]*$/i, '')
3412
+ .replace(/[.\s]+$/g, '')
3413
+ .trim();
3414
+ if (inlineSubject && !/^(?:logo|brand|character|mascot|asset|reference|image|photo|picture)$/i.test(inlineSubject)) {
3415
+ return inlineSubject.slice(0, 180).replace(/\s+\S*$/, match => inlineSubject.length > 180 ? '' : match).trim();
3416
+ }
3417
+ }
3384
3418
  const cleaned = source
3385
3419
  .replace(new RegExp(String.raw `^(?:image|photo|picture|asset)\s*(?:#|number\s*)?${index}\s*(?:\([^)]*\))?\s*:?\s*`, 'i'), '')
3386
3420
  .replace(new RegExp(String.raw `^(?:uploaded|attached|provided|reference|source|input)\s+(?:image|photo|picture|asset)\s*(?:#|number\s*)?${index}\s*:?\s*`, 'i'), '')
@@ -3488,6 +3522,51 @@ function buildStoryboardReferenceAssets(userIntentText, prompt) {
3488
3522
  preservePriority: storyboardPreservePriorityFromRole(role),
3489
3523
  }));
3490
3524
  }
3525
+ function storyboardReferenceIndexFromText(value) {
3526
+ const normalized = value.trim();
3527
+ if (!normalized)
3528
+ return null;
3529
+ const match = normalized.match(/^(?:@?\s*)?(?:image|photo|picture|asset|uploaded\s+asset|uploaded\s+image)[\s_#-]*(\d{1,2})$/i)
3530
+ ?? normalized.match(/^image_(\d{1,2})$/i);
3531
+ if (!match)
3532
+ return null;
3533
+ const index = Number(match[1]);
3534
+ return Number.isInteger(index) && index > 0 ? index : null;
3535
+ }
3536
+ function looksLikeUnknownIndexedStoryboardReference(value) {
3537
+ return /^(?:@?\s*)?(?:image|photo|picture|asset|uploaded\s+asset|uploaded\s+image)[\s_#-]*\d{1,2}$/i.test(value.trim())
3538
+ || /^image_\d{1,2}$/i.test(value.trim());
3539
+ }
3540
+ function normalizeStoryboardSceneReferenceUsage(referenceUsage, references) {
3541
+ if (referenceUsage.length === 0 || references.length === 0)
3542
+ return [];
3543
+ const knownById = new Map(references.map(ref => [ref.id.toLowerCase(), ref.id]));
3544
+ const knownByIndex = new Map(references
3545
+ .filter(ref => typeof ref.index === 'number')
3546
+ .map(ref => [ref.index, ref.id]));
3547
+ const normalized = [];
3548
+ for (const raw of referenceUsage) {
3549
+ const value = raw.trim();
3550
+ if (!value)
3551
+ continue;
3552
+ const knownId = knownById.get(value.toLowerCase());
3553
+ if (knownId) {
3554
+ normalized.push(knownId);
3555
+ continue;
3556
+ }
3557
+ const index = storyboardReferenceIndexFromText(value);
3558
+ if (index !== null) {
3559
+ const id = knownByIndex.get(index);
3560
+ if (id)
3561
+ normalized.push(id);
3562
+ continue;
3563
+ }
3564
+ if (looksLikeUnknownIndexedStoryboardReference(value))
3565
+ continue;
3566
+ normalized.push(value);
3567
+ }
3568
+ return uniqueStoryboardStrings(normalized);
3569
+ }
3491
3570
  function compileStoryboardReferenceSection(project) {
3492
3571
  const refs = project.references;
3493
3572
  if (refs.length <= 0) {
@@ -3535,6 +3614,31 @@ function extractStoryboardRequiredText(text) {
3535
3614
  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;
3536
3615
  const sceneHeadingPattern = /^\s*(?:[-*+]\s*)?(?:#{1,6}\s*)?(?:[*_]{1,3})?\s*(?:Scene|Shot|Beat|Panel|Frame)[\s_#-]*\d{1,2}\b/i;
3537
3616
  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;
3617
+ const storyboardTextCandidateLooksLikeConversationInstruction = (candidate) => {
3618
+ const raw = compactStoryboardLine(stripStoryboardMarkup(candidate))
3619
+ .replace(/^["“”'`]+|["“”'`]+$/g, '')
3620
+ .trim();
3621
+ if (!raw)
3622
+ return false;
3623
+ if (/^\[(?:uploaded|attached|provided)\s+(?:images?|files?|videos?|audio)\s*:/i.test(raw))
3624
+ return true;
3625
+ if (/^(?:yes|no|approved?|looks?\s+good|ok(?:ay)?|sure)[.!?]?$/i.test(raw))
3626
+ return true;
3627
+ const value = raw.toLowerCase();
3628
+ 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;
3629
+ if (/^use\s+your\s+best\s+judg(?:e)?ment\b/i.test(raw))
3630
+ return true;
3631
+ if (/^(?:try\s+another|another\s+take|regenerate|re-generate|redo|retry|revise)\b/i.test(raw))
3632
+ return true;
3633
+ if (/^(?:go\s+ahead|can\s+you|please)\b/i.test(raw) && workflowTerm.test(value))
3634
+ return true;
3635
+ if (/^let'?s\b|^lets\b/i.test(raw)) {
3636
+ return /\b(?:script|prompt|story\s*board|storyboard|take|version|again|redo|retry|regenerate|re-generate|revise|revision)\b/i.test(value)
3637
+ || /\b(?:generate|render|create|do|run)\s+it\b/i.test(value)
3638
+ || /\buse\s+your\s+best\s+judg(?:e)?ment\b/i.test(value);
3639
+ }
3640
+ return false;
3641
+ };
3538
3642
  const shouldIgnoreRequiredText = (value, matchIndex, precedingText) => {
3539
3643
  const lineStart = text.lastIndexOf('\n', matchIndex) + 1;
3540
3644
  const nextLineBreak = text.indexOf('\n', matchIndex);
@@ -3570,6 +3674,9 @@ function extractStoryboardRequiredText(text) {
3570
3674
  if (storyboardTextCandidateLooksLikeGenericProductionLabel(value)) {
3571
3675
  return true;
3572
3676
  }
3677
+ if (storyboardTextCandidateLooksLikeConversationInstruction(value)) {
3678
+ return true;
3679
+ }
3573
3680
  if (isNestedSceneHeadingField) {
3574
3681
  return true;
3575
3682
  }
@@ -3644,6 +3751,8 @@ function extractStoryboardRequiredText(text) {
3644
3751
  return false;
3645
3752
  if (storyboardTextCandidateLooksLikeGenericProductionLabel(value))
3646
3753
  return false;
3754
+ if (storyboardTextCandidateLooksLikeConversationInstruction(value))
3755
+ return false;
3647
3756
  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
3648
3757
  .test(context);
3649
3758
  if (!contextNamesText)
@@ -3660,7 +3769,10 @@ function extractStoryboardRequiredText(text) {
3660
3769
  || /^(?:powered|made|built|created|generated|rendered|presented)\s+by\b/i.test(value);
3661
3770
  return !productionDirection || copyOverridesProduction;
3662
3771
  };
3663
- const endCardLabelPattern = /^\s*(?:[-*+]\s*)?(?:end|final|closing)\s+(?:scene|card|frame|shot|cta|logo(?:\s+lockup)?)\s*:\s*([^\n]*)$/gim;
3772
+ const endCardFollowingLineLooksLikeContinuationInstruction = (line) => {
3773
+ return storyboardTextCandidateLooksLikeConversationInstruction(line);
3774
+ };
3775
+ 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;
3664
3776
  for (const match of text.matchAll(endCardLabelPattern)) {
3665
3777
  const matchIndex = match.index ?? 0;
3666
3778
  const lineEnd = text.indexOf('\n', matchIndex);
@@ -3672,6 +3784,8 @@ function extractStoryboardRequiredText(text) {
3672
3784
  break;
3673
3785
  if (/^(?:#{1,6}\s+|\|\s*|(?:Scene|Shot|Beat|Panel|Frame)\s*\d{1,2}\b)/i.test(trimmed))
3674
3786
  break;
3787
+ if (endCardFollowingLineLooksLikeContinuationInstruction(trimmed))
3788
+ break;
3675
3789
  if (followingLines.length >= 5)
3676
3790
  break;
3677
3791
  followingLines.push(trimmed);
@@ -4986,10 +5100,10 @@ function alignAssistantStoryboardDialogueWithUserSource(scenes, userIntentText,
4986
5100
  return { scenes, shouldRetime: false };
4987
5101
  return { scenes: repaired, shouldRetime: true };
4988
5102
  }
4989
- function minimumStoryboardSceneDurationForDialogue(scene, rules) {
5103
+ function minimumStoryboardSceneDurationForDialogue(scene, rules, wordsPerSecondMax = rules.normalWordsPerSecondMax) {
4990
5104
  const dialogueWords = countWords(scene.dialogue);
4991
5105
  let minDuration = dialogueWords > 0
4992
- ? dialogueWords / rules.normalWordsPerSecondMax
5106
+ ? dialogueWords / wordsPerSecondMax
4993
5107
  : 0.5;
4994
5108
  const sceneContext = `${scene.title}\n${scene.visual}\n${scene.textInImage.join(' ')}`;
4995
5109
  const hasReadableText = scene.textInImage.length > 0;
@@ -5012,13 +5126,22 @@ function retimeStoryboardScenesForDialogue(scenes, targetDurationSec, rules = DE
5012
5126
  const needsRetiming = scenes.some((scene, index) => (scene.durationSec ?? 0) + rules.toleranceSec < minimumDurations[index]);
5013
5127
  if (!needsRetiming)
5014
5128
  return scenes;
5015
- const minimumTotal = Math.round(minimumDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5129
+ let requiredDurations = minimumDurations;
5130
+ let minimumTotal = Math.round(requiredDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5131
+ if (minimumTotal > targetDurationSec + rules.toleranceSec && rules.fastWordsPerSecondMax > rules.normalWordsPerSecondMax) {
5132
+ const fastDurations = scenes.map(scene => minimumStoryboardSceneDurationForDialogue(scene, rules, rules.fastWordsPerSecondMax));
5133
+ const fastTotal = Math.round(fastDurations.reduce((sum, duration) => sum + duration, 0) * 100) / 100;
5134
+ if (fastTotal <= targetDurationSec + rules.toleranceSec) {
5135
+ requiredDurations = fastDurations;
5136
+ minimumTotal = fastTotal;
5137
+ }
5138
+ }
5016
5139
  if (minimumTotal > targetDurationSec + rules.toleranceSec)
5017
5140
  return scenes;
5018
5141
  const extraDuration = Math.max(0, targetDurationSec - minimumTotal);
5019
5142
  const originalDurations = scenes.map(scene => scene.durationSec ?? 0);
5020
5143
  const weightTotal = originalDurations.reduce((sum, duration) => sum + Math.max(0.01, duration), 0);
5021
- const unroundedDurations = minimumDurations.map((minimum, index) => (minimum + (extraDuration * Math.max(0.01, originalDurations[index]) / weightTotal)));
5144
+ const unroundedDurations = requiredDurations.map((minimum, index) => (minimum + (extraDuration * Math.max(0.01, originalDurations[index]) / weightTotal)));
5022
5145
  const timelineStartSec = scenes[0].startSec ?? 0;
5023
5146
  const timelineEndSec = Math.round((timelineStartSec + targetDurationSec) * 100) / 100;
5024
5147
  let cursor = timelineStartSec;
@@ -5190,6 +5313,27 @@ function finalStoryboardSceneVisibleText(scenes) {
5190
5313
  }
5191
5314
  return [];
5192
5315
  }
5316
+ function applyStoryboardEndCardTextToScenes(scenes, endCardText) {
5317
+ if (scenes.length === 0 || endCardText.length === 0)
5318
+ return scenes;
5319
+ const finalSceneIndex = scenes.length - 1;
5320
+ const explicitEndCardIndex = (() => {
5321
+ for (let index = scenes.length - 1; index >= 0; index -= 1) {
5322
+ if (storyboardSceneLooksLikeEndCard(scenes[index]))
5323
+ return index;
5324
+ }
5325
+ return finalSceneIndex;
5326
+ })();
5327
+ return scenes.map((scene, index) => index === explicitEndCardIndex
5328
+ ? {
5329
+ ...scene,
5330
+ textInImage: uniqueStoryboardStrings([
5331
+ ...scene.textInImage,
5332
+ ...endCardText,
5333
+ ]),
5334
+ }
5335
+ : scene);
5336
+ }
5193
5337
  function storyboardRequiredTextKey(value) {
5194
5338
  return compactStoryboardLine(value)
5195
5339
  .toLowerCase()
@@ -5240,6 +5384,7 @@ export function buildStoryboardProject(options) {
5240
5384
  const userIntentText = canonicalStoryboardScriptContext(rawUserIntentText) || rawUserIntentText;
5241
5385
  const narrativeUserIntentText = cleanStoryboardNarrativeSourceText(userIntentText);
5242
5386
  const approvedScriptContext = cleanStoryboardNarrativeSourceText(canonicalStoryboardScriptContext(options.approvedScriptContext));
5387
+ const referencePromptContext = cleanStoryboardNarrativeSourceText(canonicalStoryboardScriptContext(prompt) || prompt);
5243
5388
  const primarySourceBrief = selectStoryboardSourceBriefForCompile(options, userIntentText);
5244
5389
  const sourceText = stripGenericStoryboardVisibleTextMetadata(sanitizeStoryboardExternalAudioReferences([
5245
5390
  cleanStoryboardNarrativeSourceText(primarySourceBrief),
@@ -5263,7 +5408,7 @@ export function buildStoryboardProject(options) {
5263
5408
  const requestedDurationSec = inferRequestedTotalVideoDurationSeconds(userIntentText);
5264
5409
  const durationSec = requestedDurationSec ?? inferRequestedTotalVideoDurationSeconds(sourceText);
5265
5410
  const references = buildStoryboardReferenceAssets(userIntentText, [
5266
- prompt,
5411
+ referencePromptContext,
5267
5412
  approvedScriptContext,
5268
5413
  ].filter(Boolean).join('\n\n'));
5269
5414
  const approvedSections = approvedScriptContext
@@ -5294,21 +5439,25 @@ export function buildStoryboardProject(options) {
5294
5439
  return buildSceneFromSection(section, references, null, storyboardScenePlanningContractForIndex(options.planningContract, section.number));
5295
5440
  })
5296
5441
  : [];
5297
- const scenes = parsedScenes;
5442
+ const scenes = parsedScenes.map(scene => ({
5443
+ ...scene,
5444
+ referenceUsage: normalizeStoryboardSceneReferenceUsage(scene.referenceUsage, references),
5445
+ }));
5298
5446
  const timingNormalizedScenes = normalizeAssistantStoryboardSceneTiming(scenes, durationSec, options.promptAuthorship);
5299
5447
  const dialogueAlignment = alignAssistantStoryboardDialogueWithUserSource(timingNormalizedScenes, userIntentText, options.promptAuthorship);
5300
- const normalizedScenes = dialogueAlignment.shouldRetime
5448
+ const dialogueTimedScenes = (options.promptAuthorship === 'assistant' || dialogueAlignment.shouldRetime)
5301
5449
  ? retimeStoryboardScenesForDialogue(dialogueAlignment.scenes, durationSec)
5302
5450
  : dialogueAlignment.scenes;
5303
5451
  const userConstraintSource = buildStoryboardUserConstraintSource(narrativeUserIntentText, cleanStoryboardNarrativeSourceText(primarySourceBrief), options);
5304
- const { mustIncludeText, endCardText } = storyboardRequiredTextForProject(options, userConstraintSource, normalizedScenes);
5452
+ const { mustIncludeText, endCardText } = storyboardRequiredTextForProject(options, userConstraintSource, dialogueTimedScenes);
5453
+ const normalizedScenes = retimeStoryboardScenesForDialogue(applyStoryboardEndCardTextToScenes(dialogueTimedScenes, endCardText), durationSec);
5305
5454
  const voiceLines = assignVoiceLinesToScenes(normalizedScenes, sourceText);
5306
5455
  const storySpineFallback = approvedScriptContext
5307
5456
  || (options.promptAuthorship === 'assistant' ? narrativeUserIntentText : cleanStoryboardNarrativeSourceText(primarySourceBrief))
5308
5457
  || prompt
5309
5458
  || narrativeUserIntentText;
5310
5459
  const storySpine = inferStoryboardStorySpine(allText, storySpineFallback);
5311
- const productFeatureMap = inferStoryboardProductFeatureMap(allText, scenes);
5460
+ const productFeatureMap = inferStoryboardProductFeatureMap(allText, normalizedScenes);
5312
5461
  const beatMarkerScripts = [approvedScriptContext, sourceText];
5313
5462
  let directBeatMarkerCount = 0;
5314
5463
  for (const text of beatMarkerScripts) {
@@ -5538,67 +5687,17 @@ function compileStoryboardStoryContinuitySection(project) {
5538
5687
  'Keep product/feature/brand meaning in the associated notes. Do not force long product explanations or the whole script into tiny in-frame text.',
5539
5688
  ];
5540
5689
  }
5541
- function compileStoryboardCountContractSection(project, layout, expectedFrameCount = project.scenes.length) {
5542
- if (expectedFrameCount <= 0)
5543
- return [];
5544
- const sceneSlots = Array.from({ length: expectedFrameCount }, (_, index) => {
5545
- const scene = project.scenes[index];
5546
- return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
5547
- })
5548
- .join(', ');
5549
- return [
5550
- 'COUNT / GRID CONTRACT:',
5551
- `Required scene count: exactly ${expectedFrameCount} numbered storyboard scene slots; do not render fewer slots and do not add extra scene slots.`,
5552
- `Allocate the full ${expectedFrameCount}-slot storyboard grid before drawing details. Fill slots in reading order, left-to-right then top-to-bottom: ${sceneSlots}.`,
5553
- `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.`,
5554
- `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}. The final sheet should be visibly countable as ${expectedFrameCount} numbered scene slots at a glance.`,
5555
- ];
5556
- }
5557
- function compileStoryboardFrameGeometrySection(layout) {
5558
- const cellOrientation = parseAspectRatioOrientation(layout.cellAspectRatio);
5559
- if (cellOrientation === 'portrait') {
5560
- return [
5561
- 'PORTRAIT FRAME GEOMETRY:',
5562
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5563
- `Inside every numbered scene slot, draw one identical upright ${layout.cellAspectRatio} video-frame rectangle whose height is visibly greater than its width.`,
5564
- `Keep all scene numbers, timing, titles, notes, headers, footers, and production labels outside the ${layout.cellAspectRatio} video-frame rectangles.`,
5565
- `Do not make the numbered scene slots, frame artwork areas, or thumbnails square. Square cells violate the requested ${layout.cellAspectRatio} final video format.`,
5566
- 'Unused grid slots must remain blank margin/notes space only; do not stretch or square off the portrait frame rectangles to fill the grid.',
5567
- ];
5568
- }
5569
- if (cellOrientation === 'landscape') {
5570
- return [
5571
- 'LANDSCAPE FRAME GEOMETRY:',
5572
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5573
- `Inside every numbered scene slot, draw one identical wide ${layout.cellAspectRatio} video-frame rectangle whose width is visibly greater than its height.`,
5574
- `Keep all scene numbers, timing, titles, notes, headers, footers, and production labels outside the ${layout.cellAspectRatio} video-frame rectangles.`,
5575
- `Do not make the numbered scene slots, frame artwork areas, or thumbnails square. Square cells violate the requested ${layout.cellAspectRatio} final video format.`,
5576
- 'Unused grid slots must remain blank margin/notes space only; do not stretch or square off the landscape frame rectangles to fill the grid.',
5577
- ];
5578
- }
5579
- if (cellOrientation === 'square') {
5580
- return [
5581
- 'SQUARE FRAME GEOMETRY:',
5582
- 'Treat each numbered scene slot as a container, not as the artwork shape.',
5583
- `Inside every numbered scene slot, draw one identical square ${layout.cellAspectRatio} video-frame area plus compact notes outside that square.`,
5584
- `Do not stretch square frame artwork areas into portrait, landscape, full-board, or any aspect ratio other than ${layout.cellAspectRatio}.`,
5585
- ];
5586
- }
5587
- return [
5588
- 'FRAME GEOMETRY:',
5589
- `Inside every numbered scene slot, draw one identical ${layout.cellAspectRatio} video-frame rectangle plus compact notes outside that rectangle.`,
5590
- `Do not let frame artwork areas drift to square, full-board, or any aspect ratio other than ${layout.cellAspectRatio}.`,
5591
- ];
5592
- }
5593
5690
  function promptReferenceUsageForScene(referenceUsage, references, modelId) {
5594
5691
  if (referenceUsage.length === 0)
5595
5692
  return '';
5596
5693
  const referenceById = new Map(references.map((ref, index) => [ref.id, { ref, index }]));
5597
- return uniqueStoryboardStrings(referenceUsage.map(value => {
5694
+ return uniqueStoryboardStrings(referenceUsage.flatMap(value => {
5598
5695
  const found = referenceById.get(value);
5599
- if (!found)
5600
- return value;
5601
- return formatModelRef(modelId, found.ref.index ?? found.index + 1, 'image');
5696
+ if (found)
5697
+ return [formatModelRef(modelId, found.ref.index ?? found.index + 1, 'image')];
5698
+ if (looksLikeUnknownIndexedStoryboardReference(value))
5699
+ return [];
5700
+ return [value];
5602
5701
  })).join(', ');
5603
5702
  }
5604
5703
  function storyboardMetadataLabelAliases(labels) {
@@ -5733,32 +5832,36 @@ export function compileVideoStoryboardImagePrompt(options) {
5733
5832
  : `Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}.`;
5734
5833
  return [
5735
5834
  'CREATE:',
5736
- `Create exactly ${compiledFrameCount} sequential video storyboard frames as one composite storyboard image.`,
5737
- `Project title: ${project.title}.`,
5738
- project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
5739
- '',
5740
- ...compileStoryboardCountContractSection(project, layout, compiledFrameCount),
5835
+ `Create exactly ${compiledFrameCount} sequential video storyboard frames as one production storyboard sheet.`,
5741
5836
  '',
5742
- ...compileStoryboardReferenceSection(project),
5837
+ 'PROJECT:',
5838
+ `Title: ${project.title}.`,
5839
+ `Format: ${layout.targetVideoAspectRatio} video storyboard.`,
5840
+ project.durationSec !== null ? `Target duration: ${project.durationSec} seconds.` : 'Target duration: unspecified in source brief.',
5743
5841
  '',
5744
- 'CANVAS / LAYOUT:',
5842
+ 'LAYOUT CONTRACT:',
5843
+ `Create exactly ${compiledFrameCount} numbered storyboard panels; do not render fewer or more panels.`,
5844
+ `Arrange panels in reading order, left-to-right then top-to-bottom: ${Array.from({ length: compiledFrameCount }, (_, index) => {
5845
+ const scene = project.scenes[index];
5846
+ return `[${index + 1}] ${scene?.id.toUpperCase() ?? `SCENE_${String(index + 1).padStart(2, '0')}`}`;
5847
+ }).join(', ')}.`,
5745
5848
  boardSizeLine,
5746
5849
  `Individual scene-cell/frame aspect ratio: ${layout.cellAspectRatio}.`,
5747
5850
  `Target final video aspect ratio: ${layout.targetVideoAspectRatio}.`,
5748
5851
  `Layout preset: ${layout.layoutKind} - ${layout.layoutDescription}.`,
5749
- `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.`,
5750
- 'Keep margins, gutters, outside-frame numbering, note strips, and typography consistent across the full board.',
5751
- 'Each scene cell must make the frame-to-notes relationship clear while keeping production labels outside the actual video-frame artwork.',
5852
+ `Each panel must contain one distinct ${layout.cellAspectRatio} cinematic video-frame rectangle with compact notes outside the frame.`,
5853
+ 'Keep scene numbers, timecodes, titles, dialogue/VO, audio notes, and production notes outside the video-frame rectangles.',
5854
+ 'Do not merge panels, create inset thumbnails, make panels square, or overlay storyboard metadata inside the artwork frames.',
5752
5855
  '',
5753
- ...compileStoryboardFrameGeometrySection(layout),
5754
- '',
5755
- ...compileStoryboardStoryContinuitySection(project),
5856
+ ...compileStoryboardReferenceSection(project),
5756
5857
  '',
5757
- 'GLOBAL STYLE:',
5858
+ 'STYLE:',
5758
5859
  `${project.creativeBrief.visualQualityBar} with cinematic shot language, coherent art direction, readable labels, and consistent reference usage.`,
5759
5860
  '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.',
5760
5861
  '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.',
5761
5862
  '',
5863
+ ...compileStoryboardStoryContinuitySection(project),
5864
+ '',
5762
5865
  'CRITICAL REQUIREMENTS:',
5763
5866
  ...compileStoryboardCriticalRequirements().map((item, index) => `${index + 1}. ${item}`),
5764
5867
  ...project.references
@@ -5766,7 +5869,7 @@ export function compileVideoStoryboardImagePrompt(options) {
5766
5869
  .map((ref, index) => `${compileStoryboardCriticalRequirements().length + index + 1}. Critical reference lock: ${ref.id} (${ref.kind}) must remain bound to its assigned usage scope: ${ref.usageScope}.`),
5767
5870
  '',
5768
5871
  ...compileStoryboardScenesSection(project),
5769
- 'TEXT RENDERING:',
5872
+ 'TEXT RULES:',
5770
5873
  '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.',
5771
5874
  'Visible text listed on a scene belongs only in that scene; repeat visible text only on scenes that list it.',
5772
5875
  ...storyboardRequiredVisibleText(project).map(formatStoryboardRequiredVisibleTextLine),
@@ -5785,27 +5888,28 @@ export function ensureCompiledVideoStoryboardPromptPreservesSourceBrief(compiled
5785
5888
  export function lintStoryboardImagePrompt(prompt, layout, project) {
5786
5889
  const errors = [];
5787
5890
  const warnings = [];
5891
+ const hasConsolidatedLayoutContract = /LAYOUT CONTRACT:/i.test(prompt);
5788
5892
  if (!/CREATE:/i.test(prompt))
5789
5893
  errors.push('missing CREATE section');
5790
- if (!/COUNT \/ GRID CONTRACT:/i.test(prompt))
5894
+ if (!hasConsolidatedLayoutContract && !/COUNT \/ GRID CONTRACT:/i.test(prompt))
5791
5895
  errors.push('missing COUNT / GRID CONTRACT section');
5792
5896
  if (!/REFERENCE IMAGES:/i.test(prompt))
5793
5897
  errors.push('missing REFERENCE IMAGES section');
5794
- if (!/CANVAS \/ LAYOUT:/i.test(prompt))
5898
+ if (!hasConsolidatedLayoutContract && !/CANVAS \/ LAYOUT:/i.test(prompt))
5795
5899
  errors.push('missing CANVAS / LAYOUT section');
5796
- if (!/FRAME GEOMETRY:/i.test(prompt))
5900
+ if (!hasConsolidatedLayoutContract && !/FRAME GEOMETRY:/i.test(prompt))
5797
5901
  errors.push('missing FRAME GEOMETRY section');
5798
5902
  const cellOrientation = parseAspectRatioOrientation(layout.cellAspectRatio);
5799
- if (cellOrientation === 'portrait' && !/PORTRAIT FRAME GEOMETRY:/i.test(prompt)) {
5903
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'portrait' && !/PORTRAIT FRAME GEOMETRY:/i.test(prompt)) {
5800
5904
  errors.push('missing PORTRAIT FRAME GEOMETRY section');
5801
5905
  }
5802
- if (cellOrientation === 'landscape' && !/LANDSCAPE FRAME GEOMETRY:/i.test(prompt)) {
5906
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'landscape' && !/LANDSCAPE FRAME GEOMETRY:/i.test(prompt)) {
5803
5907
  errors.push('missing LANDSCAPE FRAME GEOMETRY section');
5804
5908
  }
5805
- if (cellOrientation === 'square' && !/SQUARE FRAME GEOMETRY:/i.test(prompt)) {
5909
+ if (!hasConsolidatedLayoutContract && cellOrientation === 'square' && !/SQUARE FRAME GEOMETRY:/i.test(prompt)) {
5806
5910
  errors.push('missing SQUARE FRAME GEOMETRY section');
5807
5911
  }
5808
- if (!/TEXT RENDERING:/i.test(prompt))
5912
+ if (!/(?:TEXT RENDERING|TEXT RULES):/i.test(prompt))
5809
5913
  errors.push('missing TEXT RENDERING section');
5810
5914
  if (!prompt.includes(`Overall storyboard canvas aspect ratio: ${layout.boardAspectRatio}`)
5811
5915
  && !prompt.includes(`(${layout.boardAspectRatio})`)) {
@@ -5876,7 +5980,7 @@ function inferCompiledStoryboardPromptAspectRatio(prompt, labelPattern) {
5876
5980
  return normalizeAspectRatio(match?.[1]);
5877
5981
  }
5878
5982
  function extractCompiledStoryboardScenesBlock(prompt) {
5879
- const match = prompt.match(/\nSCENES:\s*\n([\s\S]*?)(?:\nTEXT RENDERING:|\nSOURCE BRIEF TO FOLLOW:|\nNEGATIVE \/ AVOID:|$)/i);
5983
+ const match = prompt.match(/\nSCENES:\s*\n([\s\S]*?)(?:\nTEXT RENDERING:|\nTEXT RULES:|\nSOURCE BRIEF TO FOLLOW:|\nNEGATIVE \/ AVOID:|$)/i);
5880
5984
  return match?.[1] ?? '';
5881
5985
  }
5882
5986
  function parseCompiledStoryboardHeadingTiming(heading) {