@milenyumai/film-kit 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -2
- package/build/cli.js +2 -1
- package/build/lib/cli.js +4 -0
- package/build/lib/configure.js +494 -17
- package/build/lib/defaults.js +3 -1
- package/build/lib/storyboard-reference/adapters/seedance20.js +22 -8
- package/build/lib/storyboard-reference/index.d.ts +1 -1
- package/build/lib/storyboard-reference/index.js +1 -1
- package/build/lib/storyboard-reference/output-writer.js +3 -0
- package/build/lib/storyboard-reference/prompt-bundle-builder.js +61 -5
- package/build/lib/storyboard-reference/types.d.ts +4 -1
- package/build/lib/storyboard-reference/validators.d.ts +4 -2
- package/build/lib/storyboard-reference/validators.js +74 -17
- package/build/lib/templates.js +144 -36
- package/content/skills/storyboard-reference/SKILL.md +5 -1
- package/content/workflows/generate-storyboard.md +16 -0
- package/package.json +1 -1
- package/packages/gpt-image-smart/content/skills/storyboard-reference/SKILL.md +5 -1
- package/packages/hybrid/content/skills/storyboard-reference/SKILL.md +5 -1
- package/packages/hybrid-smart/content/skills/storyboard-reference/SKILL.md +5 -1
- package/packages/multi/content/skills/storyboard-reference/SKILL.md +5 -1
- package/packages/studio/build/lib/configure.js +4 -4
- package/packages/studio/content/skills/storyboard-reference/SKILL.md +5 -1
|
@@ -44,16 +44,22 @@ export class Seedance20PromptAdapter extends BaseVideoModelPromptAdapter {
|
|
|
44
44
|
}
|
|
45
45
|
buildPrompt(input) {
|
|
46
46
|
const audioPlan = buildAudioPlan(input);
|
|
47
|
+
const storyboardEntry = this.getMappingEntries(input, "storyboard_plan")[0];
|
|
48
|
+
const storyboardToken = storyboardEntry?.token ?? "@Image2";
|
|
47
49
|
const continuityInstruction = input.continuityMode === "continuous-shot"
|
|
48
50
|
? "No scene cuts throughout, one continuous shot."
|
|
49
|
-
:
|
|
51
|
+
: `Follow the generated storyboard panels in ${storyboardToken} left-to-right, top-to-bottom as the exact visual beat order. Do not reinterpret actions, poses, camera angles, emotional progression, frame variety, or the final pose. Compress the planned sequence into the full duration as concise motion snapshots; use only storyboard-motivated cuts, match cuts, whip transitions, or camera moves, and do not add unplanned cuts, extra scene jumps, or new locations.`;
|
|
50
52
|
const characterEntries = this.getMappingEntries(input, "character_identity");
|
|
51
|
-
const storyboardEntry = this.getMappingEntries(input, "storyboard_plan")[0];
|
|
52
53
|
const characterTokenText = characterEntries.map(entry => entry.token).join(", ") || "@Image1";
|
|
53
|
-
const storyboardToken = storyboardEntry?.token ?? "@Image2";
|
|
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]
|
|
@@ -63,7 +69,7 @@ ${input.request.brief}
|
|
|
63
69
|
${formatPhaseLines(input.interpretation.phases)}
|
|
64
70
|
|
|
65
71
|
[GPT IMAGE 2 STORYBOARD SOURCE]
|
|
66
|
-
Use the generated storyboard image from ${input.storyboardImagePrompt.outputPath} as ${storyboardToken}. It has ${input.storyboardImagePrompt.panelCount} panel(s), follows the shot handoff, and is not an identity source.
|
|
72
|
+
Use the generated storyboard image from ${input.storyboardImagePrompt.outputPath} as ${storyboardToken}. It has ${input.storyboardImagePrompt.panelCount} panel(s), follows the shot handoff, and is not an identity source. Treat colored arrows, panel labels, and lens notes as planning annotations only; never render arrows, notes, labels, timestamps, or storyboard text into the video.
|
|
67
73
|
|
|
68
74
|
[CAMERA]
|
|
69
75
|
${input.interpretation.cameraPlan.framing}, ${input.interpretation.cameraPlan.movement}, ${input.interpretation.cameraPlan.lens}, ${input.interpretation.cameraPlan.stabilization}. Preserve ${input.interpretation.cameraPlan.screenDirection}.
|
|
@@ -78,9 +84,9 @@ Ambience: ${audioPlan.ambience.join(", ")}.
|
|
|
78
84
|
Music: NONE.
|
|
79
85
|
|
|
80
86
|
[CONTINUITY]
|
|
81
|
-
Identity reference stays locked to ${characterTokenText}. Storyboard ${storyboardToken} controls staging and visual rhythm only. Character design, wardrobe, accessories, and visible props cannot be inherited from the storyboard. Storyboard text, panel borders, watermark, logo, and alternate character design are never identity sources.
|
|
87
|
+
Identity reference stays locked to ${characterTokenText}. Storyboard ${storyboardToken} controls staging, shot order, pose logic, camera variety, emotional progression, and visual rhythm only. Character design, wardrobe, accessories, and visible props cannot be inherited from the storyboard. Storyboard text, panel borders, arrows, colored marks, lens notes, watermark, logo, and alternate character design are never identity sources and must not appear in the rendered video.
|
|
82
88
|
|
|
83
|
-
Avoid: identity drift, face drift, outfit drift, storyboard text, panel borders, watermark, logo, distorted hands, rubbery motion, flicker, unnatural camera jumps.`;
|
|
89
|
+
Avoid: identity drift, face drift, outfit drift, storyboard text, panel borders, colored arrows, annotation marks, lens notes, timestamps, watermark, logo, distorted hands, rubbery motion, flicker, unnatural camera jumps.`;
|
|
84
90
|
const output = {
|
|
85
91
|
model: this.model,
|
|
86
92
|
displayName: getDisplayName(this.model),
|
|
@@ -115,10 +121,18 @@ Avoid: identity drift, face drift, outfit drift, storyboard text, panel borders,
|
|
|
115
121
|
&& characterTokens.every(token => output.promptText.includes(token))
|
|
116
122
|
&& /exact character identity reference/i.test(output.promptText),
|
|
117
123
|
usesStoryboardToken: output.promptText.includes(storyboardToken) && /shot storyboard reference/i.test(output.promptText),
|
|
118
|
-
|
|
124
|
+
followsStoryboardOrder: continuityMode === "continuous-shot"
|
|
125
|
+
? hasNoCutsRule
|
|
126
|
+
: /left-to-right, top-to-bottom/i.test(output.promptText)
|
|
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),
|
|
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),
|
|
119
133
|
continuityMode: continuityMode === "continuous-shot"
|
|
120
134
|
? hasNoCutsRule
|
|
121
|
-
: !hasNoCutsRule && /
|
|
135
|
+
: !hasNoCutsRule && /storyboard-motivated cuts/i.test(output.promptText),
|
|
122
136
|
musicNone: /Music: NONE/i.test(output.promptText)
|
|
123
137
|
};
|
|
124
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
|
}
|
|
@@ -180,6 +181,7 @@ ${issues.length === 0 ? "- None" : "- Resolve FAIL items before render automatio
|
|
|
180
181
|
`;
|
|
181
182
|
}
|
|
182
183
|
export async function writeStoryboardReferenceOutputs(result, rootDir = process.cwd()) {
|
|
184
|
+
assertStoryboardReferenceBuildPass(result);
|
|
183
185
|
const outputRoot = resolve(rootDir, result.request.outputDir);
|
|
184
186
|
const written = [];
|
|
185
187
|
const planPath = join(outputRoot, "storyboard-reference-plan.json");
|
|
@@ -194,6 +196,7 @@ export async function writeStoryboardReferenceOutputs(result, rootDir = process.
|
|
|
194
196
|
- Character sheet prompts: ${result.characterReferenceSheetPrompts.length}
|
|
195
197
|
- External storyboard guides: ${result.assets.storyboards.length}
|
|
196
198
|
- Generated shot storyboard prompts: ${result.bundles.length}
|
|
199
|
+
- Voice cast entries: ${result.plan.voiceCast.length}
|
|
197
200
|
- Target models: ${result.request.targetModels.join(", ")}
|
|
198
201
|
- Safety context: ${result.request.safetyContext}
|
|
199
202
|
`);
|
|
@@ -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: `
|
|
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
|
}
|
|
@@ -117,18 +139,40 @@ function buildStoryboardImagePrompt(input) {
|
|
|
117
139
|
const externalGuideText = assets.storyboards.length > 0
|
|
118
140
|
? `Use external storyboard guide tokens ${assets.storyboards.map(storyboard => storyboard.token).join(", ")} only for composition, blocking, camera direction, timing, and action rhythm.`
|
|
119
141
|
: "No external storyboard guide is required; infer the board from the screenplay brief, continuity notes, and visual world.";
|
|
142
|
+
const phaseDirection = interpretation.phases
|
|
143
|
+
.map(phase => `Panel ${phase.index}: ${phase.job}; ${phase.subjectAction}; camera behavior: ${phase.cameraBehavior}; background behavior: ${phase.backgroundBehavior}.`)
|
|
144
|
+
.join(" ");
|
|
145
|
+
const continuityStyle = interpretation.phases.length <= 1
|
|
146
|
+
? "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."
|
|
147
|
+
: "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.";
|
|
120
148
|
return {
|
|
121
149
|
shotId,
|
|
122
150
|
outputPath: `${request.outputDir}/storyboard-prompts/${shotId}-GPT-IMAGE-2-STORYBOARD.md`,
|
|
123
151
|
provider: "gpt-image-2",
|
|
124
152
|
aspectRatio: "16:9",
|
|
125
153
|
panelCount: interpretation.panelCount,
|
|
126
|
-
style: "
|
|
154
|
+
style: "professional-production-storyboard",
|
|
127
155
|
characterSheetIds,
|
|
128
156
|
externalStoryboardGuideIds,
|
|
129
157
|
previousShotHandoff: input.previousShotHandoff,
|
|
130
158
|
nextShotHandoff: input.nextShotHandoff,
|
|
131
|
-
promptText: `
|
|
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}
|
|
164
|
+
|
|
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.
|
|
166
|
+
|
|
167
|
+
Panel direction: ${continuityStyle} The scene brief is: ${request.brief} Previous handoff: ${input.previousShotHandoff}. Next handoff: ${input.nextShotHandoff}. Planned panel actions: ${phaseDirection}
|
|
168
|
+
|
|
169
|
+
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}.
|
|
170
|
+
|
|
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.
|
|
172
|
+
|
|
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.`
|
|
132
176
|
};
|
|
133
177
|
}
|
|
134
178
|
function isImageReference(asset) {
|
|
@@ -293,6 +337,11 @@ export function buildStoryboardReferencePromptBundles(input) {
|
|
|
293
337
|
assertSafeStoryboardReferenceRequest(request);
|
|
294
338
|
const assets = resolveAssetRoles(request.characterRefs, request.storyboardRefs, request.additionalRefs);
|
|
295
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);
|
|
296
345
|
const phaseBudget = buildStoryboardPhaseBudget(request.durationSeconds, request.storyboardReferenceMode.maxStoryboardPhases);
|
|
297
346
|
const providerFileLimits = buildProviderFileLimitReport(request, characterReferenceSheetPrompts.length + 1);
|
|
298
347
|
const panelCount = getStoryboardPanelCount(request, phaseBudget);
|
|
@@ -347,6 +396,7 @@ export function buildStoryboardReferencePromptBundles(input) {
|
|
|
347
396
|
assetRoles: assets.assetRoles,
|
|
348
397
|
continuityMode,
|
|
349
398
|
phaseBudget,
|
|
399
|
+
providerFileLimits,
|
|
350
400
|
handoffNote,
|
|
351
401
|
storyboardImagePrompt,
|
|
352
402
|
referenceAssetRequirements,
|
|
@@ -354,11 +404,15 @@ export function buildStoryboardReferencePromptBundles(input) {
|
|
|
354
404
|
qa: { verdict: "pass", checks: {}, issues: [], splitRequired },
|
|
355
405
|
generatedAt
|
|
356
406
|
};
|
|
357
|
-
bundle.qa = validatePromptBundle(bundle, assets);
|
|
407
|
+
bundle.qa = validatePromptBundle(bundle, assets, voiceCast);
|
|
358
408
|
bundles.push(bundle);
|
|
359
409
|
previousShotHandoff = handoffNote.exitHandoff;
|
|
360
410
|
});
|
|
361
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
|
+
}
|
|
362
416
|
const policy = buildStoryboardReferencePolicy(request, phaseBudget, providerFileLimits, bundles, providerReferenceTokens, maxPhases, splitRequired);
|
|
363
417
|
return {
|
|
364
418
|
plan: {
|
|
@@ -368,6 +422,8 @@ export function buildStoryboardReferencePromptBundles(input) {
|
|
|
368
422
|
splitRequired,
|
|
369
423
|
assetRoles: assets.assetRoles,
|
|
370
424
|
characterReferenceSheetPrompts,
|
|
425
|
+
voiceCast,
|
|
426
|
+
visual_world: visualWorld,
|
|
371
427
|
policy,
|
|
372
428
|
generatedAt
|
|
373
429
|
},
|
|
@@ -128,7 +128,7 @@ export interface StoryboardImagePrompt {
|
|
|
128
128
|
provider: "gpt-image-2";
|
|
129
129
|
aspectRatio: "16:9";
|
|
130
130
|
panelCount: number;
|
|
131
|
-
style: "
|
|
131
|
+
style: "professional-production-storyboard";
|
|
132
132
|
characterSheetIds: string[];
|
|
133
133
|
externalStoryboardGuideIds: string[];
|
|
134
134
|
previousShotHandoff: string;
|
|
@@ -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
|
|
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
|
|
19
|
+
function includesBlockedReferenceTerm(text) {
|
|
11
20
|
const normalized = text.toLowerCase();
|
|
12
|
-
return
|
|
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 =
|
|
42
|
+
const blocked = includesBlockedReferenceTerm(checkedText);
|
|
23
43
|
if (blocked) {
|
|
24
|
-
throw new Error(`Blocked storyboard-reference request:
|
|
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:
|
|
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,27 @@ 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);
|
|
67
102
|
const modelIssues = Object.values(bundle.modelPrompts)
|
|
68
103
|
.flatMap(output => output?.qa.issues ?? []);
|
|
104
|
+
const voiceCastKeys = new Set(voiceCast.map(entry => entry.speakerKey));
|
|
105
|
+
const audioPlans = Object.values(bundle.modelPrompts)
|
|
106
|
+
.map(output => output?.audioPlan)
|
|
107
|
+
.filter((plan) => Boolean(plan));
|
|
69
108
|
const checks = {
|
|
70
109
|
hasCharacterReference: assets.characters.length >= 1,
|
|
71
110
|
hasStoryboardReference: assets.storyboards.length >= 1 || Boolean(bundle.storyboardImagePrompt),
|
|
111
|
+
hasStoryboardImagePrompt: Boolean(bundle.storyboardImagePrompt?.promptText.trim()),
|
|
112
|
+
storyboardPromptHasReferenceLockStructure: hasGptImageStillPromptContract(bundle.storyboardImagePrompt?.promptText ?? ""),
|
|
72
113
|
hasAvoidLineEverywhere: Object.values(bundle.modelPrompts).every(output => Boolean(output?.qa.checks.hasAvoidLine ?? output?.qa.checks.hasAvoidOrNegative)),
|
|
114
|
+
storyboardPromptHasAvoidLine: hasAvoidOrNegativePrompt(bundle.storyboardImagePrompt?.promptText ?? ""),
|
|
73
115
|
modelGrammarPasses: Object.values(bundle.modelPrompts).every(output => output?.qa.verdict === "pass"),
|
|
74
116
|
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))
|
|
117
|
+
audioPlanPresent: Object.values(bundle.modelPrompts).every(output => Boolean(output?.audioPlan)),
|
|
118
|
+
speakingAudioPlansHaveActiveSpeaker: audioPlans.every(plan => plan.dialogueTranscript === "NONE" || Boolean(plan.activeSpeakerKey)),
|
|
119
|
+
activeSpeakerResolvesToVoiceCast: audioPlans.every(plan => !plan.activeSpeakerKey || voiceCastKeys.has(plan.activeSpeakerKey)),
|
|
120
|
+
providerFilesWithinLimits: bundle.providerFileLimits.withinLimits
|
|
76
121
|
};
|
|
77
122
|
const failedChecks = Object.entries(checks)
|
|
78
123
|
.filter(([, passed]) => !passed)
|
|
@@ -85,3 +130,15 @@ export function validatePromptBundle(bundle, assets) {
|
|
|
85
130
|
splitRequired: bundle.storyboardInterpretation.riskFlags.includes("split-required")
|
|
86
131
|
};
|
|
87
132
|
}
|
|
133
|
+
export function assertStoryboardReferenceBuildPass(result) {
|
|
134
|
+
const failedBundles = result.bundles.filter(bundle => bundle.qa.verdict !== "pass");
|
|
135
|
+
const issues = failedBundles.flatMap(bundle => (bundle.qa.issues.length > 0
|
|
136
|
+
? bundle.qa.issues.map(issue => `${bundle.shotId}: ${issue}`)
|
|
137
|
+
: [`${bundle.shotId}: bundle QA failed without a detailed issue.`]));
|
|
138
|
+
if (result.bundles.length === 0) {
|
|
139
|
+
issues.push("No storyboard-reference prompt bundles were generated.");
|
|
140
|
+
}
|
|
141
|
+
if (issues.length > 0) {
|
|
142
|
+
throw new Error(`Storyboard-reference QA failed. Refusing to write render-ready outputs. ${issues.join(" ")}`);
|
|
143
|
+
}
|
|
144
|
+
}
|