@milenyumai/film-kit 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +68 -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 +72 -13
  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 +295 -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 +54 -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 +104 -12
  31. package/packages/gpt-image-smart/content/workflows/generate-storyboard.md +89 -12
  32. package/packages/hybrid/content/skills/storyboard-reference/SKILL.md +104 -12
  33. package/packages/hybrid/content/workflows/generate-storyboard.md +89 -12
  34. package/packages/hybrid-smart/content/skills/storyboard-reference/SKILL.md +104 -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 +104 -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 +104 -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,243 @@ 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
+ return {
121
+ shotId,
122
+ outputPath: `${request.outputDir}/storyboard-prompts/${shotId}-GPT-IMAGE-2-STORYBOARD.md`,
123
+ provider: "gpt-image-2",
124
+ aspectRatio: "16:9",
125
+ panelCount: interpretation.panelCount,
126
+ style: "realistic-production-storyboard",
127
+ characterSheetIds,
128
+ externalStoryboardGuideIds,
129
+ previousShotHandoff: input.previousShotHandoff,
130
+ nextShotHandoff: input.nextShotHandoff,
131
+ promptText: `Create a professional 16:9 realistic production storyboard image for ${shotId} with exactly ${interpretation.panelCount} readable 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} The scene brief is: ${request.brief}. Continue from the previous handoff: ${input.previousShotHandoff}. End on the next handoff: ${input.nextShotHandoff}. Show the camera plan clearly: ${interpretation.cameraPlan.framing}, ${interpretation.cameraPlan.movement}, ${interpretation.cameraPlan.lens}, preserving ${interpretation.cameraPlan.screenDirection}. Maintain the same environment, lighting, atmosphere, color grade, foreground, midground, and background continuity: ${interpretation.visualWorld.environment}; ${interpretation.visualWorld.lighting}; ${interpretation.visualWorld.atmosphere}. Each panel must show only the phase action needed for the shot, with clean blocking, motivated eyelines, stable scale, readable silhouettes, and practical production-board detail. Do not create final render polish, alternate character designs, text, captions, labels, panel numbers, watermarks, logos, decorative borders, extra scene cuts, or new locations.`
132
+ };
133
+ }
134
+ function isImageReference(asset) {
135
+ return detectProviderFileKind(asset.asset.pathOrUrl) === "image";
136
+ }
137
+ function buildProviderAssetMapping(assets, characterReferenceSheetPrompts, storyboardImagePrompt) {
138
+ const mapping = {};
139
+ characterReferenceSheetPrompts.forEach((sheet, index) => {
140
+ const token = `@Image${index + 1}`;
141
+ mapping[token] = {
142
+ token,
143
+ legacyAlias: `@image${index + 1}`,
144
+ sourceToken: sheet.sourceToken,
145
+ sourceAssetId: sheet.id,
146
+ role: "character_identity",
147
+ description: "Generated character sheet: identity, face, body proportions, wardrobe, accessories, and visible props only.",
148
+ generatedPromptPath: sheet.outputPath
149
+ };
150
+ });
151
+ const storyboardIndex = characterReferenceSheetPrompts.length + 1;
152
+ const storyboardToken = `@Image${storyboardIndex}`;
153
+ mapping[storyboardToken] = {
154
+ token: storyboardToken,
155
+ legacyAlias: `@image${storyboardIndex}`,
156
+ sourceToken: `@${storyboardImagePrompt.shotId.toLowerCase()}Storyboard`,
157
+ sourceAssetId: `${storyboardImagePrompt.shotId}-storyboard-image`,
158
+ role: "storyboard_plan",
159
+ description: "Generated shot storyboard: composition, blocking, camera direction, timing, action rhythm, and phase order only.",
160
+ generatedPromptPath: storyboardImagePrompt.outputPath
161
+ };
162
+ let nextImageIndex = storyboardIndex + 1;
163
+ for (const ref of assets.additional.filter(isImageReference)) {
164
+ const token = `@Image${nextImageIndex}`;
165
+ mapping[token] = {
166
+ token,
167
+ legacyAlias: `@image${nextImageIndex}`,
168
+ sourceToken: ref.token,
169
+ sourceAssetId: ref.asset.id,
170
+ role: ref.asset.role,
171
+ description: "Additional image reference; keep below character identity and generated storyboard authority."
172
+ };
173
+ nextImageIndex += 1;
174
+ }
175
+ return mapping;
176
+ }
177
+ function buildReferenceAssetRequirements(providerAssetMapping) {
178
+ return Object.values(providerAssetMapping).map(entry => {
179
+ const source = entry.role === "character_identity"
180
+ ? "character-sheet"
181
+ : entry.role === "storyboard_plan"
182
+ ? "shot-storyboard"
183
+ : "additional-reference";
184
+ const requirement = {
185
+ token: entry.token,
186
+ source,
187
+ assetId: entry.sourceAssetId,
188
+ role: entry.role,
189
+ required: entry.role === "character_identity" || entry.role === "storyboard_plan",
190
+ notes: entry.description
191
+ };
192
+ if (entry.legacyAlias)
193
+ requirement.legacyAlias = entry.legacyAlias;
194
+ if (entry.generatedPromptPath)
195
+ requirement.generatedPromptPath = entry.generatedPromptPath;
196
+ return requirement;
197
+ });
198
+ }
199
+ function buildProviderReferenceTokens(providerAssetMapping) {
200
+ const characterIdentities = Object.values(providerAssetMapping)
201
+ .filter(entry => entry.role === "character_identity")
202
+ .map(entry => entry.token);
203
+ const storyboardReference = Object.values(providerAssetMapping)
204
+ .find(entry => entry.role === "storyboard_plan")?.token ?? "@Image2";
205
+ return {
206
+ characterIdentity: characterIdentities[0] ?? "@Image1",
207
+ characterIdentities,
208
+ storyboardReference,
209
+ legacyAliases: {
210
+ characterIdentity: characterIdentities[0]?.replace("@Image", "@image") ?? "@image1",
211
+ characterIdentities: characterIdentities.map(token => token.replace("@Image", "@image")),
212
+ storyboardReference: storyboardReference.replace("@Image", "@image")
213
+ },
214
+ videoReference: "@video1",
215
+ audioReference: "@audio1"
216
+ };
217
+ }
218
+ function buildStoryboardReferencePolicy(request, phaseBudget, providerFileLimits, bundles, providerReferenceTokens, maxPhases, splitRequired) {
219
+ const continuityModes = Array.from(new Set(bundles.map(bundle => bundle.continuityMode)));
220
+ return {
221
+ project: {
222
+ reference_mode: "storyboard-reference"
223
+ },
224
+ storyboard_reference: {
225
+ enabled: true,
226
+ max_storyboard_phases: maxPhases,
227
+ recommended_phase_budget: phaseBudget,
228
+ provider_reference_tokens: providerReferenceTokens,
229
+ continuity_mode: continuityModes.length === 1
230
+ ? continuityModes[0] ?? "multi-shot-storyboard"
231
+ : "mixed",
232
+ provider_file_limits: providerFileLimits,
233
+ character_reference_sheets: {
234
+ enabled: true,
235
+ provider: "gpt-image-2",
236
+ generated_once_per_character: true,
237
+ aspect_ratio: "16:9",
238
+ required_views: [
239
+ "front full body",
240
+ "back full body",
241
+ "left profile",
242
+ "right profile",
243
+ "three-quarter full body",
244
+ "close-up face"
245
+ ]
246
+ },
247
+ storyboard_prompt_policy: {
248
+ enabled: true,
249
+ provider: "gpt-image-2",
250
+ generated_per_shot: true,
251
+ aspect_ratio: "16:9",
252
+ max_panels_per_shot: maxPhases,
253
+ panel_budget: phaseBudget.policy,
254
+ professional_prompt_target_words: "160-240 words per shot storyboard prompt"
255
+ },
256
+ role_separation: {
257
+ character_identity: `${providerReferenceTokens.characterIdentities.join(", ")} control identity, face, hair, body proportions, wardrobe, accessories, and visible props.`,
258
+ storyboard_reference: `${providerReferenceTokens.storyboardReference} controls composition, blocking, camera direction, timing, action rhythm, and phase order only.`,
259
+ storyboard_never_identity_source: [
260
+ "storyboard text",
261
+ "panel borders",
262
+ "watermarks",
263
+ "logos",
264
+ "alternate character designs"
265
+ ]
266
+ },
267
+ split_policy: {
268
+ split_storyboard_overload: request.storyboardReferenceMode.splitStoryboardOverload,
269
+ split_at_phase_count: splitRequired ? maxPhases + 1 : phaseBudget.splitAtPhaseCount,
270
+ split_target: "SHOTNN.md"
271
+ }
272
+ },
273
+ shot_handoff_notes: bundles.map(bundle => bundle.handoffNote)
274
+ };
275
+ }
29
276
  function buildModelPrompts(request, bundleInput) {
30
277
  const modelPrompts = {};
31
278
  for (const model of request.targetModels) {
@@ -45,8 +292,11 @@ export function buildStoryboardReferencePromptBundles(input) {
45
292
  const request = normalizeVideoPromptRequest(input);
46
293
  assertSafeStoryboardReferenceRequest(request);
47
294
  const assets = resolveAssetRoles(request.characterRefs, request.storyboardRefs, request.additionalRefs);
48
- const panelCount = getStoryboardPanelCount(request);
49
- const maxPhases = request.storyboardReferenceMode.maxStoryboardPhases;
295
+ const characterReferenceSheetPrompts = buildCharacterReferenceSheetPrompts(assets, request.outputDir);
296
+ const phaseBudget = buildStoryboardPhaseBudget(request.durationSeconds, request.storyboardReferenceMode.maxStoryboardPhases);
297
+ const providerFileLimits = buildProviderFileLimitReport(request, characterReferenceSheetPrompts.length + 1);
298
+ const panelCount = getStoryboardPanelCount(request, phaseBudget);
299
+ const maxPhases = phaseBudget.effectiveMaxStoryboardPhases;
50
300
  const splitRequired = panelCount > maxPhases;
51
301
  if (splitRequired && !request.storyboardReferenceMode.splitStoryboardOverload) {
52
302
  throw new Error(`Storyboard has ${panelCount} phases, which exceeds maxStoryboardPhases=${maxPhases}. Enable splitStoryboardOverload or split the storyboard manually.`);
@@ -55,27 +305,61 @@ export function buildStoryboardReferencePromptBundles(input) {
55
305
  ? getSplitPhaseCounts(panelCount, maxPhases)
56
306
  : [Math.min(panelCount, maxPhases)];
57
307
  const generatedAt = new Date().toISOString();
58
- const bundles = phaseCounts.map((phaseCount, index) => {
308
+ const bundles = [];
309
+ let previousShotHandoff = "Project opening: start from the screenplay's first visual beat.";
310
+ phaseCounts.forEach((phaseCount, index) => {
311
+ const shotId = buildShotId(index);
59
312
  const interpretation = interpretStoryboard(request, assets, phaseCount, splitRequired);
313
+ const continuityMode = inferContinuityMode(interpretation.phases);
314
+ const handoffNote = buildShotHandoffNote({
315
+ shotId,
316
+ continuityMode,
317
+ storyboardInterpretation: interpretation
318
+ });
319
+ const storyboardImagePrompt = buildStoryboardImagePrompt({
320
+ request,
321
+ assets,
322
+ shotId,
323
+ interpretation,
324
+ characterReferenceSheetPrompts,
325
+ previousShotHandoff,
326
+ nextShotHandoff: handoffNote.exitHandoff
327
+ });
328
+ const providerAssetMapping = buildProviderAssetMapping(assets, characterReferenceSheetPrompts, storyboardImagePrompt);
329
+ const referenceAssetRequirements = buildReferenceAssetRequirements(providerAssetMapping);
60
330
  const modelPrompts = buildModelPrompts(request, {
61
331
  assets,
62
332
  interpretation,
333
+ continuityMode,
334
+ phaseBudget,
335
+ providerFileLimits,
336
+ storyboardImagePrompt,
337
+ providerAssetMapping,
63
338
  visualWorld: interpretation.visualWorld
64
339
  });
65
340
  const bundle = {
66
- shotId: buildShotId(index),
341
+ shotId,
67
342
  mode: "storyboard-reference",
68
343
  durationSeconds: request.durationSeconds,
69
344
  aspectRatio: request.aspectRatio,
70
345
  modelPrompts,
71
346
  storyboardInterpretation: interpretation,
72
347
  assetRoles: assets.assetRoles,
348
+ continuityMode,
349
+ phaseBudget,
350
+ handoffNote,
351
+ storyboardImagePrompt,
352
+ referenceAssetRequirements,
353
+ providerAssetMapping,
73
354
  qa: { verdict: "pass", checks: {}, issues: [], splitRequired },
74
355
  generatedAt
75
356
  };
76
357
  bundle.qa = validatePromptBundle(bundle, assets);
77
- return bundle;
358
+ bundles.push(bundle);
359
+ previousShotHandoff = handoffNote.exitHandoff;
78
360
  });
361
+ const providerReferenceTokens = buildProviderReferenceTokens(bundles[0]?.providerAssetMapping ?? {});
362
+ const policy = buildStoryboardReferencePolicy(request, phaseBudget, providerFileLimits, bundles, providerReferenceTokens, maxPhases, splitRequired);
79
363
  return {
80
364
  plan: {
81
365
  mode: "storyboard-reference",
@@ -83,10 +367,13 @@ export function buildStoryboardReferencePromptBundles(input) {
83
367
  shotCount: bundles.length,
84
368
  splitRequired,
85
369
  assetRoles: assets.assetRoles,
370
+ characterReferenceSheetPrompts,
371
+ policy,
86
372
  generatedAt
87
373
  },
88
374
  request,
89
375
  assets,
376
+ characterReferenceSheetPrompts,
90
377
  bundles
91
378
  };
92
379
  }
@@ -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: "realistic-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 {