mulmocast 0.0.14 → 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.
@@ -1,231 +1,161 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
- import { rgb, PDFDocument } from "pdf-lib";
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 { fontSize, textMargin, drawSize, wrapText } from "../utils/pdf.js";
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 imagesPerPage = 4;
11
- const offset = 10;
12
- const handoutImageRatio = 0.5;
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 pdfSlide = async (pageWidth, pageHeight, imagePaths, pdfDoc) => {
33
- const cellRatio = pageHeight / pageWidth;
34
- for (const imagePath of imagePaths) {
35
- const image = await readImage(imagePath, pdfDoc);
36
- const { width: origWidth, height: origHeight } = image.scale(1);
37
- const originalRatio = origHeight / origWidth;
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 pdfTalk = async (pageWidth, pageHeight, imagePaths, texts, pdfDoc, font) => {
50
- const imageRatio = 0.7;
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 pdfHandout = async (pageWidth, pageHeight, imagePaths, texts, pdfDoc, font, isLandscapeImage) => {
92
- const cellRatio = (pageHeight / imagesPerPage - offset) / (pageWidth * handoutImageRatio - offset);
93
- let index = 0;
94
- for (const chunkPaths of chunkArray(imagePaths, imagesPerPage)) {
95
- const page = pdfDoc.addPage([pageWidth, pageHeight]);
96
- for (let i = 0; i < chunkPaths.length; i++) {
97
- const imagePath = chunkPaths[i];
98
- const image = await readImage(imagePath, pdfDoc);
99
- const { width: origWidth, height: origHeight } = image.scale(1);
100
- const originalRatio = origHeight / origWidth;
101
- const fitWidth = originalRatio / cellRatio < 1;
102
- const pos = (() => {
103
- if (isLandscapeImage) {
104
- const cellHeight = pageHeight / imagesPerPage - offset;
105
- const { drawWidth, drawHeight, containerWidth } = drawSize(fitWidth, (pageWidth - offset) * handoutImageRatio, cellHeight - offset, origWidth, origHeight);
106
- const x = offset + (containerWidth - drawWidth) / 2;
107
- const y = pageHeight - (i + 1) * cellHeight + (cellHeight - drawHeight) * handoutImageRatio;
108
- return {
109
- x,
110
- y,
111
- width: drawWidth,
112
- height: drawHeight,
113
- containerWidth,
114
- };
115
- }
116
- else {
117
- const cellWidth = pageWidth / imagesPerPage;
118
- const { drawWidth, drawHeight, containerWidth } = drawSize(fitWidth, cellWidth - offset, (pageHeight - offset) * handoutImageRatio, origWidth, origHeight);
119
- const x = pageWidth - (imagesPerPage - i) * cellWidth + (cellWidth - drawWidth) * handoutImageRatio;
120
- const y = pageHeight - drawHeight - offset;
121
- return {
122
- x,
123
- y,
124
- width: drawWidth,
125
- height: drawHeight,
126
- containerWidth,
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
- const lines = wrapText(texts[index], font, fontSize, pos.width - textMargin * 2);
158
- for (const [index, line] of lines.entries()) {
159
- page.drawText(line, {
160
- ...pos,
161
- x: pos.x,
162
- y: textMargin + pos.height - fontSize - (fontSize + textMargin) * index - 2 * textMargin,
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
- index = index + 1;
179
- }
72
+ }).join("");
73
+ pages.push(`<div class="page">${pageItems}</div>`);
180
74
  }
75
+ return pages.join("");
181
76
  };
182
- const outputSize = (pdfSize, isLandscapeImage, isRotate) => {
183
- if (pdfSize === "a4") {
184
- if (isLandscapeImage !== isRotate) {
185
- return { width: 841.89, height: 595.28 };
186
- }
187
- return { width: 595.28, height: 841.89 };
188
- }
189
- // letter
190
- if (isLandscapeImage !== isRotate) {
191
- return { width: 792, height: 612 };
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 generatePdf = async (context, pdfMode, pdfSize) => {
196
- const { studio, fileDirs, lang } = context;
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
- return localizedText(beat, multiLingual?.[index], lang);
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
- const outputPdfPath = getOutputPdfFilePath(outDirPath, studio.filename, pdfMode, lang);
208
- const pdfDoc = await PDFDocument.create();
209
- pdfDoc.registerFontkit(fontkit);
210
- const fontBytes = fs.readFileSync("assets/font/NotoSansJP-Regular.ttf");
211
- const customFont = await pdfDoc.embedFont(fontBytes);
212
- if (pdfMode === "handout") {
213
- await pdfHandout(pageWidth, pageHeight, imagePaths, texts, pdfDoc, customFont, isLandscapeImage);
214
- }
215
- if (pdfMode === "slide") {
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
- if (pdfMode === "talk") {
219
- await pdfTalk(pageWidth, pageHeight, imagePaths, texts, pdfDoc, customFont);
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 generatePdf(context, pdfMode, pdfSize);
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 (beat.image?.type === "movie" && (beat.image.source.kind === "url" || beat.image.source.kind === "path")) {
28
- const pathOrUrl = beat.image.source.kind === "url" ? beat.image.source.url : beat.image.source.path;
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,4 @@
1
+ import { AgentFunction, AgentFunctionInfo } from "graphai";
2
+ export declare const imageMockAgent: AgentFunction;
3
+ declare const imageMockAgentInfo: AgentFunctionInfo;
4
+ export default imageMockAgentInfo;
@@ -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;
@@ -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, };
@@ -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,4 @@
1
+ import { AgentFunction, AgentFunctionInfo } from "graphai";
2
+ export declare const mediaMockAgent: AgentFunction;
3
+ declare const mediaMockAgentInfo: AgentFunctionInfo;
4
+ export default mediaMockAgentInfo;
@@ -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.info(e);
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
  };
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ dryRun: boolean;
10
12
  } & {
11
13
  file: string | undefined;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ dryRun: boolean;
10
12
  } & {
11
13
  file: string | undefined;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ dryRun: boolean;
10
12
  } & {
11
13
  file: string | undefined;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ dryRun: boolean;
10
12
  } & {
11
13
  file: string | undefined;
12
14
  } & {
@@ -7,6 +7,8 @@ export declare const builder: (yargs: Argv) => Argv<import("yargs").Omit<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ dryRun: boolean;
10
12
  } & {
11
13
  file: string | undefined;
12
14
  }, "file"> & {
@@ -7,6 +7,8 @@ export declare const commonOptions: (yargs: Argv) => Argv<{
7
7
  l: string | undefined;
8
8
  } & {
9
9
  f: boolean;
10
+ } & {
11
+ dryRun: boolean;
10
12
  } & {
11
13
  file: string | undefined;
12
14
  }>;
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",
@@ -100,6 +100,7 @@ export const initializeContext = async (argv) => {
100
100
  studio,
101
101
  fileDirs: files,
102
102
  force: Boolean(argv.f),
103
+ dryRun: Boolean(argv.dryRun),
103
104
  lang: argv.l,
104
105
  caption: argv.c,
105
106
  sessionState: {
@@ -6,6 +6,6 @@ export declare const MulmoScriptMethods: {
6
6
  getAllSpeechProviders(script: MulmoScript): Set<Text2SpeechProvider>;
7
7
  getTextSlideStyle(script: MulmoScript, beat: MulmoBeat): string;
8
8
  getSpeechOptions(script: MulmoScript, beat: MulmoBeat): SpeechOptions | undefined;
9
- getImageAgentInfo(script: MulmoScript): Text2ImageAgentInfo;
9
+ getImageAgentInfo(script: MulmoScript, dryRun?: boolean): Text2ImageAgentInfo;
10
10
  getImageType(_: MulmoScript, beat: MulmoBeat): BeatMediaType;
11
11
  };
@@ -40,7 +40,7 @@ export const MulmoScriptMethods = {
40
40
  getSpeechOptions(script, beat) {
41
41
  return { ...script.speechParams.speakers[beat.speaker].speechOptions, ...beat.speechOptions };
42
42
  },
43
- getImageAgentInfo(script) {
43
+ getImageAgentInfo(script, dryRun = false) {
44
44
  // Notice that we copy imageParams from script and update
45
45
  // provider and model appropriately.
46
46
  const provider = text2ImageProviderSchema.parse(script.imageParams?.provider);
@@ -49,7 +49,7 @@ export const MulmoScriptMethods = {
49
49
  };
50
50
  return {
51
51
  provider,
52
- agent: provider === "google" ? "imageGoogleAgent" : "imageOpenaiAgent",
52
+ agent: dryRun ? "mediaMockAgent" : provider === "google" ? "imageGoogleAgent" : "imageOpenaiAgent",
53
53
  imageParams: { ...defaultImageParams, ...script.imageParams },
54
54
  };
55
55
  },
@@ -45,6 +45,7 @@ export type MulmoStudioContext = {
45
45
  fileDirs: FileDirs;
46
46
  studio: MulmoStudio;
47
47
  lang?: string;
48
+ dryRun?: boolean;
48
49
  force: boolean;
49
50
  caption?: string;
50
51
  sessionState: MulmoSessionState;
@@ -22,6 +22,7 @@ export const fileCacheAgentFilter = async (context, next) => {
22
22
  }
23
23
  };
24
24
  if (await shouldUseCache()) {
25
+ GraphAILogger.debug("cache");
25
26
  return true;
26
27
  }
27
28
  try {
@@ -24,7 +24,7 @@ export const renderHTMLToImage = async (html, outputPath, width, height, isMerma
24
24
  export const renderMarkdownToImage = async (markdown, style, outputPath, width, height) => {
25
25
  const header = `<head><style>${style}</style></head>`;
26
26
  const body = await marked(markdown);
27
- const html = `<htlm>${header}<body>${body}</body></html>`;
27
+ const html = `<html>${header}<body>${body}</body></html>`;
28
28
  await renderHTMLToImage(html, outputPath, width, height);
29
29
  };
30
30
  export const interpolate = (template, data) => {