mulmocast 0.0.13 → 0.0.15
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/lib/actions/audio.d.ts +1 -0
- package/lib/actions/audio.js +8 -2
- package/lib/actions/images.d.ts +85 -1
- package/lib/actions/images.js +130 -88
- package/lib/actions/movie.d.ts +1 -0
- package/lib/actions/movie.js +14 -3
- package/lib/actions/pdf.d.ts +1 -0
- package/lib/actions/pdf.js +132 -202
- package/lib/agents/combine_audio_files_agent.js +10 -8
- 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 +3 -1
- package/lib/agents/index.js +3 -1
- package/lib/agents/media_mock_agent.d.ts +4 -0
- package/lib/agents/media_mock_agent.js +18 -0
- package/lib/agents/tts_openai_agent.js +9 -1
- package/lib/cli/commands/audio/builder.d.ts +2 -0
- package/lib/cli/commands/image/builder.d.ts +2 -0
- package/lib/cli/commands/movie/builder.d.ts +2 -0
- package/lib/cli/commands/pdf/builder.d.ts +2 -0
- package/lib/cli/commands/translate/builder.d.ts +2 -0
- package/lib/cli/common.d.ts +2 -0
- package/lib/cli/common.js +5 -0
- package/lib/cli/helpers.js +1 -0
- package/lib/methods/mulmo_script.d.ts +1 -1
- package/lib/methods/mulmo_script.js +2 -2
- package/lib/types/type.d.ts +1 -0
- package/lib/utils/filters.js +1 -0
- package/lib/utils/markdown.js +1 -1
- package/package.json +7 -8
- package/assets/font/NotoSansJP-Regular.ttf +0 -0
package/lib/actions/movie.js
CHANGED
|
@@ -110,7 +110,14 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, capti
|
|
|
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);
|
|
@@ -133,7 +140,6 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, capti
|
|
|
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,6 +162,7 @@ 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`);
|
|
@@ -163,13 +170,17 @@ const createVideo = async (audioArtifactFilePath, outputVideoPath, studio, capti
|
|
|
163
170
|
GraphAILogger.info((studio.script.references ?? []).map((reference) => `${reference.title} (${reference.url})`).join("\n"));
|
|
164
171
|
return true;
|
|
165
172
|
};
|
|
173
|
+
export const movieFilePath = (context) => {
|
|
174
|
+
const { studio, fileDirs, caption } = context;
|
|
175
|
+
return getOutputVideoFilePath(fileDirs.outDirPath, studio.filename, context.lang, caption);
|
|
176
|
+
};
|
|
166
177
|
export const movie = async (context) => {
|
|
167
178
|
MulmoStudioContextMethods.setSessionState(context, "video", true);
|
|
168
179
|
try {
|
|
169
180
|
const { studio, fileDirs, caption } = context;
|
|
170
181
|
const { outDirPath } = fileDirs;
|
|
171
182
|
const audioArtifactFilePath = getAudioArtifactFilePath(outDirPath, studio.filename);
|
|
172
|
-
const outputVideoPath =
|
|
183
|
+
const outputVideoPath = movieFilePath(context);
|
|
173
184
|
if (await createVideo(audioArtifactFilePath, outputVideoPath, studio, caption)) {
|
|
174
185
|
writingMessage(outputVideoPath);
|
|
175
186
|
}
|
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>;
|
package/lib/actions/pdf.js
CHANGED
|
@@ -1,231 +1,161 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import
|
|
4
|
-
import fontkit from "@pdf-lib/fontkit";
|
|
5
|
-
import { chunkArray, isHttp, localizedText } from "../utils/utils.js";
|
|
6
|
-
import { getOutputPdfFilePath, writingMessage } from "../utils/file.js";
|
|
3
|
+
import puppeteer from "puppeteer";
|
|
7
4
|
import { MulmoScriptMethods } from "../methods/index.js";
|
|
8
|
-
import {
|
|
5
|
+
import { localizedText, isHttp } from "../utils/utils.js";
|
|
6
|
+
import { getOutputPdfFilePath, writingMessage, getHTMLFile } from "../utils/file.js";
|
|
7
|
+
import { interpolate } from "../utils/markdown.js";
|
|
9
8
|
import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
const readImage = async (imagePath, pdfDoc) => {
|
|
14
|
-
const imageBytes = await (async () => {
|
|
15
|
-
if (isHttp(imagePath)) {
|
|
16
|
-
const res = await fetch(imagePath);
|
|
17
|
-
const arrayBuffer = await res.arrayBuffer();
|
|
18
|
-
return Buffer.from(arrayBuffer);
|
|
19
|
-
}
|
|
20
|
-
return fs.readFileSync(imagePath);
|
|
21
|
-
})();
|
|
22
|
-
const ext = path.extname(imagePath).toLowerCase();
|
|
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"));
|
|
9
|
+
const isCI = process.env.CI === "true";
|
|
10
|
+
const getPdfSize = (pdfSize) => {
|
|
11
|
+
return pdfSize === "a4" ? "A4" : "Letter";
|
|
31
12
|
};
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
const fitWidth = originalRatio / cellRatio < 1;
|
|
39
|
-
const { drawWidth, drawHeight } = drawSize(fitWidth, pageWidth, pageHeight, origWidth, origHeight);
|
|
40
|
-
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
41
|
-
page.drawImage(image, {
|
|
42
|
-
x: 0,
|
|
43
|
-
y: 0,
|
|
44
|
-
width: drawWidth,
|
|
45
|
-
height: drawHeight,
|
|
46
|
-
});
|
|
13
|
+
const loadImage = async (imagePath) => {
|
|
14
|
+
try {
|
|
15
|
+
const imageData = isHttp(imagePath) ? Buffer.from(await (await fetch(imagePath)).arrayBuffer()) : fs.readFileSync(imagePath);
|
|
16
|
+
const ext = path.extname(imagePath).toLowerCase().replace(".", "");
|
|
17
|
+
const mimeType = ext === "jpg" ? "jpeg" : ext;
|
|
18
|
+
return `data:image/${mimeType};base64,${imageData.toString("base64")}`;
|
|
47
19
|
}
|
|
48
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
const textMargin = 8;
|
|
52
|
-
const textY = pageHeight * (1 - imageRatio) - textMargin;
|
|
53
|
-
const targetWidth = pageWidth - offset;
|
|
54
|
-
const targetHeight = pageHeight * imageRatio - offset;
|
|
55
|
-
const cellRatio = targetHeight / targetWidth;
|
|
56
|
-
for (const [index, imagePath] of imagePaths.entries()) {
|
|
57
|
-
const text = texts[index];
|
|
58
|
-
const image = await readImage(imagePath, pdfDoc);
|
|
59
|
-
const { width: origWidth, height: origHeight } = image.scale(1);
|
|
60
|
-
const originalRatio = origHeight / origWidth;
|
|
61
|
-
const fitWidth = originalRatio / cellRatio < 1;
|
|
62
|
-
const { drawWidth, drawHeight } = drawSize(fitWidth, targetWidth, targetHeight, origWidth, origHeight);
|
|
63
|
-
const x = (pageWidth - drawWidth) / 2;
|
|
64
|
-
const y = pageHeight - drawHeight - offset;
|
|
65
|
-
const page = pdfDoc.addPage([pageWidth, pageHeight]);
|
|
66
|
-
const pos = {
|
|
67
|
-
x,
|
|
68
|
-
y,
|
|
69
|
-
width: drawWidth,
|
|
70
|
-
height: drawHeight,
|
|
71
|
-
};
|
|
72
|
-
page.drawImage(image, pos);
|
|
73
|
-
page.drawRectangle({
|
|
74
|
-
...pos,
|
|
75
|
-
borderColor: rgb(0, 0, 0),
|
|
76
|
-
borderWidth: 1,
|
|
77
|
-
});
|
|
78
|
-
const lines = wrapText(text, font, fontSize, pageWidth - textMargin * 2);
|
|
79
|
-
for (const [index, line] of lines.entries()) {
|
|
80
|
-
page.drawText(line, {
|
|
81
|
-
x: textMargin,
|
|
82
|
-
y: textY - fontSize - (fontSize + 2) * index,
|
|
83
|
-
size: fontSize,
|
|
84
|
-
color: rgb(0, 0, 0),
|
|
85
|
-
maxWidth: pageWidth - 2 * textMargin,
|
|
86
|
-
font,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
20
|
+
catch (__error) {
|
|
21
|
+
const placeholderData = fs.readFileSync("assets/images/mulmocast_credit.png");
|
|
22
|
+
return `data:image/png;base64,${placeholderData.toString("base64")}`;
|
|
89
23
|
}
|
|
90
24
|
};
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
})();
|
|
130
|
-
page.drawRectangle({
|
|
131
|
-
...pos,
|
|
132
|
-
borderColor: rgb(0, 0, 0),
|
|
133
|
-
borderWidth: 1,
|
|
134
|
-
});
|
|
135
|
-
page.drawImage(image, pos);
|
|
136
|
-
if (isLandscapeImage) {
|
|
137
|
-
const lines = wrapText(texts[index], font, fontSize, pos.width - textMargin * 2);
|
|
138
|
-
for (const [index, line] of lines.entries()) {
|
|
139
|
-
page.drawText(line, {
|
|
140
|
-
...pos,
|
|
141
|
-
x: offset + pos.containerWidth + textMargin,
|
|
142
|
-
y: pos.y + pos.height - fontSize - (fontSize + 2) * index,
|
|
143
|
-
size: fontSize,
|
|
144
|
-
font,
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
/*
|
|
148
|
-
page.drawRectangle({
|
|
149
|
-
...pos,
|
|
150
|
-
x: pos.x + pos.width ,
|
|
151
|
-
borderColor: rgb(0, 0, 0),
|
|
152
|
-
borderWidth: 1,
|
|
153
|
-
});
|
|
154
|
-
*/
|
|
25
|
+
const formatTextAsParagraphs = (text) => text
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((line) => `<p>${line}</p>`)
|
|
28
|
+
.join("");
|
|
29
|
+
const generateSlideHTML = (imageDataUrls) => imageDataUrls
|
|
30
|
+
.map((imageUrl) => `
|
|
31
|
+
<div class="page">
|
|
32
|
+
<img src="${imageUrl}" alt="">
|
|
33
|
+
</div>`)
|
|
34
|
+
.join("");
|
|
35
|
+
const generateTalkHTML = (imageDataUrls, texts) => imageDataUrls
|
|
36
|
+
.map((imageUrl, index) => `
|
|
37
|
+
<div class="page">
|
|
38
|
+
<div class="image-container">
|
|
39
|
+
<img src="${imageUrl}" alt="">
|
|
40
|
+
</div>
|
|
41
|
+
<div class="text-container">
|
|
42
|
+
${formatTextAsParagraphs(texts[index])}
|
|
43
|
+
</div>
|
|
44
|
+
</div>`)
|
|
45
|
+
.join("");
|
|
46
|
+
const generateHandoutHTML = (imageDataUrls, texts) => {
|
|
47
|
+
const itemsPerPage = 4;
|
|
48
|
+
const pages = [];
|
|
49
|
+
for (let i = 0; i < imageDataUrls.length; i += itemsPerPage) {
|
|
50
|
+
const pageItems = Array.from({ length: itemsPerPage }, (_, j) => {
|
|
51
|
+
const index = i + j;
|
|
52
|
+
const hasContent = index < imageDataUrls.length;
|
|
53
|
+
if (hasContent) {
|
|
54
|
+
return `
|
|
55
|
+
<div class="handout-item">
|
|
56
|
+
<div class="handout-image">
|
|
57
|
+
<img src="${imageDataUrls[index]}" alt="">
|
|
58
|
+
</div>
|
|
59
|
+
<div class="handout-text">
|
|
60
|
+
${formatTextAsParagraphs(texts[index])}
|
|
61
|
+
</div>
|
|
62
|
+
</div>`;
|
|
155
63
|
}
|
|
156
64
|
else {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
size: fontSize,
|
|
164
|
-
font,
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
/*
|
|
168
|
-
page.drawRectangle({
|
|
169
|
-
...pos,
|
|
170
|
-
x: pos.x,
|
|
171
|
-
y: textMargin,
|
|
172
|
-
height: pos.height - 2 * textMargin,
|
|
173
|
-
borderColor: rgb(0, 0, 0),
|
|
174
|
-
borderWidth: 1,
|
|
175
|
-
});
|
|
176
|
-
*/
|
|
65
|
+
// Empty slot to maintain 4-item grid layout
|
|
66
|
+
return `
|
|
67
|
+
<div class="handout-item">
|
|
68
|
+
<div class="handout-image"></div>
|
|
69
|
+
<div class="handout-text"></div>
|
|
70
|
+
</div>`;
|
|
177
71
|
}
|
|
178
|
-
|
|
179
|
-
}
|
|
72
|
+
}).join("");
|
|
73
|
+
pages.push(`<div class="page">${pageItems}</div>`);
|
|
180
74
|
}
|
|
75
|
+
return pages.join("");
|
|
181
76
|
};
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
77
|
+
const generatePagesHTML = (pdfMode, imageDataUrls, texts) => {
|
|
78
|
+
switch (pdfMode) {
|
|
79
|
+
case "slide":
|
|
80
|
+
return generateSlideHTML(imageDataUrls);
|
|
81
|
+
case "talk":
|
|
82
|
+
return generateTalkHTML(imageDataUrls, texts);
|
|
83
|
+
case "handout":
|
|
84
|
+
return generateHandoutHTML(imageDataUrls, texts);
|
|
85
|
+
default:
|
|
86
|
+
return "";
|
|
192
87
|
}
|
|
193
|
-
return { width: 612, height: 792 };
|
|
194
88
|
};
|
|
195
|
-
const
|
|
196
|
-
|
|
89
|
+
const getHandoutTemplateData = (isLandscapeImage) => ({
|
|
90
|
+
page_layout: isLandscapeImage ? "flex" : "grid",
|
|
91
|
+
page_direction: isLandscapeImage ? "flex-direction: column;" : "grid-template-columns: repeat(4, 1fr);",
|
|
92
|
+
flex_direction: isLandscapeImage ? "row" : "column",
|
|
93
|
+
image_size: isLandscapeImage ? "width: 45%;" : "height: 60%;",
|
|
94
|
+
text_size: isLandscapeImage ? "width: 55%;" : "height: 40%;",
|
|
95
|
+
item_flex: isLandscapeImage ? "flex: 1;" : "",
|
|
96
|
+
});
|
|
97
|
+
const generatePDFHTML = async (context, pdfMode, pdfSize) => {
|
|
98
|
+
const { studio, lang = "en" } = context;
|
|
197
99
|
const { multiLingual } = studio;
|
|
198
|
-
const { outDirPath } = fileDirs;
|
|
199
100
|
const { width: imageWidth, height: imageHeight } = MulmoScriptMethods.getCanvasSize(studio.script);
|
|
200
101
|
const isLandscapeImage = imageWidth > imageHeight;
|
|
201
|
-
const isRotate = pdfMode === "handout";
|
|
202
|
-
const { width: pageWidth, height: pageHeight } = outputSize(pdfSize, isLandscapeImage, isRotate);
|
|
203
102
|
const imagePaths = studio.beats.map((beat) => beat.imageFile);
|
|
204
|
-
const texts = studio.script.beats.map((beat, index) =>
|
|
205
|
-
|
|
103
|
+
const texts = studio.script.beats.map((beat, index) => localizedText(beat, multiLingual?.[index], lang));
|
|
104
|
+
const imageDataUrls = await Promise.all(imagePaths.map(loadImage));
|
|
105
|
+
const pageSize = pdfMode === "handout" ? `${getPdfSize(pdfSize)} portrait` : `${getPdfSize(pdfSize)} ${isLandscapeImage ? "landscape" : "portrait"}`;
|
|
106
|
+
const pagesHTML = generatePagesHTML(pdfMode, imageDataUrls, texts);
|
|
107
|
+
const template = getHTMLFile(`pdf_${pdfMode}`);
|
|
108
|
+
const baseTemplateData = {
|
|
109
|
+
lang,
|
|
110
|
+
title: studio.script.title || "MulmoCast PDF",
|
|
111
|
+
page_size: pageSize,
|
|
112
|
+
pages: pagesHTML,
|
|
113
|
+
};
|
|
114
|
+
const templateData = pdfMode === "handout" ? { ...baseTemplateData, ...getHandoutTemplateData(isLandscapeImage) } : baseTemplateData;
|
|
115
|
+
return interpolate(template, templateData);
|
|
116
|
+
};
|
|
117
|
+
const createPDFOptions = (pdfSize, pdfMode) => {
|
|
118
|
+
const baseOptions = {
|
|
119
|
+
format: getPdfSize(pdfSize),
|
|
120
|
+
margin: {
|
|
121
|
+
top: "0",
|
|
122
|
+
bottom: "0",
|
|
123
|
+
left: "0",
|
|
124
|
+
right: "0",
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
// handout mode always uses portrait orientation
|
|
128
|
+
return pdfMode === "handout" ? { ...baseOptions, landscape: false } : baseOptions;
|
|
129
|
+
};
|
|
130
|
+
export const pdfFilePath = (context, pdfMode) => {
|
|
131
|
+
const { studio, fileDirs, lang = "en" } = context;
|
|
132
|
+
return getOutputPdfFilePath(fileDirs.outDirPath, studio.filename, pdfMode, lang);
|
|
133
|
+
};
|
|
134
|
+
const generatePDF = async (context, pdfMode, pdfSize) => {
|
|
135
|
+
const outputPdfPath = pdfFilePath(context, pdfMode);
|
|
136
|
+
const html = await generatePDFHTML(context, pdfMode, pdfSize);
|
|
137
|
+
const pdfOptions = createPDFOptions(pdfSize, pdfMode);
|
|
138
|
+
const browser = await puppeteer.launch({
|
|
139
|
+
args: isCI ? ["--no-sandbox"] : [],
|
|
206
140
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
await pdfSlide(pageWidth, pageHeight, imagePaths, pdfDoc);
|
|
141
|
+
try {
|
|
142
|
+
const page = await browser.newPage();
|
|
143
|
+
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
144
|
+
await page.pdf({
|
|
145
|
+
path: outputPdfPath,
|
|
146
|
+
printBackground: true,
|
|
147
|
+
...pdfOptions,
|
|
148
|
+
});
|
|
149
|
+
writingMessage(outputPdfPath);
|
|
217
150
|
}
|
|
218
|
-
|
|
219
|
-
await
|
|
151
|
+
finally {
|
|
152
|
+
await browser.close();
|
|
220
153
|
}
|
|
221
|
-
const pdfBytes = await pdfDoc.save();
|
|
222
|
-
fs.writeFileSync(outputPdfPath, pdfBytes);
|
|
223
|
-
writingMessage(outputPdfPath);
|
|
224
154
|
};
|
|
225
155
|
export const pdf = async (context, pdfMode, pdfSize) => {
|
|
226
156
|
try {
|
|
227
157
|
MulmoStudioContextMethods.setSessionState(context, "pdf", true);
|
|
228
|
-
await
|
|
158
|
+
await generatePDF(context, pdfMode, pdfSize);
|
|
229
159
|
}
|
|
230
160
|
finally {
|
|
231
161
|
MulmoStudioContextMethods.setSessionState(context, "pdf", false);
|
|
@@ -11,6 +11,13 @@ const combineAudioFilesAgent = async ({ namedInputs, }) => {
|
|
|
11
11
|
const inputIds = (await Promise.all(context.studio.beats.map(async (studioBeat, index) => {
|
|
12
12
|
const beat = context.studio.script.beats[index];
|
|
13
13
|
const isClosingGap = index === context.studio.beats.length - 2;
|
|
14
|
+
const movieDuration = await (() => {
|
|
15
|
+
if (beat.image?.type === "movie" && (beat.image.source.kind === "url" || beat.image.source.kind === "path")) {
|
|
16
|
+
const pathOrUrl = beat.image.source.kind === "url" ? beat.image.source.url : beat.image.source.path;
|
|
17
|
+
return ffmpegGetMediaDuration(pathOrUrl);
|
|
18
|
+
}
|
|
19
|
+
return 0;
|
|
20
|
+
})();
|
|
14
21
|
if (studioBeat.audioFile) {
|
|
15
22
|
const audioId = FfmpegContextInputFormattedAudio(ffmpegContext, studioBeat.audioFile);
|
|
16
23
|
const padding = (() => {
|
|
@@ -24,13 +31,8 @@ const combineAudioFilesAgent = async ({ namedInputs, }) => {
|
|
|
24
31
|
})();
|
|
25
32
|
const audioDuration = await ffmpegGetMediaDuration(studioBeat.audioFile);
|
|
26
33
|
const totalPadding = await (async () => {
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
// NOTE: We respect the duration of the movie, only if the movie is specified as a madia source, NOT generated.
|
|
30
|
-
const movieDuration = await ffmpegGetMediaDuration(pathOrUrl);
|
|
31
|
-
if (movieDuration > audioDuration) {
|
|
32
|
-
return padding + (movieDuration - audioDuration);
|
|
33
|
-
}
|
|
34
|
+
if (movieDuration > 0) {
|
|
35
|
+
return padding + (movieDuration - audioDuration);
|
|
34
36
|
}
|
|
35
37
|
else if (beat.duration && beat.duration > audioDuration) {
|
|
36
38
|
return padding + (beat.duration - audioDuration);
|
|
@@ -49,7 +51,7 @@ const combineAudioFilesAgent = async ({ namedInputs, }) => {
|
|
|
49
51
|
}
|
|
50
52
|
else {
|
|
51
53
|
// NOTE: We come here when the text is empty and no audio property is specified.
|
|
52
|
-
studioBeat.duration = beat.duration ?? 1.0;
|
|
54
|
+
studioBeat.duration = beat.duration ?? (movieDuration > 0 ? movieDuration : 1.0);
|
|
53
55
|
const silentId = silentIds.pop();
|
|
54
56
|
ffmpegContext.filterComplex.push(`${silentId}atrim=start=0:end=${studioBeat.duration}[silent_${index}]`);
|
|
55
57
|
return [`[silent_${index}]`];
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GraphAILogger } from "graphai";
|
|
2
|
+
export const imageMockAgent = async () => {
|
|
3
|
+
GraphAILogger.debug("agent dryRun");
|
|
4
|
+
return { buffer: Buffer.from([]) };
|
|
5
|
+
};
|
|
6
|
+
const imageMockAgentInfo = {
|
|
7
|
+
name: "imageMockAgent",
|
|
8
|
+
agent: imageMockAgent,
|
|
9
|
+
mock: imageMockAgent,
|
|
10
|
+
samples: [],
|
|
11
|
+
description: "Image mock agent",
|
|
12
|
+
category: ["image"],
|
|
13
|
+
author: "Receptron Team",
|
|
14
|
+
repository: "https://github.com/receptron/mulmocast-cli/",
|
|
15
|
+
license: "MIT",
|
|
16
|
+
environmentVariables: [],
|
|
17
|
+
};
|
|
18
|
+
export default imageMockAgentInfo;
|
package/lib/agents/index.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import addBGMAgent from "./add_bgm_agent.js";
|
|
|
2
2
|
import combineAudioFilesAgent from "./combine_audio_files_agent.js";
|
|
3
3
|
import imageGoogleAgent from "./image_google_agent.js";
|
|
4
4
|
import imageOpenaiAgent from "./image_openai_agent.js";
|
|
5
|
+
import movieGoogleAgent from "./movie_google_agent.js";
|
|
6
|
+
import mediaMockAgent from "./media_mock_agent.js";
|
|
5
7
|
import ttsElevenlabsAgent from "./tts_elevenlabs_agent.js";
|
|
6
8
|
import ttsNijivoiceAgent from "./tts_nijivoice_agent.js";
|
|
7
9
|
import ttsOpenaiAgent from "./tts_openai_agent.js";
|
|
@@ -10,4 +12,4 @@ import { browserlessAgent } from "@graphai/browserless_agent";
|
|
|
10
12
|
import { textInputAgent } from "@graphai/input_agents";
|
|
11
13
|
import { openAIAgent } from "@graphai/openai_agent";
|
|
12
14
|
import { fileWriteAgent } from "@graphai/vanilla_node_agents";
|
|
13
|
-
export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGoogleAgent, imageOpenaiAgent, ttsElevenlabsAgent, ttsNijivoiceAgent, ttsOpenaiAgent, validateSchemaAgent, };
|
|
15
|
+
export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGoogleAgent, imageOpenaiAgent, movieGoogleAgent, mediaMockAgent, ttsElevenlabsAgent, ttsNijivoiceAgent, ttsOpenaiAgent, validateSchemaAgent, };
|
package/lib/agents/index.js
CHANGED
|
@@ -2,6 +2,8 @@ import addBGMAgent from "./add_bgm_agent.js";
|
|
|
2
2
|
import combineAudioFilesAgent from "./combine_audio_files_agent.js";
|
|
3
3
|
import imageGoogleAgent from "./image_google_agent.js";
|
|
4
4
|
import imageOpenaiAgent from "./image_openai_agent.js";
|
|
5
|
+
import movieGoogleAgent from "./movie_google_agent.js";
|
|
6
|
+
import mediaMockAgent from "./media_mock_agent.js";
|
|
5
7
|
import ttsElevenlabsAgent from "./tts_elevenlabs_agent.js";
|
|
6
8
|
import ttsNijivoiceAgent from "./tts_nijivoice_agent.js";
|
|
7
9
|
import ttsOpenaiAgent from "./tts_openai_agent.js";
|
|
@@ -11,4 +13,4 @@ import { textInputAgent } from "@graphai/input_agents";
|
|
|
11
13
|
import { openAIAgent } from "@graphai/openai_agent";
|
|
12
14
|
// import * as vanilla from "@graphai/vanilla";
|
|
13
15
|
import { fileWriteAgent } from "@graphai/vanilla_node_agents";
|
|
14
|
-
export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGoogleAgent, imageOpenaiAgent, ttsElevenlabsAgent, ttsNijivoiceAgent, ttsOpenaiAgent, validateSchemaAgent, };
|
|
16
|
+
export { openAIAgent, fileWriteAgent, browserlessAgent, textInputAgent, addBGMAgent, combineAudioFilesAgent, imageGoogleAgent, imageOpenaiAgent, movieGoogleAgent, mediaMockAgent, ttsElevenlabsAgent, ttsNijivoiceAgent, ttsOpenaiAgent, validateSchemaAgent, };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { GraphAILogger } from "graphai";
|
|
2
|
+
export const mediaMockAgent = async () => {
|
|
3
|
+
GraphAILogger.debug("agent dryRun");
|
|
4
|
+
return { buffer: Buffer.from([]) };
|
|
5
|
+
};
|
|
6
|
+
const mediaMockAgentInfo = {
|
|
7
|
+
name: "mediaMockAgent",
|
|
8
|
+
agent: mediaMockAgent,
|
|
9
|
+
mock: mediaMockAgent,
|
|
10
|
+
samples: [],
|
|
11
|
+
description: "Image mock agent",
|
|
12
|
+
category: ["image"],
|
|
13
|
+
author: "Receptron Team",
|
|
14
|
+
repository: "https://github.com/receptron/mulmocast-cli/",
|
|
15
|
+
license: "MIT",
|
|
16
|
+
environmentVariables: [],
|
|
17
|
+
};
|
|
18
|
+
export default mediaMockAgentInfo;
|
|
@@ -24,7 +24,15 @@ export const ttsOpenaiAgent = async ({ namedInputs, params }) => {
|
|
|
24
24
|
error: e,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
-
GraphAILogger.
|
|
27
|
+
GraphAILogger.error(e);
|
|
28
|
+
if (e && typeof e === "object" && "error" in e) {
|
|
29
|
+
GraphAILogger.info("tts_openai_agent: ");
|
|
30
|
+
GraphAILogger.info(e.error);
|
|
31
|
+
}
|
|
32
|
+
else if (e instanceof Error) {
|
|
33
|
+
GraphAILogger.info("tts_openai_agent: ");
|
|
34
|
+
GraphAILogger.info(e.message);
|
|
35
|
+
}
|
|
28
36
|
throw new Error("TTS OpenAI Error");
|
|
29
37
|
}
|
|
30
38
|
};
|
package/lib/cli/common.d.ts
CHANGED
package/lib/cli/common.js
CHANGED
|
@@ -25,6 +25,11 @@ export const commonOptions = (yargs) => {
|
|
|
25
25
|
describe: "Force regenerate",
|
|
26
26
|
type: "boolean",
|
|
27
27
|
default: false,
|
|
28
|
+
})
|
|
29
|
+
.option("dryRun", {
|
|
30
|
+
describe: "Dry run",
|
|
31
|
+
type: "boolean",
|
|
32
|
+
default: false,
|
|
28
33
|
})
|
|
29
34
|
.positional("file", {
|
|
30
35
|
describe: "Mulmo Script File",
|