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.
@@ -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 === "movie");
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
- const frameFillColor = movieParams.frameFillColor ?? "black";
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 === "movie";
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 prompt = `${image.prompt}\n${imageAgentInfo.imageParams.style || ""}`;
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 = referenceImagePath ? [referenceImagePath] : undefined;
45
+ const referenceImages = references ? references.map((ref) => ref.path) : undefined;
18
46
  const image_graph_data = {
19
- version: 0.5,
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 referenceImageName, movie)
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 === "imagePrompt" && !image.referenceImageName) {
71
- const refPath = image.referenceImage ? await MulmoMediaSourceMethods.imageReference(image.referenceImage, context, key) : undefined;
72
- imageRefs[key] = await generateReferenceImage({ context, key, index, image, referenceImagePath: refPath, force: false });
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 === "image") {
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 === "movie") {
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 referenceImageName (depends on Stage 1 results)
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 === "imagePrompt" && image.referenceImageName) {
87
- const refPath = imageRefs[image.referenceImageName];
88
- if (!refPath) {
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: 0.5,
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 referenceImageName), movie (parallel)
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 === "imagePrompt" && !entry.referenceImageName) {
154
- const refPath = entry.referenceImage ? await MulmoMediaSourceMethods.imageReference(entry.referenceImage, context, key) : undefined;
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
- referenceImagePath: refPath,
185
+ references,
161
186
  });
162
187
  }
163
- else if (entry.type === "image") {
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 === "movie") {
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 referenceImageName (depends on Stage 1)
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 === "imagePrompt" && entry.referenceImageName) {
177
- const refPath = combinedImageRefsForImagePrompt[entry.referenceImageName];
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
- referenceImagePath: refPath,
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 === "moviePrompt") {
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,
@@ -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;
@@ -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: [":imageGenerator", ":imagePlugin"], // to wait for imageGenerator to finish
189
+ onComplete: [":frameResolver"], // frameResolver itself waits for imageGenerator/imagePlugin
178
190
  prompt: ":beat.moviePrompt",
179
- imagePath: ":preprocessor.referenceImageForMovie",
180
- lastFrameImagePath: ":preprocessor.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: {
@@ -1,4 +1,4 @@
1
- import { MulmoStudioContext, MulmoBeat, MulmoTransition, MulmoCanvasDimension, MulmoFillOption, MulmoVideoFilter } from "../types/index.js";
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;