@milenyumai/film-kit 2.3.1 → 2.3.3

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.
@@ -54,6 +54,12 @@ export class Seedance20PromptAdapter extends BaseVideoModelPromptAdapter {
54
54
  const promptText = `${this.formatCharacterRoleLine(input)}
55
55
  ${this.formatStoryboardRoleLine(input)}
56
56
 
57
+ [REFERENCE ROLES]
58
+ - Identity reference: ${characterTokenText} locks face, hair, body proportions, wardrobe, accessories, visible props, and material continuity.
59
+ - Camera reference: ${storyboardToken} controls composition, camera direction, framing, screen direction, shot order, and lens rhythm only.
60
+ - Action reference: ${storyboardToken} controls blocking, pose logic, timing, action rhythm, phase order, and emotional progression only.
61
+ - Audio reference: use the Audio Plan text below for dialogue, SFX, ambience, and music intent; do not infer audio from storyboard annotations unless explicitly requested.
62
+
57
63
  ${continuityInstruction}
58
64
 
59
65
  [CORE INTENT]
@@ -119,10 +125,14 @@ Avoid: identity drift, face drift, outfit drift, storyboard text, panel borders,
119
125
  ? hasNoCutsRule
120
126
  : /left-to-right, top-to-bottom/i.test(output.promptText)
121
127
  && /Do not reinterpret actions, poses, camera angles, emotional progression/i.test(output.promptText),
128
+ separatesReferenceRoles: /Identity reference:/i.test(output.promptText)
129
+ && /Camera reference:/i.test(output.promptText)
130
+ && /Action reference:/i.test(output.promptText)
131
+ && /Audio reference:/i.test(output.promptText),
122
132
  blocksStoryboardIdentitySources: /Storyboard text, panel borders, arrows, colored marks, lens notes, watermark, logo, and alternate character design are never identity sources/i.test(output.promptText),
123
133
  continuityMode: continuityMode === "continuous-shot"
124
134
  ? hasNoCutsRule
125
- : !hasNoCutsRule && /planned storyboard phase transitions/i.test(output.promptText),
135
+ : !hasNoCutsRule && /storyboard-motivated cuts/i.test(output.promptText),
126
136
  musicNone: /Music: NONE/i.test(output.promptText)
127
137
  };
128
138
  const issues = [
@@ -3,5 +3,5 @@ export { normalizeVideoPromptRequest } from "./request-normalizer.js";
3
3
  export { resolveAssetRoles } from "./asset-role-resolver.js";
4
4
  export { interpretStoryboard } from "./storyboard-interpreter.js";
5
5
  export { renderShotMarkdown, writeStoryboardReferenceOutputs } from "./output-writer.js";
6
- export { assertSafeStoryboardReferenceRequest, validateModelPromptOutput, validatePromptBundle, validateResolvedAssetRoles } from "./validators.js";
6
+ export { assertSafeStoryboardReferenceRequest, assertStoryboardReferenceBuildPass, validateModelPromptOutput, validatePromptBundle, validateResolvedAssetRoles } from "./validators.js";
7
7
  export type { AdapterInput, AudioPlan, CameraPlan, CharacterReferenceSheetPrompt, ContinuityAnchors, DialogueLine, ModelPromptOutput, ModelPromptQa, ModelRouteMetadata, NormalizedVideoPromptRequest, PromptBundle, PromptBundleQa, ProviderAssetMappingEntry, ProviderFileLimitReport, ProviderReferenceTokenPolicy, ReferenceAsset, ReferenceAssetRequirement, ReferenceAssetRole, ReferenceLockStrength, ResolvedAssetRoles, SeedanceContinuityMode, ShotHandoffNote, StoryboardImagePrompt, StoryboardInterpretation, StoryboardPhase, StoryboardPhaseBudget, StoryboardReferenceBuildResult, StoryboardReferencePlan, StoryboardReferencePolicy, StoryboardReferenceWriteResult, VideoModelPromptAdapter, VideoPromptRequest, VisualWorld, VoiceCastEntry } from "./types.js";
@@ -3,4 +3,4 @@ export { normalizeVideoPromptRequest } from "./request-normalizer.js";
3
3
  export { resolveAssetRoles } from "./asset-role-resolver.js";
4
4
  export { interpretStoryboard } from "./storyboard-interpreter.js";
5
5
  export { renderShotMarkdown, writeStoryboardReferenceOutputs } from "./output-writer.js";
6
- export { assertSafeStoryboardReferenceRequest, validateModelPromptOutput, validatePromptBundle, validateResolvedAssetRoles } from "./validators.js";
6
+ export { assertSafeStoryboardReferenceRequest, assertStoryboardReferenceBuildPass, validateModelPromptOutput, validatePromptBundle, validateResolvedAssetRoles } from "./validators.js";
@@ -1,5 +1,6 @@
1
1
  import { resolve, join } from "node:path";
2
2
  import { writeText } from "../fs.js";
3
+ import { assertStoryboardReferenceBuildPass } from "./validators.js";
3
4
  function getAudioPlan(bundle) {
4
5
  return Object.values(bundle.modelPrompts).find(Boolean)?.audioPlan;
5
6
  }
@@ -40,6 +41,37 @@ function formatProviderTokens(bundle) {
40
41
  })
41
42
  .join("\n");
42
43
  }
44
+ function getProviderToken(bundle, role, fallback) {
45
+ return Object.values(bundle.providerAssetMapping)
46
+ .find(entry => entry.role === role)?.token ?? fallback;
47
+ }
48
+ function formatCoverageReferences(bundle) {
49
+ const interpretation = bundle.storyboardInterpretation;
50
+ const characterTokens = Object.values(bundle.providerAssetMapping)
51
+ .filter(entry => entry.role === "character_identity")
52
+ .map(entry => entry.token)
53
+ .join(", ") || "@Image1";
54
+ const storyboardToken = getProviderToken(bundle, "storyboard_plan", "@Image2");
55
+ const visualWorld = interpretation.visualWorld;
56
+ const camera = interpretation.cameraPlan;
57
+ const firstPhase = interpretation.phases[0];
58
+ const lastPhase = interpretation.phases.at(-1) ?? firstPhase;
59
+ return `## Coverage References In Same File
60
+
61
+ Coverage is standalone editorial material for this shot only. Do not chain coverage into the next main shot. Keep the same generated character identity, wardrobe, props, lighting direction, environment, screen direction, and visual_world.
62
+
63
+ ### ${bundle.shotId}A - ECU Reaction | 3s
64
+
65
+ \`\`\`text
66
+ Use ${characterTokens} as the exact identity source and ${storyboardToken} only for scene continuity. Extreme close-up reaction coverage during ${firstPhase?.timeRange ?? "the opening beat"}: ${firstPhase?.subjectAction ?? "the character registers the story beat"}. Keep ${visualWorld.lighting}, ${visualWorld.atmosphere}, and ${camera.screenDirection}. Static or barely drifting camera, shallow depth of field, natural skin texture, visible breath or eye tension, no new action beat, no new identity. Audio: close breath, subtle clothing sound, matched ambience, Music: NONE. Avoid: identity drift, face drift, wardrobe drift, beauty filter, plastic skin, distorted eyes, flicker, warping, on-screen text, watermark, logo.
67
+ \`\`\`
68
+
69
+ ### ${bundle.shotId}B - Insert / Environmental Detail | 3s
70
+
71
+ \`\`\`text
72
+ Use ${storyboardToken} only for composition and environment continuity; do not introduce a new character identity. Tight insert coverage tied to ${lastPhase?.timeRange ?? "the final beat"}: ${lastPhase?.subjectAction ?? "the action resolves into a clear visual detail"}. Show a physically plausible detail from the same visual world: foreground texture, prop contact, reflection, light spill, hand tension, fabric motion, or environment response. Keep ${visualWorld.environment}; ${visualWorld.foreground}; ${visualWorld.background}; ${visualWorld.lighting}. Audio: matched practical SFX and ambience, no dialogue unless explicitly present in the main Audio Plan, Music: NONE. Avoid: new location, new character, clear logo, readable text, impossible physics, wrong reflection angle, identity override, flicker, warping, cartoon style, CGI look.
73
+ \`\`\``;
74
+ }
43
75
  export function renderShotMarkdown(bundle) {
44
76
  const interpretation = bundle.storyboardInterpretation;
45
77
  const audioPlan = getAudioPlan(bundle);
@@ -103,6 +135,8 @@ ${JSON.stringify(audioPlan ?? null, null, 2)}
103
135
 
104
136
  ${modelPrompts}
105
137
 
138
+ ${formatCoverageReferences(bundle)}
139
+
106
140
  ## QA
107
141
  ${formatQa(bundle)}
108
142
  `;
@@ -180,6 +214,7 @@ ${issues.length === 0 ? "- None" : "- Resolve FAIL items before render automatio
180
214
  `;
181
215
  }
182
216
  export async function writeStoryboardReferenceOutputs(result, rootDir = process.cwd()) {
217
+ assertStoryboardReferenceBuildPass(result);
183
218
  const outputRoot = resolve(rootDir, result.request.outputDir);
184
219
  const written = [];
185
220
  const planPath = join(outputRoot, "storyboard-reference-plan.json");
@@ -194,6 +229,7 @@ export async function writeStoryboardReferenceOutputs(result, rootDir = process.
194
229
  - Character sheet prompts: ${result.characterReferenceSheetPrompts.length}
195
230
  - External storyboard guides: ${result.assets.storyboards.length}
196
231
  - Generated shot storyboard prompts: ${result.bundles.length}
232
+ - Voice cast entries: ${result.plan.voiceCast.length}
197
233
  - Target models: ${result.request.targetModels.join(", ")}
198
234
  - Safety context: ${result.request.safetyContext}
199
235
  `);
@@ -4,7 +4,7 @@ import { Seedance20PromptAdapter } from "./adapters/seedance20.js";
4
4
  import { Veo31PromptAdapter } from "./adapters/veo31.js";
5
5
  import { normalizeVideoPromptRequest } from "./request-normalizer.js";
6
6
  import { buildStoryboardPhaseBudget, inferContinuityMode, interpretStoryboard } from "./storyboard-interpreter.js";
7
- import { assertSafeStoryboardReferenceRequest, validateModelPromptOutput, validatePromptBundle } from "./validators.js";
7
+ import { assertSafeStoryboardReferenceRequest, validateCharacterReferenceSheetPrompts, validateModelPromptOutput, validatePromptBundle } from "./validators.js";
8
8
  const ADAPTERS = {
9
9
  veo31: new Veo31PromptAdapter(),
10
10
  "seedance-2.0": new Seedance20PromptAdapter(),
@@ -74,6 +74,22 @@ function buildProviderFileLimitReport(request, generatedImageReferenceCount) {
74
74
  };
75
75
  return report;
76
76
  }
77
+ function buildVoiceCast(request) {
78
+ const speakers = new Map();
79
+ for (const line of request.dialogue) {
80
+ const speakerKey = line.speaker.trim();
81
+ if (!speakerKey || speakers.has(speakerKey)) {
82
+ continue;
83
+ }
84
+ speakers.set(speakerKey, {
85
+ speakerKey,
86
+ displayName: speakerKey,
87
+ language: request.language,
88
+ voiceIdentityPrompt: `Original fictional voice for ${speakerKey}; keep the same age impression, accent family, breath texture, pacing, and emotional baseline across all storyboard-reference shots.`
89
+ });
90
+ }
91
+ return Array.from(speakers.values());
92
+ }
77
93
  function buildShotHandoffNote(input) {
78
94
  const firstPhase = input.storyboardInterpretation.phases[0];
79
95
  const lastPhase = input.storyboardInterpretation.phases.at(-1);
@@ -103,7 +119,13 @@ function buildCharacterReferenceSheetPrompts(assets, outputDir) {
103
119
  aspectRatio: "16:9",
104
120
  cellCount: 6,
105
121
  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."}`
122
+ promptText: `REFERENCE LOCK: Use ${character.token} (${sourceLabel}) as the immutable identity source for this original fictional character. This generated sheet becomes the identity reference for all later shot storyboards.
123
+
124
+ Keep same: exact face, skull shape, hair, skin texture, body proportions, wardrobe, accessories, visible props, material behavior, silhouette, and natural asymmetry. Do not beautify, redesign, age-shift, stylize, or change brand-free clothing.
125
+
126
+ Change only: convert the source reference into one 16:9 realistic production storyboard character reference sheet. 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. 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.${index === 0 ? "" : " Match visual finish to the earlier character sheets."}
127
+
128
+ Avoid: text, labels, arrows, captions, panel numbers, watermarks, logos, extra characters, alternate outfits, alternate character designs, background story elements, celebrity resemblance, public-figure likeness, brand marks, beauty filter, cartoon style, anime style, CGI look, final-render polish, distorted face, inconsistent identity between cells, bad anatomy, extra limbs, extra fingers, deformed hands, waxy skin, plastic skin.`
107
129
  };
108
130
  });
109
131
  }
@@ -134,7 +156,11 @@ function buildStoryboardImagePrompt(input) {
134
156
  externalStoryboardGuideIds,
135
157
  previousShotHandoff: input.previousShotHandoff,
136
158
  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}
159
+ promptText: `REFERENCE LOCK: Use the generated character sheet reference${characterReferenceSheetPrompts.length === 1 ? "" : "s"} (${sheetRefs}) as the only source for character identity, face, hair, body proportions, wardrobe, accessories, material behavior, and visible props.
160
+
161
+ Keep same: every character identity detail from the character sheet${characterReferenceSheetPrompts.length === 1 ? "" : "s"}, the declared visual world, screen direction, environmental continuity, lighting direction, prop continuity, and shot handoff logic. Storyboard text, arrows, labels, panel borders, watermarks, logos, and alternate character designs are never identity sources.
162
+
163
+ Change only: create a professional 16:9 production storyboard page for ${shotId} with exactly ${interpretation.panelCount} cinematic panel${interpretation.panelCount === 1 ? "" : "s"}. The storyboard controls composition, blocking, camera direction, timing, phase order, action rhythm, and emotional progression only. ${externalGuideText}
138
164
 
139
165
  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
166
 
@@ -144,7 +170,9 @@ Camera and staging: show ${interpretation.cameraPlan.framing}, ${interpretation.
144
170
 
145
171
  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
172
 
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.`
173
+ 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. Keep the location minimal and readable.
174
+
175
+ Avoid: identity drift, face drift, hair drift, wardrobe drift, prop drift, material drift, alternate character design, static standing poses unless explicitly required by the beat, timestamps, long text notes, storyboard text becoming scene text, new characters, new locations, logos, watermarks, decorative borders, broken perspective, impossible contact or weight, inconsistent screen direction, inconsistent light direction, photorealistic final-render polish, cartoon style, anime style, CGI look, unsafe public-figure likeness, trademarked branding.`
148
176
  };
149
177
  }
150
178
  function isImageReference(asset) {
@@ -309,6 +337,11 @@ export function buildStoryboardReferencePromptBundles(input) {
309
337
  assertSafeStoryboardReferenceRequest(request);
310
338
  const assets = resolveAssetRoles(request.characterRefs, request.storyboardRefs, request.additionalRefs);
311
339
  const characterReferenceSheetPrompts = buildCharacterReferenceSheetPrompts(assets, request.outputDir);
340
+ const characterPromptIssues = validateCharacterReferenceSheetPrompts(characterReferenceSheetPrompts);
341
+ if (characterPromptIssues.length > 0) {
342
+ throw new Error(`Invalid character sheet prompt contract: ${characterPromptIssues.join("; ")}`);
343
+ }
344
+ const voiceCast = buildVoiceCast(request);
312
345
  const phaseBudget = buildStoryboardPhaseBudget(request.durationSeconds, request.storyboardReferenceMode.maxStoryboardPhases);
313
346
  const providerFileLimits = buildProviderFileLimitReport(request, characterReferenceSheetPrompts.length + 1);
314
347
  const panelCount = getStoryboardPanelCount(request, phaseBudget);
@@ -363,6 +396,7 @@ export function buildStoryboardReferencePromptBundles(input) {
363
396
  assetRoles: assets.assetRoles,
364
397
  continuityMode,
365
398
  phaseBudget,
399
+ providerFileLimits,
366
400
  handoffNote,
367
401
  storyboardImagePrompt,
368
402
  referenceAssetRequirements,
@@ -370,11 +404,15 @@ export function buildStoryboardReferencePromptBundles(input) {
370
404
  qa: { verdict: "pass", checks: {}, issues: [], splitRequired },
371
405
  generatedAt
372
406
  };
373
- bundle.qa = validatePromptBundle(bundle, assets);
407
+ bundle.qa = validatePromptBundle(bundle, assets, voiceCast);
374
408
  bundles.push(bundle);
375
409
  previousShotHandoff = handoffNote.exitHandoff;
376
410
  });
377
411
  const providerReferenceTokens = buildProviderReferenceTokens(bundles[0]?.providerAssetMapping ?? {});
412
+ const visualWorld = bundles[0]?.storyboardInterpretation.visualWorld;
413
+ if (!visualWorld) {
414
+ throw new Error("Missing visual_world continuity contract for storyboard-reference plan.");
415
+ }
378
416
  const policy = buildStoryboardReferencePolicy(request, phaseBudget, providerFileLimits, bundles, providerReferenceTokens, maxPhases, splitRequired);
379
417
  return {
380
418
  plan: {
@@ -384,6 +422,8 @@ export function buildStoryboardReferencePromptBundles(input) {
384
422
  splitRequired,
385
423
  assetRoles: assets.assetRoles,
386
424
  characterReferenceSheetPrompts,
425
+ voiceCast,
426
+ visual_world: visualWorld,
387
427
  policy,
388
428
  generatedAt
389
429
  },
@@ -251,6 +251,7 @@ export interface PromptBundle {
251
251
  assetRoles: ResolvedAssetRoles["assetRoles"];
252
252
  continuityMode: SeedanceContinuityMode;
253
253
  phaseBudget: StoryboardPhaseBudget;
254
+ providerFileLimits: ProviderFileLimitReport;
254
255
  handoffNote: ShotHandoffNote;
255
256
  storyboardImagePrompt: StoryboardImagePrompt;
256
257
  referenceAssetRequirements: ReferenceAssetRequirement[];
@@ -326,6 +327,8 @@ export interface StoryboardReferencePlan {
326
327
  splitRequired: boolean;
327
328
  assetRoles: ResolvedAssetRoles["assetRoles"];
328
329
  characterReferenceSheetPrompts: CharacterReferenceSheetPrompt[];
330
+ voiceCast: VoiceCastEntry[];
331
+ visual_world: VisualWorld;
329
332
  policy: StoryboardReferencePolicy;
330
333
  generatedAt: string;
331
334
  }
@@ -1,5 +1,7 @@
1
- import type { ModelPromptOutput, ModelPromptQa, NormalizedVideoPromptRequest, PromptBundle, PromptBundleQa, ResolvedAssetRoles } from "./types.js";
1
+ import type { CharacterReferenceSheetPrompt, ModelPromptOutput, ModelPromptQa, NormalizedVideoPromptRequest, PromptBundle, PromptBundleQa, ResolvedAssetRoles, StoryboardReferenceBuildResult, VoiceCastEntry } from "./types.js";
2
2
  export declare function assertSafeStoryboardReferenceRequest(request: NormalizedVideoPromptRequest): void;
3
+ export declare function validateCharacterReferenceSheetPrompts(prompts: CharacterReferenceSheetPrompt[]): string[];
3
4
  export declare function validateResolvedAssetRoles(assets: ResolvedAssetRoles): string[];
4
5
  export declare function validateModelPromptOutput(output: ModelPromptOutput): ModelPromptQa;
5
- export declare function validatePromptBundle(bundle: PromptBundle, assets: ResolvedAssetRoles): PromptBundleQa;
6
+ export declare function validatePromptBundle(bundle: PromptBundle, assets: ResolvedAssetRoles, voiceCast?: VoiceCastEntry[]): PromptBundleQa;
7
+ export declare function assertStoryboardReferenceBuildPass(result: StoryboardReferenceBuildResult): void;
@@ -1,28 +1,63 @@
1
- const BLOCKED_REAL_PERSON_TERMS = [
2
- "atatürk",
3
- "mustafa kemal",
4
- "elon musk",
5
- "taylor swift",
6
- "beyonce",
7
- "messi",
8
- "ronaldo"
1
+ const BLOCKED_PUBLIC_FIGURE_OR_BRAND_TERMS = [
2
+ { term: "atatürk", reason: "real person/public figure" },
3
+ { term: "mustafa kemal", reason: "real person/public figure" },
4
+ { term: "elon musk", reason: "real person/public figure" },
5
+ { term: "taylor swift", reason: "real person/public figure" },
6
+ { term: "beyonce", reason: "real person/public figure" },
7
+ { term: "messi", reason: "real person/public figure" },
8
+ { term: "ronaldo", reason: "real person/public figure" },
9
+ { term: "nike", reason: "trademark/brand" },
10
+ { term: "adidas", reason: "trademark/brand" },
11
+ { term: "coca-cola", reason: "trademark/brand" },
12
+ { term: "mcdonald", reason: "trademark/brand" },
13
+ { term: "starbucks", reason: "trademark/brand" },
14
+ { term: "iphone", reason: "trademark/brand" },
15
+ { term: "apple logo", reason: "trademark/brand" },
16
+ { term: "disney", reason: "trademark/brand" },
17
+ { term: "marvel", reason: "trademark/brand" }
9
18
  ];
10
- function includesBlockedRealPersonTerm(text) {
19
+ function includesBlockedReferenceTerm(text) {
11
20
  const normalized = text.toLowerCase();
12
- return BLOCKED_REAL_PERSON_TERMS.find(term => normalized.includes(term));
21
+ return BLOCKED_PUBLIC_FIGURE_OR_BRAND_TERMS.find(entry => normalized.includes(entry.term));
22
+ }
23
+ function hasAvoidOrNegativePrompt(text) {
24
+ return /Avoid:|\[NEGATIVE PROMPT\]/i.test(text);
25
+ }
26
+ function hasGptImageStillPromptContract(text) {
27
+ return /REFERENCE LOCK:/i.test(text)
28
+ && /Keep same:/i.test(text)
29
+ && /Change only:/i.test(text)
30
+ && hasAvoidOrNegativePrompt(text);
13
31
  }
14
32
  export function assertSafeStoryboardReferenceRequest(request) {
15
33
  const checkedText = [
16
34
  request.brief,
17
35
  request.styleIntent,
18
36
  request.safetyContext,
19
- ...request.characterRefs.map(ref => `${ref.label ?? ""} ${ref.notes ?? ""}`),
20
- ...request.storyboardRefs.map(ref => `${ref.label ?? ""} ${ref.notes ?? ""}`)
37
+ ...request.characterRefs.map(ref => `${ref.id} ${ref.label ?? ""} ${ref.notes ?? ""}`),
38
+ ...request.storyboardRefs.map(ref => `${ref.id} ${ref.label ?? ""} ${ref.notes ?? ""}`),
39
+ ...request.additionalRefs.map(ref => `${ref.id} ${ref.label ?? ""} ${ref.notes ?? ""}`),
40
+ ...request.dialogue.map(line => `${line.speaker}: ${line.text}`)
21
41
  ].join("\n");
22
- const blocked = includesBlockedRealPersonTerm(checkedText);
42
+ const blocked = includesBlockedReferenceTerm(checkedText);
23
43
  if (blocked) {
24
- throw new Error(`Blocked storyboard-reference request: real person/public figure reference '${blocked}' must be removed or safely anonymized before prompt generation.`);
44
+ throw new Error(`Blocked storyboard-reference request: ${blocked.reason} reference '${blocked.term}' must be removed or safely anonymized before prompt generation.`);
45
+ }
46
+ }
47
+ export function validateCharacterReferenceSheetPrompts(prompts) {
48
+ const issues = [];
49
+ if (prompts.length === 0) {
50
+ issues.push("Missing character sheet prompt.");
51
+ }
52
+ for (const prompt of prompts) {
53
+ if (!prompt.promptText.trim()) {
54
+ issues.push(`${prompt.id} is missing prompt text.`);
55
+ }
56
+ if (!hasGptImageStillPromptContract(prompt.promptText)) {
57
+ issues.push(`${prompt.id} must include REFERENCE LOCK / Keep same / Change only / Avoid.`);
58
+ }
25
59
  }
60
+ return issues;
26
61
  }
27
62
  export function validateResolvedAssetRoles(assets) {
28
63
  const issues = [];
@@ -48,7 +83,7 @@ export function validateModelPromptOutput(output) {
48
83
  const prompt = output.promptText;
49
84
  const checks = {
50
85
  hasPromptText: prompt.trim().length > 0,
51
- hasAvoidOrNegative: /Avoid:|\[NEGATIVE PROMPT\]/i.test(prompt),
86
+ hasAvoidOrNegative: hasAvoidOrNegativePrompt(prompt),
52
87
  separatesStoryboardRole: /storyboard/i.test(prompt) && /identity|character reference|@image1/i.test(prompt),
53
88
  blocksStoryboardTextCopy: /Do not copy storyboard text|storyboard text|on-screen text/i.test(prompt),
54
89
  musicNone: /Music: NONE|Music NONE/i.test(prompt)
@@ -62,17 +97,32 @@ export function validateModelPromptOutput(output) {
62
97
  issues
63
98
  };
64
99
  }
65
- export function validatePromptBundle(bundle, assets) {
100
+ export function validatePromptBundle(bundle, assets, voiceCast = []) {
66
101
  const roleIssues = validateResolvedAssetRoles(assets);
102
+ const modelValidationResults = Object.values(bundle.modelPrompts)
103
+ .filter((output) => Boolean(output))
104
+ .map(validateModelPromptOutput);
67
105
  const modelIssues = Object.values(bundle.modelPrompts)
68
- .flatMap(output => output?.qa.issues ?? []);
106
+ .flatMap(output => output?.qa.issues ?? [])
107
+ .concat(modelValidationResults.flatMap(result => result.issues));
108
+ const voiceCastKeys = new Set(voiceCast.map(entry => entry.speakerKey));
109
+ const audioPlans = Object.values(bundle.modelPrompts)
110
+ .map(output => output?.audioPlan)
111
+ .filter((plan) => Boolean(plan));
69
112
  const checks = {
70
113
  hasCharacterReference: assets.characters.length >= 1,
71
114
  hasStoryboardReference: assets.storyboards.length >= 1 || Boolean(bundle.storyboardImagePrompt),
72
- hasAvoidLineEverywhere: Object.values(bundle.modelPrompts).every(output => Boolean(output?.qa.checks.hasAvoidLine ?? output?.qa.checks.hasAvoidOrNegative)),
73
- modelGrammarPasses: Object.values(bundle.modelPrompts).every(output => output?.qa.verdict === "pass"),
115
+ hasStoryboardImagePrompt: Boolean(bundle.storyboardImagePrompt?.promptText.trim()),
116
+ storyboardPromptHasReferenceLockStructure: hasGptImageStillPromptContract(bundle.storyboardImagePrompt?.promptText ?? ""),
117
+ hasAvoidLineEverywhere: Object.values(bundle.modelPrompts).every(output => Boolean(output) && hasAvoidOrNegativePrompt(output.promptText)),
118
+ storyboardPromptHasAvoidLine: hasAvoidOrNegativePrompt(bundle.storyboardImagePrompt?.promptText ?? ""),
119
+ modelGrammarPasses: Object.values(bundle.modelPrompts).every(output => output?.qa.verdict === "pass")
120
+ && modelValidationResults.every(result => result.verdict === "pass"),
74
121
  storyboardDoesNotOverrideIdentity: Object.values(bundle.modelPrompts).every(output => /storyboard.*staging|storyboard.*planning|storyboard.*composition|storyboard.*only/i.test(output?.promptText ?? "")),
75
- audioPlanPresent: Object.values(bundle.modelPrompts).every(output => Boolean(output?.audioPlan))
122
+ audioPlanPresent: Object.values(bundle.modelPrompts).every(output => Boolean(output?.audioPlan)),
123
+ speakingAudioPlansHaveActiveSpeaker: audioPlans.every(plan => plan.dialogueTranscript === "NONE" || Boolean(plan.activeSpeakerKey)),
124
+ activeSpeakerResolvesToVoiceCast: audioPlans.every(plan => !plan.activeSpeakerKey || voiceCastKeys.has(plan.activeSpeakerKey)),
125
+ providerFilesWithinLimits: bundle.providerFileLimits.withinLimits
76
126
  };
77
127
  const failedChecks = Object.entries(checks)
78
128
  .filter(([, passed]) => !passed)
@@ -85,3 +135,25 @@ export function validatePromptBundle(bundle, assets) {
85
135
  splitRequired: bundle.storyboardInterpretation.riskFlags.includes("split-required")
86
136
  };
87
137
  }
138
+ export function assertStoryboardReferenceBuildPass(result) {
139
+ const issues = validateCharacterReferenceSheetPrompts(result.characterReferenceSheetPrompts)
140
+ .map(issue => `character-sheet: ${issue}`);
141
+ for (const bundle of result.bundles) {
142
+ const currentQa = validatePromptBundle(bundle, result.assets, result.plan.voiceCast);
143
+ if (bundle.qa.verdict !== "pass" || currentQa.verdict !== "pass") {
144
+ const bundleIssues = [
145
+ ...bundle.qa.issues,
146
+ ...currentQa.issues
147
+ ];
148
+ issues.push(...(bundleIssues.length > 0
149
+ ? bundleIssues.map(issue => `${bundle.shotId}: ${issue}`)
150
+ : [`${bundle.shotId}: bundle QA failed without a detailed issue.`]));
151
+ }
152
+ }
153
+ if (result.bundles.length === 0) {
154
+ issues.push("No storyboard-reference prompt bundles were generated.");
155
+ }
156
+ if (issues.length > 0) {
157
+ throw new Error(`Storyboard-reference QA failed. Refusing to write render-ready outputs. ${issues.join(" ")}`);
158
+ }
159
+ }
@@ -11,7 +11,7 @@ export function buildCameraPlan(request) {
11
11
  lens: wideShot ? "35mm lens feel" : closeShot ? "85mm lens feel" : "50mm lens feel",
12
12
  framing: wideShot ? "layered wide composition" : closeShot ? "eye-level close framing" : "stable mid-shot framing",
13
13
  stabilization: "tripod-stable with minimal organic drift",
14
- screenDirection: "preserve the storyboard's screen direction and eyeline relationships"
14
+ screenDirection: "storyboard screen direction and eyeline relationships"
15
15
  };
16
16
  }
17
17
  export function buildVisualWorld(request) {