@milenyumai/film-kit 2.2.1 → 2.3.1

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 (55) hide show
  1. package/README.md +69 -17
  2. package/build/index.d.ts +1 -1
  3. package/build/lib/cli.js +2 -2
  4. package/build/lib/film-kit.js +6 -4
  5. package/build/lib/storyboard-reference/adapters/kling30.js +3 -1
  6. package/build/lib/storyboard-reference/adapters/seedance20.d.ts +4 -0
  7. package/build/lib/storyboard-reference/adapters/seedance20.js +77 -14
  8. package/build/lib/storyboard-reference/adapters/veo31.js +3 -1
  9. package/build/lib/storyboard-reference/index.d.ts +1 -1
  10. package/build/lib/storyboard-reference/output-writer.js +84 -6
  11. package/build/lib/storyboard-reference/prompt-bundle-builder.js +311 -8
  12. package/build/lib/storyboard-reference/request-normalizer.js +8 -4
  13. package/build/lib/storyboard-reference/storyboard-interpreter.d.ts +3 -1
  14. package/build/lib/storyboard-reference/storyboard-interpreter.js +21 -1
  15. package/build/lib/storyboard-reference/types.d.ts +151 -2
  16. package/build/lib/storyboard-reference/validators.js +2 -5
  17. package/build/lib/templates.js +10 -6
  18. package/content/ARCHITECTURE.md +4 -4
  19. package/content/MASTER.md +2 -2
  20. package/content/RULES.md +4 -4
  21. package/content/agents/prompt-engineer.md +7 -7
  22. package/content/skills/prompt-structure/SKILL.md +14 -11
  23. package/content/skills/reference-locking/SKILL.md +6 -4
  24. package/content/skills/semantic-consistency/SKILL.md +1 -1
  25. package/content/skills/storyboard-reference/SKILL.md +58 -13
  26. package/content/workflows/generate-storyboard.md +37 -16
  27. package/content/workflows/generate.md +7 -7
  28. package/content/workflows/safety-check.md +2 -2
  29. package/package.json +1 -1
  30. package/packages/gpt-image-smart/content/skills/storyboard-reference/SKILL.md +108 -12
  31. package/packages/gpt-image-smart/content/workflows/generate-storyboard.md +89 -12
  32. package/packages/hybrid/content/skills/storyboard-reference/SKILL.md +108 -12
  33. package/packages/hybrid/content/workflows/generate-storyboard.md +89 -12
  34. package/packages/hybrid-smart/content/skills/storyboard-reference/SKILL.md +108 -12
  35. package/packages/hybrid-smart/content/workflows/generate-storyboard.md +89 -12
  36. package/packages/multi/build/cli.js +39 -0
  37. package/packages/multi/build/index.d.ts +1 -1
  38. package/packages/multi/build/lib/configure.js +208 -1
  39. package/packages/multi/build/lib/defaults.d.ts +3 -1
  40. package/packages/multi/build/lib/defaults.js +32 -0
  41. package/packages/multi/build/lib/templates.js +146 -60
  42. package/packages/multi/build/lib/types.d.ts +16 -0
  43. package/packages/multi/content/agents/continuity-editor.md +6 -6
  44. package/packages/multi/content/agents/delivery-editor.md +2 -2
  45. package/packages/multi/content/agents/lead-director.md +18 -10
  46. package/packages/multi/content/agents/semantic-auditor.md +4 -5
  47. package/packages/multi/content/agents/shot-generator.md +9 -27
  48. package/packages/multi/content/skills/storyboard-reference/SKILL.md +108 -12
  49. package/packages/multi/content/workflows/chain-multi.md +4 -4
  50. package/packages/multi/content/workflows/generate-multi.md +6 -6
  51. package/packages/multi/content/workflows/generate-storyboard.md +89 -12
  52. package/packages/multi/content/workflows/generate-teammate.md +8 -14
  53. package/packages/multi/content/workflows/safety-check-multi.md +7 -11
  54. package/packages/studio/content/skills/storyboard-reference/SKILL.md +108 -12
  55. package/packages/studio/content/workflows/generate-storyboard.md +89 -12
@@ -3,19 +3,29 @@ import { Kling30PromptAdapter } from "./adapters/kling30.js";
3
3
  import { Seedance20PromptAdapter } from "./adapters/seedance20.js";
4
4
  import { Veo31PromptAdapter } from "./adapters/veo31.js";
5
5
  import { normalizeVideoPromptRequest } from "./request-normalizer.js";
6
- import { interpretStoryboard } from "./storyboard-interpreter.js";
6
+ import { buildStoryboardPhaseBudget, inferContinuityMode, interpretStoryboard } from "./storyboard-interpreter.js";
7
7
  import { assertSafeStoryboardReferenceRequest, validateModelPromptOutput, validatePromptBundle } from "./validators.js";
8
8
  const ADAPTERS = {
9
9
  veo31: new Veo31PromptAdapter(),
10
10
  "seedance-2.0": new Seedance20PromptAdapter(),
11
11
  "kling-3.0": new Kling30PromptAdapter()
12
12
  };
13
- function getStoryboardPanelCount(request) {
14
- return request.storyboardPanelCountHint ?? request.shotCountHint ?? Math.min(3, request.storyboardReferenceMode.maxStoryboardPhases);
13
+ function getStoryboardPanelCount(request, phaseBudget) {
14
+ return request.storyboardPanelCountHint
15
+ ?? request.shotCountHint
16
+ ?? phaseBudget.recommendedMaxPhases;
15
17
  }
16
18
  function buildShotId(index) {
17
19
  return `SHOT${String(index + 1).padStart(2, "0")}`;
18
20
  }
21
+ function safeFileId(value) {
22
+ const normalized = value
23
+ .trim()
24
+ .replace(/[^a-z0-9_-]+/gi, "-")
25
+ .replace(/^-+|-+$/g, "")
26
+ .toUpperCase();
27
+ return normalized || "REFERENCE";
28
+ }
19
29
  function getSplitPhaseCounts(panelCount, maxPhases) {
20
30
  const counts = [];
21
31
  let remaining = panelCount;
@@ -26,6 +36,259 @@ function getSplitPhaseCounts(panelCount, maxPhases) {
26
36
  }
27
37
  return counts.length > 0 ? counts : [1];
28
38
  }
39
+ function detectProviderFileKind(pathOrUrl) {
40
+ const cleanPath = pathOrUrl.toLowerCase().split("?")[0] ?? "";
41
+ if (/\.(png|jpe?g|webp|gif|avif|heic|heif)$/.test(cleanPath))
42
+ return "image";
43
+ if (/\.(mp4|mov|webm|m4v|avi|mkv)$/.test(cleanPath))
44
+ return "video";
45
+ if (/\.(mp3|wav|m4a|aac|ogg|flac|aiff?)$/.test(cleanPath))
46
+ return "audio";
47
+ return "unknown";
48
+ }
49
+ function buildProviderFileLimitReport(request, generatedImageReferenceCount) {
50
+ const counts = {
51
+ image: generatedImageReferenceCount,
52
+ video: 0,
53
+ audio: 0,
54
+ unknown: 0
55
+ };
56
+ for (const asset of request.additionalRefs) {
57
+ counts[detectProviderFileKind(asset.pathOrUrl)] += 1;
58
+ }
59
+ const observedTotalFiles = generatedImageReferenceCount + request.additionalRefs.length;
60
+ const report = {
61
+ maxImages: 9,
62
+ maxVideos: 3,
63
+ maxAudio: 3,
64
+ maxTotalFiles: 12,
65
+ observedImages: counts.image,
66
+ observedVideos: counts.video,
67
+ observedAudio: counts.audio,
68
+ observedUnknown: counts.unknown,
69
+ observedTotalFiles,
70
+ withinLimits: counts.image <= 9
71
+ && counts.video <= 3
72
+ && counts.audio <= 3
73
+ && observedTotalFiles <= 12
74
+ };
75
+ return report;
76
+ }
77
+ function buildShotHandoffNote(input) {
78
+ const firstPhase = input.storyboardInterpretation.phases[0];
79
+ const lastPhase = input.storyboardInterpretation.phases.at(-1);
80
+ return {
81
+ shotId: input.shotId,
82
+ continuityMode: input.continuityMode,
83
+ phaseCount: input.storyboardInterpretation.phases.length,
84
+ entryHandoff: firstPhase
85
+ ? `${firstPhase.timeRange}: ${firstPhase.subjectAction}`
86
+ : "No storyboard phase entry available.",
87
+ exitHandoff: lastPhase
88
+ ? `${lastPhase.timeRange}: ${lastPhase.handoffTrigger ?? lastPhase.subjectAction}`
89
+ : "No storyboard phase exit available."
90
+ };
91
+ }
92
+ function buildCharacterReferenceSheetPrompts(assets, outputDir) {
93
+ return assets.characters.map((character, index) => {
94
+ const id = `character-sheet-${character.asset.id}`;
95
+ const sourceLabel = character.asset.label ?? character.asset.id;
96
+ const outputPath = `${outputDir}/reference-prep/CHARACTER-SHEET-${safeFileId(character.asset.id)}.md`;
97
+ return {
98
+ id,
99
+ characterAssetId: character.asset.id,
100
+ sourceToken: character.token,
101
+ outputPath,
102
+ provider: "gpt-image-2",
103
+ aspectRatio: "16:9",
104
+ cellCount: 6,
105
+ generatedOnce: true,
106
+ promptText: `Create one 16:9 realistic production storyboard character reference sheet from ${character.token} (${sourceLabel}). Use the uploaded reference only as the identity source. Preserve the exact face, skull shape, hair, skin texture, body proportions, wardrobe, accessories, and visible props; do not beautify, redesign, age-shift, or stylize the character. Arrange six clean storyboard cells in a single widescreen sheet: front full body, back full body, left profile, right profile, three-quarter full body, and close-up face. Keep the same wardrobe and props in every cell. Use neutral studio lighting, consistent scale, uncluttered pale gray background, readable silhouette, realistic pencil-and-wash production storyboard finish, subtle tonal shading, and no dramatic lens distortion. No text, labels, arrows, captions, panel numbers, watermarks, logos, extra characters, alternate outfits, or background story elements. This sheet is generated once and becomes the identity reference for all later shot storyboards.${index === 0 ? "" : " Match visual finish to the earlier character sheets."}`
107
+ };
108
+ });
109
+ }
110
+ function buildStoryboardImagePrompt(input) {
111
+ const { request, assets, shotId, interpretation, characterReferenceSheetPrompts } = input;
112
+ const characterSheetIds = characterReferenceSheetPrompts.map(prompt => prompt.id);
113
+ const externalStoryboardGuideIds = assets.storyboards.map(storyboard => storyboard.asset.id);
114
+ const sheetRefs = characterReferenceSheetPrompts
115
+ .map((prompt, index) => `${prompt.id} as character ${index + 1}`)
116
+ .join(", ");
117
+ const externalGuideText = assets.storyboards.length > 0
118
+ ? `Use external storyboard guide tokens ${assets.storyboards.map(storyboard => storyboard.token).join(", ")} only for composition, blocking, camera direction, timing, and action rhythm.`
119
+ : "No external storyboard guide is required; infer the board from the screenplay brief, continuity notes, and visual world.";
120
+ const phaseDirection = interpretation.phases
121
+ .map(phase => `Panel ${phase.index}: ${phase.job}; ${phase.subjectAction}; camera behavior: ${phase.cameraBehavior}; background behavior: ${phase.backgroundBehavior}.`)
122
+ .join(" ");
123
+ const continuityStyle = interpretation.phases.length <= 1
124
+ ? "Because this is a single uninterrupted beat, show one decisive action moment with a clear entry pose, motion direction, and final body intention inside the same panel."
125
+ : "Read panels left-to-right, top-to-bottom as a planned visual beat sequence. Each panel must show a distinct body pose, camera size/angle, movement logic, and emotional escalation, not decorative micro-poses.";
126
+ return {
127
+ shotId,
128
+ outputPath: `${request.outputDir}/storyboard-prompts/${shotId}-GPT-IMAGE-2-STORYBOARD.md`,
129
+ provider: "gpt-image-2",
130
+ aspectRatio: "16:9",
131
+ panelCount: interpretation.panelCount,
132
+ style: "professional-production-storyboard",
133
+ characterSheetIds,
134
+ externalStoryboardGuideIds,
135
+ previousShotHandoff: input.previousShotHandoff,
136
+ nextShotHandoff: input.nextShotHandoff,
137
+ promptText: `Create a professional 16:9 production storyboard page for ${shotId} with exactly ${interpretation.panelCount} cinematic panel${interpretation.panelCount === 1 ? "" : "s"}. Use the generated character sheet reference${characterReferenceSheetPrompts.length === 1 ? "" : "s"} (${sheetRefs}) as the only source for character identity, face, body proportions, wardrobe, accessories, and visible props. ${externalGuideText}
138
+
139
+ Storyboard drawing style: raw contemporary film storyboard, black-and-white rough pencil linework for the artwork only, minimal detail, rapid gesture energy, simple anatomy construction, strong silhouette readability, unfinished choreography-previsualization feel, not polished concept art or final render.
140
+
141
+ Panel direction: ${continuityStyle} The scene brief is: ${request.brief} Previous handoff: ${input.previousShotHandoff}. Next handoff: ${input.nextShotHandoff}. Planned panel actions: ${phaseDirection}
142
+
143
+ Camera and staging: show ${interpretation.cameraPlan.framing}, ${interpretation.cameraPlan.movement}, ${interpretation.cameraPlan.lens}; preserve ${interpretation.cameraPlan.screenDirection}. Use cinematic camera variety only when motivated by the beat: handheld energy, push-in, orbit, overhead, low angle, side silhouette, insert, aggressive close-up, long-lens compression, or extreme negative space. Maintain the same environment, lighting, atmosphere, foreground, midground, and background continuity: ${interpretation.visualWorld.environment}; ${interpretation.visualWorld.lighting}; ${interpretation.visualWorld.foreground}; ${interpretation.visualWorld.midground}; ${interpretation.visualWorld.background}; ${interpretation.visualWorld.atmosphere}.
144
+
145
+ Annotation color system: red arrows for body movement, blue arrows for camera movement, green marks for framing/composition, orange marks for lighting direction, purple marks for vocal or emotional emphasis when relevant, black text only for very short lens notes and panel labels. No timestamps.
146
+
147
+ Every panel must contain visible momentum or clear behavioral change: gaze shift, breath, weight transfer, contact with ground/prop, fabric motion, hand tension, or a readable pose transition. Avoid static standing poses unless the brief requires a deliberately held beat. Keep the location minimal and readable; do not add new characters, new locations, alternate wardrobe, alternate identity, logos, watermarks, decorative borders, final-render polish, or photorealistic video frames.`
148
+ };
149
+ }
150
+ function isImageReference(asset) {
151
+ return detectProviderFileKind(asset.asset.pathOrUrl) === "image";
152
+ }
153
+ function buildProviderAssetMapping(assets, characterReferenceSheetPrompts, storyboardImagePrompt) {
154
+ const mapping = {};
155
+ characterReferenceSheetPrompts.forEach((sheet, index) => {
156
+ const token = `@Image${index + 1}`;
157
+ mapping[token] = {
158
+ token,
159
+ legacyAlias: `@image${index + 1}`,
160
+ sourceToken: sheet.sourceToken,
161
+ sourceAssetId: sheet.id,
162
+ role: "character_identity",
163
+ description: "Generated character sheet: identity, face, body proportions, wardrobe, accessories, and visible props only.",
164
+ generatedPromptPath: sheet.outputPath
165
+ };
166
+ });
167
+ const storyboardIndex = characterReferenceSheetPrompts.length + 1;
168
+ const storyboardToken = `@Image${storyboardIndex}`;
169
+ mapping[storyboardToken] = {
170
+ token: storyboardToken,
171
+ legacyAlias: `@image${storyboardIndex}`,
172
+ sourceToken: `@${storyboardImagePrompt.shotId.toLowerCase()}Storyboard`,
173
+ sourceAssetId: `${storyboardImagePrompt.shotId}-storyboard-image`,
174
+ role: "storyboard_plan",
175
+ description: "Generated shot storyboard: composition, blocking, camera direction, timing, action rhythm, and phase order only.",
176
+ generatedPromptPath: storyboardImagePrompt.outputPath
177
+ };
178
+ let nextImageIndex = storyboardIndex + 1;
179
+ for (const ref of assets.additional.filter(isImageReference)) {
180
+ const token = `@Image${nextImageIndex}`;
181
+ mapping[token] = {
182
+ token,
183
+ legacyAlias: `@image${nextImageIndex}`,
184
+ sourceToken: ref.token,
185
+ sourceAssetId: ref.asset.id,
186
+ role: ref.asset.role,
187
+ description: "Additional image reference; keep below character identity and generated storyboard authority."
188
+ };
189
+ nextImageIndex += 1;
190
+ }
191
+ return mapping;
192
+ }
193
+ function buildReferenceAssetRequirements(providerAssetMapping) {
194
+ return Object.values(providerAssetMapping).map(entry => {
195
+ const source = entry.role === "character_identity"
196
+ ? "character-sheet"
197
+ : entry.role === "storyboard_plan"
198
+ ? "shot-storyboard"
199
+ : "additional-reference";
200
+ const requirement = {
201
+ token: entry.token,
202
+ source,
203
+ assetId: entry.sourceAssetId,
204
+ role: entry.role,
205
+ required: entry.role === "character_identity" || entry.role === "storyboard_plan",
206
+ notes: entry.description
207
+ };
208
+ if (entry.legacyAlias)
209
+ requirement.legacyAlias = entry.legacyAlias;
210
+ if (entry.generatedPromptPath)
211
+ requirement.generatedPromptPath = entry.generatedPromptPath;
212
+ return requirement;
213
+ });
214
+ }
215
+ function buildProviderReferenceTokens(providerAssetMapping) {
216
+ const characterIdentities = Object.values(providerAssetMapping)
217
+ .filter(entry => entry.role === "character_identity")
218
+ .map(entry => entry.token);
219
+ const storyboardReference = Object.values(providerAssetMapping)
220
+ .find(entry => entry.role === "storyboard_plan")?.token ?? "@Image2";
221
+ return {
222
+ characterIdentity: characterIdentities[0] ?? "@Image1",
223
+ characterIdentities,
224
+ storyboardReference,
225
+ legacyAliases: {
226
+ characterIdentity: characterIdentities[0]?.replace("@Image", "@image") ?? "@image1",
227
+ characterIdentities: characterIdentities.map(token => token.replace("@Image", "@image")),
228
+ storyboardReference: storyboardReference.replace("@Image", "@image")
229
+ },
230
+ videoReference: "@video1",
231
+ audioReference: "@audio1"
232
+ };
233
+ }
234
+ function buildStoryboardReferencePolicy(request, phaseBudget, providerFileLimits, bundles, providerReferenceTokens, maxPhases, splitRequired) {
235
+ const continuityModes = Array.from(new Set(bundles.map(bundle => bundle.continuityMode)));
236
+ return {
237
+ project: {
238
+ reference_mode: "storyboard-reference"
239
+ },
240
+ storyboard_reference: {
241
+ enabled: true,
242
+ max_storyboard_phases: maxPhases,
243
+ recommended_phase_budget: phaseBudget,
244
+ provider_reference_tokens: providerReferenceTokens,
245
+ continuity_mode: continuityModes.length === 1
246
+ ? continuityModes[0] ?? "multi-shot-storyboard"
247
+ : "mixed",
248
+ provider_file_limits: providerFileLimits,
249
+ character_reference_sheets: {
250
+ enabled: true,
251
+ provider: "gpt-image-2",
252
+ generated_once_per_character: true,
253
+ aspect_ratio: "16:9",
254
+ required_views: [
255
+ "front full body",
256
+ "back full body",
257
+ "left profile",
258
+ "right profile",
259
+ "three-quarter full body",
260
+ "close-up face"
261
+ ]
262
+ },
263
+ storyboard_prompt_policy: {
264
+ enabled: true,
265
+ provider: "gpt-image-2",
266
+ generated_per_shot: true,
267
+ aspect_ratio: "16:9",
268
+ max_panels_per_shot: maxPhases,
269
+ panel_budget: phaseBudget.policy,
270
+ professional_prompt_target_words: "160-240 words per shot storyboard prompt"
271
+ },
272
+ role_separation: {
273
+ character_identity: `${providerReferenceTokens.characterIdentities.join(", ")} control identity, face, hair, body proportions, wardrobe, accessories, and visible props.`,
274
+ storyboard_reference: `${providerReferenceTokens.storyboardReference} controls composition, blocking, camera direction, timing, action rhythm, and phase order only.`,
275
+ storyboard_never_identity_source: [
276
+ "storyboard text",
277
+ "panel borders",
278
+ "watermarks",
279
+ "logos",
280
+ "alternate character designs"
281
+ ]
282
+ },
283
+ split_policy: {
284
+ split_storyboard_overload: request.storyboardReferenceMode.splitStoryboardOverload,
285
+ split_at_phase_count: splitRequired ? maxPhases + 1 : phaseBudget.splitAtPhaseCount,
286
+ split_target: "SHOTNN.md"
287
+ }
288
+ },
289
+ shot_handoff_notes: bundles.map(bundle => bundle.handoffNote)
290
+ };
291
+ }
29
292
  function buildModelPrompts(request, bundleInput) {
30
293
  const modelPrompts = {};
31
294
  for (const model of request.targetModels) {
@@ -45,8 +308,11 @@ export function buildStoryboardReferencePromptBundles(input) {
45
308
  const request = normalizeVideoPromptRequest(input);
46
309
  assertSafeStoryboardReferenceRequest(request);
47
310
  const assets = resolveAssetRoles(request.characterRefs, request.storyboardRefs, request.additionalRefs);
48
- const panelCount = getStoryboardPanelCount(request);
49
- const maxPhases = request.storyboardReferenceMode.maxStoryboardPhases;
311
+ const characterReferenceSheetPrompts = buildCharacterReferenceSheetPrompts(assets, request.outputDir);
312
+ const phaseBudget = buildStoryboardPhaseBudget(request.durationSeconds, request.storyboardReferenceMode.maxStoryboardPhases);
313
+ const providerFileLimits = buildProviderFileLimitReport(request, characterReferenceSheetPrompts.length + 1);
314
+ const panelCount = getStoryboardPanelCount(request, phaseBudget);
315
+ const maxPhases = phaseBudget.effectiveMaxStoryboardPhases;
50
316
  const splitRequired = panelCount > maxPhases;
51
317
  if (splitRequired && !request.storyboardReferenceMode.splitStoryboardOverload) {
52
318
  throw new Error(`Storyboard has ${panelCount} phases, which exceeds maxStoryboardPhases=${maxPhases}. Enable splitStoryboardOverload or split the storyboard manually.`);
@@ -55,27 +321,61 @@ export function buildStoryboardReferencePromptBundles(input) {
55
321
  ? getSplitPhaseCounts(panelCount, maxPhases)
56
322
  : [Math.min(panelCount, maxPhases)];
57
323
  const generatedAt = new Date().toISOString();
58
- const bundles = phaseCounts.map((phaseCount, index) => {
324
+ const bundles = [];
325
+ let previousShotHandoff = "Project opening: start from the screenplay's first visual beat.";
326
+ phaseCounts.forEach((phaseCount, index) => {
327
+ const shotId = buildShotId(index);
59
328
  const interpretation = interpretStoryboard(request, assets, phaseCount, splitRequired);
329
+ const continuityMode = inferContinuityMode(interpretation.phases);
330
+ const handoffNote = buildShotHandoffNote({
331
+ shotId,
332
+ continuityMode,
333
+ storyboardInterpretation: interpretation
334
+ });
335
+ const storyboardImagePrompt = buildStoryboardImagePrompt({
336
+ request,
337
+ assets,
338
+ shotId,
339
+ interpretation,
340
+ characterReferenceSheetPrompts,
341
+ previousShotHandoff,
342
+ nextShotHandoff: handoffNote.exitHandoff
343
+ });
344
+ const providerAssetMapping = buildProviderAssetMapping(assets, characterReferenceSheetPrompts, storyboardImagePrompt);
345
+ const referenceAssetRequirements = buildReferenceAssetRequirements(providerAssetMapping);
60
346
  const modelPrompts = buildModelPrompts(request, {
61
347
  assets,
62
348
  interpretation,
349
+ continuityMode,
350
+ phaseBudget,
351
+ providerFileLimits,
352
+ storyboardImagePrompt,
353
+ providerAssetMapping,
63
354
  visualWorld: interpretation.visualWorld
64
355
  });
65
356
  const bundle = {
66
- shotId: buildShotId(index),
357
+ shotId,
67
358
  mode: "storyboard-reference",
68
359
  durationSeconds: request.durationSeconds,
69
360
  aspectRatio: request.aspectRatio,
70
361
  modelPrompts,
71
362
  storyboardInterpretation: interpretation,
72
363
  assetRoles: assets.assetRoles,
364
+ continuityMode,
365
+ phaseBudget,
366
+ handoffNote,
367
+ storyboardImagePrompt,
368
+ referenceAssetRequirements,
369
+ providerAssetMapping,
73
370
  qa: { verdict: "pass", checks: {}, issues: [], splitRequired },
74
371
  generatedAt
75
372
  };
76
373
  bundle.qa = validatePromptBundle(bundle, assets);
77
- return bundle;
374
+ bundles.push(bundle);
375
+ previousShotHandoff = handoffNote.exitHandoff;
78
376
  });
377
+ const providerReferenceTokens = buildProviderReferenceTokens(bundles[0]?.providerAssetMapping ?? {});
378
+ const policy = buildStoryboardReferencePolicy(request, phaseBudget, providerFileLimits, bundles, providerReferenceTokens, maxPhases, splitRequired);
79
379
  return {
80
380
  plan: {
81
381
  mode: "storyboard-reference",
@@ -83,10 +383,13 @@ export function buildStoryboardReferencePromptBundles(input) {
83
383
  shotCount: bundles.length,
84
384
  splitRequired,
85
385
  assetRoles: assets.assetRoles,
386
+ characterReferenceSheetPrompts,
387
+ policy,
86
388
  generatedAt
87
389
  },
88
390
  request,
89
391
  assets,
392
+ characterReferenceSheetPrompts,
90
393
  bundles
91
394
  };
92
395
  }
@@ -25,8 +25,11 @@ function assertAspectRatio(aspectRatio) {
25
25
  }
26
26
  return aspectRatio;
27
27
  }
28
- function assertReferenceAssets(assets, fieldName) {
29
- if (assets.length === 0) {
28
+ function assertReferenceAssets(assets, fieldName, options) {
29
+ if (!assets || assets.length === 0) {
30
+ if (!options.required) {
31
+ return;
32
+ }
30
33
  throw new Error(`storyboard-reference mode requires at least one ${fieldName}.`);
31
34
  }
32
35
  for (const asset of assets) {
@@ -42,8 +45,8 @@ export function normalizeVideoPromptRequest(input) {
42
45
  if (!input.brief.trim()) {
43
46
  throw new Error("storyboard-reference mode requires a non-empty brief.");
44
47
  }
45
- assertReferenceAssets(input.characterRefs, "character reference");
46
- assertReferenceAssets(input.storyboardRefs, "storyboard reference");
48
+ assertReferenceAssets(input.characterRefs, "character reference", { required: true });
49
+ assertReferenceAssets(input.storyboardRefs, "storyboard reference", { required: false });
47
50
  const storyboardReferenceMode = resolveStoryboardReferenceRuntime(input.storyboardReferenceMode);
48
51
  if (!storyboardReferenceMode.enabled) {
49
52
  throw new Error("storyboard-reference mode is disabled by storyboardReferenceMode.enabled=false.");
@@ -51,6 +54,7 @@ export function normalizeVideoPromptRequest(input) {
51
54
  const normalized = {
52
55
  ...input,
53
56
  targetModels: assertSupportedModels(input.targetModels),
57
+ storyboardRefs: input.storyboardRefs ?? [],
54
58
  durationSeconds: normalizePositiveInteger(input.durationSeconds, DEFAULT_STORYBOARD_DURATION_SECONDS, "durationSeconds"),
55
59
  aspectRatio: assertAspectRatio(input.aspectRatio ?? DEFAULT_STORYBOARD_ASPECT_RATIO),
56
60
  language: input.language?.trim() || DEFAULT_STORYBOARD_LANGUAGE,
@@ -1,2 +1,4 @@
1
- import type { NormalizedVideoPromptRequest, ResolvedAssetRoles, StoryboardInterpretation } from "./types.js";
1
+ import type { NormalizedVideoPromptRequest, ResolvedAssetRoles, SeedanceContinuityMode, StoryboardInterpretation, StoryboardPhaseBudget, StoryboardPhase } from "./types.js";
2
+ export declare function buildStoryboardPhaseBudget(durationSeconds: number, configuredMaxStoryboardPhases: number): StoryboardPhaseBudget;
3
+ export declare function inferContinuityMode(phases: StoryboardPhase[]): SeedanceContinuityMode;
2
4
  export declare function interpretStoryboard(request: NormalizedVideoPromptRequest, assets: ResolvedAssetRoles, phaseCount: number, splitRequired: boolean): StoryboardInterpretation;
@@ -36,6 +36,26 @@ function formatTimeRange(index, phaseCount, durationSeconds) {
36
36
  const end = (durationSeconds * (index + 1)) / phaseCount;
37
37
  return `${start.toFixed(1)}s-${end.toFixed(1)}s`;
38
38
  }
39
+ export function buildStoryboardPhaseBudget(durationSeconds, configuredMaxStoryboardPhases) {
40
+ const durationPolicy = durationSeconds <= 6
41
+ ? { min: 1, max: 2, policy: "4-6s:1-2" }
42
+ : durationSeconds <= 10
43
+ ? { min: 2, max: 3, policy: "7-10s:2-3" }
44
+ : { min: 3, max: 4, policy: "11-15s:3-4" };
45
+ const effectiveMaxStoryboardPhases = Math.max(1, Math.min(configuredMaxStoryboardPhases, durationPolicy.max));
46
+ return {
47
+ durationSeconds,
48
+ providerDurationRangeSeconds: { min: 4, max: 15 },
49
+ recommendedMinPhases: durationPolicy.min,
50
+ recommendedMaxPhases: durationPolicy.max,
51
+ effectiveMaxStoryboardPhases,
52
+ splitAtPhaseCount: effectiveMaxStoryboardPhases + 1,
53
+ policy: durationPolicy.policy
54
+ };
55
+ }
56
+ export function inferContinuityMode(phases) {
57
+ return phases.length <= 1 ? "continuous-shot" : "multi-shot-storyboard";
58
+ }
39
59
  function buildPhases(request, phaseCount) {
40
60
  return Array.from({ length: phaseCount }, (_, index) => {
41
61
  const job = PHASE_JOBS[Math.min(index, PHASE_JOBS.length - 1)] ?? "handoff";
@@ -63,7 +83,7 @@ export function interpretStoryboard(request, assets, phaseCount, splitRequired)
63
83
  riskFlags.push("dialogue-needs-voice-cast");
64
84
  }
65
85
  return {
66
- storyboardAssetId: assets.storyboards[0]?.asset.id ?? "storyboard1",
86
+ storyboardAssetId: assets.storyboards[0]?.asset.id ?? "generated-shot-storyboard",
67
87
  panelCount: phaseCount,
68
88
  phases: buildPhases(request, phaseCount),
69
89
  editorialFunction: inferEditorialFunction(request.brief),
@@ -24,7 +24,7 @@ export interface VideoPromptRequest {
24
24
  targetModels: SupportedModel[];
25
25
  brief: string;
26
26
  characterRefs: ReferenceAsset[];
27
- storyboardRefs: ReferenceAsset[];
27
+ storyboardRefs?: ReferenceAsset[];
28
28
  additionalRefs?: ReferenceAsset[];
29
29
  durationSeconds?: number;
30
30
  aspectRatio?: HybridAspectRatio;
@@ -39,6 +39,7 @@ export interface VideoPromptRequest {
39
39
  storyboardReferenceMode?: Partial<StoryboardReferenceConfig>;
40
40
  }
41
41
  export interface NormalizedVideoPromptRequest extends VideoPromptRequest {
42
+ storyboardRefs: ReferenceAsset[];
42
43
  durationSeconds: number;
43
44
  aspectRatio: HybridAspectRatio;
44
45
  language: string;
@@ -66,6 +67,94 @@ export interface ResolvedAssetRoles {
66
67
  label?: string;
67
68
  }>;
68
69
  }
70
+ export type SeedanceContinuityMode = "continuous-shot" | "multi-shot-storyboard";
71
+ export interface StoryboardPhaseBudget {
72
+ durationSeconds: number;
73
+ providerDurationRangeSeconds: {
74
+ min: 4;
75
+ max: 15;
76
+ };
77
+ recommendedMinPhases: number;
78
+ recommendedMaxPhases: number;
79
+ effectiveMaxStoryboardPhases: number;
80
+ splitAtPhaseCount: number;
81
+ policy: "4-6s:1-2" | "7-10s:2-3" | "11-15s:3-4";
82
+ }
83
+ export interface ProviderFileLimitReport {
84
+ maxImages: 9;
85
+ maxVideos: 3;
86
+ maxAudio: 3;
87
+ maxTotalFiles: 12;
88
+ observedImages: number;
89
+ observedVideos: number;
90
+ observedAudio: number;
91
+ observedUnknown: number;
92
+ observedTotalFiles: number;
93
+ withinLimits: boolean;
94
+ }
95
+ export interface ProviderReferenceTokenPolicy {
96
+ characterIdentity: string;
97
+ characterIdentities: string[];
98
+ storyboardReference: string;
99
+ legacyAliases: {
100
+ characterIdentity: string;
101
+ characterIdentities: string[];
102
+ storyboardReference: string;
103
+ };
104
+ videoReference: "@video1";
105
+ audioReference: "@audio1";
106
+ }
107
+ export interface ShotHandoffNote {
108
+ shotId: string;
109
+ continuityMode: SeedanceContinuityMode;
110
+ phaseCount: number;
111
+ entryHandoff: string;
112
+ exitHandoff: string;
113
+ }
114
+ export interface CharacterReferenceSheetPrompt {
115
+ id: string;
116
+ characterAssetId: string;
117
+ sourceToken: string;
118
+ outputPath: string;
119
+ provider: "gpt-image-2";
120
+ aspectRatio: "16:9";
121
+ cellCount: 6;
122
+ generatedOnce: true;
123
+ promptText: string;
124
+ }
125
+ export interface StoryboardImagePrompt {
126
+ shotId: string;
127
+ outputPath: string;
128
+ provider: "gpt-image-2";
129
+ aspectRatio: "16:9";
130
+ panelCount: number;
131
+ style: "professional-production-storyboard";
132
+ characterSheetIds: string[];
133
+ externalStoryboardGuideIds: string[];
134
+ previousShotHandoff: string;
135
+ nextShotHandoff: string;
136
+ promptText: string;
137
+ }
138
+ export type ProviderAssetMappingRole = "character_identity" | "storyboard_plan" | "style_reference" | "camera_reference" | "action_reference" | "prop_reference";
139
+ export interface ProviderAssetMappingEntry {
140
+ token: string;
141
+ sourceToken: string;
142
+ sourceAssetId: string;
143
+ role: ProviderAssetMappingRole;
144
+ description: string;
145
+ legacyAlias?: string;
146
+ generatedPromptPath?: string;
147
+ }
148
+ export interface ReferenceAssetRequirement {
149
+ token: string;
150
+ legacyAlias?: string;
151
+ source: "character-sheet" | "shot-storyboard" | "additional-reference";
152
+ assetId: string;
153
+ role: ProviderAssetMappingRole;
154
+ required: boolean;
155
+ generatedPromptPath?: string;
156
+ notes: string;
157
+ }
69
158
  export interface StoryboardPhase {
70
159
  index: number;
71
160
  timeRange: string;
@@ -131,10 +220,16 @@ export interface ModelPromptQa {
131
220
  checks: Record<string, boolean>;
132
221
  issues: string[];
133
222
  }
223
+ export interface ModelRouteMetadata extends Record<string, unknown> {
224
+ continuityMode?: SeedanceContinuityMode;
225
+ providerAssetMapping?: Record<string, ProviderAssetMappingEntry>;
226
+ phaseBudget?: StoryboardPhaseBudget;
227
+ providerFileLimits?: ProviderFileLimitReport;
228
+ }
134
229
  export interface ModelPromptOutput {
135
230
  model: SupportedModel;
136
231
  displayName: string;
137
- routeMetadata: Record<string, unknown>;
232
+ routeMetadata: ModelRouteMetadata;
138
233
  promptText: string;
139
234
  audioPlan?: AudioPlan;
140
235
  warnings: string[];
@@ -154,6 +249,12 @@ export interface PromptBundle {
154
249
  modelPrompts: Partial<Record<SupportedModel, ModelPromptOutput>>;
155
250
  storyboardInterpretation: StoryboardInterpretation;
156
251
  assetRoles: ResolvedAssetRoles["assetRoles"];
252
+ continuityMode: SeedanceContinuityMode;
253
+ phaseBudget: StoryboardPhaseBudget;
254
+ handoffNote: ShotHandoffNote;
255
+ storyboardImagePrompt: StoryboardImagePrompt;
256
+ referenceAssetRequirements: ReferenceAssetRequirement[];
257
+ providerAssetMapping: Record<string, ProviderAssetMappingEntry>;
157
258
  qa: PromptBundleQa;
158
259
  generatedAt: string;
159
260
  }
@@ -161,6 +262,11 @@ export interface AdapterInput {
161
262
  request: NormalizedVideoPromptRequest;
162
263
  assets: ResolvedAssetRoles;
163
264
  interpretation: StoryboardInterpretation;
265
+ continuityMode: SeedanceContinuityMode;
266
+ phaseBudget: StoryboardPhaseBudget;
267
+ providerFileLimits: ProviderFileLimitReport;
268
+ storyboardImagePrompt: StoryboardImagePrompt;
269
+ providerAssetMapping: Record<string, ProviderAssetMappingEntry>;
164
270
  voiceCast?: VoiceCastEntry[];
165
271
  visualWorld: VisualWorld;
166
272
  }
@@ -173,18 +279,61 @@ export interface VideoModelPromptAdapter {
173
279
  buildPrompt(input: AdapterInput): ModelPromptOutput;
174
280
  validate(output: ModelPromptOutput): ModelPromptQa;
175
281
  }
282
+ export interface StoryboardReferencePolicy {
283
+ project: {
284
+ reference_mode: "storyboard-reference";
285
+ };
286
+ storyboard_reference: {
287
+ enabled: true;
288
+ max_storyboard_phases: number;
289
+ recommended_phase_budget: StoryboardPhaseBudget;
290
+ provider_reference_tokens: ProviderReferenceTokenPolicy;
291
+ continuity_mode: SeedanceContinuityMode | "mixed";
292
+ provider_file_limits: ProviderFileLimitReport;
293
+ character_reference_sheets: {
294
+ enabled: true;
295
+ provider: "gpt-image-2";
296
+ generated_once_per_character: true;
297
+ aspect_ratio: "16:9";
298
+ required_views: string[];
299
+ };
300
+ storyboard_prompt_policy: {
301
+ enabled: true;
302
+ provider: "gpt-image-2";
303
+ generated_per_shot: true;
304
+ aspect_ratio: "16:9";
305
+ max_panels_per_shot: number;
306
+ panel_budget: string;
307
+ professional_prompt_target_words: string;
308
+ };
309
+ role_separation: {
310
+ character_identity: string;
311
+ storyboard_reference: string;
312
+ storyboard_never_identity_source: string[];
313
+ };
314
+ split_policy: {
315
+ split_storyboard_overload: boolean;
316
+ split_at_phase_count: number;
317
+ split_target: "SHOTNN.md";
318
+ };
319
+ };
320
+ shot_handoff_notes: ShotHandoffNote[];
321
+ }
176
322
  export interface StoryboardReferencePlan {
177
323
  mode: "storyboard-reference";
178
324
  targetModels: SupportedModel[];
179
325
  shotCount: number;
180
326
  splitRequired: boolean;
181
327
  assetRoles: ResolvedAssetRoles["assetRoles"];
328
+ characterReferenceSheetPrompts: CharacterReferenceSheetPrompt[];
329
+ policy: StoryboardReferencePolicy;
182
330
  generatedAt: string;
183
331
  }
184
332
  export interface StoryboardReferenceBuildResult {
185
333
  plan: StoryboardReferencePlan;
186
334
  request: NormalizedVideoPromptRequest;
187
335
  assets: ResolvedAssetRoles;
336
+ characterReferenceSheetPrompts: CharacterReferenceSheetPrompt[];
188
337
  bundles: PromptBundle[];
189
338
  }
190
339
  export interface StoryboardReferenceWriteResult {