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.
- package/README.md +5 -1
- package/assets/html/pdf_handout.html +85 -0
- package/assets/html/pdf_slide.html +55 -0
- package/assets/html/pdf_talk.html +76 -0
- package/assets/templates/text_and_image.json +6 -0
- package/assets/templates/text_only.json +6 -0
- package/lib/actions/audio.d.ts +3 -1
- package/lib/actions/audio.js +84 -45
- package/lib/actions/captions.js +1 -1
- package/lib/actions/images.d.ts +89 -1
- package/lib/actions/images.js +160 -99
- package/lib/actions/movie.js +28 -21
- package/lib/actions/pdf.d.ts +1 -0
- package/lib/actions/pdf.js +134 -204
- package/lib/actions/translate.js +1 -1
- package/lib/agents/add_bgm_agent.js +3 -3
- package/lib/agents/combine_audio_files_agent.js +11 -9
- package/lib/agents/image_mock_agent.d.ts +4 -0
- package/lib/agents/image_mock_agent.js +18 -0
- package/lib/agents/index.d.ts +4 -1
- package/lib/agents/index.js +4 -1
- package/lib/agents/media_mock_agent.d.ts +4 -0
- package/lib/agents/media_mock_agent.js +18 -0
- package/lib/agents/tavily_agent.d.ts +15 -0
- package/lib/agents/tavily_agent.js +130 -0
- package/lib/agents/tts_openai_agent.js +9 -1
- package/lib/cli/commands/audio/builder.d.ts +4 -0
- package/lib/cli/commands/image/builder.d.ts +4 -0
- package/lib/cli/commands/movie/builder.d.ts +4 -0
- package/lib/cli/commands/pdf/builder.d.ts +4 -0
- package/lib/cli/commands/translate/builder.d.ts +4 -0
- package/lib/cli/common.d.ts +4 -0
- package/lib/cli/common.js +11 -0
- package/lib/cli/helpers.d.ts +5 -1
- package/lib/cli/helpers.js +19 -2
- package/lib/methods/index.d.ts +1 -1
- package/lib/methods/index.js +1 -1
- package/lib/methods/mulmo_presentation_style.d.ts +14 -0
- package/lib/methods/mulmo_presentation_style.js +70 -0
- package/lib/methods/mulmo_script.d.ts +1 -1
- package/lib/methods/mulmo_script.js +2 -2
- package/lib/methods/mulmo_studio_context.d.ts +14 -0
- package/lib/methods/mulmo_studio_context.js +20 -2
- package/lib/tools/deep_research.d.ts +2 -0
- package/lib/tools/deep_research.js +265 -0
- package/lib/types/schema.d.ts +31 -0
- package/lib/types/schema.js +1 -1
- package/lib/types/type.d.ts +4 -1
- package/lib/utils/ffmpeg_utils.d.ts +1 -0
- package/lib/utils/ffmpeg_utils.js +10 -0
- package/lib/utils/file.d.ts +1 -3
- package/lib/utils/file.js +4 -11
- package/lib/utils/filters.js +1 -0
- package/lib/utils/markdown.js +1 -1
- package/lib/utils/preprocess.js +1 -0
- package/lib/utils/prompt.d.ts +3 -0
- package/lib/utils/prompt.js +52 -0
- package/package.json +10 -10
- package/assets/font/NotoSansJP-Regular.ttf +0 -0
- package/assets/music/StarsBeyondEx.mp3 +0 -0
package/lib/actions/images.js
CHANGED
|
@@ -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/
|
|
9
|
-
import
|
|
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
|
-
|
|
17
|
+
import { extractImageFromMovie } from "../utils/ffmpeg_utils.js";
|
|
18
|
+
const htmlStyle = (context, beat) => {
|
|
19
19
|
return {
|
|
20
|
-
canvasSize:
|
|
21
|
-
textSlideStyle:
|
|
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
|
|
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
|
|
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 =
|
|
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" ||
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
+
};
|
package/lib/actions/movie.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { GraphAILogger, assert } from "graphai";
|
|
2
2
|
import { mulmoTransitionSchema } from "../types/index.js";
|
|
3
|
-
import {
|
|
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,
|
|
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 =
|
|
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" :
|
|
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
|
|
90
|
+
return context.presentationStyle.audioParams.introPadding;
|
|
91
91
|
}
|
|
92
|
-
else if (index === studio.beats.length - 1) {
|
|
93
|
-
return
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
132
|
-
const transition = mulmoTransitionSchema.parse(
|
|
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,
|
|
184
|
+
if (await createVideo(audioArtifactFilePath, outputVideoPath, context, caption)) {
|
|
178
185
|
writingMessage(outputVideoPath);
|
|
179
186
|
}
|
|
180
187
|
}
|
package/lib/actions/pdf.d.ts
CHANGED
|
@@ -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>;
|