mulmocast 2.1.25 → 2.1.27

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 CHANGED
@@ -285,9 +285,38 @@ mulmo tool scripting -i
285
285
  ```
286
286
 
287
287
  Note:
288
+
288
289
  - When -i is specified, --input-file value will be ignored
289
290
  - When --input-file is specified, -u value will be ignored
290
291
 
292
+ ### Complete MulmoScript from minimal beats
293
+
294
+ You can create a minimal JSON with just beats and complete it with `mulmo tool complete`:
295
+
296
+ ```json
297
+ {
298
+ "beats": [
299
+ { "text": "Hello, welcome to MulmoCast!" },
300
+ { "text": "This is a simple example." }
301
+ ]
302
+ }
303
+ ```
304
+
305
+ Save this as `my_beats.json`, then complete it with a style:
306
+
307
+ ```bash
308
+ # Complete with built-in style
309
+ mulmo tool complete my_beats.json -s ani
310
+
311
+ # Complete with custom style file
312
+ mulmo tool complete my_beats.json -s ./my_style.json
313
+
314
+ # Output to specific file
315
+ mulmo tool complete my_beats.json -s ani -o my_script.json
316
+ ```
317
+
318
+ This generates a complete MulmoScript with all required fields (canvasSize, speechParams, imageParams, etc.) from the style.
319
+
291
320
 
292
321
  ## Generate content from MulmoScript
293
322
 
@@ -536,9 +565,10 @@ mulmo tool <command>
536
565
  Generate Mulmo script and other tools
537
566
 
538
567
  Commands:
539
- mulmo tool scripting Generate mulmocast script
540
- mulmo tool prompt Dump prompt from template
541
- mulmo tool schema Dump mulmocast schema
568
+ mulmo tool scripting Generate mulmocast script
569
+ mulmo tool complete <file> Complete partial MulmoScript with defaults
570
+ mulmo tool prompt Dump prompt from template
571
+ mulmo tool schema Dump mulmocast schema
542
572
 
543
573
  Options:
544
574
  --version Show version number [boolean]
@@ -626,6 +656,36 @@ Options:
626
656
  -h, --help Show help [boolean]
627
657
  ```
628
658
 
659
+ ```
660
+ mulmo tool complete <file>
661
+
662
+ Complete partial MulmoScript with schema defaults and optional style/template
663
+
664
+ Positionals:
665
+ file Input beats file path (JSON) [string] [required]
666
+
667
+ Options:
668
+ --version Show version number [boolean]
669
+ -v, --verbose verbose log [boolean] [required] [default: false]
670
+ -h, --help Show help [boolean]
671
+ -o, --output Output file path (default: <file>_completed.json) [string]
672
+ -t, --template Template name to apply [string]
673
+ -s, --style Style name or file path (.json) [string]
674
+
675
+ Examples:
676
+ # Complete minimal script with schema defaults
677
+ mulmo tool complete input.json
678
+
679
+ # Apply built-in style
680
+ mulmo tool complete input.json -s ani
681
+
682
+ # Apply custom style file
683
+ mulmo tool complete input.json -s ./my_style.json
684
+
685
+ # Apply template
686
+ mulmo tool complete input.json -t children_book
687
+ ```
688
+
629
689
 
630
690
 
631
691
  ## Contributing
@@ -17,21 +17,33 @@
17
17
  <body>
18
18
  <h1>${title}</h1>
19
19
  <div class="chart-container">
20
- <canvas id="myChart"></canvas>
20
+ <canvas id="myChart" data-chart-ready="false"></canvas>
21
21
  </div>
22
22
 
23
23
  <!-- Plain JavaScript instead of TypeScript -->
24
24
  <script>
25
- // Wait for DOM to be fully loaded
26
- document.addEventListener('DOMContentLoaded', function() {
27
- // Get the canvas element
25
+ // Wait for DOM and Chart.js to be ready, then render.
26
+ function initChart() {
27
+ if (!window.Chart) return false;
28
28
  const ctx = document.getElementById('myChart');
29
-
30
- // Create the data object (no TypeScript interfaces)
29
+ if (!ctx) return false;
31
30
  const chartData = ${chart_data};
32
-
33
- // Initialize the chart
34
31
  new Chart(ctx, chartData);
32
+ requestAnimationFrame(() => {
33
+ requestAnimationFrame(() => {
34
+ ctx.dataset.chartReady = "true";
35
+ });
36
+ });
37
+ return true;
38
+ }
39
+
40
+ function waitForChart() {
41
+ if (initChart()) return;
42
+ setTimeout(waitForChart, 50);
43
+ }
44
+
45
+ document.addEventListener('DOMContentLoaded', function() {
46
+ waitForChart();
35
47
  });
36
48
  </script>
37
49
  </body>
package/lib/cli/bin.js CHANGED
File without changes
@@ -3,6 +3,8 @@ export declare const builder: (yargs: Argv) => Argv<{
3
3
  o: string | undefined;
4
4
  } & {
5
5
  t: string | undefined;
6
+ } & {
7
+ s: string | undefined;
6
8
  } & {
7
9
  file: string;
8
10
  }>;
@@ -10,10 +10,16 @@ export const builder = (yargs) => {
10
10
  })
11
11
  .option("t", {
12
12
  alias: "template",
13
- description: "Template/style name to apply",
13
+ description: "Template name to apply",
14
14
  demandOption: false,
15
15
  choices: availableTemplateNames,
16
16
  type: "string",
17
+ })
18
+ .option("s", {
19
+ alias: "style",
20
+ description: "Style name or file path (.json)",
21
+ demandOption: false,
22
+ type: "string",
17
23
  })
18
24
  .positional("file", {
19
25
  description: "Input beats file path (JSON)",
@@ -2,6 +2,7 @@ type CompleteHandlerArgs = {
2
2
  file: string;
3
3
  o?: string;
4
4
  t?: string;
5
+ s?: string;
5
6
  v?: boolean;
6
7
  };
7
8
  export declare const handler: (argv: CompleteHandlerArgs) => Promise<void>;
@@ -1,9 +1,9 @@
1
1
  import { readFileSync, writeFileSync } from "fs";
2
2
  import path from "path";
3
3
  import { GraphAILogger } from "graphai";
4
- import { completeScript, templateExists } from "../../../../tools/complete_script.js";
4
+ import { completeScript, templateExists, styleExists } from "../../../../tools/complete_script.js";
5
5
  export const handler = async (argv) => {
6
- const { file, o: outputPath, t: templateName, v: verbose } = argv;
6
+ const { file, o: outputPath, t: templateName, s: styleName, v: verbose } = argv;
7
7
  if (!file) {
8
8
  GraphAILogger.error("Error: Input file is required");
9
9
  process.exit(1);
@@ -23,7 +23,10 @@ export const handler = async (argv) => {
23
23
  if (templateName && !templateExists(templateName)) {
24
24
  GraphAILogger.warn(`Warning: Template '${templateName}' not found`);
25
25
  }
26
- const result = completeScript(inputData, templateName);
26
+ if (styleName && !styleExists(styleName)) {
27
+ GraphAILogger.warn(`Warning: Style '${styleName}' not found`);
28
+ }
29
+ const result = completeScript(inputData, { templateName, styleName });
27
30
  if (!result.success) {
28
31
  GraphAILogger.error("Validation errors:");
29
32
  result.error.issues.forEach((issue) => {
@@ -31,8 +34,11 @@ export const handler = async (argv) => {
31
34
  });
32
35
  process.exit(1);
33
36
  }
34
- if (verbose && templateName) {
35
- GraphAILogger.info(`Applied template: ${templateName}`);
37
+ if (verbose) {
38
+ if (styleName)
39
+ GraphAILogger.info(`Applied style: ${styleName}`);
40
+ if (templateName)
41
+ GraphAILogger.info(`Applied template: ${templateName}`);
36
42
  }
37
43
  const outputFilePath = outputPath ? path.resolve(outputPath) : inputPath.replace(/\.json$/, "_completed.json");
38
44
  writeFileSync(outputFilePath, JSON.stringify(result.data, null, 2));
package/lib/mcp/server.js CHANGED
File without changes
@@ -6,16 +6,47 @@ type PartialMulmoScript = Record<string, unknown>;
6
6
  */
7
7
  export declare const addMulmocastVersion: (data: PartialMulmoScript) => PartialMulmoScript;
8
8
  /**
9
- * Merge input data with template (input takes precedence)
9
+ * Merge base with override (override takes precedence)
10
10
  */
11
- export declare const mergeWithTemplate: (data: PartialMulmoScript, template: MulmoScript) => PartialMulmoScript;
11
+ export declare const mergeScripts: (base: PartialMulmoScript, override: PartialMulmoScript) => PartialMulmoScript;
12
+ /**
13
+ * Get style by name or file path
14
+ */
15
+ export declare const getStyle: (style: string) => PartialMulmoScript | undefined;
12
16
  export type CompleteScriptResult = ZodSafeParseResult<MulmoScript>;
17
+ type CompleteScriptOptions = {
18
+ templateName?: string;
19
+ styleName?: string;
20
+ };
13
21
  /**
14
- * Complete a partial MulmoScript with schema defaults and optional template
22
+ * Complete a partial MulmoScript with schema defaults, optional style or template
23
+ *
24
+ * @param data - Partial MulmoScript to complete (highest precedence)
25
+ * @param options - Optional template or style to use as base
26
+ * @param options.templateName - Template name (e.g., "children_book"). Mutually exclusive with styleName.
27
+ * @param options.styleName - Style name or file path. Mutually exclusive with templateName.
28
+ * @returns Zod safe parse result with completed MulmoScript or validation errors
29
+ * @throws Error if both templateName and styleName are specified
30
+ *
31
+ * @example
32
+ * // With template
33
+ * completeScript(data, { templateName: "children_book" })
34
+ *
35
+ * @example
36
+ * // With style
37
+ * completeScript(data, { styleName: "ghibli_comic" })
38
+ *
39
+ * @example
40
+ * // With style from file
41
+ * completeScript(data, { styleName: "./my-style.json" })
15
42
  */
16
- export declare const completeScript: (data: PartialMulmoScript, templateName?: string) => CompleteScriptResult;
43
+ export declare const completeScript: (data: PartialMulmoScript, options?: CompleteScriptOptions) => CompleteScriptResult;
17
44
  /**
18
45
  * Check if template exists
19
46
  */
20
47
  export declare const templateExists: (templateName: string) => boolean;
48
+ /**
49
+ * Check if style exists (by name or file path)
50
+ */
51
+ export declare const styleExists: (style: string) => boolean;
21
52
  export {};
@@ -1,6 +1,9 @@
1
+ import { readFileSync, existsSync } from "fs";
2
+ import path from "path";
1
3
  import { mulmoScriptSchema } from "../types/schema.js";
2
4
  import { getScriptFromPromptTemplate } from "../utils/file.js";
3
5
  import { currentMulmoScriptVersion } from "../types/const.js";
6
+ import { promptTemplates } from "../data/index.js";
4
7
  /**
5
8
  * Add $mulmocast version if not present
6
9
  */
@@ -15,29 +18,91 @@ export const addMulmocastVersion = (data) => {
15
18
  };
16
19
  const deepMergeKeys = ["speechParams", "imageParams", "movieParams", "audioParams"];
17
20
  /**
18
- * Merge input data with template (input takes precedence)
21
+ * Merge base with override (override takes precedence)
19
22
  */
20
- export const mergeWithTemplate = (data, template) => {
21
- const merged = { ...template, ...data };
23
+ export const mergeScripts = (base, override) => {
24
+ const merged = { ...base, ...override };
22
25
  deepMergeKeys.forEach((key) => {
23
- if (template[key] && data[key]) {
24
- merged[key] = { ...template[key], ...data[key] };
26
+ if (base[key] && override[key]) {
27
+ merged[key] = { ...base[key], ...override[key] };
25
28
  }
26
29
  });
27
30
  return merged;
28
31
  };
29
32
  /**
30
- * Complete a partial MulmoScript with schema defaults and optional template
33
+ * Check if style specifier is a file path
31
34
  */
32
- export const completeScript = (data, templateName) => {
33
- const withVersion = addMulmocastVersion(data);
34
- const withTemplate = templateName
35
- ? (() => {
36
- const template = getScriptFromPromptTemplate(templateName);
37
- return template ? mergeWithTemplate(withVersion, template) : withVersion;
38
- })()
39
- : withVersion;
40
- return mulmoScriptSchema.safeParse(withTemplate);
35
+ const isFilePath = (style) => {
36
+ return style.endsWith(".json") || style.includes("/") || style.includes("\\");
37
+ };
38
+ /**
39
+ * Get style by name from promptTemplates
40
+ */
41
+ const getStyleByName = (styleName) => {
42
+ const template = promptTemplates.find((t) => t.filename === styleName);
43
+ return template?.presentationStyle;
44
+ };
45
+ /**
46
+ * Get style from file path
47
+ */
48
+ const getStyleFromFile = (filePath) => {
49
+ const resolvedPath = path.resolve(filePath);
50
+ if (!existsSync(resolvedPath)) {
51
+ return undefined;
52
+ }
53
+ const content = readFileSync(resolvedPath, "utf-8");
54
+ return JSON.parse(content);
55
+ };
56
+ /**
57
+ * Get style by name or file path
58
+ */
59
+ export const getStyle = (style) => {
60
+ return isFilePath(style) ? getStyleFromFile(style) : getStyleByName(style);
61
+ };
62
+ /**
63
+ * Complete a partial MulmoScript with schema defaults, optional style or template
64
+ *
65
+ * @param data - Partial MulmoScript to complete (highest precedence)
66
+ * @param options - Optional template or style to use as base
67
+ * @param options.templateName - Template name (e.g., "children_book"). Mutually exclusive with styleName.
68
+ * @param options.styleName - Style name or file path. Mutually exclusive with templateName.
69
+ * @returns Zod safe parse result with completed MulmoScript or validation errors
70
+ * @throws Error if both templateName and styleName are specified
71
+ *
72
+ * @example
73
+ * // With template
74
+ * completeScript(data, { templateName: "children_book" })
75
+ *
76
+ * @example
77
+ * // With style
78
+ * completeScript(data, { styleName: "ghibli_comic" })
79
+ *
80
+ * @example
81
+ * // With style from file
82
+ * completeScript(data, { styleName: "./my-style.json" })
83
+ */
84
+ export const completeScript = (data, options = {}) => {
85
+ const { templateName, styleName } = options;
86
+ // template and style are mutually exclusive
87
+ if (templateName && styleName) {
88
+ throw new Error("Cannot specify both templateName and styleName. They are mutually exclusive.");
89
+ }
90
+ // Get base config from template or style
91
+ const getBase = () => {
92
+ if (templateName) {
93
+ return getScriptFromPromptTemplate(templateName);
94
+ }
95
+ if (styleName) {
96
+ return getStyle(styleName);
97
+ }
98
+ return undefined;
99
+ };
100
+ const base = getBase();
101
+ // Merge base with input data (input data has highest precedence)
102
+ const merged = base ? mergeScripts(base, data) : data;
103
+ // Add version if not present
104
+ const withVersion = addMulmocastVersion(merged);
105
+ return mulmoScriptSchema.safeParse(withVersion);
41
106
  };
42
107
  /**
43
108
  * Check if template exists
@@ -45,3 +110,9 @@ export const completeScript = (data, templateName) => {
45
110
  export const templateExists = (templateName) => {
46
111
  return getScriptFromPromptTemplate(templateName) !== undefined;
47
112
  };
113
+ /**
114
+ * Check if style exists (by name or file path)
115
+ */
116
+ export const styleExists = (style) => {
117
+ return getStyle(style) !== undefined;
118
+ };
@@ -1,35 +1,113 @@
1
1
  import { marked } from "marked";
2
2
  import puppeteer from "puppeteer";
3
3
  const isCI = process.env.CI === "true";
4
+ const reuseBrowser = process.env.MULMO_PUPPETEER_REUSE !== "0";
5
+ const browserLaunchArgs = isCI ? ["--no-sandbox"] : [];
6
+ // Shared browser to avoid spawning a new Chromium per render.
7
+ let sharedBrowserPromise = null;
8
+ let sharedBrowserRefs = 0;
9
+ let sharedBrowserCloseTimer = null;
10
+ // Acquire a browser instance; reuse a shared one when enabled.
11
+ const acquireBrowser = async () => {
12
+ if (!reuseBrowser) {
13
+ return await puppeteer.launch({ args: browserLaunchArgs });
14
+ }
15
+ sharedBrowserRefs += 1;
16
+ if (sharedBrowserCloseTimer) {
17
+ clearTimeout(sharedBrowserCloseTimer);
18
+ sharedBrowserCloseTimer = null;
19
+ }
20
+ if (!sharedBrowserPromise) {
21
+ sharedBrowserPromise = puppeteer.launch({ args: browserLaunchArgs });
22
+ }
23
+ const currentPromise = sharedBrowserPromise;
24
+ try {
25
+ return await currentPromise;
26
+ }
27
+ catch (error) {
28
+ if (sharedBrowserPromise === currentPromise) {
29
+ sharedBrowserPromise = null;
30
+ }
31
+ sharedBrowserRefs = Math.max(0, sharedBrowserRefs - 1);
32
+ throw error;
33
+ }
34
+ };
35
+ // Release the browser; close only after a short idle window.
36
+ const releaseBrowser = async (browser) => {
37
+ if (!reuseBrowser) {
38
+ await browser.close().catch(() => { });
39
+ return;
40
+ }
41
+ sharedBrowserRefs = Math.max(0, sharedBrowserRefs - 1);
42
+ if (sharedBrowserRefs > 0 || !sharedBrowserPromise) {
43
+ return;
44
+ }
45
+ // Delay close to allow back-to-back renders to reuse the browser.
46
+ sharedBrowserCloseTimer = setTimeout(async () => {
47
+ const current = sharedBrowserPromise;
48
+ sharedBrowserPromise = null;
49
+ sharedBrowserCloseTimer = null;
50
+ if (current) {
51
+ await (await current).close().catch(() => { });
52
+ }
53
+ }, 300);
54
+ };
55
+ // Wait for a single animation frame to let canvas paints settle.
56
+ const waitForNextFrame = async (page) => {
57
+ await page.evaluate(() => new Promise((resolve) => {
58
+ requestAnimationFrame(() => resolve());
59
+ }));
60
+ };
4
61
  export const renderHTMLToImage = async (html, outputPath, width, height, isMermaid = false, omitBackground = false) => {
5
- // Use Puppeteer to render HTML to an image
6
- const browser = await puppeteer.launch({
7
- args: isCI ? ["--no-sandbox"] : [],
8
- });
9
- const page = await browser.newPage();
10
- // Set the page content to the HTML generated from the Markdown
11
- await page.setContent(html);
12
- // Adjust page settings if needed (like width, height, etc.)
13
- await page.setViewport({ width, height });
14
- await page.addStyleTag({ content: "html,body{margin:0;padding:0;overflow:hidden}" });
15
- if (isMermaid) {
16
- await page.waitForFunction(() => {
17
- const el = document.querySelector(".mermaid");
18
- return el && el.dataset.ready === "true";
19
- }, { timeout: 20000 });
20
- }
21
- // Measure the size of the page and scale the page to the width and height
22
- await page.evaluate(({ vw, vh }) => {
23
- const de = document.documentElement;
24
- const sw = Math.max(de.scrollWidth, document.body.scrollWidth || 0);
25
- const sh = Math.max(de.scrollHeight, document.body.scrollHeight || 0);
26
- const scale = Math.min(vw / (sw || vw), vh / (sh || vh), 1); // <=1 で縮小のみ
27
- de.style.overflow = "hidden";
28
- document.body.style.zoom = String(scale);
29
- }, { vw: width, vh: height });
30
- // Step 3: Capture screenshot of the page (which contains the Markdown-rendered HTML)
31
- await page.screenshot({ path: outputPath, omitBackground });
32
- await browser.close();
62
+ // Charts are rendered in a dedicated browser to avoid shared-page timing issues.
63
+ const useSharedBrowser = reuseBrowser && !html.includes("data-chart-ready");
64
+ const browser = useSharedBrowser ? await acquireBrowser() : await puppeteer.launch({ args: browserLaunchArgs });
65
+ let page = null;
66
+ try {
67
+ page = await browser.newPage();
68
+ // Adjust page settings if needed (like width, height, etc.)
69
+ await page.setViewport({ width, height });
70
+ // Set the page content to the HTML generated from the Markdown
71
+ await page.setContent(html, { waitUntil: "domcontentloaded" });
72
+ await page.addStyleTag({ content: "html,body{margin:0;padding:0;overflow:hidden}" });
73
+ if (isMermaid) {
74
+ await page.waitForFunction(() => {
75
+ const element = document.querySelector(".mermaid");
76
+ return element && element.dataset.ready === "true";
77
+ }, { timeout: 20000 });
78
+ }
79
+ if (html.includes("data-chart-ready")) {
80
+ await page.waitForFunction(() => {
81
+ const canvas = document.querySelector("canvas[data-chart-ready='true']");
82
+ return !!canvas;
83
+ }, { timeout: 20000 });
84
+ // Give the browser a couple of frames to paint the canvas.
85
+ await waitForNextFrame(page);
86
+ await waitForNextFrame(page);
87
+ }
88
+ // Measure the size of the page and scale the page to the width and height
89
+ await page.evaluate(({ vw, vh }) => {
90
+ const documentElement = document.documentElement;
91
+ const scrollWidth = Math.max(documentElement.scrollWidth, document.body.scrollWidth || 0);
92
+ const scrollHeight = Math.max(documentElement.scrollHeight, document.body.scrollHeight || 0);
93
+ const scale = Math.min(vw / (scrollWidth || vw), vh / (scrollHeight || vh), 1); // <=1 で縮小のみ
94
+ documentElement.style.overflow = "hidden";
95
+ document.body.style.zoom = String(scale);
96
+ }, { vw: width, vh: height });
97
+ // Step 3: Capture screenshot of the page (which contains the Markdown-rendered HTML)
98
+ await page.screenshot({ path: outputPath, omitBackground });
99
+ }
100
+ finally {
101
+ if (page) {
102
+ await page.close().catch(() => { });
103
+ }
104
+ if (useSharedBrowser) {
105
+ await releaseBrowser(browser);
106
+ }
107
+ else {
108
+ await browser.close().catch(() => { });
109
+ }
110
+ }
33
111
  };
34
112
  export const renderMarkdownToImage = async (markdown, style, outputPath, width, height) => {
35
113
  const header = `<head><style>${style}</style></head>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.1.25",
3
+ "version": "2.1.27",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",