mulmocast 0.0.5 → 0.0.7
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 +294 -39
- package/assets/audio/silent60sec.mp3 +0 -0
- package/assets/html/caption.html +45 -0
- package/assets/html/chart.html +1 -1
- package/assets/html/mermaid.html +6 -2
- package/assets/html/tailwind.html +13 -0
- package/assets/templates/business.json +2 -128
- package/assets/templates/children_book.json +1 -128
- package/assets/templates/coding.json +2 -136
- package/assets/templates/comic_strips.json +6 -0
- package/assets/templates/ghibli_strips.json +6 -0
- package/assets/templates/sensei_and_taro.json +1 -118
- package/lib/actions/audio.js +62 -39
- package/lib/actions/captions.d.ts +2 -0
- package/lib/actions/captions.js +75 -0
- package/lib/actions/images.js +34 -13
- package/lib/actions/index.d.ts +1 -0
- package/lib/actions/index.js +1 -0
- package/lib/actions/movie.js +102 -101
- package/lib/actions/pdf.js +26 -6
- package/lib/actions/translate.js +60 -39
- package/lib/agents/add_bgm_agent.js +15 -39
- package/lib/agents/combine_audio_files_agent.js +53 -35
- package/lib/agents/index.d.ts +2 -3
- package/lib/agents/index.js +2 -3
- package/lib/agents/tts_google_agent.d.ts +4 -0
- package/lib/agents/tts_google_agent.js +51 -0
- package/lib/agents/validate_schema_agent.d.ts +19 -0
- package/lib/agents/validate_schema_agent.js +36 -0
- package/lib/cli/args.d.ts +2 -0
- package/lib/cli/args.js +9 -2
- package/lib/cli/bin.d.ts +3 -0
- package/lib/cli/bin.js +38 -0
- package/lib/cli/cli.js +34 -7
- package/lib/cli/commands/audio/builder.d.ts +14 -0
- package/lib/cli/commands/audio/builder.js +6 -0
- package/lib/cli/commands/audio/handler.d.ts +4 -0
- package/lib/cli/commands/audio/handler.js +7 -0
- package/lib/cli/commands/audio/index.d.ts +4 -0
- package/lib/cli/commands/audio/index.js +4 -0
- package/lib/cli/commands/image/builder.d.ts +14 -0
- package/lib/cli/commands/image/builder.js +6 -0
- package/lib/cli/commands/image/handler.d.ts +4 -0
- package/lib/cli/commands/image/handler.js +7 -0
- package/lib/cli/commands/image/index.d.ts +4 -0
- package/lib/cli/commands/image/index.js +4 -0
- package/lib/cli/commands/movie/builder.d.ts +18 -0
- package/lib/cli/commands/movie/builder.js +19 -0
- package/lib/cli/commands/movie/handler.d.ts +6 -0
- package/lib/cli/commands/movie/handler.js +12 -0
- package/lib/cli/commands/movie/index.d.ts +4 -0
- package/lib/cli/commands/movie/index.js +4 -0
- package/lib/cli/commands/pdf/builder.d.ts +18 -0
- package/lib/cli/commands/pdf/builder.js +19 -0
- package/lib/cli/commands/pdf/handler.d.ts +6 -0
- package/lib/cli/commands/pdf/handler.js +8 -0
- package/lib/cli/commands/pdf/index.d.ts +4 -0
- package/lib/cli/commands/pdf/index.js +4 -0
- package/lib/cli/commands/tool/index.d.ts +6 -0
- package/lib/cli/commands/tool/index.js +8 -0
- package/lib/cli/commands/tool/prompt/builder.d.ts +4 -0
- package/lib/cli/commands/tool/prompt/builder.js +11 -0
- package/lib/cli/commands/tool/prompt/handler.d.ts +4 -0
- package/lib/cli/commands/tool/prompt/handler.js +14 -0
- package/lib/cli/commands/tool/prompt/index.d.ts +4 -0
- package/lib/cli/commands/tool/prompt/index.js +4 -0
- package/lib/cli/commands/tool/schema/builder.d.ts +2 -0
- package/lib/cli/commands/tool/schema/builder.js +3 -0
- package/lib/cli/commands/tool/schema/handler.d.ts +2 -0
- package/lib/cli/commands/tool/schema/handler.js +12 -0
- package/lib/cli/commands/tool/schema/index.d.ts +4 -0
- package/lib/cli/commands/tool/schema/index.js +4 -0
- package/lib/cli/commands/tool/scripting/builder.d.ts +20 -0
- package/lib/cli/commands/tool/scripting/builder.js +63 -0
- package/lib/cli/commands/tool/scripting/handler.d.ts +13 -0
- package/lib/cli/commands/tool/scripting/handler.js +36 -0
- package/lib/cli/commands/tool/scripting/index.d.ts +4 -0
- package/lib/cli/commands/tool/scripting/index.js +4 -0
- package/lib/cli/commands/tool/story_to_script/builder.d.ts +20 -0
- package/lib/cli/commands/tool/story_to_script/builder.js +61 -0
- package/lib/cli/commands/tool/story_to_script/handler.d.ts +13 -0
- package/lib/cli/commands/tool/story_to_script/handler.js +36 -0
- package/lib/cli/commands/tool/story_to_script/index.d.ts +4 -0
- package/lib/cli/commands/tool/story_to_script/index.js +4 -0
- package/lib/cli/commands/translate/builder.d.ts +14 -0
- package/lib/cli/commands/translate/builder.js +5 -0
- package/lib/cli/commands/translate/handler.d.ts +4 -0
- package/lib/cli/commands/translate/handler.js +6 -0
- package/lib/cli/commands/translate/index.d.ts +4 -0
- package/lib/cli/commands/translate/index.js +4 -0
- package/lib/cli/common.d.ts +6 -2
- package/lib/cli/common.js +18 -7
- package/lib/cli/helpers.d.ts +38 -0
- package/lib/cli/helpers.js +115 -0
- package/lib/cli/tool-args.d.ts +1 -0
- package/lib/cli/tool-args.js +1 -1
- package/lib/cli/tool-cli.js +8 -0
- package/lib/methods/mulmo_script.d.ts +0 -1
- package/lib/methods/mulmo_script.js +4 -7
- package/lib/methods/mulmo_script_template.d.ts +2 -2
- package/lib/methods/mulmo_script_template.js +3 -13
- package/lib/methods/mulmo_studio.d.ts +8 -0
- package/lib/methods/mulmo_studio.js +24 -0
- package/lib/tools/create_mulmo_script_from_url.d.ts +1 -1
- package/lib/tools/create_mulmo_script_from_url.js +43 -14
- package/lib/tools/create_mulmo_script_interactively.d.ts +1 -1
- package/lib/tools/create_mulmo_script_interactively.js +21 -20
- package/lib/tools/dump_prompt.js +2 -0
- package/lib/tools/story_to_script.d.ts +12 -0
- package/lib/tools/story_to_script.js +275 -0
- package/lib/types/cli_types.d.ts +14 -0
- package/lib/types/cli_types.js +1 -0
- package/lib/types/schema.d.ts +637 -1766
- package/lib/types/schema.js +77 -8
- package/lib/types/type.d.ts +10 -3
- package/lib/utils/const.d.ts +5 -0
- package/lib/utils/const.js +5 -0
- package/lib/utils/ffmpeg_utils.d.ts +12 -0
- package/lib/utils/ffmpeg_utils.js +63 -0
- package/lib/utils/file.d.ts +8 -3
- package/lib/utils/file.js +40 -9
- package/lib/utils/filters.js +16 -11
- package/lib/utils/image_plugins/chart.js +6 -1
- package/lib/utils/image_plugins/html_tailwind.d.ts +3 -0
- package/lib/utils/image_plugins/html_tailwind.js +18 -0
- package/lib/utils/image_plugins/index.d.ts +2 -1
- package/lib/utils/image_plugins/index.js +2 -1
- package/lib/utils/image_plugins/mermaid.js +1 -1
- package/lib/utils/image_plugins/tailwind.d.ts +3 -0
- package/lib/utils/image_plugins/tailwind.js +18 -0
- package/lib/utils/image_plugins/text_slide.js +9 -2
- package/lib/utils/markdown.d.ts +1 -1
- package/lib/utils/markdown.js +8 -4
- package/lib/utils/preprocess.d.ts +40 -10
- package/lib/utils/preprocess.js +7 -2
- package/lib/utils/prompt.d.ts +16 -0
- package/lib/utils/prompt.js +74 -0
- package/lib/utils/utils.d.ts +10 -5
- package/lib/utils/utils.js +37 -17
- package/package.json +27 -23
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { GraphAI, GraphAILogger } from "graphai";
|
|
2
|
+
import * as agents from "@graphai/vanilla";
|
|
3
|
+
import { getHTMLFile } from "../utils/file.js";
|
|
4
|
+
import { renderHTMLToImage, interpolate } from "../utils/markdown.js";
|
|
5
|
+
import { MulmoStudioMethods } from "../methods/mulmo_studio.js";
|
|
6
|
+
const { default: __, ...vanillaAgents } = agents;
|
|
7
|
+
const graph_data = {
|
|
8
|
+
version: 0.5,
|
|
9
|
+
nodes: {
|
|
10
|
+
context: {},
|
|
11
|
+
map: {
|
|
12
|
+
agent: "mapAgent",
|
|
13
|
+
inputs: { rows: ":context.studio.script.beats", context: ":context" },
|
|
14
|
+
isResult: true,
|
|
15
|
+
params: {
|
|
16
|
+
rowKey: "beat",
|
|
17
|
+
compositeResult: true,
|
|
18
|
+
},
|
|
19
|
+
graph: {
|
|
20
|
+
nodes: {
|
|
21
|
+
generateCaption: {
|
|
22
|
+
agent: async (namedInputs) => {
|
|
23
|
+
const { beat, context, index } = namedInputs;
|
|
24
|
+
try {
|
|
25
|
+
MulmoStudioMethods.setBeatSessionState(context.studio, "caption", index, true);
|
|
26
|
+
const { fileDirs } = namedInputs.context;
|
|
27
|
+
const { caption } = context;
|
|
28
|
+
const { imageDirPath } = fileDirs;
|
|
29
|
+
const { canvasSize } = context.studio.script;
|
|
30
|
+
const imagePath = `${imageDirPath}/${context.studio.filename}/${index}_caption.png`;
|
|
31
|
+
const template = getHTMLFile("caption");
|
|
32
|
+
const text = (() => {
|
|
33
|
+
const multiLingual = context.studio.multiLingual;
|
|
34
|
+
if (caption && multiLingual) {
|
|
35
|
+
return multiLingual[index].multiLingualTexts[caption].text;
|
|
36
|
+
}
|
|
37
|
+
GraphAILogger.warn(`No multiLingual caption found for beat ${index}, lang: ${caption}`);
|
|
38
|
+
return beat.text;
|
|
39
|
+
})();
|
|
40
|
+
const htmlData = interpolate(template, {
|
|
41
|
+
caption: text,
|
|
42
|
+
width: `${canvasSize.width}`,
|
|
43
|
+
height: `${canvasSize.height}`,
|
|
44
|
+
});
|
|
45
|
+
await renderHTMLToImage(htmlData, imagePath, canvasSize.width, canvasSize.height, false, true);
|
|
46
|
+
context.studio.beats[index].captionFile = imagePath;
|
|
47
|
+
return imagePath;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
MulmoStudioMethods.setBeatSessionState(context.studio, "caption", index, false);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
inputs: {
|
|
54
|
+
beat: ":beat",
|
|
55
|
+
context: ":context",
|
|
56
|
+
index: ":__mapIndex",
|
|
57
|
+
},
|
|
58
|
+
isResult: true,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
export const captions = async (context) => {
|
|
66
|
+
try {
|
|
67
|
+
MulmoStudioMethods.setSessionState(context.studio, "caption", true);
|
|
68
|
+
const graph = new GraphAI(graph_data, { ...vanillaAgents });
|
|
69
|
+
graph.injectValue("context", context);
|
|
70
|
+
await graph.run();
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
MulmoStudioMethods.setSessionState(context.studio, "caption", false);
|
|
74
|
+
}
|
|
75
|
+
};
|
package/lib/actions/images.js
CHANGED
|
@@ -8,10 +8,12 @@ import imageGoogleAgent from "../agents/image_google_agent.js";
|
|
|
8
8
|
import imageOpenaiAgent from "../agents/image_openai_agent.js";
|
|
9
9
|
import { MulmoScriptMethods } from "../methods/index.js";
|
|
10
10
|
import { imagePlugins } from "../utils/image_plugins/index.js";
|
|
11
|
+
import { imagePrompt } from "../utils/prompt.js";
|
|
11
12
|
const { default: __, ...vanillaAgents } = agents;
|
|
12
13
|
dotenv.config();
|
|
13
14
|
// const openai = new OpenAI();
|
|
14
15
|
import { GoogleAuth } from "google-auth-library";
|
|
16
|
+
import { MulmoStudioMethods } from "../methods/mulmo_studio.js";
|
|
15
17
|
const htmlStyle = (script, beat) => {
|
|
16
18
|
return {
|
|
17
19
|
canvasSize: MulmoScriptMethods.getCanvasSize(script),
|
|
@@ -29,18 +31,24 @@ const imagePreprocessAgent = async (namedInputs) => {
|
|
|
29
31
|
if (beat.image) {
|
|
30
32
|
const plugin = imagePlugins.find((plugin) => plugin.imageType === beat?.image?.type);
|
|
31
33
|
if (plugin) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
try {
|
|
35
|
+
MulmoStudioMethods.setBeatSessionState(context.studio, "image", index, true);
|
|
36
|
+
const processorParams = { beat, context, imagePath, ...htmlStyle(context.studio.script, beat) };
|
|
37
|
+
const path = await plugin.process(processorParams);
|
|
38
|
+
// undefined prompt indicates that image generation is not needed
|
|
39
|
+
return { path, ...returnValue };
|
|
40
|
+
}
|
|
41
|
+
finally {
|
|
42
|
+
MulmoStudioMethods.setBeatSessionState(context.studio, "image", index, false);
|
|
43
|
+
}
|
|
36
44
|
}
|
|
37
45
|
}
|
|
38
|
-
const prompt = (beat
|
|
46
|
+
const prompt = imagePrompt(beat, imageParams.style);
|
|
39
47
|
return { path: imagePath, prompt, ...returnValue };
|
|
40
48
|
};
|
|
41
49
|
const graph_data = {
|
|
42
50
|
version: 0.5,
|
|
43
|
-
concurrency:
|
|
51
|
+
concurrency: 4,
|
|
44
52
|
nodes: {
|
|
45
53
|
context: {},
|
|
46
54
|
imageDirPath: {},
|
|
@@ -70,17 +78,21 @@ const graph_data = {
|
|
|
70
78
|
imageGenerator: {
|
|
71
79
|
if: ":preprocessor.prompt",
|
|
72
80
|
agent: ":imageAgentInfo.agent",
|
|
73
|
-
|
|
74
|
-
model: ":preprocessor.imageParams.model",
|
|
75
|
-
size: ":preprocessor.imageParams.size",
|
|
76
|
-
moderation: ":preprocessor.imageParams.moderation",
|
|
77
|
-
aspectRatio: ":preprocessor.aspectRatio",
|
|
78
|
-
},
|
|
81
|
+
retry: 3,
|
|
79
82
|
inputs: {
|
|
80
83
|
prompt: ":preprocessor.prompt",
|
|
81
84
|
file: ":preprocessor.path", // only for fileCacheAgentFilter
|
|
82
85
|
text: ":preprocessor.prompt", // only for fileCacheAgentFilter
|
|
83
86
|
force: ":context.force",
|
|
87
|
+
studio: ":context.studio", // for cache
|
|
88
|
+
index: ":__mapIndex", // for cache
|
|
89
|
+
sessionType: "image", // for cache
|
|
90
|
+
params: {
|
|
91
|
+
model: ":preprocessor.imageParams.model",
|
|
92
|
+
size: ":preprocessor.imageParams.size",
|
|
93
|
+
moderation: ":preprocessor.imageParams.moderation",
|
|
94
|
+
aspectRatio: ":preprocessor.aspectRatio",
|
|
95
|
+
},
|
|
84
96
|
},
|
|
85
97
|
defaultValue: {},
|
|
86
98
|
},
|
|
@@ -132,7 +144,7 @@ const googleAuth = async () => {
|
|
|
132
144
|
const accessToken = await client.getAccessToken();
|
|
133
145
|
return accessToken.token;
|
|
134
146
|
};
|
|
135
|
-
|
|
147
|
+
const generateImages = async (context) => {
|
|
136
148
|
const { studio, fileDirs } = context;
|
|
137
149
|
const { outDirPath, imageDirPath } = fileDirs;
|
|
138
150
|
mkdir(`${imageDirPath}/${studio.filename}`);
|
|
@@ -171,3 +183,12 @@ export const images = async (context) => {
|
|
|
171
183
|
});
|
|
172
184
|
await graph.run();
|
|
173
185
|
};
|
|
186
|
+
export const images = async (context) => {
|
|
187
|
+
try {
|
|
188
|
+
MulmoStudioMethods.setSessionState(context.studio, "image", true);
|
|
189
|
+
await generateImages(context);
|
|
190
|
+
}
|
|
191
|
+
finally {
|
|
192
|
+
MulmoStudioMethods.setSessionState(context.studio, "image", false);
|
|
193
|
+
}
|
|
194
|
+
};
|
package/lib/actions/index.d.ts
CHANGED
package/lib/actions/index.js
CHANGED
package/lib/actions/movie.js
CHANGED
|
@@ -1,26 +1,29 @@
|
|
|
1
|
-
import ffmpeg from "fluent-ffmpeg";
|
|
2
1
|
import { GraphAILogger } from "graphai";
|
|
3
2
|
import { MulmoScriptMethods } from "../methods/index.js";
|
|
4
3
|
import { getAudioArtifactFilePath, getOutputVideoFilePath, writingMessage } from "../utils/file.js";
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
import { FfmpegContextAddInput, FfmpegContextInit, FfmpegContextPushFormattedAudio, FfmpegContextGenerateOutput } from "../utils/ffmpeg_utils.js";
|
|
5
|
+
import { MulmoStudioMethods } from "../methods/mulmo_studio.js";
|
|
6
|
+
// const isMac = process.platform === "darwin";
|
|
7
|
+
const videoCodec = "libx264"; // "h264_videotoolbox" (macOS only) is too noisy
|
|
7
8
|
export const getVideoPart = (inputIndex, mediaType, duration, canvasInfo) => {
|
|
8
9
|
const videoId = `v${inputIndex}`;
|
|
10
|
+
const videoFilters = [];
|
|
11
|
+
// Handle different media types
|
|
12
|
+
if (mediaType === "image") {
|
|
13
|
+
videoFilters.push("loop=loop=-1:size=1:start=0");
|
|
14
|
+
}
|
|
15
|
+
else if (mediaType === "movie") {
|
|
16
|
+
// For videos, extend with last frame if shorter than required duration
|
|
17
|
+
// tpad will extend the video by cloning the last frame, then trim will ensure exact duration
|
|
18
|
+
videoFilters.push(`tpad=stop_mode=clone:stop_duration=${duration * 2}`); // Use 2x duration to ensure coverage
|
|
19
|
+
}
|
|
20
|
+
// Common filters for all media types
|
|
21
|
+
videoFilters.push(`trim=duration=${duration}`, "fps=30", "setpts=PTS-STARTPTS", `scale=w=${canvasInfo.width}:h=${canvasInfo.height}:force_original_aspect_ratio=decrease`,
|
|
22
|
+
// In case of the aspect ratio mismatch, we fill the extra space with black color.
|
|
23
|
+
`pad=${canvasInfo.width}:${canvasInfo.height}:(ow-iw)/2:(oh-ih)/2:color=black`, "setsar=1", "format=yuv420p");
|
|
9
24
|
return {
|
|
10
25
|
videoId,
|
|
11
|
-
videoPart: `[${inputIndex}:v]` +
|
|
12
|
-
[
|
|
13
|
-
mediaType === "image" ? "loop=loop=-1:size=1:start=0" : "",
|
|
14
|
-
`trim=duration=${duration}`,
|
|
15
|
-
"fps=30",
|
|
16
|
-
"setpts=PTS-STARTPTS",
|
|
17
|
-
`scale=${canvasInfo.width}:${canvasInfo.height}`,
|
|
18
|
-
"setsar=1",
|
|
19
|
-
"format=yuv420p",
|
|
20
|
-
]
|
|
21
|
-
.filter((a) => a)
|
|
22
|
-
.join(",") +
|
|
23
|
-
`[${videoId}]`,
|
|
26
|
+
videoPart: `[${inputIndex}:v]` + videoFilters.filter((a) => a).join(",") + `[${videoId}]`,
|
|
24
27
|
};
|
|
25
28
|
};
|
|
26
29
|
export const getAudioPart = (inputIndex, duration, delay) => {
|
|
@@ -29,112 +32,110 @@ export const getAudioPart = (inputIndex, duration, delay) => {
|
|
|
29
32
|
audioId,
|
|
30
33
|
audioPart: `[${inputIndex}:a]` +
|
|
31
34
|
`atrim=duration=${duration},` + // Trim to beat duration
|
|
32
|
-
`adelay=${delay}|${delay},` +
|
|
35
|
+
`adelay=${delay * 1000}|${delay * 1000},` +
|
|
33
36
|
`aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo` +
|
|
34
37
|
`[${audioId}]`,
|
|
35
38
|
};
|
|
36
39
|
};
|
|
37
40
|
const getOutputOption = (audioId) => {
|
|
38
41
|
return [
|
|
39
|
-
"-preset
|
|
42
|
+
"-preset medium", // Changed from veryfast to medium for better compression
|
|
40
43
|
"-map [v]", // Map the video stream
|
|
41
44
|
`-map ${audioId}`, // Map the audio stream
|
|
42
45
|
`-c:v ${videoCodec}`, // Set video codec
|
|
46
|
+
...(videoCodec === "libx264" ? ["-crf", "26"] : []), // Add CRF for libx264
|
|
43
47
|
"-threads 8",
|
|
44
48
|
"-filter_threads 8",
|
|
45
|
-
"-b:v
|
|
49
|
+
"-b:v 2M", // Reduced from 5M to 2M
|
|
46
50
|
"-bufsize",
|
|
47
|
-
"
|
|
51
|
+
"4M", // Reduced buffer size
|
|
48
52
|
"-maxrate",
|
|
49
|
-
"
|
|
53
|
+
"3M", // Reduced from 7M to 3M
|
|
50
54
|
"-r 30", // Set frame rate
|
|
51
55
|
"-pix_fmt yuv420p", // Set pixel format for better compatibility
|
|
56
|
+
"-c:a aac", // Audio codec
|
|
57
|
+
"-b:a 128k", // Audio bitrate
|
|
52
58
|
];
|
|
53
59
|
};
|
|
54
|
-
const createVideo = (audioArtifactFilePath, outputVideoPath, studio) => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, caption) => {
|
|
61
|
+
const start = performance.now();
|
|
62
|
+
const ffmpegContext = FfmpegContextInit();
|
|
63
|
+
if (studio.beats.some((beat) => !beat.imageFile)) {
|
|
64
|
+
GraphAILogger.info("beat.imageFile is not set. Please run `yarn run images ${file}` ");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const canvasInfo = MulmoScriptMethods.getCanvasSize(studio.script);
|
|
68
|
+
// Add each image input
|
|
69
|
+
const filterComplexVideoIds = [];
|
|
70
|
+
const filterComplexAudioIds = [];
|
|
71
|
+
studio.beats.reduce((timestamp, beat, index) => {
|
|
72
|
+
if (!beat.imageFile || !beat.duration) {
|
|
73
|
+
throw new Error(`beat.imageFile or beat.duration is not set: index=${index}`);
|
|
65
74
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
// Add each image input
|
|
73
|
-
const filterComplexParts = [];
|
|
74
|
-
const filterComplexVideoIds = [];
|
|
75
|
-
const filterComplexAudioIds = [];
|
|
76
|
-
studio.beats.reduce((timestamp, beat, index) => {
|
|
77
|
-
if (!beat.imageFile || !beat.duration) {
|
|
78
|
-
throw new Error(`beat.imageFile is not set: index=${index}`);
|
|
79
|
-
}
|
|
80
|
-
const inputIndex = addInput(beat.imageFile);
|
|
81
|
-
const mediaType = MulmoScriptMethods.getImageType(studio.script, studio.script.beats[index]);
|
|
82
|
-
const headOrTail = index === 0 || index === studio.beats.length - 1;
|
|
83
|
-
const duration = beat.duration + (headOrTail ? padding : 0);
|
|
84
|
-
const { videoId, videoPart } = getVideoPart(inputIndex, mediaType, duration, canvasInfo);
|
|
85
|
-
filterComplexVideoIds.push(videoId);
|
|
86
|
-
filterComplexParts.push(videoPart);
|
|
87
|
-
if (mediaType === "movie") {
|
|
88
|
-
const { audioId, audioPart } = getAudioPart(inputIndex, duration, timestamp * 1000);
|
|
89
|
-
filterComplexAudioIds.push(audioId);
|
|
90
|
-
filterComplexParts.push(audioPart);
|
|
75
|
+
const inputIndex = FfmpegContextAddInput(ffmpegContext, beat.imageFile);
|
|
76
|
+
const mediaType = MulmoScriptMethods.getImageType(studio.script, studio.script.beats[index]);
|
|
77
|
+
const extraPadding = (() => {
|
|
78
|
+
// We need to consider only intro and outro padding because the other paddings were already added to the beat.duration
|
|
79
|
+
if (index === 0) {
|
|
80
|
+
return studio.script.audioParams.introPadding;
|
|
91
81
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// console.log("*** images", images.audioIds);
|
|
95
|
-
// Concatenate the trimmed images
|
|
96
|
-
filterComplexParts.push(`${filterComplexVideoIds.map((id) => `[${id}]`).join("")}concat=n=${studio.beats.length}:v=1:a=0[v]`);
|
|
97
|
-
const audioIndex = addInput(audioArtifactFilePath); // Add audio input
|
|
98
|
-
const artifactAudioId = `${audioIndex}:a`;
|
|
99
|
-
const ffmpegContextAudioId = (() => {
|
|
100
|
-
if (filterComplexAudioIds.length > 0) {
|
|
101
|
-
const mainAudioId = "mainaudio";
|
|
102
|
-
const compositeAudioId = "composite";
|
|
103
|
-
const audioIds = filterComplexAudioIds.map((id) => `[${id}]`).join("");
|
|
104
|
-
filterComplexParts.push(`[${artifactAudioId}]aformat=sample_fmts=fltp:sample_rates=44100:channel_layouts=stereo[${mainAudioId}]`);
|
|
105
|
-
filterComplexParts.push(`[${mainAudioId}]${audioIds}amix=inputs=${filterComplexAudioIds.length + 1}:duration=first:dropout_transition=2[${compositeAudioId}]`);
|
|
106
|
-
return `[${compositeAudioId}]`; // notice that we need to use [mainaudio] instead of mainaudio
|
|
82
|
+
else if (index === studio.beats.length - 1) {
|
|
83
|
+
return studio.script.audioParams.outroPadding;
|
|
107
84
|
}
|
|
108
|
-
return
|
|
85
|
+
return 0;
|
|
109
86
|
})();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
});
|
|
87
|
+
const duration = beat.duration + extraPadding;
|
|
88
|
+
const { videoId, videoPart } = getVideoPart(inputIndex, mediaType, duration, canvasInfo);
|
|
89
|
+
ffmpegContext.filterComplex.push(videoPart);
|
|
90
|
+
if (caption && beat.captionFile) {
|
|
91
|
+
const captionInputIndex = FfmpegContextAddInput(ffmpegContext, beat.captionFile);
|
|
92
|
+
const compositeVideoId = `c${index}`;
|
|
93
|
+
ffmpegContext.filterComplex.push(`[${videoId}][${captionInputIndex}:v]overlay=format=auto[${compositeVideoId}]`);
|
|
94
|
+
filterComplexVideoIds.push(compositeVideoId);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
filterComplexVideoIds.push(videoId);
|
|
98
|
+
}
|
|
99
|
+
if (mediaType === "movie") {
|
|
100
|
+
const { audioId, audioPart } = getAudioPart(inputIndex, duration, timestamp);
|
|
101
|
+
filterComplexAudioIds.push(audioId);
|
|
102
|
+
ffmpegContext.filterComplex.push(audioPart);
|
|
103
|
+
}
|
|
104
|
+
return timestamp + duration;
|
|
105
|
+
}, 0);
|
|
106
|
+
// console.log("*** images", images.audioIds);
|
|
107
|
+
// Concatenate the trimmed images
|
|
108
|
+
ffmpegContext.filterComplex.push(`${filterComplexVideoIds.map((id) => `[${id}]`).join("")}concat=n=${studio.beats.length}:v=1:a=0[v]`);
|
|
109
|
+
const audioIndex = FfmpegContextAddInput(ffmpegContext, audioArtifactFilePath); // Add audio input
|
|
110
|
+
const artifactAudioId = `${audioIndex}:a`;
|
|
111
|
+
const ffmpegContextAudioId = (() => {
|
|
112
|
+
if (filterComplexAudioIds.length > 0) {
|
|
113
|
+
const mainAudioId = "mainaudio";
|
|
114
|
+
const compositeAudioId = "composite";
|
|
115
|
+
const audioIds = filterComplexAudioIds.map((id) => `[${id}]`).join("");
|
|
116
|
+
FfmpegContextPushFormattedAudio(ffmpegContext, `[${artifactAudioId}]`, `[${mainAudioId}]`);
|
|
117
|
+
ffmpegContext.filterComplex.push(`[${mainAudioId}]${audioIds}amix=inputs=${filterComplexAudioIds.length + 1}:duration=first:dropout_transition=2[${compositeAudioId}]`);
|
|
118
|
+
return `[${compositeAudioId}]`; // notice that we need to use [mainaudio] instead of mainaudio
|
|
119
|
+
}
|
|
120
|
+
return artifactAudioId;
|
|
121
|
+
})();
|
|
122
|
+
await FfmpegContextGenerateOutput(ffmpegContext, outputVideoPath, getOutputOption(ffmpegContextAudioId));
|
|
123
|
+
const end = performance.now();
|
|
124
|
+
GraphAILogger.info(`Video created successfully! ${Math.round(end - start) / 1000} sec`);
|
|
125
|
+
GraphAILogger.info(studio.script.title);
|
|
126
|
+
GraphAILogger.info((studio.script.references ?? []).map((reference) => `${reference.title} (${reference.url})`).join("\n"));
|
|
132
127
|
};
|
|
133
128
|
export const movie = async (context) => {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
129
|
+
MulmoStudioMethods.setSessionState(context.studio, "video", true);
|
|
130
|
+
try {
|
|
131
|
+
const { studio, fileDirs, caption } = context;
|
|
132
|
+
const { outDirPath } = fileDirs;
|
|
133
|
+
const audioArtifactFilePath = getAudioArtifactFilePath(outDirPath, studio.filename);
|
|
134
|
+
const outputVideoPath = getOutputVideoFilePath(outDirPath, studio.filename, context.lang, caption);
|
|
135
|
+
await createVideo(audioArtifactFilePath, outputVideoPath, studio, caption);
|
|
136
|
+
writingMessage(outputVideoPath);
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
MulmoStudioMethods.setSessionState(context.studio, "video", false);
|
|
140
|
+
}
|
|
140
141
|
};
|
package/lib/actions/pdf.js
CHANGED
|
@@ -2,10 +2,11 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { rgb, PDFDocument } from "pdf-lib";
|
|
4
4
|
import fontkit from "@pdf-lib/fontkit";
|
|
5
|
-
import { chunkArray, isHttp } from "../utils/utils.js";
|
|
5
|
+
import { chunkArray, isHttp, localizedText } from "../utils/utils.js";
|
|
6
6
|
import { getOutputPdfFilePath, writingMessage } from "../utils/file.js";
|
|
7
7
|
import { MulmoScriptMethods } from "../methods/index.js";
|
|
8
8
|
import { fontSize, textMargin, drawSize, wrapText } from "../utils/pdf.js";
|
|
9
|
+
import { MulmoStudioMethods } from "../methods/mulmo_studio.js";
|
|
9
10
|
const imagesPerPage = 4;
|
|
10
11
|
const offset = 10;
|
|
11
12
|
const handoutImageRatio = 0.5;
|
|
@@ -19,7 +20,14 @@ const readImage = async (imagePath, pdfDoc) => {
|
|
|
19
20
|
return fs.readFileSync(imagePath);
|
|
20
21
|
})();
|
|
21
22
|
const ext = path.extname(imagePath).toLowerCase();
|
|
22
|
-
|
|
23
|
+
if (ext === ".jpg" || ext === ".jpeg") {
|
|
24
|
+
return await pdfDoc.embedJpg(imageBytes);
|
|
25
|
+
}
|
|
26
|
+
if (ext === ".png") {
|
|
27
|
+
return await pdfDoc.embedPng(imageBytes);
|
|
28
|
+
}
|
|
29
|
+
// workaround. TODO: movie, image should convert to png/jpeg image
|
|
30
|
+
return await pdfDoc.embedPng(fs.readFileSync("assets/images/mulmocast_credit.png"));
|
|
23
31
|
};
|
|
24
32
|
const pdfSlide = async (pageWidth, pageHeight, imagePaths, pdfDoc) => {
|
|
25
33
|
const cellRatio = pageHeight / pageWidth;
|
|
@@ -182,16 +190,19 @@ const outputSize = (pdfSize, isLandscapeImage, isRotate) => {
|
|
|
182
190
|
}
|
|
183
191
|
return { width: 612, height: 792 };
|
|
184
192
|
};
|
|
185
|
-
|
|
186
|
-
const { studio, fileDirs } = context;
|
|
193
|
+
const generatePdf = async (context, pdfMode, pdfSize) => {
|
|
194
|
+
const { studio, fileDirs, lang } = context;
|
|
195
|
+
const { multiLingual } = studio;
|
|
187
196
|
const { outDirPath } = fileDirs;
|
|
188
197
|
const { width: imageWidth, height: imageHeight } = MulmoScriptMethods.getCanvasSize(studio.script);
|
|
189
198
|
const isLandscapeImage = imageWidth > imageHeight;
|
|
190
199
|
const isRotate = pdfMode === "handout";
|
|
191
200
|
const { width: pageWidth, height: pageHeight } = outputSize(pdfSize, isLandscapeImage, isRotate);
|
|
192
201
|
const imagePaths = studio.beats.map((beat) => beat.imageFile);
|
|
193
|
-
const texts = studio.script.beats.map((beat) =>
|
|
194
|
-
|
|
202
|
+
const texts = studio.script.beats.map((beat, index) => {
|
|
203
|
+
return localizedText(beat, multiLingual?.[index], lang);
|
|
204
|
+
});
|
|
205
|
+
const outputPdfPath = getOutputPdfFilePath(outDirPath, studio.filename, pdfMode, lang);
|
|
195
206
|
const pdfDoc = await PDFDocument.create();
|
|
196
207
|
pdfDoc.registerFontkit(fontkit);
|
|
197
208
|
const fontBytes = fs.readFileSync("assets/font/NotoSansJP-Regular.ttf");
|
|
@@ -209,3 +220,12 @@ export const pdf = async (context, pdfMode, pdfSize) => {
|
|
|
209
220
|
fs.writeFileSync(outputPdfPath, pdfBytes);
|
|
210
221
|
writingMessage(outputPdfPath);
|
|
211
222
|
};
|
|
223
|
+
export const pdf = async (context, pdfMode, pdfSize) => {
|
|
224
|
+
try {
|
|
225
|
+
MulmoStudioMethods.setSessionState(context.studio, "pdf", true);
|
|
226
|
+
await generatePdf(context, pdfMode, pdfSize);
|
|
227
|
+
}
|
|
228
|
+
finally {
|
|
229
|
+
MulmoStudioMethods.setSessionState(context.studio, "pdf", false);
|
|
230
|
+
}
|
|
231
|
+
};
|