mulmocast 0.0.14 → 0.0.16

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 (60) hide show
  1. package/README.md +5 -1
  2. package/assets/html/pdf_handout.html +85 -0
  3. package/assets/html/pdf_slide.html +55 -0
  4. package/assets/html/pdf_talk.html +76 -0
  5. package/assets/templates/text_and_image.json +6 -0
  6. package/assets/templates/text_only.json +6 -0
  7. package/lib/actions/audio.d.ts +3 -1
  8. package/lib/actions/audio.js +84 -45
  9. package/lib/actions/captions.js +1 -1
  10. package/lib/actions/images.d.ts +89 -1
  11. package/lib/actions/images.js +160 -99
  12. package/lib/actions/movie.js +28 -21
  13. package/lib/actions/pdf.d.ts +1 -0
  14. package/lib/actions/pdf.js +134 -204
  15. package/lib/actions/translate.js +1 -1
  16. package/lib/agents/add_bgm_agent.js +3 -3
  17. package/lib/agents/combine_audio_files_agent.js +11 -9
  18. package/lib/agents/image_mock_agent.d.ts +4 -0
  19. package/lib/agents/image_mock_agent.js +18 -0
  20. package/lib/agents/index.d.ts +4 -1
  21. package/lib/agents/index.js +4 -1
  22. package/lib/agents/media_mock_agent.d.ts +4 -0
  23. package/lib/agents/media_mock_agent.js +18 -0
  24. package/lib/agents/tavily_agent.d.ts +15 -0
  25. package/lib/agents/tavily_agent.js +130 -0
  26. package/lib/agents/tts_openai_agent.js +9 -1
  27. package/lib/cli/commands/audio/builder.d.ts +4 -0
  28. package/lib/cli/commands/image/builder.d.ts +4 -0
  29. package/lib/cli/commands/movie/builder.d.ts +4 -0
  30. package/lib/cli/commands/pdf/builder.d.ts +4 -0
  31. package/lib/cli/commands/translate/builder.d.ts +4 -0
  32. package/lib/cli/common.d.ts +4 -0
  33. package/lib/cli/common.js +11 -0
  34. package/lib/cli/helpers.d.ts +5 -1
  35. package/lib/cli/helpers.js +19 -2
  36. package/lib/methods/index.d.ts +1 -1
  37. package/lib/methods/index.js +1 -1
  38. package/lib/methods/mulmo_presentation_style.d.ts +14 -0
  39. package/lib/methods/mulmo_presentation_style.js +70 -0
  40. package/lib/methods/mulmo_script.d.ts +1 -1
  41. package/lib/methods/mulmo_script.js +2 -2
  42. package/lib/methods/mulmo_studio_context.d.ts +14 -0
  43. package/lib/methods/mulmo_studio_context.js +20 -2
  44. package/lib/tools/deep_research.d.ts +2 -0
  45. package/lib/tools/deep_research.js +265 -0
  46. package/lib/types/schema.d.ts +31 -0
  47. package/lib/types/schema.js +1 -1
  48. package/lib/types/type.d.ts +4 -1
  49. package/lib/utils/ffmpeg_utils.d.ts +1 -0
  50. package/lib/utils/ffmpeg_utils.js +10 -0
  51. package/lib/utils/file.d.ts +1 -3
  52. package/lib/utils/file.js +4 -11
  53. package/lib/utils/filters.js +1 -0
  54. package/lib/utils/markdown.js +1 -1
  55. package/lib/utils/preprocess.js +1 -0
  56. package/lib/utils/prompt.d.ts +3 -0
  57. package/lib/utils/prompt.js +52 -0
  58. package/package.json +10 -10
  59. package/assets/font/NotoSansJP-Regular.ttf +0 -0
  60. package/assets/music/StarsBeyondEx.mp3 +0 -0
@@ -1,27 +1,27 @@
1
1
  import dotenv from "dotenv";
2
2
  import fs from "fs";
3
3
  import { GraphAI, GraphAILogger } from "graphai";
4
+ import { TaskManager } from "graphai/lib/task_manager.js";
4
5
  import * as agents from "@graphai/vanilla";
5
6
  import { fileWriteAgent } from "@graphai/vanilla_node_agents";
6
7
  import { getOutputStudioFilePath, mkdir } from "../utils/file.js";
7
8
  import { fileCacheAgentFilter } from "../utils/filters.js";
8
- import imageGoogleAgent from "../agents/image_google_agent.js";
9
- import imageOpenaiAgent from "../agents/image_openai_agent.js";
10
- import movieGoogleAgent from "../agents/movie_google_agent.js";
11
- import { MulmoScriptMethods, MulmoStudioContextMethods } from "../methods/index.js";
9
+ import { imageGoogleAgent, imageOpenaiAgent, movieGoogleAgent, mediaMockAgent } from "../agents/index.js";
10
+ import { MulmoPresentationStyleMethods, MulmoStudioContextMethods } from "../methods/index.js";
12
11
  import { imagePlugins } from "../utils/image_plugins/index.js";
13
12
  import { imagePrompt } from "../utils/prompt.js";
14
13
  const vanillaAgents = agents.default ?? agents;
15
14
  dotenv.config();
16
15
  // const openai = new OpenAI();
17
16
  import { GoogleAuth } from "google-auth-library";
18
- const htmlStyle = (script, beat) => {
17
+ import { extractImageFromMovie } from "../utils/ffmpeg_utils.js";
18
+ const htmlStyle = (context, beat) => {
19
19
  return {
20
- canvasSize: MulmoScriptMethods.getCanvasSize(script),
21
- textSlideStyle: MulmoScriptMethods.getTextSlideStyle(script, beat),
20
+ canvasSize: MulmoPresentationStyleMethods.getCanvasSize(context.presentationStyle),
21
+ textSlideStyle: MulmoPresentationStyleMethods.getTextSlideStyle(context.presentationStyle, beat),
22
22
  };
23
23
  };
24
- const imagePreprocessAgent = async (namedInputs) => {
24
+ export const imagePreprocessAgent = async (namedInputs) => {
25
25
  const { context, beat, index, suffix, imageDirPath, imageAgentInfo, imageRefs } = namedInputs;
26
26
  const imageParams = { ...imageAgentInfo.imageParams, ...beat.imageParams };
27
27
  const imagePath = `${imageDirPath}/${context.studio.filename}/${index}${suffix}.png`;
@@ -34,10 +34,10 @@ const imagePreprocessAgent = async (namedInputs) => {
34
34
  if (plugin) {
35
35
  try {
36
36
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, true);
37
- const processorParams = { beat, context, imagePath, ...htmlStyle(context.studio.script, beat) };
37
+ const processorParams = { beat, context, imagePath, ...htmlStyle(context, beat) };
38
38
  const path = await plugin.process(processorParams);
39
39
  // undefined prompt indicates that image generation is not needed
40
- return { imagePath: path, ...returnValue };
40
+ return { imagePath: path, referenceImage: path, ...returnValue };
41
41
  }
42
42
  finally {
43
43
  MulmoStudioContextMethods.setBeatSessionState(context, "image", index, false);
@@ -51,10 +51,102 @@ const imagePreprocessAgent = async (namedInputs) => {
51
51
  return sources.filter((source) => source !== undefined);
52
52
  })();
53
53
  if (beat.moviePrompt && !beat.imagePrompt) {
54
- return { ...returnValue, images }; // no image prompt, only movie prompt
54
+ return { ...returnValue, imagePath, images, imageFromMovie: true }; // no image prompt, only movie prompt
55
55
  }
56
56
  const prompt = imagePrompt(beat, imageParams.style);
57
- return { imagePath, prompt, ...returnValue, images };
57
+ return { imagePath, referenceImage: imagePath, prompt, ...returnValue, images };
58
+ };
59
+ const beat_graph_data = {
60
+ version: 0.5,
61
+ concurrency: 4,
62
+ nodes: {
63
+ context: {},
64
+ imageDirPath: {},
65
+ imageAgentInfo: {},
66
+ movieAgentInfo: {},
67
+ imageRefs: {},
68
+ beat: {},
69
+ __mapIndex: {},
70
+ preprocessor: {
71
+ agent: imagePreprocessAgent,
72
+ inputs: {
73
+ context: ":context",
74
+ beat: ":beat",
75
+ index: ":__mapIndex",
76
+ suffix: "p",
77
+ imageDirPath: ":imageDirPath",
78
+ imageAgentInfo: ":imageAgentInfo",
79
+ imageRefs: ":imageRefs",
80
+ },
81
+ },
82
+ imageGenerator: {
83
+ if: ":preprocessor.prompt",
84
+ agent: ":imageAgentInfo.agent",
85
+ retry: 3,
86
+ inputs: {
87
+ prompt: ":preprocessor.prompt",
88
+ images: ":preprocessor.images",
89
+ file: ":preprocessor.imagePath", // only for fileCacheAgentFilter
90
+ text: ":preprocessor.prompt", // only for fileCacheAgentFilter
91
+ force: ":context.force", // only for fileCacheAgentFilter
92
+ mulmoContext: ":context", // for fileCacheAgentFilter
93
+ index: ":__mapIndex", // for fileCacheAgentFilter
94
+ sessionType: "image", // for fileCacheAgentFilter
95
+ params: {
96
+ model: ":preprocessor.imageParams.model",
97
+ moderation: ":preprocessor.imageParams.moderation",
98
+ canvasSize: ":context.presentationStyle.canvasSize",
99
+ },
100
+ },
101
+ defaultValue: {},
102
+ },
103
+ movieGenerator: {
104
+ if: ":preprocessor.movieFile",
105
+ agent: ":movieAgentInfo.agent",
106
+ inputs: {
107
+ onComplete: ":imageGenerator", // to wait for imageGenerator to finish
108
+ prompt: ":beat.moviePrompt",
109
+ imagePath: ":preprocessor.referenceImage",
110
+ file: ":preprocessor.movieFile",
111
+ studio: ":context.studio", // for cache
112
+ mulmoContext: ":context", // for fileCacheAgentFilter
113
+ index: ":__mapIndex", // for cache
114
+ sessionType: "movie", // for cache
115
+ params: {
116
+ model: ":context.presentationStyle.movieParams.model",
117
+ duration: ":beat.duration",
118
+ canvasSize: ":context.presentationStyle.canvasSize",
119
+ },
120
+ },
121
+ defaultValue: {},
122
+ },
123
+ imageFromMovie: {
124
+ if: ":preprocessor.imageFromMovie",
125
+ agent: async (namedInputs) => {
126
+ await extractImageFromMovie(namedInputs.movieFile, namedInputs.imageFile);
127
+ return { generatedImage: true };
128
+ },
129
+ inputs: {
130
+ onComplete: ":movieGenerator", // to wait for movieGenerator to finish
131
+ imageFile: ":preprocessor.imagePath",
132
+ movieFile: ":preprocessor.movieFile",
133
+ },
134
+ defaultValue: { generatedImage: false },
135
+ },
136
+ output: {
137
+ agent: "copyAgent",
138
+ inputs: {
139
+ onComplete: ":imageFromMovie", // to wait for imageFromMovie to finish
140
+ imageFile: ":preprocessor.imagePath",
141
+ movieFile: ":preprocessor.movieFile",
142
+ },
143
+ output: {
144
+ imageFile: ".imageFile",
145
+ movieFile: ".movieFile",
146
+ },
147
+ isResult: true,
148
+ },
149
+ },
58
150
  };
59
151
  const graph_data = {
60
152
  version: 0.5,
@@ -63,6 +155,7 @@ const graph_data = {
63
155
  context: {},
64
156
  imageDirPath: {},
65
157
  imageAgentInfo: {},
158
+ movieAgentInfo: {},
66
159
  outputStudioFilePath: {},
67
160
  imageRefs: {},
68
161
  map: {
@@ -71,6 +164,7 @@ const graph_data = {
71
164
  rows: ":context.studio.script.beats",
72
165
  context: ":context",
73
166
  imageAgentInfo: ":imageAgentInfo",
167
+ movieAgentInfo: ":movieAgentInfo",
74
168
  imageDirPath: ":imageDirPath",
75
169
  imageRefs: ":imageRefs",
76
170
  },
@@ -79,80 +173,10 @@ const graph_data = {
79
173
  rowKey: "beat",
80
174
  compositeResult: true,
81
175
  },
82
- graph: {
83
- nodes: {
84
- preprocessor: {
85
- agent: imagePreprocessAgent,
86
- inputs: {
87
- context: ":context",
88
- beat: ":beat",
89
- index: ":__mapIndex",
90
- suffix: "p",
91
- imageDirPath: ":imageDirPath",
92
- imageAgentInfo: ":imageAgentInfo",
93
- imageRefs: ":imageRefs",
94
- },
95
- },
96
- imageGenerator: {
97
- if: ":preprocessor.prompt",
98
- agent: ":imageAgentInfo.agent",
99
- retry: 3,
100
- inputs: {
101
- prompt: ":preprocessor.prompt",
102
- images: ":preprocessor.images",
103
- file: ":preprocessor.imagePath", // only for fileCacheAgentFilter
104
- text: ":preprocessor.prompt", // only for fileCacheAgentFilter
105
- force: ":context.force", // only for fileCacheAgentFilter
106
- mulmoContext: ":context", // for fileCacheAgentFilter
107
- index: ":__mapIndex", // for fileCacheAgentFilter
108
- sessionType: "image", // for fileCacheAgentFilter
109
- params: {
110
- model: ":preprocessor.imageParams.model",
111
- moderation: ":preprocessor.imageParams.moderation",
112
- canvasSize: ":context.studio.script.canvasSize",
113
- },
114
- },
115
- defaultValue: {},
116
- },
117
- movieGenerator: {
118
- if: ":preprocessor.movieFile",
119
- agent: "movieGoogleAgent",
120
- inputs: {
121
- onComplete: ":imageGenerator", // to wait for imageGenerator to finish
122
- prompt: ":beat.moviePrompt",
123
- imagePath: ":preprocessor.imagePath",
124
- file: ":preprocessor.movieFile",
125
- studio: ":context.studio", // for cache
126
- index: ":__mapIndex", // for cache
127
- sessionType: "movie", // for cache
128
- params: {
129
- model: ":context.studio.script.movieParams.model",
130
- duration: ":beat.duration",
131
- canvasSize: ":context.studio.script.canvasSize",
132
- },
133
- },
134
- defaultValue: {},
135
- },
136
- onComplete: {
137
- agent: "copyAgent",
138
- inputs: {
139
- onComplete: ":movieGenerator", // to wait for movieGenerator to finish
140
- imageFile: ":preprocessor.imagePath",
141
- movieFile: ":preprocessor.movieFile",
142
- },
143
- },
144
- output: {
145
- agent: "copyAgent",
146
- inputs: {
147
- imageFile: ":onComplete.imageFile",
148
- movieFile: ":onComplete.movieFile",
149
- },
150
- isResult: true,
151
- },
152
- },
153
- },
176
+ graph: beat_graph_data,
154
177
  },
155
178
  mergeResult: {
179
+ isResult: true,
156
180
  agent: (namedInputs) => {
157
181
  const { array, context } = namedInputs;
158
182
  const { studio } = context;
@@ -207,10 +231,7 @@ const googleAuth = async () => {
207
231
  throw error;
208
232
  }
209
233
  };
210
- const generateImages = async (context, callbacks) => {
211
- const { studio, fileDirs } = context;
212
- const { outDirPath, imageDirPath } = fileDirs;
213
- mkdir(`${imageDirPath}/${studio.filename}`);
234
+ const graphOption = async (context) => {
214
235
  const agentFilters = [
215
236
  {
216
237
  name: "fileCacheAgentFilter",
@@ -218,12 +239,14 @@ const generateImages = async (context, callbacks) => {
218
239
  nodeIds: ["imageGenerator", "movieGenerator"],
219
240
  },
220
241
  ];
242
+ const taskManager = new TaskManager(getConcurrency(context));
221
243
  const options = {
222
244
  agentFilters,
245
+ taskManager,
223
246
  };
224
- const imageAgentInfo = MulmoScriptMethods.getImageAgentInfo(studio.script);
247
+ const imageAgentInfo = MulmoPresentationStyleMethods.getImageAgentInfo(context.presentationStyle);
225
248
  // We need to get google's auth token only if the google is the text2image provider.
226
- if (imageAgentInfo.provider === "google" || studio.script.movieParams?.provider === "google") {
249
+ if (imageAgentInfo.provider === "google" || context.presentationStyle.movieParams?.provider === "google") {
227
250
  GraphAILogger.log("google was specified as text2image engine");
228
251
  const token = await googleAuth();
229
252
  options.config = {
@@ -237,14 +260,15 @@ const generateImages = async (context, callbacks) => {
237
260
  },
238
261
  };
239
262
  }
240
- if (imageAgentInfo.provider === "openai") {
241
- // NOTE: Here are the rate limits of OpenAI's text2image API (1token = 32x32 patch).
242
- // dall-e-3: 7,500 RPM、15 images per minute (4 images for max resolution)
243
- // gpt-image-1:3,000,000 TPM、150 images per minute
244
- graph_data.concurrency = imageAgentInfo.imageParams.model === "dall-e-3" ? 4 : 16;
245
- }
263
+ return options;
264
+ };
265
+ const prepareGenerateImages = async (context) => {
266
+ const { studio, fileDirs } = context;
267
+ const { outDirPath, imageDirPath } = fileDirs;
268
+ mkdir(`${imageDirPath}/${studio.filename}`);
269
+ const imageAgentInfo = MulmoPresentationStyleMethods.getImageAgentInfo(context.presentationStyle, context.dryRun);
246
270
  const imageRefs = {};
247
- const images = studio.script.imageParams?.images;
271
+ const images = context.presentationStyle.imageParams?.images;
248
272
  if (images) {
249
273
  await Promise.all(Object.keys(images).map(async (key) => {
250
274
  const image = images[key];
@@ -285,11 +309,29 @@ const generateImages = async (context, callbacks) => {
285
309
  const injections = {
286
310
  context,
287
311
  imageAgentInfo,
312
+ movieAgentInfo: {
313
+ agent: context.dryRun ? "mediaMockAgent" : "movieGoogleAgent",
314
+ },
288
315
  outputStudioFilePath: getOutputStudioFilePath(outDirPath, studio.filename),
289
316
  imageDirPath,
290
317
  imageRefs,
291
318
  };
292
- const graph = new GraphAI(graph_data, { ...vanillaAgents, imageGoogleAgent, movieGoogleAgent, imageOpenaiAgent, fileWriteAgent }, options);
319
+ return injections;
320
+ };
321
+ const getConcurrency = (context) => {
322
+ const imageAgentInfo = MulmoPresentationStyleMethods.getImageAgentInfo(context.presentationStyle);
323
+ if (imageAgentInfo.provider === "openai") {
324
+ // NOTE: Here are the rate limits of OpenAI's text2image API (1token = 32x32 patch).
325
+ // dall-e-3: 7,500 RPM、15 images per minute (4 images for max resolution)
326
+ // gpt-image-1:3,000,000 TPM、150 images per minute
327
+ return imageAgentInfo.imageParams.model === "dall-e-3" ? 4 : 16;
328
+ }
329
+ return 4;
330
+ };
331
+ const generateImages = async (context, callbacks) => {
332
+ const options = await graphOption(context);
333
+ const injections = await prepareGenerateImages(context);
334
+ const graph = new GraphAI(graph_data, { ...vanillaAgents, imageGoogleAgent, movieGoogleAgent, imageOpenaiAgent, mediaMockAgent, fileWriteAgent }, options);
293
335
  Object.keys(injections).forEach((key) => {
294
336
  graph.injectValue(key, injections[key]);
295
337
  });
@@ -298,7 +340,8 @@ const generateImages = async (context, callbacks) => {
298
340
  graph.registerCallback(callback);
299
341
  });
300
342
  }
301
- await graph.run();
343
+ const res = await graph.run();
344
+ return res.mergeResult;
302
345
  };
303
346
  export const images = async (context, callbacks) => {
304
347
  try {
@@ -309,3 +352,21 @@ export const images = async (context, callbacks) => {
309
352
  MulmoStudioContextMethods.setSessionState(context, "image", false);
310
353
  }
311
354
  };
355
+ export const generateBeatImage = async (index, context, callbacks) => {
356
+ const options = await graphOption(context);
357
+ const injections = await prepareGenerateImages(context);
358
+ const graph = new GraphAI(beat_graph_data, { ...vanillaAgents, imageGoogleAgent, movieGoogleAgent, imageOpenaiAgent, mediaMockAgent, fileWriteAgent }, options);
359
+ Object.keys(injections).forEach((key) => {
360
+ if ("outputStudioFilePath" !== key) {
361
+ graph.injectValue(key, injections[key]);
362
+ }
363
+ });
364
+ graph.injectValue("__mapIndex", index);
365
+ graph.injectValue("beat", context.studio.script.beats[index]);
366
+ if (callbacks) {
367
+ callbacks.forEach((callback) => {
368
+ graph.registerCallback(callback);
369
+ });
370
+ }
371
+ await graph.run();
372
+ };
@@ -1,6 +1,6 @@
1
1
  import { GraphAILogger, assert } from "graphai";
2
2
  import { mulmoTransitionSchema } from "../types/index.js";
3
- import { MulmoScriptMethods } from "../methods/index.js";
3
+ import { MulmoPresentationStyleMethods } from "../methods/index.js";
4
4
  import { getAudioArtifactFilePath, getOutputVideoFilePath, writingMessage } from "../utils/file.js";
5
5
  import { FfmpegContextAddInput, FfmpegContextInit, FfmpegContextPushFormattedAudio, FfmpegContextGenerateOutput } from "../utils/ffmpeg_utils.js";
6
6
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
@@ -59,22 +59,22 @@ const getOutputOption = (audioId, videoId) => {
59
59
  "-b:a 128k", // Audio bitrate
60
60
  ];
61
61
  };
62
- const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, caption) => {
62
+ const createVideo = async (audioArtifactFilePath, outputVideoPath, context, caption) => {
63
63
  const start = performance.now();
64
64
  const ffmpegContext = FfmpegContextInit();
65
- const missingIndex = studio.beats.findIndex((beat) => !beat.imageFile && !beat.movieFile);
65
+ const missingIndex = context.studio.beats.findIndex((beat) => !beat.imageFile && !beat.movieFile);
66
66
  if (missingIndex !== -1) {
67
67
  GraphAILogger.info(`ERROR: beat.imageFile or beat.movieFile is not set on beat ${missingIndex}.`);
68
68
  return false;
69
69
  }
70
- const canvasInfo = MulmoScriptMethods.getCanvasSize(studio.script);
70
+ const canvasInfo = MulmoPresentationStyleMethods.getCanvasSize(context.presentationStyle);
71
71
  // Add each image input
72
72
  const filterComplexVideoIds = [];
73
73
  const filterComplexAudioIds = [];
74
74
  const transitionVideoIds = [];
75
75
  const beatTimestamps = [];
76
- studio.beats.reduce((timestamp, studioBeat, index) => {
77
- const beat = studio.script.beats[index];
76
+ context.studio.beats.reduce((timestamp, studioBeat, index) => {
77
+ const beat = context.studio.script.beats[index];
78
78
  const sourceFile = studioBeat.movieFile ?? studioBeat.imageFile;
79
79
  if (!sourceFile) {
80
80
  throw new Error(`studioBeat.imageFile or studioBeat.movieFile is not set: index=${index}`);
@@ -83,14 +83,14 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, capti
83
83
  throw new Error(`studioBeat.duration is not set: index=${index}`);
84
84
  }
85
85
  const inputIndex = FfmpegContextAddInput(ffmpegContext, sourceFile);
86
- const mediaType = studioBeat.movieFile ? "movie" : MulmoScriptMethods.getImageType(studio.script, beat);
86
+ const mediaType = studioBeat.movieFile ? "movie" : MulmoPresentationStyleMethods.getImageType(context.presentationStyle, beat);
87
87
  const extraPadding = (() => {
88
88
  // We need to consider only intro and outro padding because the other paddings were already added to the beat.duration
89
89
  if (index === 0) {
90
- return studio.script.audioParams.introPadding;
90
+ return context.presentationStyle.audioParams.introPadding;
91
91
  }
92
- else if (index === studio.beats.length - 1) {
93
- return studio.script.audioParams.outroPadding;
92
+ else if (index === context.studio.beats.length - 1) {
93
+ return context.presentationStyle.audioParams.outroPadding;
94
94
  }
95
95
  return 0;
96
96
  })();
@@ -106,11 +106,18 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, capti
106
106
  else {
107
107
  filterComplexVideoIds.push(videoId);
108
108
  }
109
- if (studio.script.movieParams?.transition && index < studio.beats.length - 1) {
109
+ if (context.presentationStyle.movieParams?.transition && index < context.studio.beats.length - 1) {
110
110
  const sourceId = filterComplexVideoIds.pop();
111
111
  ffmpegContext.filterComplex.push(`[${sourceId}]split=2[${sourceId}_0][${sourceId}_1]`);
112
112
  filterComplexVideoIds.push(`${sourceId}_0`);
113
- transitionVideoIds.push(`${sourceId}_1`);
113
+ if (mediaType === "movie") {
114
+ // For movie beats, extract the last frame for transition
115
+ ffmpegContext.filterComplex.push(`[${sourceId}_1]reverse,select='eq(n,0)',reverse,tpad=stop_mode=clone:stop_duration=${duration},fps=30,setpts=PTS-STARTPTS[${sourceId}_2]`);
116
+ transitionVideoIds.push(`${sourceId}_2`);
117
+ }
118
+ else {
119
+ transitionVideoIds.push(`${sourceId}_1`);
120
+ }
114
121
  }
115
122
  if (beat.image?.type == "movie" && beat.image.mixAudio > 0.0) {
116
123
  const { audioId, audioPart } = getAudioPart(inputIndex, duration, timestamp, beat.image.mixAudio);
@@ -120,20 +127,19 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, capti
120
127
  beatTimestamps.push(timestamp);
121
128
  return timestamp + duration;
122
129
  }, 0);
123
- assert(filterComplexVideoIds.length === studio.beats.length, "videoIds.length !== studio.beats.length");
124
- assert(beatTimestamps.length === studio.beats.length, "beatTimestamps.length !== studio.beats.length");
130
+ assert(filterComplexVideoIds.length === context.studio.beats.length, "videoIds.length !== studio.beats.length");
131
+ assert(beatTimestamps.length === context.studio.beats.length, "beatTimestamps.length !== studio.beats.length");
125
132
  // console.log("*** images", images.audioIds);
126
133
  // Concatenate the trimmed images
127
134
  const concatVideoId = "concat_video";
128
- ffmpegContext.filterComplex.push(`${filterComplexVideoIds.map((id) => `[${id}]`).join("")}concat=n=${studio.beats.length}:v=1:a=0[${concatVideoId}]`);
135
+ ffmpegContext.filterComplex.push(`${filterComplexVideoIds.map((id) => `[${id}]`).join("")}concat=n=${context.studio.beats.length}:v=1:a=0[${concatVideoId}]`);
129
136
  // Add tranditions if needed
130
137
  const mixedVideoId = (() => {
131
- if (studio.script.movieParams?.transition && transitionVideoIds.length > 1) {
132
- const transition = mulmoTransitionSchema.parse(studio.script.movieParams.transition);
138
+ if (context.presentationStyle.movieParams?.transition && transitionVideoIds.length > 0) {
139
+ const transition = mulmoTransitionSchema.parse(context.presentationStyle.movieParams.transition);
133
140
  return transitionVideoIds.reduce((acc, transitionVideoId, index) => {
134
141
  const transitionStartTime = beatTimestamps[index + 1] - 0.05; // 0.05 is to avoid flickering
135
142
  const processedVideoId = `${transitionVideoId}_f`;
136
- // TODO: This mechanism does not work for video beats yet. It works only with image beats.
137
143
  // If we can to add other transition types than fade, we need to add them here.
138
144
  ffmpegContext.filterComplex.push(`[${transitionVideoId}]format=yuva420p,fade=t=out:d=${transition.duration}:alpha=1,setpts=PTS-STARTPTS+${transitionStartTime}/TB[${processedVideoId}]`);
139
145
  const outputId = `${transitionVideoId}_o`;
@@ -156,11 +162,12 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, capti
156
162
  }
157
163
  return artifactAudioId;
158
164
  })();
165
+ // GraphAILogger.debug("filterComplex", ffmpegContext.filterComplex);
159
166
  await FfmpegContextGenerateOutput(ffmpegContext, outputVideoPath, getOutputOption(ffmpegContextAudioId, mixedVideoId));
160
167
  const end = performance.now();
161
168
  GraphAILogger.info(`Video created successfully! ${Math.round(end - start) / 1000} sec`);
162
- GraphAILogger.info(studio.script.title);
163
- GraphAILogger.info((studio.script.references ?? []).map((reference) => `${reference.title} (${reference.url})`).join("\n"));
169
+ GraphAILogger.info(context.studio.script.title);
170
+ GraphAILogger.info((context.studio.script.references ?? []).map((reference) => `${reference.title} (${reference.url})`).join("\n"));
164
171
  return true;
165
172
  };
166
173
  export const movieFilePath = (context) => {
@@ -174,7 +181,7 @@ export const movie = async (context) => {
174
181
  const { outDirPath } = fileDirs;
175
182
  const audioArtifactFilePath = getAudioArtifactFilePath(outDirPath, studio.filename);
176
183
  const outputVideoPath = movieFilePath(context);
177
- if (await createVideo(audioArtifactFilePath, outputVideoPath, studio, caption)) {
184
+ if (await createVideo(audioArtifactFilePath, outputVideoPath, context, caption)) {
178
185
  writingMessage(outputVideoPath);
179
186
  }
180
187
  }
@@ -1,2 +1,3 @@
1
1
  import { MulmoStudioContext, PDFMode, PDFSize } from "../types/index.js";
2
+ export declare const pdfFilePath: (context: MulmoStudioContext, pdfMode: PDFMode) => string;
2
3
  export declare const pdf: (context: MulmoStudioContext, pdfMode: PDFMode, pdfSize: PDFSize) => Promise<void>;