mulmocast 2.8.1 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/actions/image_agents.d.ts +20 -0
- package/lib/actions/image_agents.js +83 -15
- package/lib/actions/image_references.d.ts +3 -0
- package/lib/actions/image_references.js +55 -33
- package/lib/actions/images.d.ts +30 -0
- package/lib/actions/images.js +16 -4
- package/lib/actions/movie.d.ts +2 -9
- package/lib/actions/movie.js +149 -147
- package/lib/methods/index.d.ts +1 -0
- package/lib/methods/index.js +1 -0
- package/lib/methods/mulmo_beat.d.ts +3 -0
- package/lib/methods/mulmo_beat.js +17 -0
- package/lib/methods/mulmo_beat_node.d.ts +3 -0
- package/lib/methods/mulmo_image_prompt_media.d.ts +14 -0
- package/lib/methods/mulmo_image_prompt_media.js +45 -0
- package/lib/methods/mulmo_presentation_style.d.ts +5 -1
- package/lib/methods/mulmo_presentation_style.js +43 -1
- package/lib/methods/mulmo_studio_context.d.ts +6 -1
- package/lib/methods/mulmo_studio_context.js +36 -1
- package/lib/types/provider2agent.d.ts +15 -0
- package/lib/types/provider2agent.js +31 -0
- package/lib/types/schema.d.ts +192 -0
- package/lib/types/schema.js +42 -12
- package/lib/types/type.d.ts +2 -1
- package/lib/utils/context.d.ts +70 -0
- package/lib/utils/error_cause.d.ts +12 -0
- package/lib/utils/error_cause.js +15 -0
- package/package.json +9 -9
- package/scripts/test/test_voice_over_transition_no_audio.json +71 -0
|
@@ -24,6 +24,9 @@ type ImagePreprocessAgentReturnValue = {
|
|
|
24
24
|
};
|
|
25
25
|
firstFrameImagePath?: string;
|
|
26
26
|
lastFrameImagePath?: string;
|
|
27
|
+
firstFrameIsBeatImage?: boolean;
|
|
28
|
+
lastFrameIsBeatImage?: boolean;
|
|
29
|
+
frameFillColor?: string;
|
|
27
30
|
movieReferenceImages?: {
|
|
28
31
|
imagePath: string;
|
|
29
32
|
referenceType: "ASSET" | "STYLE";
|
|
@@ -62,6 +65,23 @@ export declare const imagePreprocessAgent: (namedInputs: {
|
|
|
62
65
|
index: number;
|
|
63
66
|
imageRefs?: Record<string, string>;
|
|
64
67
|
}) => Promise<ImagePreprocessAgentResponse>;
|
|
68
|
+
export type BeatFramePreprocessorInput = {
|
|
69
|
+
imagePath?: string;
|
|
70
|
+
lastFrameImagePath?: string;
|
|
71
|
+
firstFrameIsBeatImage?: boolean;
|
|
72
|
+
lastFrameIsBeatImage?: boolean;
|
|
73
|
+
frameFillColor?: string;
|
|
74
|
+
referenceImageForMovie?: string;
|
|
75
|
+
};
|
|
76
|
+
export declare const beatFrameResolverAgent: (namedInputs: {
|
|
77
|
+
context: MulmoStudioContext;
|
|
78
|
+
beat: MulmoBeat;
|
|
79
|
+
index: number;
|
|
80
|
+
preprocessor?: BeatFramePreprocessorInput;
|
|
81
|
+
}) => Promise<{
|
|
82
|
+
lastFrameImagePath: string | undefined;
|
|
83
|
+
referenceImageForMovie: string | undefined;
|
|
84
|
+
}>;
|
|
65
85
|
export declare const imagePluginAgent: (namedInputs: {
|
|
66
86
|
context: MulmoStudioContext;
|
|
67
87
|
beat: MulmoBeat;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { GraphAILogger } from "graphai";
|
|
4
|
+
import { BEAT_IMAGE_SENTINEL, ImageMediaType, beatImageTypesWithoutOwnStill } from "../types/schema.js";
|
|
4
5
|
import { MulmoPresentationStyleMethods, MulmoStudioContextMethods, MulmoBeatMethods, MulmoMediaSourceMethods } from "../methods/index.js";
|
|
5
6
|
import { getBeatPngImagePath, getBeatMoviePaths, getBeatAnimatedVideoPath, getAudioFilePath, getGroupedAudioFilePath, getReferenceImagePath, } from "../utils/file.js";
|
|
6
7
|
import { ffmpegGetImageDimensions, padImageToCanvas } from "../utils/ffmpeg_utils.js";
|
|
8
|
+
import { beatImageSentinelError, bothFramesTarget, missingBeatImageTarget, unsupportedImageTypeTarget, missingFirstFrameTarget } from "../utils/error_cause.js";
|
|
7
9
|
import { imagePrompt, htmlImageSystemPrompt } from "../utils/prompt.js";
|
|
8
10
|
import { renderHTMLToImage } from "../utils/html_render.js";
|
|
9
11
|
import { beatId } from "../utils/utils.js";
|
|
@@ -71,6 +73,64 @@ export const conformFrameImageToCanvas = async (context, imageName, imagePath, f
|
|
|
71
73
|
conformingInFlight.delete(destPath);
|
|
72
74
|
}
|
|
73
75
|
};
|
|
76
|
+
const validateBeatImageSentinel = (movieParams, beat, index) => {
|
|
77
|
+
if (movieParams.firstFrameImageName === BEAT_IMAGE_SENTINEL && movieParams.lastFrameImageName === BEAT_IMAGE_SENTINEL) {
|
|
78
|
+
throw new Error(`${BEAT_IMAGE_SENTINEL} cannot be used for both firstFrameImageName and lastFrameImageName. beat: ${index}`, {
|
|
79
|
+
cause: beatImageSentinelError(index, bothFramesTarget),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!beat.imagePrompt && !beat.image) {
|
|
83
|
+
throw new Error(`${BEAT_IMAGE_SENTINEL} requires the beat to have an imagePrompt or image. beat: ${index}`, {
|
|
84
|
+
cause: beatImageSentinelError(index, missingBeatImageTarget),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (beat.image && beatImageTypesWithoutOwnStill.includes(beat.image.type)) {
|
|
88
|
+
throw new Error(`${BEAT_IMAGE_SENTINEL} cannot be used with image type "${beat.image.type}" (the beat image is not its own still). beat: ${index}`, {
|
|
89
|
+
cause: beatImageSentinelError(index, unsupportedImageTypeTarget),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// The implicit first frame (the raw beat image) is not conformed to the canvas, so pairing
|
|
93
|
+
// it with a conformed $beatImage last frame would feed mismatched frame sizes to strict
|
|
94
|
+
// image-to-video models (e.g. wan-2.2-i2v-fast). Require an explicit, conformed first frame.
|
|
95
|
+
if (movieParams.lastFrameImageName === BEAT_IMAGE_SENTINEL && !movieParams.firstFrameImageName) {
|
|
96
|
+
throw new Error(`${BEAT_IMAGE_SENTINEL} as lastFrameImageName requires firstFrameImageName. beat: ${index}`, {
|
|
97
|
+
cause: beatImageSentinelError(index, missingFirstFrameTarget),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
// Resolve firstFrameImageName/lastFrameImageName. Named refs conform to the canvas here;
|
|
102
|
+
// the $beatImage sentinel only sets flags (the beat image does not exist yet at preprocess
|
|
103
|
+
// time) and is resolved by beatFrameResolverAgent after image generation.
|
|
104
|
+
const applyFrameImageParams = async (returnValue, context, beat, index, movieParams, imageRefs) => {
|
|
105
|
+
const frameFillColor = movieParams.frameFillColor ?? "black";
|
|
106
|
+
// A style-level $beatImage merges into every beat; beats that generate no movie
|
|
107
|
+
// (no moviePrompt) ignore the sentinel instead of failing validation.
|
|
108
|
+
const sentinelActive = Boolean(beat.moviePrompt);
|
|
109
|
+
const firstFrameImageName = !sentinelActive && movieParams.firstFrameImageName === BEAT_IMAGE_SENTINEL ? undefined : movieParams.firstFrameImageName;
|
|
110
|
+
const lastFrameImageName = !sentinelActive && movieParams.lastFrameImageName === BEAT_IMAGE_SENTINEL ? undefined : movieParams.lastFrameImageName;
|
|
111
|
+
if (firstFrameImageName === BEAT_IMAGE_SENTINEL || lastFrameImageName === BEAT_IMAGE_SENTINEL) {
|
|
112
|
+
validateBeatImageSentinel(movieParams, beat, index);
|
|
113
|
+
returnValue.frameFillColor = frameFillColor;
|
|
114
|
+
}
|
|
115
|
+
if (firstFrameImageName === BEAT_IMAGE_SENTINEL) {
|
|
116
|
+
returnValue.firstFrameIsBeatImage = true;
|
|
117
|
+
}
|
|
118
|
+
else if (firstFrameImageName && imageRefs) {
|
|
119
|
+
const firstFramePath = imageRefs[firstFrameImageName];
|
|
120
|
+
if (firstFramePath) {
|
|
121
|
+
returnValue.firstFrameImagePath = await conformFrameImageToCanvas(context, firstFrameImageName, firstFramePath, frameFillColor);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (lastFrameImageName === BEAT_IMAGE_SENTINEL) {
|
|
125
|
+
returnValue.lastFrameIsBeatImage = true;
|
|
126
|
+
}
|
|
127
|
+
else if (lastFrameImageName && imageRefs) {
|
|
128
|
+
const lastFramePath = imageRefs[lastFrameImageName];
|
|
129
|
+
if (lastFramePath) {
|
|
130
|
+
returnValue.lastFrameImagePath = await conformFrameImageToCanvas(context, lastFrameImageName, lastFramePath, frameFillColor);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
74
134
|
export const imagePreprocessAgent = async (namedInputs) => {
|
|
75
135
|
const { context, beat, index, imageRefs } = namedInputs;
|
|
76
136
|
const studioBeat = context.studio.beats[index];
|
|
@@ -88,7 +148,7 @@ export const imagePreprocessAgent = async (namedInputs) => {
|
|
|
88
148
|
movieFile: beat.moviePrompt ? moviePaths.movieFile : undefined,
|
|
89
149
|
beatDuration: beat.duration ?? studioBeat?.duration,
|
|
90
150
|
};
|
|
91
|
-
const isMovie = Boolean(beat.moviePrompt || beat?.image?.type ===
|
|
151
|
+
const isMovie = Boolean(beat.moviePrompt || beat?.image?.type === ImageMediaType.Movie);
|
|
92
152
|
if (beat.soundEffectPrompt) {
|
|
93
153
|
if (isMovie) {
|
|
94
154
|
returnValue.soundEffectAgentInfo = MulmoPresentationStyleMethods.getSoundEffectAgentInfo(context.presentationStyle, beat);
|
|
@@ -130,19 +190,7 @@ export const imagePreprocessAgent = async (namedInputs) => {
|
|
|
130
190
|
// Shallow-merge like getMovieAgentInfo: beat-level fields override style-level fields
|
|
131
191
|
// individually, so a beat setting only lastFrameImageName keeps the global frameFillColor.
|
|
132
192
|
const movieParams = { ...context.presentationStyle.movieParams, ...beat.movieParams };
|
|
133
|
-
|
|
134
|
-
if (movieParams.firstFrameImageName && imageRefs) {
|
|
135
|
-
const firstFramePath = imageRefs[movieParams.firstFrameImageName];
|
|
136
|
-
if (firstFramePath) {
|
|
137
|
-
returnValue.firstFrameImagePath = await conformFrameImageToCanvas(context, movieParams.firstFrameImageName, firstFramePath, frameFillColor);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (movieParams.lastFrameImageName && imageRefs) {
|
|
141
|
-
const lastFramePath = imageRefs[movieParams.lastFrameImageName];
|
|
142
|
-
if (lastFramePath) {
|
|
143
|
-
returnValue.lastFrameImagePath = await conformFrameImageToCanvas(context, movieParams.lastFrameImageName, lastFramePath, frameFillColor);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
193
|
+
await applyFrameImageParams(returnValue, context, beat, index, movieParams, imageRefs);
|
|
146
194
|
if (movieParams.referenceImages && imageRefs) {
|
|
147
195
|
returnValue.movieReferenceImages = movieParams.referenceImages
|
|
148
196
|
.map((ref) => {
|
|
@@ -156,7 +204,7 @@ export const imagePreprocessAgent = async (namedInputs) => {
|
|
|
156
204
|
const pluginPath = plugin.path({ beat, context, imagePath, ...htmlStyle(context, beat) });
|
|
157
205
|
const markdown = plugin.markdown ? plugin.markdown({ beat, context, imagePath, ...htmlStyle(context, beat) }) : undefined;
|
|
158
206
|
const html = plugin.html ? await plugin.html({ beat, context, imagePath, ...htmlStyle(context, beat) }) : undefined;
|
|
159
|
-
const isTypeMovie = beat.image.type ===
|
|
207
|
+
const isTypeMovie = beat.image.type === ImageMediaType.Movie;
|
|
160
208
|
const isAnimatedHtml = MulmoBeatMethods.isAnimatedHtmlTailwind(beat);
|
|
161
209
|
// animation and moviePrompt cannot be used together
|
|
162
210
|
if (isAnimatedHtml && beat.moviePrompt) {
|
|
@@ -203,6 +251,26 @@ export const imagePreprocessAgent = async (namedInputs) => {
|
|
|
203
251
|
const movieFirstFramePath = returnValue.firstFrameImagePath ?? imagePath;
|
|
204
252
|
return { ...returnValue, imagePath, referenceImageForMovie: movieFirstFramePath, imageAgentInfo, prompt, referenceImages };
|
|
205
253
|
};
|
|
254
|
+
// Resolves $beatImage frame references after the beat's image exists. imagePreprocessAgent runs
|
|
255
|
+
// before image generation, so it can only flag the sentinel; this agent runs after
|
|
256
|
+
// imageGenerator/imagePlugin and conforms the now-existing beat image to the canvas.
|
|
257
|
+
// movieGenerator consumes only referenceImageForMovie (the movie's first frame / i2v input)
|
|
258
|
+
// and lastFrameImagePath.
|
|
259
|
+
export const beatFrameResolverAgent = async (namedInputs) => {
|
|
260
|
+
const { context, beat, index, preprocessor } = namedInputs;
|
|
261
|
+
if (preprocessor?.imagePath && (preprocessor.firstFrameIsBeatImage || preprocessor.lastFrameIsBeatImage)) {
|
|
262
|
+
const name = `${beatId(beat.id, index)}_beatImage`;
|
|
263
|
+
const conformed = await conformFrameImageToCanvas(context, name, preprocessor.imagePath, preprocessor.frameFillColor ?? "black");
|
|
264
|
+
return {
|
|
265
|
+
lastFrameImagePath: preprocessor.lastFrameIsBeatImage ? conformed : preprocessor.lastFrameImagePath,
|
|
266
|
+
referenceImageForMovie: preprocessor.firstFrameIsBeatImage ? conformed : preprocessor.referenceImageForMovie,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return {
|
|
270
|
+
lastFrameImagePath: preprocessor?.lastFrameImagePath,
|
|
271
|
+
referenceImageForMovie: preprocessor?.referenceImageForMovie,
|
|
272
|
+
};
|
|
273
|
+
};
|
|
206
274
|
export const imagePluginAgent = async (namedInputs) => {
|
|
207
275
|
const { context, beat, index, imageRefs, movieRefs } = namedInputs;
|
|
208
276
|
const { imagePath } = getBeatPngImagePath(context, index);
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { MulmoStudioContext, MulmoBeat, MulmoImagePromptMedia } from "../types/index.js";
|
|
2
|
+
import type { ResolvedImageReference } from "../methods/mulmo_image_prompt_media.js";
|
|
3
|
+
export type { ResolvedImageReference } from "../methods/mulmo_image_prompt_media.js";
|
|
2
4
|
export declare const generateReferenceImage: (inputs: {
|
|
3
5
|
context: MulmoStudioContext;
|
|
4
6
|
key: string;
|
|
5
7
|
index: number;
|
|
6
8
|
image: MulmoImagePromptMedia;
|
|
7
9
|
referenceImagePath?: string;
|
|
10
|
+
references?: ResolvedImageReference[];
|
|
8
11
|
force?: boolean;
|
|
9
12
|
}) => Promise<string>;
|
|
10
13
|
export type MediaRefs = {
|
|
@@ -1,10 +1,36 @@
|
|
|
1
|
-
import { GraphAI, GraphAILogger } from "graphai";
|
|
1
|
+
import { GraphAI, GraphAILogger, graphDataLatestVersion } from "graphai";
|
|
2
2
|
import { getReferenceImagePath } from "../utils/file.js";
|
|
3
3
|
import { imageGraphOption } from "./graph_option.js";
|
|
4
4
|
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
5
|
-
import { MulmoPresentationStyleMethods, MulmoMediaSourceMethods } from "../methods/index.js";
|
|
5
|
+
import { MulmoPresentationStyleMethods, MulmoMediaSourceMethods, MulmoImagePromptMediaMethods } from "../methods/index.js";
|
|
6
|
+
import { ImageMediaType, } from "../types/index.js";
|
|
6
7
|
import { imageOpenaiAgent, mediaMockAgent, imageGenAIAgent, imageReplicateAgent, movieGenAIAgent, movieReplicateAgent } from "../agents/index.js";
|
|
7
8
|
import { agentGenerationError, imageReferenceAction, imageFileTarget, movieFileTarget } from "../utils/error_cause.js";
|
|
9
|
+
// Resolve the normalized reference list to local file paths (with labels), in reference order.
|
|
10
|
+
// imageKey is the imageRefs key of the image being generated (used for warnings and url
|
|
11
|
+
// download file naming). A named reference that is missing from imageRefs warns and is
|
|
12
|
+
// skipped (existing behavior).
|
|
13
|
+
const resolveImageReferences = async (context, imageKey, image, imageRefs) => {
|
|
14
|
+
const imageReferences = MulmoImagePromptMediaMethods.getReferences(image);
|
|
15
|
+
const resolved = await Promise.all(imageReferences.map(async (ref, i) => {
|
|
16
|
+
if (ref.name !== undefined) {
|
|
17
|
+
const path = imageRefs?.[ref.name];
|
|
18
|
+
if (!path) {
|
|
19
|
+
GraphAILogger.warn(`imagePrompt "${imageKey}": reference "${ref.name}" not found in imageRefs — generating without this reference`);
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return { path, label: ref.label };
|
|
23
|
+
}
|
|
24
|
+
if (ref.source !== undefined) {
|
|
25
|
+
// The legacy referenceImage keeps the bare imageKey so url downloads reuse their historical cache file name.
|
|
26
|
+
const sourceKey = ref.legacy ? imageKey : `${imageKey}_ref${i}`;
|
|
27
|
+
return { path: await MulmoMediaSourceMethods.imageReference(ref.source, context, sourceKey), label: ref.label };
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}));
|
|
31
|
+
const references = resolved.filter((ref) => ref !== undefined);
|
|
32
|
+
return references.length > 0 ? references : undefined;
|
|
33
|
+
};
|
|
8
34
|
// public api
|
|
9
35
|
// Application may call this function directly to generate reference image.
|
|
10
36
|
export const generateReferenceImage = async (inputs) => {
|
|
@@ -12,11 +38,13 @@ export const generateReferenceImage = async (inputs) => {
|
|
|
12
38
|
const imagePath = getReferenceImagePath(context, key, "png");
|
|
13
39
|
// generate image
|
|
14
40
|
const imageAgentInfo = MulmoPresentationStyleMethods.getImageAgentInfo(context.presentationStyle);
|
|
15
|
-
const
|
|
41
|
+
const references = MulmoImagePromptMediaMethods.limitReferences(inputs.references ?? (referenceImagePath ? [{ path: referenceImagePath }] : undefined), imageAgentInfo, key);
|
|
42
|
+
const preamble = MulmoImagePromptMediaMethods.buildReferencePreamble((references ?? []).map((ref) => ref.label));
|
|
43
|
+
const prompt = `${preamble}${image.prompt}\n${imageAgentInfo.imageParams.style || ""}`;
|
|
16
44
|
GraphAILogger.info(`Generating reference image for ${key}: ${prompt}`);
|
|
17
|
-
const referenceImages =
|
|
45
|
+
const referenceImages = references ? references.map((ref) => ref.path) : undefined;
|
|
18
46
|
const image_graph_data = {
|
|
19
|
-
version:
|
|
47
|
+
version: graphDataLatestVersion,
|
|
20
48
|
nodes: {
|
|
21
49
|
imageGenerator: {
|
|
22
50
|
agent: imageAgentInfo.agent,
|
|
@@ -62,33 +90,30 @@ export const getMediaRefs = async (context) => {
|
|
|
62
90
|
}
|
|
63
91
|
const imageRefs = {};
|
|
64
92
|
const movieRefs = {};
|
|
65
|
-
// Stage 1: resolve non-referencing entries (image, imagePrompt without
|
|
93
|
+
// Stage 1: resolve non-referencing entries (image, imagePrompt without named references, movie)
|
|
66
94
|
await Promise.all(Object.keys(images)
|
|
67
95
|
.sort()
|
|
68
96
|
.map(async (key, index) => {
|
|
69
97
|
const image = images[key];
|
|
70
|
-
if (image.type ===
|
|
71
|
-
const
|
|
72
|
-
imageRefs[key] = await generateReferenceImage({ context, key, index, image,
|
|
98
|
+
if (image.type === ImageMediaType.ImagePrompt && !MulmoImagePromptMediaMethods.hasNamedReference(image)) {
|
|
99
|
+
const references = await resolveImageReferences(context, key, image);
|
|
100
|
+
imageRefs[key] = await generateReferenceImage({ context, key, index, image, references, force: false });
|
|
73
101
|
}
|
|
74
|
-
else if (image.type ===
|
|
102
|
+
else if (image.type === ImageMediaType.Image) {
|
|
75
103
|
imageRefs[key] = await MulmoMediaSourceMethods.imageReference(image.source, context, key);
|
|
76
104
|
}
|
|
77
|
-
else if (image.type ===
|
|
105
|
+
else if (image.type === ImageMediaType.Movie) {
|
|
78
106
|
movieRefs[key] = await resolveMovieReference(image, context, key);
|
|
79
107
|
}
|
|
80
108
|
}));
|
|
81
|
-
// Stage 2: resolve imagePrompt with
|
|
109
|
+
// Stage 2: resolve imagePrompt with named references (depends on Stage 1 results)
|
|
82
110
|
await Promise.all(Object.keys(images)
|
|
83
111
|
.sort()
|
|
84
112
|
.map(async (key, index) => {
|
|
85
113
|
const image = images[key];
|
|
86
|
-
if (image.type ===
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
GraphAILogger.warn(`imagePrompt "${key}": referenceImageName "${image.referenceImageName}" not found in imageRefs — generating without reference`);
|
|
90
|
-
}
|
|
91
|
-
imageRefs[key] = await generateReferenceImage({ context, key, index, image, referenceImagePath: refPath, force: false });
|
|
114
|
+
if (image.type === ImageMediaType.ImagePrompt && MulmoImagePromptMediaMethods.hasNamedReference(image)) {
|
|
115
|
+
const references = await resolveImageReferences(context, key, image, imageRefs);
|
|
116
|
+
imageRefs[key] = await generateReferenceImage({ context, key, index, image, references, force: false });
|
|
92
117
|
}
|
|
93
118
|
}));
|
|
94
119
|
return { imageRefs, movieRefs };
|
|
@@ -102,7 +127,7 @@ const generateReferenceMovie = async (inputs) => {
|
|
|
102
127
|
const movieAgentInfo = MulmoPresentationStyleMethods.getMovieAgentInfo(context.presentationStyle);
|
|
103
128
|
GraphAILogger.info(`Generating reference movie for ${key}: ${moviePrompt.prompt}`);
|
|
104
129
|
const movie_graph_data = {
|
|
105
|
-
version:
|
|
130
|
+
version: graphDataLatestVersion,
|
|
106
131
|
nodes: {
|
|
107
132
|
movieGenerator: {
|
|
108
133
|
agent: movieAgentInfo.agent,
|
|
@@ -145,45 +170,42 @@ const generateReferenceMovie = async (inputs) => {
|
|
|
145
170
|
const resolveLocalRefs = async (context, images, beatIndex, globalImageRefs) => {
|
|
146
171
|
const localImageRefs = {};
|
|
147
172
|
const localMovieRefs = {};
|
|
148
|
-
// Stage 1: image, imagePrompt (without
|
|
173
|
+
// Stage 1: image, imagePrompt (without named references), movie (parallel)
|
|
149
174
|
await Promise.all(Object.keys(images)
|
|
150
175
|
.sort()
|
|
151
176
|
.map(async (key, i) => {
|
|
152
177
|
const entry = images[key];
|
|
153
|
-
if (entry.type ===
|
|
154
|
-
const
|
|
178
|
+
if (entry.type === ImageMediaType.ImagePrompt && !MulmoImagePromptMediaMethods.hasNamedReference(entry)) {
|
|
179
|
+
const references = await resolveImageReferences(context, key, entry);
|
|
155
180
|
localImageRefs[key] = await generateReferenceImage({
|
|
156
181
|
context,
|
|
157
182
|
key,
|
|
158
183
|
index: beatIndex * 100 + i,
|
|
159
184
|
image: entry,
|
|
160
|
-
|
|
185
|
+
references,
|
|
161
186
|
});
|
|
162
187
|
}
|
|
163
|
-
else if (entry.type ===
|
|
188
|
+
else if (entry.type === ImageMediaType.Image) {
|
|
164
189
|
localImageRefs[key] = await MulmoMediaSourceMethods.imageReference(entry.source, context, key);
|
|
165
190
|
}
|
|
166
|
-
else if (entry.type ===
|
|
191
|
+
else if (entry.type === ImageMediaType.Movie) {
|
|
167
192
|
localMovieRefs[key] = await resolveMovieReference(entry, context, key);
|
|
168
193
|
}
|
|
169
194
|
}));
|
|
170
|
-
// Stage 2: imagePrompt with
|
|
195
|
+
// Stage 2: imagePrompt with named references (depends on Stage 1)
|
|
171
196
|
const combinedImageRefsForImagePrompt = { ...globalImageRefs, ...localImageRefs };
|
|
172
197
|
await Promise.all(Object.keys(images)
|
|
173
198
|
.sort()
|
|
174
199
|
.map(async (key, i) => {
|
|
175
200
|
const entry = images[key];
|
|
176
|
-
if (entry.type ===
|
|
177
|
-
const
|
|
178
|
-
if (!refPath) {
|
|
179
|
-
GraphAILogger.warn(`imagePrompt "${key}": referenceImageName "${entry.referenceImageName}" not found — generating without reference`);
|
|
180
|
-
}
|
|
201
|
+
if (entry.type === ImageMediaType.ImagePrompt && MulmoImagePromptMediaMethods.hasNamedReference(entry)) {
|
|
202
|
+
const references = await resolveImageReferences(context, key, entry, combinedImageRefsForImagePrompt);
|
|
181
203
|
localImageRefs[key] = await generateReferenceImage({
|
|
182
204
|
context,
|
|
183
205
|
key,
|
|
184
206
|
index: beatIndex * 100 + i,
|
|
185
207
|
image: entry,
|
|
186
|
-
|
|
208
|
+
references,
|
|
187
209
|
});
|
|
188
210
|
}
|
|
189
211
|
}));
|
|
@@ -193,7 +215,7 @@ const resolveLocalRefs = async (context, images, beatIndex, globalImageRefs) =>
|
|
|
193
215
|
.sort()
|
|
194
216
|
.map(async (key, i) => {
|
|
195
217
|
const entry = images[key];
|
|
196
|
-
if (entry.type ===
|
|
218
|
+
if (entry.type === ImageMediaType.MoviePrompt) {
|
|
197
219
|
const refImagePath = entry.imageName ? combinedImageRefs[entry.imageName] : undefined;
|
|
198
220
|
localMovieRefs[key] = await generateReferenceMovie({
|
|
199
221
|
context,
|
package/lib/actions/images.d.ts
CHANGED
|
@@ -72,6 +72,9 @@ export declare const beat_graph_data: {
|
|
|
72
72
|
};
|
|
73
73
|
firstFrameImagePath?: string;
|
|
74
74
|
lastFrameImagePath?: string;
|
|
75
|
+
firstFrameIsBeatImage?: boolean;
|
|
76
|
+
lastFrameIsBeatImage?: boolean;
|
|
77
|
+
frameFillColor?: string;
|
|
75
78
|
movieReferenceImages?: {
|
|
76
79
|
imagePath: string;
|
|
77
80
|
referenceType: "ASSET" | "STYLE";
|
|
@@ -103,6 +106,9 @@ export declare const beat_graph_data: {
|
|
|
103
106
|
};
|
|
104
107
|
firstFrameImagePath?: string;
|
|
105
108
|
lastFrameImagePath?: string;
|
|
109
|
+
firstFrameIsBeatImage?: boolean;
|
|
110
|
+
lastFrameIsBeatImage?: boolean;
|
|
111
|
+
frameFillColor?: string;
|
|
106
112
|
movieReferenceImages?: {
|
|
107
113
|
imagePath: string;
|
|
108
114
|
referenceType: "ASSET" | "STYLE";
|
|
@@ -145,6 +151,9 @@ export declare const beat_graph_data: {
|
|
|
145
151
|
};
|
|
146
152
|
firstFrameImagePath?: string;
|
|
147
153
|
lastFrameImagePath?: string;
|
|
154
|
+
firstFrameIsBeatImage?: boolean;
|
|
155
|
+
lastFrameIsBeatImage?: boolean;
|
|
156
|
+
frameFillColor?: string;
|
|
148
157
|
movieReferenceImages?: {
|
|
149
158
|
imagePath: string;
|
|
150
159
|
referenceType: "ASSET" | "STYLE";
|
|
@@ -179,6 +188,9 @@ export declare const beat_graph_data: {
|
|
|
179
188
|
};
|
|
180
189
|
firstFrameImagePath?: string;
|
|
181
190
|
lastFrameImagePath?: string;
|
|
191
|
+
firstFrameIsBeatImage?: boolean;
|
|
192
|
+
lastFrameIsBeatImage?: boolean;
|
|
193
|
+
frameFillColor?: string;
|
|
182
194
|
movieReferenceImages?: {
|
|
183
195
|
imagePath: string;
|
|
184
196
|
referenceType: "ASSET" | "STYLE";
|
|
@@ -298,6 +310,24 @@ export declare const beat_graph_data: {
|
|
|
298
310
|
};
|
|
299
311
|
defaultValue: {};
|
|
300
312
|
};
|
|
313
|
+
frameResolver: {
|
|
314
|
+
agent: (namedInputs: {
|
|
315
|
+
context: MulmoStudioContext;
|
|
316
|
+
beat: import("../types/type.js").MulmoBeat;
|
|
317
|
+
index: number;
|
|
318
|
+
preprocessor?: import("./image_agents.js").BeatFramePreprocessorInput;
|
|
319
|
+
}) => Promise<{
|
|
320
|
+
lastFrameImagePath: string | undefined;
|
|
321
|
+
referenceImageForMovie: string | undefined;
|
|
322
|
+
}>;
|
|
323
|
+
inputs: {
|
|
324
|
+
onComplete: string[];
|
|
325
|
+
context: string;
|
|
326
|
+
beat: string;
|
|
327
|
+
index: string;
|
|
328
|
+
preprocessor: string;
|
|
329
|
+
};
|
|
330
|
+
};
|
|
301
331
|
movieGenerator: {
|
|
302
332
|
if: string;
|
|
303
333
|
agent: string;
|
package/lib/actions/images.js
CHANGED
|
@@ -13,7 +13,7 @@ import { getOutputStudioFilePath, mkdir } from "../utils/file.js";
|
|
|
13
13
|
import { audioCheckerError } from "../utils/error_cause.js";
|
|
14
14
|
import { extractImageFromMovie, ffmpegGetMediaDuration, trimMusic } from "../utils/ffmpeg_utils.js";
|
|
15
15
|
import { getMediaRefs, resolveBeatLocalRefs } from "./image_references.js";
|
|
16
|
-
import { imagePreprocessAgent, imagePluginAgent, htmlImageGeneratorAgent } from "./image_agents.js";
|
|
16
|
+
import { imagePreprocessAgent, imagePluginAgent, htmlImageGeneratorAgent, beatFrameResolverAgent } from "./image_agents.js";
|
|
17
17
|
import { imageGraphOption } from "./graph_option.js";
|
|
18
18
|
import { createUsageCallback } from "../utils/usage_callback.js";
|
|
19
19
|
const vanillaAgents = vanilla.default ?? vanilla;
|
|
@@ -169,15 +169,27 @@ export const beat_graph_data = {
|
|
|
169
169
|
},
|
|
170
170
|
defaultValue: {},
|
|
171
171
|
},
|
|
172
|
+
// Resolves $beatImage frame references once the beat image exists (after imageGenerator/imagePlugin);
|
|
173
|
+
// passes the preprocessor's frame paths through unchanged for non-sentinel beats.
|
|
174
|
+
frameResolver: {
|
|
175
|
+
agent: beatFrameResolverAgent,
|
|
176
|
+
inputs: {
|
|
177
|
+
onComplete: [":imageGenerator", ":imagePlugin"],
|
|
178
|
+
context: ":context",
|
|
179
|
+
beat: ":beat",
|
|
180
|
+
index: ":__mapIndex",
|
|
181
|
+
preprocessor: ":preprocessor",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
172
184
|
movieGenerator: {
|
|
173
185
|
if: ":beat.moviePrompt",
|
|
174
186
|
agent: ":preprocessor.movieAgentInfo.agent",
|
|
175
187
|
inputs: {
|
|
176
188
|
media: "movie",
|
|
177
|
-
onComplete: [":
|
|
189
|
+
onComplete: [":frameResolver"], // frameResolver itself waits for imageGenerator/imagePlugin
|
|
178
190
|
prompt: ":beat.moviePrompt",
|
|
179
|
-
imagePath: ":
|
|
180
|
-
lastFrameImagePath: ":
|
|
191
|
+
imagePath: ":frameResolver.referenceImageForMovie",
|
|
192
|
+
lastFrameImagePath: ":frameResolver.lastFrameImagePath",
|
|
181
193
|
referenceImages: ":preprocessor.movieReferenceImages",
|
|
182
194
|
movieFile: ":preprocessor.movieFile", // for google genai agent
|
|
183
195
|
cache: {
|
package/lib/actions/movie.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MulmoStudioContext,
|
|
1
|
+
import { MulmoStudioContext, MulmoTransition, MulmoCanvasDimension, MulmoFillOption, MulmoVideoFilter } from "../types/index.js";
|
|
2
2
|
import { FfmpegContext } from "../utils/ffmpeg_utils.js";
|
|
3
3
|
type VideoId = string | undefined;
|
|
4
4
|
export declare const getVideoPart: (inputIndex: number, isMovie: boolean, duration: number, canvasInfo: MulmoCanvasDimension, fillOption: MulmoFillOption, speed: number, filters?: MulmoVideoFilter[], frameCount?: number) => {
|
|
@@ -11,15 +11,8 @@ export declare const getAudioPart: (inputIndex: number, duration: number, delay:
|
|
|
11
11
|
};
|
|
12
12
|
export declare const getOutOverlayCoords: (transitionType: string, d: number, t: number) => string;
|
|
13
13
|
export declare const getInOverlayCoords: (transitionType: string, d: number, t: number) => string;
|
|
14
|
-
export declare const getNeedFirstFrame: (context: MulmoStudioContext) => boolean[];
|
|
15
|
-
export declare const getNeedLastFrame: (context: MulmoStudioContext) => boolean[];
|
|
16
|
-
export declare const resolveMovieVolume: (beat: MulmoBeat, context: MulmoStudioContext) => number;
|
|
17
14
|
export declare const isExplicitMixMode: (context: MulmoStudioContext) => boolean;
|
|
18
15
|
export declare const mixAudiosFromMovieBeats: (ffmpegContext: FfmpegContext, artifactAudioId: string, audioIdsFromMovieBeats: string[], context: MulmoStudioContext) => string;
|
|
19
|
-
export declare const getExtraPadding: (context: MulmoStudioContext, index: number) => number;
|
|
20
|
-
export declare const getFillOption: (context: MulmoStudioContext, beat: MulmoBeat) => {
|
|
21
|
-
style: "aspectFit" | "aspectFill";
|
|
22
|
-
};
|
|
23
16
|
export declare const getTransitionVideoId: (transition: MulmoTransition, videoIdsForBeats: VideoId[], index: number) => {
|
|
24
17
|
videoId: string;
|
|
25
18
|
nextVideoId: undefined;
|
|
@@ -28,7 +21,7 @@ export declare const getTransitionVideoId: (transition: MulmoTransition, videoId
|
|
|
28
21
|
videoId: string;
|
|
29
22
|
nextVideoId: string;
|
|
30
23
|
beatIndex: number;
|
|
31
|
-
};
|
|
24
|
+
} | null;
|
|
32
25
|
export declare const getConcatVideoFilter: (concatVideoId: string, videoIdsForBeats: VideoId[]) => string;
|
|
33
26
|
export declare const getTransitionFrameDurations: (context: MulmoStudioContext, index: number) => {
|
|
34
27
|
firstDuration: number;
|