mulmocast 2.6.2 → 2.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/README.md +2 -1
  2. package/assets/html/chart.html +40 -7
  3. package/assets/html/js/animation_runtime.js +10 -0
  4. package/assets/html/js/auto_render.js +14 -0
  5. package/lib/actions/image_agents.d.ts +6 -0
  6. package/lib/actions/image_agents.js +31 -5
  7. package/lib/actions/image_references.d.ts +8 -1
  8. package/lib/actions/image_references.js +101 -2
  9. package/lib/actions/images.d.ts +42 -0
  10. package/lib/actions/images.js +17 -5
  11. package/lib/agents/movie_genai_agent.js +36 -6
  12. package/lib/agents/movie_replicate_agent.js +20 -3
  13. package/lib/methods/mulmo_presentation_style.d.ts +6 -0
  14. package/lib/slide/layouts/index.js +3 -0
  15. package/lib/slide/layouts/waterfall.d.ts +2 -0
  16. package/lib/slide/layouts/waterfall.js +63 -0
  17. package/lib/slide/render.js +4 -1
  18. package/lib/slide/schema.d.ts +176 -0
  19. package/lib/slide/schema.js +18 -0
  20. package/lib/slide/utils.d.ts +1 -0
  21. package/lib/slide/utils.js +21 -1
  22. package/lib/types/agent.d.ts +6 -0
  23. package/lib/types/provider2agent.d.ts +6 -11
  24. package/lib/types/provider2agent.js +10 -0
  25. package/lib/types/schema.d.ts +545 -0
  26. package/lib/types/schema.js +28 -1
  27. package/lib/types/slide.d.ts +176 -0
  28. package/lib/types/slide.js +18 -0
  29. package/lib/types/type.d.ts +2 -1
  30. package/lib/utils/context.d.ts +196 -0
  31. package/lib/utils/html_render.d.ts +6 -0
  32. package/lib/utils/html_render.js +33 -3
  33. package/lib/utils/image_plugins/chart.js +19 -6
  34. package/lib/utils/image_plugins/html_tailwind.js +48 -34
  35. package/lib/utils/image_plugins/mermaid.js +5 -1
  36. package/package.json +7 -7
  37. package/scripts/test/test_beat_local_refs.json +211 -0
  38. package/scripts/test/test_ir_visualizations.json +317 -0
  39. package/scripts/test/test_movie_references.json +101 -0
  40. package/scripts/test/test_plugin_features.json +151 -0
package/README.md CHANGED
@@ -396,8 +396,9 @@ MulmoCast includes a powerful **Slide DSL** (`type: "slide"`) for creating struc
396
396
 
397
397
  ### Features
398
398
 
399
- - **11 Layouts**: title, columns, comparison, grid, bigQuote, stats, timeline, split, matrix, table, funnel
399
+ - **12 Layouts**: title, columns, comparison, grid, bigQuote, stats, timeline, split, matrix, table, funnel, waterfall
400
400
  - **10 Content Block Types**: text, bullets, code, callout, metric, divider, image, imageRef, chart, mermaid
401
+ - **Chart.js Plugins**: sankey (`chartjs-chart-sankey`) and treemap (`chartjs-chart-treemap`) auto-loaded by chart type
401
402
  - **13-Color Theme System**: Semantic color palette with dark/light support
402
403
  - **6 Preset Themes**: dark, pop, warm, creative, minimal, corporate
403
404
 
@@ -5,14 +5,18 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Simple Chart.js Bar Chart</title>
7
7
  <style>
8
+ html, body { height: 100%; margin: 0; }
9
+ body { display: flex; flex-direction: column; align-items: center; justify-content: center; }
8
10
  ${style}
9
11
  .chart-container {
10
12
  width: ${chart_width}px;
11
- margin: 0 auto;
13
+ height: 70vh;
14
+ position: relative;
12
15
  }
13
16
  </style>
14
17
  <!-- Include Chart.js from CDN -->
15
- <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
18
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4"></script>
19
+ ${chart_plugins}
16
20
  </head>
17
21
  <body>
18
22
  <h1>${title}</h1>
@@ -20,9 +24,7 @@
20
24
  <canvas id="myChart" data-chart-ready="false"></canvas>
21
25
  </div>
22
26
 
23
- <!-- Plain JavaScript instead of TypeScript -->
24
27
  <script>
25
- // Wait for DOM and Chart.js to be ready, then render.
26
28
  function initChart() {
27
29
  if (!window.Chart) return false;
28
30
  const ctx = document.getElementById('myChart');
@@ -32,10 +34,41 @@
32
34
  // Disable animation for static image generation
33
35
  if (!chartData.options) chartData.options = {};
34
36
  chartData.options.animation = false;
37
+ chartData.options.responsive = true;
38
+ chartData.options.maintainAspectRatio = false;
35
39
 
36
- // Initialize the chart
40
+ // Treemap: convert backgroundColor array to scriptable function
41
+ if (chartData.type === 'treemap') {
42
+ chartData.data.datasets.forEach((ds) => {
43
+ if (Array.isArray(ds.backgroundColor)) {
44
+ const colors = ds.backgroundColor;
45
+ ds.backgroundColor = (c) => colors[c.dataIndex % colors.length];
46
+ }
47
+ });
48
+ }
49
+
50
+ // Sankey: convert colorFrom/colorTo objects to lookup functions
51
+ if (chartData.type === 'sankey') {
52
+ chartData.data.datasets.forEach((ds) => {
53
+ if (ds.colorFrom && typeof ds.colorFrom === 'object') {
54
+ const fromMap = ds.colorFrom;
55
+ ds.colorFrom = (c) => fromMap[c.dataset.data[c.dataIndex].from] || '#999';
56
+ }
57
+ if (ds.colorTo && typeof ds.colorTo === 'object') {
58
+ const toMap = ds.colorTo;
59
+ ds.colorTo = (c) => toMap[c.dataset.data[c.dataIndex].to] || '#999';
60
+ }
61
+ });
62
+ }
63
+
64
+ try {
65
+ const chart = new Chart(ctx, chartData);
66
+ chart.resize();
67
+ chart.update();
68
+ } catch (e) {
69
+ console.error('Chart init error:', e);
70
+ }
37
71
 
38
- new Chart(ctx, chartData);
39
72
  requestAnimationFrame(() => {
40
73
  requestAnimationFrame(() => {
41
74
  ctx.dataset.chartReady = "true";
@@ -49,7 +82,7 @@
49
82
  setTimeout(waitForChart, 50);
50
83
  }
51
84
 
52
- document.addEventListener('DOMContentLoaded', function() {
85
+ document.addEventListener('DOMContentLoaded', () => {
53
86
  waitForChart();
54
87
  });
55
88
  </script>
@@ -234,6 +234,16 @@ MulmoAnimation.prototype._applyCoverBaseStyle = function (el, iw, ih) {
234
234
  el.style.height = ih + "px";
235
235
  };
236
236
 
237
+ /**
238
+ * Render all animations at their final state (last frame).
239
+ * Used for generating static images (PDF, thumbnails) from animated content.
240
+ * @param {number} fps - frames per second
241
+ */
242
+ MulmoAnimation.prototype.renderFinal = function (fps) {
243
+ const lastFrame = Math.max(0, window.__MULMO.totalFrames - 1);
244
+ this.update(lastFrame, fps);
245
+ };
246
+
237
247
  /**
238
248
  * Update all registered animations for the given frame.
239
249
  * @param {number} frame - current frame number
@@ -25,6 +25,20 @@ if (typeof window.render === "function") {
25
25
  }
26
26
  }
27
27
 
28
+ /**
29
+ * Render the final frame of the animation (all content fully visible).
30
+ * Used by Puppeteer to capture a static image for PDF/thumbnail generation.
31
+ * Returns a Promise (or value) from the render function.
32
+ */
33
+ window.renderFinal = function () {
34
+ const mulmo = window.__MULMO;
35
+ const lastFrame = Math.max(0, (mulmo.totalFrames || 0) - 1);
36
+ mulmo.frame = lastFrame;
37
+ if (typeof window.render === "function") {
38
+ return window.render(lastFrame, mulmo.totalFrames, mulmo.fps);
39
+ }
40
+ };
41
+
28
42
  /**
29
43
  * Play animation in real-time using requestAnimationFrame.
30
44
  * Returns a Promise that resolves when all frames have been rendered.
@@ -22,6 +22,12 @@ type ImagePreprocessAgentReturnValue = {
22
22
  agent: string;
23
23
  movieParams: MulmoMovieParams;
24
24
  };
25
+ firstFrameImagePath?: string;
26
+ lastFrameImagePath?: string;
27
+ movieReferenceImages?: {
28
+ imagePath: string;
29
+ referenceType: "ASSET" | "STYLE";
30
+ }[];
25
31
  };
26
32
  type ImagePreprocessAgentResponseBase = ImagePreprocessAgentReturnValue & {
27
33
  imagePath?: string;
@@ -66,6 +66,28 @@ export const imagePreprocessAgent = async (namedInputs) => {
66
66
  }
67
67
  }
68
68
  returnValue.movieAgentInfo = MulmoPresentationStyleMethods.getMovieAgentInfo(context.presentationStyle, beat);
69
+ // Resolve movie reference images from imageRefs
70
+ const movieParams = beat.movieParams ?? context.presentationStyle.movieParams;
71
+ if (movieParams?.firstFrameImageName && imageRefs) {
72
+ const firstFramePath = imageRefs[movieParams.firstFrameImageName];
73
+ if (firstFramePath) {
74
+ returnValue.firstFrameImagePath = firstFramePath;
75
+ }
76
+ }
77
+ if (movieParams?.lastFrameImageName && imageRefs) {
78
+ const lastFramePath = imageRefs[movieParams.lastFrameImageName];
79
+ if (lastFramePath) {
80
+ returnValue.lastFrameImagePath = lastFramePath;
81
+ }
82
+ }
83
+ if (movieParams?.referenceImages && imageRefs) {
84
+ returnValue.movieReferenceImages = movieParams.referenceImages
85
+ .map((ref) => {
86
+ const refPath = imageRefs[ref.imageName];
87
+ return refPath ? { imagePath: refPath, referenceType: ref.referenceType } : undefined;
88
+ })
89
+ .filter((r) => r !== undefined);
90
+ }
69
91
  if (beat.image) {
70
92
  const plugin = MulmoBeatMethods.getPlugin(beat);
71
93
  const pluginPath = plugin.path({ beat, context, imagePath, ...htmlStyle(context, beat) });
@@ -80,12 +102,12 @@ export const imagePreprocessAgent = async (namedInputs) => {
80
102
  if (isAnimatedHtml) {
81
103
  const animatedVideoPath = getBeatAnimatedVideoPath(context, index);
82
104
  // ImagePluginPreprocessAgentResponse
105
+ // imageFromMovie is false: the plugin generates both the .mp4 video AND
106
+ // a high-quality final-frame PNG directly from HTML (better than extracting from compressed video).
83
107
  return {
84
108
  ...returnValue,
85
- imagePath, // for thumbnail extraction
109
+ imagePath, // static final-frame PNG (generated by the plugin)
86
110
  movieFile: animatedVideoPath, // .mp4 path for the pipeline
87
- imageFromMovie: true, // triggers extractImageFromMovie
88
- useLastFrame: true, // extract last frame for PDF/static (animation complete state)
89
111
  referenceImageForMovie: pluginPath,
90
112
  markdown,
91
113
  html,
@@ -106,13 +128,17 @@ export const imagePreprocessAgent = async (namedInputs) => {
106
128
  }
107
129
  if (beat.moviePrompt && !beat.imagePrompt) {
108
130
  // ImageOnlyMoviePreprocessAgentResponse
109
- return { ...returnValue, imagePath, imageFromMovie: true }; // no image prompt, only movie prompt
131
+ // If firstFrameImageName is specified, use the resolved ref image as the movie's first frame
132
+ const base = { ...returnValue, imagePath, imageFromMovie: true };
133
+ return returnValue.firstFrameImagePath ? { ...base, referenceImageForMovie: returnValue.firstFrameImagePath } : base;
110
134
  }
111
135
  // referenceImages for "edit_image", openai agent.
112
136
  const referenceImages = MulmoBeatMethods.getImageReferenceForImageGenerator(beat, imageRefs ?? {});
113
137
  const prompt = imagePrompt(beat, imageAgentInfo.imageParams.style);
114
138
  // ImageGenearalPreprocessAgentResponse
115
- return { ...returnValue, imagePath, referenceImageForMovie: imagePath, imageAgentInfo, prompt, referenceImages };
139
+ // firstFrameImagePath (from movieParams.firstFrameImageName) takes precedence over generated image
140
+ const movieFirstFramePath = returnValue.firstFrameImagePath ?? imagePath;
141
+ return { ...returnValue, imagePath, referenceImageForMovie: movieFirstFramePath, imageAgentInfo, prompt, referenceImages };
116
142
  };
117
143
  export const imagePluginAgent = async (namedInputs) => {
118
144
  const { context, beat, index, imageRefs, movieRefs } = namedInputs;
@@ -1,4 +1,4 @@
1
- import { MulmoStudioContext, MulmoImagePromptMedia } from "../types/index.js";
1
+ import { MulmoStudioContext, MulmoBeat, MulmoImagePromptMedia } from "../types/index.js";
2
2
  export declare const generateReferenceImage: (inputs: {
3
3
  context: MulmoStudioContext;
4
4
  key: string;
@@ -11,5 +11,12 @@ export type MediaRefs = {
11
11
  movieRefs: Record<string, string>;
12
12
  };
13
13
  export declare const getMediaRefs: (context: MulmoStudioContext) => Promise<MediaRefs>;
14
+ export declare const resolveBeatLocalRefs: (namedInputs: {
15
+ context: MulmoStudioContext;
16
+ beat: MulmoBeat;
17
+ index: number;
18
+ imageRefs: Record<string, string>;
19
+ movieRefs: Record<string, string>;
20
+ }) => Promise<MediaRefs>;
14
21
  /** @deprecated Use getMediaRefs instead */
15
22
  export declare const getImageRefs: (context: MulmoStudioContext) => Promise<Record<string, string>>;
@@ -2,8 +2,8 @@ import { GraphAI, GraphAILogger } from "graphai";
2
2
  import { getReferenceImagePath } from "../utils/file.js";
3
3
  import { graphOption } from "./images.js";
4
4
  import { MulmoPresentationStyleMethods, MulmoMediaSourceMethods } from "../methods/index.js";
5
- import { imageOpenaiAgent, mediaMockAgent, imageGenAIAgent, imageReplicateAgent } from "../agents/index.js";
6
- import { agentGenerationError, imageReferenceAction, imageFileTarget } from "../utils/error_cause.js";
5
+ import { imageOpenaiAgent, mediaMockAgent, imageGenAIAgent, imageReplicateAgent, movieGenAIAgent, movieReplicateAgent } from "../agents/index.js";
6
+ import { agentGenerationError, imageReferenceAction, imageFileTarget, movieFileTarget } from "../utils/error_cause.js";
7
7
  // public api
8
8
  // Application may call this function directly to generate reference image.
9
9
  export const generateReferenceImage = async (inputs) => {
@@ -77,6 +77,105 @@ export const getMediaRefs = async (context) => {
77
77
  const resolveMovieReference = async (movie, context, key) => {
78
78
  return MulmoMediaSourceMethods.imageReference(movie.source, context, key);
79
79
  };
80
+ const generateReferenceMovie = async (inputs) => {
81
+ const { context, key, index, moviePrompt, imagePath } = inputs;
82
+ const moviePath = getReferenceImagePath(context, key, "mp4");
83
+ const movieAgentInfo = MulmoPresentationStyleMethods.getMovieAgentInfo(context.presentationStyle);
84
+ GraphAILogger.info(`Generating reference movie for ${key}: ${moviePrompt.prompt}`);
85
+ const movie_graph_data = {
86
+ version: 0.5,
87
+ nodes: {
88
+ movieGenerator: {
89
+ agent: movieAgentInfo.agent,
90
+ inputs: {
91
+ media: "movie",
92
+ prompt: moviePrompt.prompt,
93
+ imagePath: imagePath ?? null,
94
+ movieFile: moviePath,
95
+ cache: {
96
+ force: [context.force],
97
+ file: moviePath,
98
+ index,
99
+ id: key,
100
+ mulmoContext: context,
101
+ sessionType: "imageReference",
102
+ },
103
+ },
104
+ params: {
105
+ model: movieAgentInfo.movieParams.model,
106
+ canvasSize: context.presentationStyle.canvasSize,
107
+ },
108
+ },
109
+ },
110
+ };
111
+ try {
112
+ const options = await graphOption(context);
113
+ const graph = new GraphAI(movie_graph_data, { movieGenAIAgent, movieReplicateAgent, mediaMockAgent }, options);
114
+ await graph.run();
115
+ return moviePath;
116
+ }
117
+ catch (error) {
118
+ GraphAILogger.error(error);
119
+ throw new Error(`generateReferenceMovie: generate error: key=${key}`, {
120
+ cause: agentGenerationError(movieAgentInfo.agent, imageReferenceAction, movieFileTarget),
121
+ });
122
+ }
123
+ };
124
+ const resolveLocalRefs = async (context, images, beatIndex, globalImageRefs) => {
125
+ const localImageRefs = {};
126
+ const localMovieRefs = {};
127
+ // Stage 1: image, imagePrompt, movie (parallel)
128
+ await Promise.all(Object.keys(images)
129
+ .sort()
130
+ .map(async (key, i) => {
131
+ const entry = images[key];
132
+ if (entry.type === "imagePrompt") {
133
+ localImageRefs[key] = await generateReferenceImage({
134
+ context,
135
+ key,
136
+ index: beatIndex * 100 + i,
137
+ image: entry,
138
+ });
139
+ }
140
+ else if (entry.type === "image") {
141
+ localImageRefs[key] = await MulmoMediaSourceMethods.imageReference(entry.source, context, key);
142
+ }
143
+ else if (entry.type === "movie") {
144
+ localMovieRefs[key] = await resolveMovieReference(entry, context, key);
145
+ }
146
+ }));
147
+ // Stage 2: moviePrompt (imageName references imageRefs only)
148
+ const combinedImageRefs = { ...globalImageRefs, ...localImageRefs };
149
+ await Promise.all(Object.keys(images)
150
+ .sort()
151
+ .map(async (key, i) => {
152
+ const entry = images[key];
153
+ if (entry.type === "moviePrompt") {
154
+ const mp = entry;
155
+ const refImagePath = mp.imageName ? combinedImageRefs[mp.imageName] : undefined;
156
+ localMovieRefs[key] = await generateReferenceMovie({
157
+ context,
158
+ key,
159
+ index: beatIndex * 100 + i,
160
+ moviePrompt: mp,
161
+ imagePath: refImagePath,
162
+ });
163
+ }
164
+ }));
165
+ return { localImageRefs, localMovieRefs };
166
+ };
167
+ export const resolveBeatLocalRefs = async (namedInputs) => {
168
+ const { context, beat, index, imageRefs, movieRefs } = namedInputs;
169
+ const images = beat.images;
170
+ if (!images) {
171
+ return { imageRefs, movieRefs };
172
+ }
173
+ const { localImageRefs, localMovieRefs } = await resolveLocalRefs(context, images, index, imageRefs);
174
+ return {
175
+ imageRefs: { ...imageRefs, ...localImageRefs },
176
+ movieRefs: { ...movieRefs, ...localMovieRefs },
177
+ };
178
+ };
80
179
  /** @deprecated Use getMediaRefs instead */
81
180
  export const getImageRefs = async (context) => {
82
181
  const { imageRefs } = await getMediaRefs(context);
@@ -25,6 +25,22 @@ export declare const beat_graph_data: {
25
25
  withBackup: {
26
26
  value: boolean;
27
27
  };
28
+ localRefs: {
29
+ agent: (namedInputs: {
30
+ context: MulmoStudioContext;
31
+ beat: import("../types/type.js").MulmoBeat;
32
+ index: number;
33
+ imageRefs: Record<string, string>;
34
+ movieRefs: Record<string, string>;
35
+ }) => Promise<import("./image_references.js").MediaRefs>;
36
+ inputs: {
37
+ context: string;
38
+ beat: string;
39
+ index: string;
40
+ imageRefs: string;
41
+ movieRefs: string;
42
+ };
43
+ };
28
44
  preprocessor: {
29
45
  agent: (namedInputs: {
30
46
  context: MulmoStudioContext;
@@ -54,6 +70,12 @@ export declare const beat_graph_data: {
54
70
  agent: string;
55
71
  movieParams: import("../types/type.js").MulmoMovieParams;
56
72
  };
73
+ firstFrameImagePath?: string;
74
+ lastFrameImagePath?: string;
75
+ movieReferenceImages?: {
76
+ imagePath: string;
77
+ referenceType: "ASSET" | "STYLE";
78
+ }[];
57
79
  } & {
58
80
  imagePath?: string;
59
81
  }) | ({
@@ -79,6 +101,12 @@ export declare const beat_graph_data: {
79
101
  agent: string;
80
102
  movieParams: import("../types/type.js").MulmoMovieParams;
81
103
  };
104
+ firstFrameImagePath?: string;
105
+ lastFrameImagePath?: string;
106
+ movieReferenceImages?: {
107
+ imagePath: string;
108
+ referenceType: "ASSET" | "STYLE";
109
+ }[];
82
110
  } & {
83
111
  imagePath?: string;
84
112
  } & {
@@ -115,6 +143,12 @@ export declare const beat_graph_data: {
115
143
  agent: string;
116
144
  movieParams: import("../types/type.js").MulmoMovieParams;
117
145
  };
146
+ firstFrameImagePath?: string;
147
+ lastFrameImagePath?: string;
148
+ movieReferenceImages?: {
149
+ imagePath: string;
150
+ referenceType: "ASSET" | "STYLE";
151
+ }[];
118
152
  } & {
119
153
  imagePath?: string;
120
154
  } & {
@@ -143,6 +177,12 @@ export declare const beat_graph_data: {
143
177
  agent: string;
144
178
  movieParams: import("../types/type.js").MulmoMovieParams;
145
179
  };
180
+ firstFrameImagePath?: string;
181
+ lastFrameImagePath?: string;
182
+ movieReferenceImages?: {
183
+ imagePath: string;
184
+ referenceType: "ASSET" | "STYLE";
185
+ }[];
146
186
  } & {
147
187
  imagePath?: string;
148
188
  } & {
@@ -266,6 +306,8 @@ export declare const beat_graph_data: {
266
306
  onComplete: string[];
267
307
  prompt: string;
268
308
  imagePath: string;
309
+ lastFrameImagePath: string;
310
+ referenceImages: string;
269
311
  movieFile: string;
270
312
  cache: {
271
313
  force: string[];
@@ -14,7 +14,7 @@ import { fileCacheAgentFilter } from "../utils/filters.js";
14
14
  import { settings2GraphAIConfig } from "../utils/utils.js";
15
15
  import { audioCheckerError } from "../utils/error_cause.js";
16
16
  import { extractImageFromMovie, ffmpegGetMediaDuration, trimMusic } from "../utils/ffmpeg_utils.js";
17
- import { getMediaRefs } from "./image_references.js";
17
+ import { getMediaRefs, resolveBeatLocalRefs } from "./image_references.js";
18
18
  import { imagePreprocessAgent, imagePluginAgent, htmlImageGeneratorAgent } from "./image_agents.js";
19
19
  const vanillaAgents = vanilla.default ?? vanilla;
20
20
  const imageAgents = {
@@ -60,8 +60,8 @@ export const beat_graph_data = {
60
60
  forceLipSync: { value: false },
61
61
  forceSoundEffect: { value: false },
62
62
  withBackup: { value: false },
63
- preprocessor: {
64
- agent: imagePreprocessAgent,
63
+ localRefs: {
64
+ agent: resolveBeatLocalRefs,
65
65
  inputs: {
66
66
  context: ":context",
67
67
  beat: ":beat",
@@ -70,6 +70,16 @@ export const beat_graph_data = {
70
70
  movieRefs: ":movieRefs",
71
71
  },
72
72
  },
73
+ preprocessor: {
74
+ agent: imagePreprocessAgent,
75
+ inputs: {
76
+ context: ":context",
77
+ beat: ":beat",
78
+ index: ":__mapIndex",
79
+ imageRefs: ":localRefs.imageRefs",
80
+ movieRefs: ":localRefs.movieRefs",
81
+ },
82
+ },
73
83
  imagePlugin: {
74
84
  if: ":beat.image",
75
85
  defaultValue: {},
@@ -78,8 +88,8 @@ export const beat_graph_data = {
78
88
  context: ":context",
79
89
  beat: ":beat",
80
90
  index: ":__mapIndex",
81
- imageRefs: ":imageRefs",
82
- movieRefs: ":movieRefs",
91
+ imageRefs: ":localRefs.imageRefs",
92
+ movieRefs: ":localRefs.movieRefs",
83
93
  onComplete: [":preprocessor"],
84
94
  },
85
95
  },
@@ -167,6 +177,8 @@ export const beat_graph_data = {
167
177
  onComplete: [":imageGenerator", ":imagePlugin"], // to wait for imageGenerator to finish
168
178
  prompt: ":beat.moviePrompt",
169
179
  imagePath: ":preprocessor.referenceImageForMovie",
180
+ lastFrameImagePath: ":preprocessor.lastFrameImagePath",
181
+ referenceImages: ":preprocessor.movieReferenceImages",
170
182
  movieFile: ":preprocessor.movieFile", // for google genai agent
171
183
  cache: {
172
184
  force: [":context.force", ":forceMovie"],
@@ -94,25 +94,55 @@ const generateExtendedVideo = async (ai, model, prompt, aspectRatio, imagePath,
94
94
  }
95
95
  return downloadVideo(ai, result.video, movieFile, isVertexAI);
96
96
  };
97
- const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath, duration, movieFile, isVertexAI) => {
98
- const isVeo3 = model === "veo-3.0-generate-001" || model === "veo-3.1-generate-preview";
97
+ const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI) => {
98
+ const capabilities = provider2MovieAgent.google.modelParams[model];
99
99
  const payload = {
100
100
  model,
101
101
  prompt,
102
102
  config: {
103
- durationSeconds: isVeo3 ? undefined : duration,
103
+ durationSeconds: capabilities?.supportsPersonGeneration === false ? undefined : duration,
104
104
  aspectRatio,
105
- personGeneration: imagePath ? undefined : PersonGeneration.ALLOW_ALL,
105
+ personGeneration: imagePath || !capabilities?.supportsPersonGeneration ? undefined : PersonGeneration.ALLOW_ALL,
106
106
  },
107
107
  image: imagePath ? loadImageAsBase64(imagePath) : undefined,
108
108
  };
109
+ // Validate and apply lastFrame
110
+ if (lastFrameImagePath) {
111
+ if (!capabilities?.supportsLastFrame) {
112
+ GraphAILogger.warn(`movieGenAIAgent: model ${model} does not support lastFrame — ignoring lastFrameImageName`);
113
+ }
114
+ else if (!imagePath) {
115
+ GraphAILogger.warn(`movieGenAIAgent: lastFrame requires a first frame image (imagePrompt or firstFrameImageName) — ignoring lastFrameImageName`);
116
+ }
117
+ else {
118
+ payload.config.lastFrame = loadImageAsBase64(lastFrameImagePath);
119
+ }
120
+ }
121
+ // Validate and apply referenceImages (mutually exclusive with image/lastFrame)
122
+ if (referenceImages && referenceImages.length > 0) {
123
+ if (!capabilities?.supportsReferenceImages) {
124
+ GraphAILogger.warn(`movieGenAIAgent: model ${model} does not support referenceImages — ignoring`);
125
+ }
126
+ else if (imagePath) {
127
+ GraphAILogger.warn(`movieGenAIAgent: referenceImages cannot be combined with first frame image — ignoring referenceImages`);
128
+ }
129
+ else if (lastFrameImagePath) {
130
+ GraphAILogger.warn(`movieGenAIAgent: referenceImages cannot be combined with lastFrame — ignoring referenceImages`);
131
+ }
132
+ else {
133
+ payload.config.referenceImages = referenceImages.map((ref) => ({
134
+ image: loadImageAsBase64(ref.imagePath),
135
+ referenceType: ref.referenceType,
136
+ }));
137
+ }
138
+ }
109
139
  const operation = await ai.models.generateVideos(payload);
110
140
  const response = await pollUntilDone(ai, operation);
111
141
  const video = getVideoFromResponse(response);
112
142
  return downloadVideo(ai, video, movieFile, isVertexAI);
113
143
  };
114
144
  export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
115
- const { prompt, imagePath, movieFile } = namedInputs;
145
+ const { prompt, imagePath, lastFrameImagePath, referenceImages, movieFile } = namedInputs;
116
146
  const aspectRatio = getAspectRatio(params.canvasSize, ASPECT_RATIOS);
117
147
  const model = params.model ?? provider2MovieAgent.google.defaultModel;
118
148
  const apiKey = config?.apiKey;
@@ -144,7 +174,7 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
144
174
  return generateExtendedVideo(ai, model, prompt, aspectRatio, imagePath, requestedDuration, movieFile, isVertexAI);
145
175
  }
146
176
  // Standard mode
147
- return generateStandardVideo(ai, model, prompt, aspectRatio, imagePath, duration, movieFile, isVertexAI);
177
+ return generateStandardVideo(ai, model, prompt, aspectRatio, imagePath, lastFrameImagePath, referenceImages, duration, movieFile, isVertexAI);
148
178
  }
149
179
  catch (error) {
150
180
  GraphAILogger.info("Failed to generate movie:", error.message);
@@ -3,7 +3,7 @@ import { GraphAILogger } from "graphai";
3
3
  import Replicate from "replicate";
4
4
  import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, imageAction, movieFileTarget, videoDurationTarget, unsupportedModelTarget, } from "../utils/error_cause.js";
5
5
  import { provider2MovieAgent, getModelDuration } from "../types/provider2agent.js";
6
- async function generateMovie(model, apiKey, prompt, imagePath, aspectRatio, duration) {
6
+ async function generateMovie(model, apiKey, prompt, imagePath, lastFrameImagePath, aspectRatio, duration) {
7
7
  const replicate = new Replicate({
8
8
  auth: apiKey,
9
9
  });
@@ -37,6 +37,23 @@ async function generateMovie(model, apiKey, prompt, imagePath, aspectRatio, dura
37
37
  input.image = base64Image;
38
38
  }
39
39
  }
40
+ // Add last frame image if provided and model supports it
41
+ if (lastFrameImagePath) {
42
+ const lastImageParam = provider2MovieAgent.replicate.modelParams[model]?.last_image;
43
+ if (lastImageParam) {
44
+ if (!imagePath) {
45
+ GraphAILogger.warn(`movieReplicateAgent: model ${model} requires a first frame image to use lastFrame — ignoring lastFrameImageName`);
46
+ }
47
+ else {
48
+ const buffer = readFileSync(lastFrameImagePath);
49
+ const base64Image = `data:image/png;base64,${buffer.toString("base64")}`;
50
+ input[lastImageParam] = base64Image;
51
+ }
52
+ }
53
+ else {
54
+ GraphAILogger.warn(`movieReplicateAgent: model ${model} does not support lastFrame — ignoring lastFrameImageName`);
55
+ }
56
+ }
40
57
  try {
41
58
  const output = await replicate.run(model, { input });
42
59
  // Download the generated video
@@ -72,7 +89,7 @@ export const getAspectRatio = (canvasSize) => {
72
89
  return "9:16";
73
90
  };
74
91
  export const movieReplicateAgent = async ({ namedInputs, params, config, }) => {
75
- const { prompt, imagePath } = namedInputs;
92
+ const { prompt, imagePath, lastFrameImagePath } = namedInputs;
76
93
  const aspectRatio = getAspectRatio(params.canvasSize);
77
94
  const model = params.model ?? provider2MovieAgent.replicate.defaultModel;
78
95
  if (!provider2MovieAgent.replicate.modelParams[model]) {
@@ -93,7 +110,7 @@ export const movieReplicateAgent = async ({ namedInputs, params, config, }) => {
93
110
  });
94
111
  }
95
112
  try {
96
- const buffer = await generateMovie(model, apiKey, prompt, imagePath, aspectRatio, duration);
113
+ const buffer = await generateMovie(model, apiKey, prompt, imagePath, lastFrameImagePath, aspectRatio, duration);
97
114
  if (buffer) {
98
115
  return { buffer };
99
116
  }
@@ -173,6 +173,12 @@ export declare const MulmoPresentationStyleMethods: {
173
173
  })[] | undefined;
174
174
  vertexai_project?: string | undefined;
175
175
  vertexai_location?: string | undefined;
176
+ firstFrameImageName?: string | undefined;
177
+ lastFrameImageName?: string | undefined;
178
+ referenceImages?: {
179
+ imageName: string;
180
+ referenceType: "ASSET" | "STYLE";
181
+ }[] | undefined;
176
182
  speed?: number | undefined;
177
183
  };
178
184
  keyName: string;
@@ -9,6 +9,7 @@ import { layoutSplit } from "./split.js";
9
9
  import { layoutMatrix } from "./matrix.js";
10
10
  import { layoutTable } from "./table.js";
11
11
  import { layoutFunnel } from "./funnel.js";
12
+ import { layoutWaterfall } from "./waterfall.js";
12
13
  import { escapeHtml } from "../utils.js";
13
14
  /** Render the inner content of a slide (without the wrapper div) */
14
15
  export const renderSlideContent = (slide) => {
@@ -35,6 +36,8 @@ export const renderSlideContent = (slide) => {
35
36
  return layoutTable(slide);
36
37
  case "funnel":
37
38
  return layoutFunnel(slide);
39
+ case "waterfall":
40
+ return layoutWaterfall(slide);
38
41
  default: {
39
42
  const _exhaustive = slide;
40
43
  return `<p class="text-white p-8">Unknown layout: ${escapeHtml(String(_exhaustive.layout))}</p>`;
@@ -0,0 +1,2 @@
1
+ import type { WaterfallSlide } from "../schema.js";
2
+ export declare const layoutWaterfall: (data: WaterfallSlide) => string;