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.
Files changed (140) hide show
  1. package/README.md +294 -39
  2. package/assets/audio/silent60sec.mp3 +0 -0
  3. package/assets/html/caption.html +45 -0
  4. package/assets/html/chart.html +1 -1
  5. package/assets/html/mermaid.html +6 -2
  6. package/assets/html/tailwind.html +13 -0
  7. package/assets/templates/business.json +2 -128
  8. package/assets/templates/children_book.json +1 -128
  9. package/assets/templates/coding.json +2 -136
  10. package/assets/templates/comic_strips.json +6 -0
  11. package/assets/templates/ghibli_strips.json +6 -0
  12. package/assets/templates/sensei_and_taro.json +1 -118
  13. package/lib/actions/audio.js +62 -39
  14. package/lib/actions/captions.d.ts +2 -0
  15. package/lib/actions/captions.js +75 -0
  16. package/lib/actions/images.js +34 -13
  17. package/lib/actions/index.d.ts +1 -0
  18. package/lib/actions/index.js +1 -0
  19. package/lib/actions/movie.js +102 -101
  20. package/lib/actions/pdf.js +26 -6
  21. package/lib/actions/translate.js +60 -39
  22. package/lib/agents/add_bgm_agent.js +15 -39
  23. package/lib/agents/combine_audio_files_agent.js +53 -35
  24. package/lib/agents/index.d.ts +2 -3
  25. package/lib/agents/index.js +2 -3
  26. package/lib/agents/tts_google_agent.d.ts +4 -0
  27. package/lib/agents/tts_google_agent.js +51 -0
  28. package/lib/agents/validate_schema_agent.d.ts +19 -0
  29. package/lib/agents/validate_schema_agent.js +36 -0
  30. package/lib/cli/args.d.ts +2 -0
  31. package/lib/cli/args.js +9 -2
  32. package/lib/cli/bin.d.ts +3 -0
  33. package/lib/cli/bin.js +38 -0
  34. package/lib/cli/cli.js +34 -7
  35. package/lib/cli/commands/audio/builder.d.ts +14 -0
  36. package/lib/cli/commands/audio/builder.js +6 -0
  37. package/lib/cli/commands/audio/handler.d.ts +4 -0
  38. package/lib/cli/commands/audio/handler.js +7 -0
  39. package/lib/cli/commands/audio/index.d.ts +4 -0
  40. package/lib/cli/commands/audio/index.js +4 -0
  41. package/lib/cli/commands/image/builder.d.ts +14 -0
  42. package/lib/cli/commands/image/builder.js +6 -0
  43. package/lib/cli/commands/image/handler.d.ts +4 -0
  44. package/lib/cli/commands/image/handler.js +7 -0
  45. package/lib/cli/commands/image/index.d.ts +4 -0
  46. package/lib/cli/commands/image/index.js +4 -0
  47. package/lib/cli/commands/movie/builder.d.ts +18 -0
  48. package/lib/cli/commands/movie/builder.js +19 -0
  49. package/lib/cli/commands/movie/handler.d.ts +6 -0
  50. package/lib/cli/commands/movie/handler.js +12 -0
  51. package/lib/cli/commands/movie/index.d.ts +4 -0
  52. package/lib/cli/commands/movie/index.js +4 -0
  53. package/lib/cli/commands/pdf/builder.d.ts +18 -0
  54. package/lib/cli/commands/pdf/builder.js +19 -0
  55. package/lib/cli/commands/pdf/handler.d.ts +6 -0
  56. package/lib/cli/commands/pdf/handler.js +8 -0
  57. package/lib/cli/commands/pdf/index.d.ts +4 -0
  58. package/lib/cli/commands/pdf/index.js +4 -0
  59. package/lib/cli/commands/tool/index.d.ts +6 -0
  60. package/lib/cli/commands/tool/index.js +8 -0
  61. package/lib/cli/commands/tool/prompt/builder.d.ts +4 -0
  62. package/lib/cli/commands/tool/prompt/builder.js +11 -0
  63. package/lib/cli/commands/tool/prompt/handler.d.ts +4 -0
  64. package/lib/cli/commands/tool/prompt/handler.js +14 -0
  65. package/lib/cli/commands/tool/prompt/index.d.ts +4 -0
  66. package/lib/cli/commands/tool/prompt/index.js +4 -0
  67. package/lib/cli/commands/tool/schema/builder.d.ts +2 -0
  68. package/lib/cli/commands/tool/schema/builder.js +3 -0
  69. package/lib/cli/commands/tool/schema/handler.d.ts +2 -0
  70. package/lib/cli/commands/tool/schema/handler.js +12 -0
  71. package/lib/cli/commands/tool/schema/index.d.ts +4 -0
  72. package/lib/cli/commands/tool/schema/index.js +4 -0
  73. package/lib/cli/commands/tool/scripting/builder.d.ts +20 -0
  74. package/lib/cli/commands/tool/scripting/builder.js +63 -0
  75. package/lib/cli/commands/tool/scripting/handler.d.ts +13 -0
  76. package/lib/cli/commands/tool/scripting/handler.js +36 -0
  77. package/lib/cli/commands/tool/scripting/index.d.ts +4 -0
  78. package/lib/cli/commands/tool/scripting/index.js +4 -0
  79. package/lib/cli/commands/tool/story_to_script/builder.d.ts +20 -0
  80. package/lib/cli/commands/tool/story_to_script/builder.js +61 -0
  81. package/lib/cli/commands/tool/story_to_script/handler.d.ts +13 -0
  82. package/lib/cli/commands/tool/story_to_script/handler.js +36 -0
  83. package/lib/cli/commands/tool/story_to_script/index.d.ts +4 -0
  84. package/lib/cli/commands/tool/story_to_script/index.js +4 -0
  85. package/lib/cli/commands/translate/builder.d.ts +14 -0
  86. package/lib/cli/commands/translate/builder.js +5 -0
  87. package/lib/cli/commands/translate/handler.d.ts +4 -0
  88. package/lib/cli/commands/translate/handler.js +6 -0
  89. package/lib/cli/commands/translate/index.d.ts +4 -0
  90. package/lib/cli/commands/translate/index.js +4 -0
  91. package/lib/cli/common.d.ts +6 -2
  92. package/lib/cli/common.js +18 -7
  93. package/lib/cli/helpers.d.ts +38 -0
  94. package/lib/cli/helpers.js +115 -0
  95. package/lib/cli/tool-args.d.ts +1 -0
  96. package/lib/cli/tool-args.js +1 -1
  97. package/lib/cli/tool-cli.js +8 -0
  98. package/lib/methods/mulmo_script.d.ts +0 -1
  99. package/lib/methods/mulmo_script.js +4 -7
  100. package/lib/methods/mulmo_script_template.d.ts +2 -2
  101. package/lib/methods/mulmo_script_template.js +3 -13
  102. package/lib/methods/mulmo_studio.d.ts +8 -0
  103. package/lib/methods/mulmo_studio.js +24 -0
  104. package/lib/tools/create_mulmo_script_from_url.d.ts +1 -1
  105. package/lib/tools/create_mulmo_script_from_url.js +43 -14
  106. package/lib/tools/create_mulmo_script_interactively.d.ts +1 -1
  107. package/lib/tools/create_mulmo_script_interactively.js +21 -20
  108. package/lib/tools/dump_prompt.js +2 -0
  109. package/lib/tools/story_to_script.d.ts +12 -0
  110. package/lib/tools/story_to_script.js +275 -0
  111. package/lib/types/cli_types.d.ts +14 -0
  112. package/lib/types/cli_types.js +1 -0
  113. package/lib/types/schema.d.ts +637 -1766
  114. package/lib/types/schema.js +77 -8
  115. package/lib/types/type.d.ts +10 -3
  116. package/lib/utils/const.d.ts +5 -0
  117. package/lib/utils/const.js +5 -0
  118. package/lib/utils/ffmpeg_utils.d.ts +12 -0
  119. package/lib/utils/ffmpeg_utils.js +63 -0
  120. package/lib/utils/file.d.ts +8 -3
  121. package/lib/utils/file.js +40 -9
  122. package/lib/utils/filters.js +16 -11
  123. package/lib/utils/image_plugins/chart.js +6 -1
  124. package/lib/utils/image_plugins/html_tailwind.d.ts +3 -0
  125. package/lib/utils/image_plugins/html_tailwind.js +18 -0
  126. package/lib/utils/image_plugins/index.d.ts +2 -1
  127. package/lib/utils/image_plugins/index.js +2 -1
  128. package/lib/utils/image_plugins/mermaid.js +1 -1
  129. package/lib/utils/image_plugins/tailwind.d.ts +3 -0
  130. package/lib/utils/image_plugins/tailwind.js +18 -0
  131. package/lib/utils/image_plugins/text_slide.js +9 -2
  132. package/lib/utils/markdown.d.ts +1 -1
  133. package/lib/utils/markdown.js +8 -4
  134. package/lib/utils/preprocess.d.ts +40 -10
  135. package/lib/utils/preprocess.js +7 -2
  136. package/lib/utils/prompt.d.ts +16 -0
  137. package/lib/utils/prompt.js +74 -0
  138. package/lib/utils/utils.d.ts +10 -5
  139. package/lib/utils/utils.js +37 -17
  140. 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
+ };
@@ -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
- const processorParams = { beat, context, imagePath, ...htmlStyle(context.studio.script, beat) };
33
- const path = await plugin.process(processorParams);
34
- // undefined prompt indicates that image generation is not needed
35
- return { path, ...returnValue };
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.imagePrompt || beat.text) + "\n" + (imageParams.style || "");
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: 2,
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
- params: {
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
- export const images = async (context) => {
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
+ };
@@ -3,3 +3,4 @@ export * from "./images.js";
3
3
  export * from "./movie.js";
4
4
  export * from "./pdf.js";
5
5
  export * from "./translate.js";
6
+ export * from "./captions.js";
@@ -3,3 +3,4 @@ export * from "./images.js";
3
3
  export * from "./movie.js";
4
4
  export * from "./pdf.js";
5
5
  export * from "./translate.js";
6
+ export * from "./captions.js";
@@ -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
- const isMac = process.platform === "darwin";
6
- const videoCodec = isMac ? "h264_videotoolbox" : "libx264";
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 veryfast", // Faster encoding
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 5M", // bitrate (only for videotoolbox)
49
+ "-b:v 2M", // Reduced from 5M to 2M
46
50
  "-bufsize",
47
- "10M", // Add buffer size for better quality
51
+ "4M", // Reduced buffer size
48
52
  "-maxrate",
49
- "7M", // Maximum bitrate
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
- return new Promise((resolve, reject) => {
56
- const start = performance.now();
57
- const ffmpegContext = {
58
- command: ffmpeg(),
59
- inputCount: 0,
60
- };
61
- function addInput(input) {
62
- ffmpegContext.command = ffmpegContext.command.input(input);
63
- ffmpegContext.inputCount++;
64
- return ffmpegContext.inputCount - 1; // returned the index of the input
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
- if (studio.beats.some((beat) => !beat.imageFile)) {
67
- GraphAILogger.info("beat.imageFile is not set. Please run `yarn run images ${file}` ");
68
- return;
69
- }
70
- const canvasInfo = MulmoScriptMethods.getCanvasSize(studio.script);
71
- const padding = MulmoScriptMethods.getPadding(studio.script) / 1000;
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
- return timestamp + duration;
93
- }, 0);
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 artifactAudioId;
85
+ return 0;
109
86
  })();
110
- // Apply the filter complex for concatenation and map audio input
111
- ffmpegContext.command
112
- .complexFilter(filterComplexParts)
113
- .outputOptions(getOutputOption(ffmpegContextAudioId))
114
- .on("start", (__cmdLine) => {
115
- GraphAILogger.log("Started FFmpeg ..."); // with command:', cmdLine);
116
- })
117
- .on("error", (err, stdout, stderr) => {
118
- GraphAILogger.error("Error occurred:", err);
119
- GraphAILogger.error("FFmpeg stdout:", stdout);
120
- GraphAILogger.error("FFmpeg stderr:", stderr);
121
- GraphAILogger.info("Video creation failed. An unexpected error occurred.");
122
- reject();
123
- })
124
- .on("end", () => {
125
- const end = performance.now();
126
- GraphAILogger.info(`Video created successfully! ${Math.round(end - start) / 1000} sec`);
127
- resolve(0);
128
- })
129
- .output(outputVideoPath)
130
- .run();
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
- const { studio, fileDirs } = context;
135
- const { outDirPath } = fileDirs;
136
- const audioArtifactFilePath = getAudioArtifactFilePath(outDirPath, studio.filename);
137
- const outputVideoPath = getOutputVideoFilePath(outDirPath, studio.filename);
138
- await createVideo(audioArtifactFilePath, outputVideoPath, studio);
139
- writingMessage(outputVideoPath);
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
  };
@@ -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
- return ext === ".jpg" || ext === ".jpeg" ? await pdfDoc.embedJpg(imageBytes) : await pdfDoc.embedPng(imageBytes);
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
- export const pdf = async (context, pdfMode, pdfSize) => {
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) => beat.text);
194
- const outputPdfPath = getOutputPdfFilePath(outDirPath, studio.filename, pdfMode);
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
+ };