mulmocast 2.10.0 → 2.12.0

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 (34) hide show
  1. package/assets/schemas/mulmo_script.json +15431 -190
  2. package/lib/actions/html.js +22 -4
  3. package/lib/actions/image_agents.js +2 -2
  4. package/lib/actions/markdown.js +5 -2
  5. package/lib/mcp/server.js +5 -3
  6. package/lib/methods/mulmo_beat_node.d.ts +3 -3
  7. package/lib/types/type.d.ts +8 -0
  8. package/lib/utils/beat_html/type.d.ts +8 -1
  9. package/lib/utils/context.d.ts +24 -0
  10. package/lib/utils/context.js +1 -1
  11. package/lib/utils/html_escape.d.ts +2 -0
  12. package/lib/utils/html_escape.js +16 -0
  13. package/lib/utils/image_plugins/beat.d.ts +2 -2
  14. package/lib/utils/image_plugins/bg_image_util.d.ts +4 -1
  15. package/lib/utils/image_plugins/bg_image_util.js +5 -3
  16. package/lib/utils/image_plugins/chart.d.ts +3 -3
  17. package/lib/utils/image_plugins/html_tailwind.d.ts +3 -3
  18. package/lib/utils/image_plugins/image.d.ts +1 -1
  19. package/lib/utils/image_plugins/index.d.ts +7 -7
  20. package/lib/utils/image_plugins/markdown.d.ts +4 -4
  21. package/lib/utils/image_plugins/mermaid.d.ts +4 -4
  22. package/lib/utils/image_plugins/movie.d.ts +4 -4
  23. package/lib/utils/image_plugins/slide.d.ts +3 -3
  24. package/lib/utils/image_plugins/slide.js +21 -2
  25. package/lib/utils/image_plugins/source.d.ts +2 -2
  26. package/lib/utils/image_plugins/text_slide.d.ts +4 -4
  27. package/lib/utils/image_plugins/utils.d.ts +2 -2
  28. package/lib/utils/image_plugins/vision.d.ts +3 -3
  29. package/lib/utils/image_plugins/voice_over.d.ts +2 -2
  30. package/lib/utils/swipe_to_html.d.ts +7 -0
  31. package/lib/utils/swipe_to_html.js +8 -1
  32. package/package.json +12 -9
  33. package/lib/utils/sdk_timeout.d.ts +0 -3
  34. package/lib/utils/sdk_timeout.js +0 -7
@@ -1,9 +1,21 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { isNull } from "graphai";
4
+ import { escapeHtml, slideUtilityCss } from "@mulmocast/deck";
4
5
  import { localizedText } from "../utils/utils.js";
5
6
  import { writingMessage } from "../utils/file.js";
6
7
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
8
+ /**
9
+ * The HTML document for a studio.
10
+ *
11
+ * Every value that reaches an attribute or `<title>` comes from the script, and a beat with a
12
+ * `source: { kind: "path" }` image puts the author's own path into `src`, so each one is
13
+ * escaped for the context it lands in.
14
+ *
15
+ * Not exported: `actions/index.ts` re-exports this module with `export *` and `index.node.ts`
16
+ * re-exports that, so anything exported here becomes public API of the published package. The
17
+ * tests drive it through `html()` instead.
18
+ */
7
19
  const generateHtmlContent = (context, imageWidth) => {
8
20
  const { studio, multiLingual, lang = "en" } = context;
9
21
  const title = studio.script.title || "MulmoCast Content";
@@ -20,11 +32,14 @@ const generateHtmlContent = (context, imageWidth) => {
20
32
  html += `${studioBeat.html}\n\n`;
21
33
  }
22
34
  else if (studioBeat?.imageFile && isNull(studioBeat.html)) {
23
- const imagePath = path.relative(context.fileDirs.outDirPath, studioBeat.imageFile);
24
- const altText = `Beat ${index + 1}`;
35
+ // A `source: { kind: "path" }` beat puts the author's own path here, so it reaches an
36
+ // HTML attribute unfiltered unless it is escaped. Every value in these attributes goes
37
+ // through the same rule rather than each being argued safe on its own.
38
+ const imagePath = escapeHtml(path.relative(context.fileDirs.outDirPath, studioBeat.imageFile));
39
+ const altText = escapeHtml(`Beat ${index + 1}`);
25
40
  if (imageWidth) {
26
41
  // Use HTML img tag for width control
27
- html += `<img src="${imagePath}" alt="${altText}" width="${imageWidth}" />\n\n`;
42
+ html += `<img src="${imagePath}" alt="${altText}" width="${escapeHtml(imageWidth)}" />\n\n`;
28
43
  }
29
44
  else {
30
45
  // Use standard html image syntax
@@ -42,13 +57,16 @@ const generateHtmlContent = (context, imageWidth) => {
42
57
  <head>
43
58
  <meta charset="UTF-8" />
44
59
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
45
- <title>${title}</title>
60
+ <title>${escapeHtml(title)}</title>
46
61
  <!-- Tailwind CSS CDN -->
47
62
  <script src="https://cdn.tailwindcss.com"></script>
48
63
  <!-- Chart.js CDN -->
49
64
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
50
65
  <!-- Mermaid CDN -->
51
66
  <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
67
+ <!-- The utilities every slide fragment is written against. Shared, so it belongs to the
68
+ page: a slide beat emits only its own scoped rules. -->
69
+ <style>${slideUtilityCss}</style>
52
70
  </head>
53
71
  <body class="min-h-screen flex flex-col">
54
72
  ${html}
@@ -202,8 +202,8 @@ export const imagePreprocessAgent = async (namedInputs) => {
202
202
  if (beat.image) {
203
203
  const plugin = MulmoBeatMethods.getPlugin(beat);
204
204
  const pluginPath = plugin.path({ beat, context, imagePath, ...htmlStyle(context, beat) });
205
- const markdown = plugin.markdown ? plugin.markdown({ beat, context, imagePath, ...htmlStyle(context, beat) }) : undefined;
206
- const html = plugin.html ? await plugin.html({ beat, context, imagePath, ...htmlStyle(context, beat) }) : undefined;
205
+ const markdown = plugin.markdown ? plugin.markdown({ beat, context }) : undefined;
206
+ const html = plugin.html ? await plugin.html({ beat, context }) : undefined;
207
207
  const isTypeMovie = beat.image.type === ImageMediaType.Movie;
208
208
  const isAnimatedHtml = MulmoBeatMethods.isAnimatedHtmlTailwind(beat);
209
209
  // animation and moviePrompt cannot be used together
@@ -3,6 +3,7 @@ import { localizedText } from "../utils/utils.js";
3
3
  import { writingMessage } from "../utils/file.js";
4
4
  import { MulmoStudioContextMethods } from "../methods/mulmo_studio_context.js";
5
5
  import path from "path";
6
+ import { escapeHtml } from "@mulmocast/deck";
6
7
  const generateMarkdownContent = (context, imageWidth) => {
7
8
  const { studio, multiLingual, lang = "en" } = context;
8
9
  const title = studio.script.title || "MulmoCast Content";
@@ -21,9 +22,11 @@ const generateMarkdownContent = (context, imageWidth) => {
21
22
  else if (studioBeat?.imageFile) {
22
23
  const imagePath = path.relative(context.fileDirs.outDirPath, studioBeat.imageFile);
23
24
  if (imageWidth) {
24
- // Use HTML img tag for width control
25
+ // An HTML tag is HTML wherever the markdown is rendered, and a
26
+ // `source: { kind: "path" }` beat puts the author's own path here — the same shape
27
+ // and the same input as the `<img>` in actions/html.ts.
25
28
  const altText = `Beat ${index + 1}`;
26
- markdown += `<img src="${imagePath}" alt="${altText}" width="${imageWidth}" />\n\n`;
29
+ markdown += `<img src="${escapeHtml(imagePath)}" alt="${escapeHtml(altText)}" width="${escapeHtml(imageWidth)}" />\n\n`;
27
30
  }
28
31
  else {
29
32
  // Use standard markdown image syntax
package/lib/mcp/server.js CHANGED
@@ -15,8 +15,10 @@ import { MulmoScriptMethods } from "../methods/index.js";
15
15
  dotenv.config({ quiet: true });
16
16
  const __filename = fileURLToPath(import.meta.url);
17
17
  const __dirname = path.dirname(__filename);
18
- // Load MulmoScript JSON Schema from file
19
- const MULMO_SCRIPT_JSON_SCHEMA = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../assets/schemas/html_prompt.json"), "utf8"));
18
+ // The `mulmoScript` argument's shape, as advertised to an MCP client. It is html_prompt.json,
19
+ // not mulmo_script.json — the switch was deliberate, but the old name outlived it and is why
20
+ // nobody noticed that mulmo_script.json had no reader left for fourteen months.
21
+ const HTML_PROMPT_JSON_SCHEMA = JSON.parse(fs.readFileSync(path.resolve(__dirname, "../../assets/schemas/html_prompt.json"), "utf8"));
20
22
  const server = new Server({
21
23
  name: "mulmocast-mcp",
22
24
  version: "0.1.0",
@@ -53,7 +55,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
53
55
  enum: ["movie", "pdf"],
54
56
  description: "Command to execute: 'movie' to generate video, 'pdf' to generate PDF",
55
57
  },
56
- mulmoScript: MULMO_SCRIPT_JSON_SCHEMA,
58
+ mulmoScript: HTML_PROMPT_JSON_SCHEMA,
57
59
  options: {
58
60
  type: "object",
59
61
  description: "Optional generation parameters",
@@ -3,9 +3,9 @@ export declare const MulmoBeatMethods: {
3
3
  getPlugin(beat: MulmoBeat): {
4
4
  imageType: string;
5
5
  process: (params: import("../types/type.js").ImageProcessorParams) => Promise<string | undefined> | void;
6
- path: (params: import("../types/type.js").ImageProcessorParams) => string | undefined;
7
- markdown?: (params: import("../types/type.js").ImageProcessorParams) => string | undefined;
8
- html?: (params: import("../types/type.js").ImageProcessorParams) => Promise<string | undefined>;
6
+ path: (params: import("../types/type.js").BeatPathParams) => string | undefined;
7
+ markdown?: (params: import("../types/type.js").BeatRenderParams) => string | undefined;
8
+ html?: (params: import("../types/type.js").BeatRenderParams) => Promise<string | undefined>;
9
9
  };
10
10
  isAnimationEnabled: (animation: unknown) => animation is true | {
11
11
  fps?: number;
@@ -93,6 +93,14 @@ export type ImageProcessorParams = {
93
93
  movieRefs?: Record<string, string>;
94
94
  beatDuration?: number;
95
95
  };
96
+ /**
97
+ * What a plugin's `markdown()` / `html()` reads. They return a string and touch no file, so
98
+ * the paths and sizes `process()` needs are not theirs. `imageRefs` is here because a slide
99
+ * resolves image references while rendering; it is optional, so a caller with none omits it.
100
+ */
101
+ export type BeatRenderParams = Pick<ImageProcessorParams, "beat" | "context" | "imageRefs">;
102
+ /** What a plugin's `path()` reads: the source-backed ones resolve against beat and context. */
103
+ export type BeatPathParams = Pick<ImageProcessorParams, "beat" | "context" | "imagePath">;
96
104
  export type PDFMode = (typeof pdf_modes)[number];
97
105
  export type PDFSize = (typeof pdf_sizes)[number];
98
106
  export type Text2ImageAgentInfo = {
@@ -21,7 +21,14 @@ export type BeatHtmlFragment = {
21
21
  * since it has no DOM and must stay dependency-light to remain bundleable.
22
22
  */
23
23
  html: string;
24
- /** Rules the markup needs, scoped by the caller. Absent when the markup needs none. */
24
+ /**
25
+ * Rules the markup needs, scoped by the caller. Absent when the markup needs none.
26
+ *
27
+ * It cannot carry everything: an `html_tailwind` beat written with `elements` is a root
28
+ * asking for 100% of its parent, and how tall that parent should be is the host's layout,
29
+ * not the beat's. The root is marked `data-mulmo-swipe-root` so a host can give it one —
30
+ * and a host that does not gets an empty box, silently.
31
+ */
25
32
  css?: string;
26
33
  /** External runtimes the host must load once for the page, not once per beat. */
27
34
  requires?: BeatRuntime[];
@@ -3,6 +3,30 @@ import { UsageCollector } from "./usage_collector.js";
3
3
  export declare const silentMp3 = "https://github.com/receptron/mulmocast-cli/raw/refs/heads/main/assets/audio/silent300.mp3";
4
4
  export type MulmoErrorFormatter = (error: unknown) => string | null;
5
5
  export declare const setMulmoErrorFormatter: (formatter: MulmoErrorFormatter | null) => void;
6
+ export declare const initSessionState: () => {
7
+ inSession: {
8
+ audio: boolean;
9
+ image: boolean;
10
+ video: boolean;
11
+ multiLingual: boolean;
12
+ caption: boolean;
13
+ pdf: boolean;
14
+ markdown: boolean;
15
+ html: boolean;
16
+ viewer: boolean;
17
+ };
18
+ inBeatSession: {
19
+ audio: {};
20
+ image: {};
21
+ movie: {};
22
+ multiLingual: {};
23
+ caption: {};
24
+ html: {};
25
+ imageReference: {};
26
+ soundEffect: {};
27
+ lipSync: {};
28
+ };
29
+ };
6
30
  export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: string, videoCaptionLang?: string, presentationStyle?: MulmoPresentationStyle | null) => {
7
31
  script: {
8
32
  $mulmocast: {
@@ -36,7 +36,7 @@ const mulmoCredit = (speaker, isPortrait) => {
36
36
  },
37
37
  };
38
38
  };
39
- const initSessionState = () => {
39
+ export const initSessionState = () => {
40
40
  return {
41
41
  inSession: {
42
42
  audio: false,
@@ -11,4 +11,6 @@
11
11
  */
12
12
  /** Neutralize the output of JSON.stringify for embedding inside an inline `<script>`. */
13
13
  export declare const escapeJsonForScript: (json: string) => string;
14
+ /** Neutralize a value placed inside a quoted CSS string, such as the argument of `url()`. */
15
+ export declare const escapeCssString: (value: string) => string;
14
16
  export declare const neutralizeStyleTerminator: (css: string) => string;
@@ -22,6 +22,22 @@ const SCRIPT_JSON_ESCAPES = new Map([
22
22
  ]);
23
23
  /** Neutralize the output of JSON.stringify for embedding inside an inline `<script>`. */
24
24
  export const escapeJsonForScript = (json) => json.replace(/[<>&\u2028\u2029]/g, (character) => SCRIPT_JSON_ESCAPES.get(character) ?? character);
25
+ // A CSS string ends at the first unescaped quote of the kind that opened it, and a raw
26
+ // newline ends the declaration outright, so a value carrying either escapes the string it
27
+ // was placed in. Backslash is what CSS defines for both; a line break becomes its hex escape
28
+ // with the terminating space CSS requires. Both quote characters are escaped so a caller may
29
+ // use either, and every other character — including the base64 body of a data URL — is left
30
+ // exactly as it was.
31
+ const CSS_STRING_ESCAPES = new Map([
32
+ ["\\", "\\\\"],
33
+ ["'", "\\'"],
34
+ ['"', '\\"'],
35
+ ["\n", "\\a "],
36
+ ["\r", "\\d "],
37
+ ["\f", "\\c "],
38
+ ]);
39
+ /** Neutralize a value placed inside a quoted CSS string, such as the argument of `url()`. */
40
+ export const escapeCssString = (value) => value.replace(/[\\'"\n\r\f]/g, (character) => CSS_STRING_ESCAPES.get(character) ?? character);
25
41
  // A <style> element holds raw text: nothing inside it is parsed as markup except the closing
26
42
  // tag, so `</style` is the only sequence that can end it. Neutralizing just that keeps
27
43
  // author-supplied CSS working, which escaping the whole string would not. `\3c ` is the CSS
@@ -1,5 +1,5 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatPathParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "beat";
3
3
  export declare const processBeatReference: (__: ImageProcessorParams) => Promise<undefined>;
4
4
  export declare const process: (__: ImageProcessorParams) => Promise<undefined>;
5
- export declare const path: (__: ImageProcessorParams) => undefined;
5
+ export declare const path: (__: BeatPathParams) => undefined;
@@ -5,9 +5,12 @@ import { BackgroundImage, MulmoStudioContext, ImageProcessorParams } from "../..
5
5
  * null explicitly disables background image.
6
6
  */
7
7
  export declare const resolveBackgroundImage: (beatBackgroundImage: BackgroundImage | undefined, globalBackgroundImage: BackgroundImage | undefined) => BackgroundImage | undefined;
8
+ /** The background image comes from the script, the style from the beat; the paths and sizes `process()` needs are not read here. */
9
+ type CombinedStyleParams = Pick<ImageProcessorParams, "context" | "textSlideStyle">;
8
10
  /**
9
11
  * Resolve combined style from background image and custom style.
10
12
  * Common pattern used by markdown, textSlide, mermaid plugins.
11
13
  */
12
- export declare const resolveCombinedStyle: (params: ImageProcessorParams, beatBackgroundImage: BackgroundImage | undefined, beatStyle: string | undefined) => Promise<string>;
14
+ export declare const resolveCombinedStyle: (params: CombinedStyleParams, beatBackgroundImage: BackgroundImage | undefined, beatStyle: string | undefined) => Promise<string>;
13
15
  export declare const backgroundImageToCSS: (backgroundImage: BackgroundImage | undefined, context: MulmoStudioContext) => Promise<string>;
16
+ export {};
@@ -1,7 +1,7 @@
1
1
  import { MulmoMediaSourceMethods } from "../../methods/mulmo_media_source.js";
2
2
  import { resolveStyle } from "./utils.js";
3
3
  import { safeFetch, DEFAULT_FETCH_TIMEOUT_MS } from "../fetch.js";
4
- import { neutralizeStyleTerminator } from "../html_escape.js";
4
+ import { escapeCssString, neutralizeStyleTerminator } from "../html_escape.js";
5
5
  /**
6
6
  * Resolve background image from beat level and global level settings.
7
7
  * Beat level takes precedence over global level.
@@ -58,6 +58,8 @@ export const backgroundImageToCSS = async (backgroundImage, context) => {
58
58
  return "";
59
59
  }
60
60
  const isSimpleUrl = typeof backgroundImage === "string";
61
+ // Escaped at each use rather than here: the base64 body cannot carry a quote, but the
62
+ // content-type in front of it is whatever the remote server's response header said.
61
63
  const imageUrl = isSimpleUrl ? await fetchUrlAsDataUrl(backgroundImage) : await MulmoMediaSourceMethods.toDataUrl(backgroundImage.source, context);
62
64
  const size = sizeToCSS(isSimpleUrl ? "cover" : (backgroundImage.size ?? "cover"));
63
65
  const opacity = isSimpleUrl ? 1 : (backgroundImage.opacity ?? 1);
@@ -75,7 +77,7 @@ export const backgroundImageToCSS = async (backgroundImage, context) => {
75
77
  left: 0;
76
78
  right: 0;
77
79
  bottom: 0;
78
- background-image: url('${imageUrl}');
80
+ background-image: url('${escapeCssString(imageUrl)}');
79
81
  background-size: ${size};
80
82
  background-position: center;
81
83
  background-repeat: no-repeat;
@@ -90,7 +92,7 @@ export const backgroundImageToCSS = async (backgroundImage, context) => {
90
92
  margin: 0;
91
93
  }
92
94
  body {
93
- background-image: url('${imageUrl}');
95
+ background-image: url('${escapeCssString(imageUrl)}');
94
96
  background-size: ${size};
95
97
  background-position: center;
96
98
  background-repeat: no-repeat;
@@ -1,5 +1,5 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "chart";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
- export declare const path: (params: ImageProcessorParams) => string;
5
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
4
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
5
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -1,4 +1,4 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "html_tailwind";
3
3
  /**
4
4
  * Resolve image:name references to file:// absolute paths using imageRefs.
@@ -21,5 +21,5 @@ export declare const resolveRelativeImagePaths: (html: string, baseDirPath: stri
21
21
  */
22
22
  export declare const resolveRelativeModelPathsInScript: (html: string, baseDirPath: string) => string;
23
23
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
24
- export declare const path: (params: ImageProcessorParams) => string;
25
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
24
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
25
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -1,3 +1,3 @@
1
1
  export declare const imageType = "image";
2
2
  export declare const process: (params: import("../../index.common.js").ImageProcessorParams) => Promise<string | undefined>;
3
- export declare const path: (params: import("../../index.common.js").ImageProcessorParams) => string | undefined;
3
+ export declare const path: (params: import("../../index.common.js").BeatPathParams) => string | undefined;
@@ -1,15 +1,15 @@
1
- import { ImageProcessorParams, MulmoBeat } from "../../types/index.js";
1
+ import { BeatPathParams, BeatRenderParams, ImageProcessorParams, MulmoBeat } from "../../types/index.js";
2
2
  export declare const findImagePlugin: (imageType?: string) => {
3
3
  imageType: string;
4
4
  process: (params: ImageProcessorParams) => Promise<string | undefined> | void;
5
- path: (params: ImageProcessorParams) => string | undefined;
6
- markdown?: (params: ImageProcessorParams) => string | undefined;
7
- html?: (params: ImageProcessorParams) => Promise<string | undefined>;
5
+ path: (params: BeatPathParams) => string | undefined;
6
+ markdown?: (params: BeatRenderParams) => string | undefined;
7
+ html?: (params: BeatRenderParams) => Promise<string | undefined>;
8
8
  } | undefined;
9
9
  export declare const getBeatPlugin: (beat: MulmoBeat) => {
10
10
  imageType: string;
11
11
  process: (params: ImageProcessorParams) => Promise<string | undefined> | void;
12
- path: (params: ImageProcessorParams) => string | undefined;
13
- markdown?: (params: ImageProcessorParams) => string | undefined;
14
- html?: (params: ImageProcessorParams) => Promise<string | undefined>;
12
+ path: (params: BeatPathParams) => string | undefined;
13
+ markdown?: (params: BeatRenderParams) => string | undefined;
14
+ html?: (params: BeatRenderParams) => Promise<string | undefined>;
15
15
  };
@@ -1,6 +1,6 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "markdown";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
- export declare const path: (params: ImageProcessorParams) => string;
5
- export declare const markdown: (params: ImageProcessorParams) => string | undefined;
6
- export declare const html: (params: ImageProcessorParams) => Promise<string>;
4
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
5
+ export declare const markdown: (params: BeatRenderParams) => string | undefined;
6
+ export declare const html: (params: BeatRenderParams) => Promise<string>;
@@ -1,8 +1,8 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "mermaid";
3
3
  /** Mermaid markup for this path. The markup itself is shared with the browser path. */
4
4
  export declare const generateMermaidHtml: (code: string, title?: string) => string;
5
5
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
6
- export declare const path: (params: ImageProcessorParams) => string;
7
- export declare const markdown: (params: ImageProcessorParams) => string | undefined;
8
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
6
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
7
+ export declare const markdown: (params: BeatRenderParams) => string | undefined;
8
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -1,5 +1,5 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams } from "../../types/index.js";
2
2
  export declare const imageType = "movie";
3
- export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
- export declare const path: (params: ImageProcessorParams) => string | undefined;
5
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
3
+ export declare const process: (params: import("../../types/type.js").ImageProcessorParams) => Promise<string | undefined>;
4
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string | undefined;
5
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -1,4 +1,4 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  import type { SlideLayout, ContentBlock } from "@mulmocast/deck";
3
3
  export declare const imageType = "slide";
4
4
  /**
@@ -15,5 +15,5 @@ export declare const collectContentArrays: (slide: SlideLayout) => ContentBlock[
15
15
  */
16
16
  export declare const resolveSlideImageRefs: (slide: SlideLayout, imageRefs: Record<string, string>, converter?: (filePath: string) => string) => SlideLayout;
17
17
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
18
- export declare const path: (params: ImageProcessorParams) => string;
19
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
18
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
19
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -1,6 +1,6 @@
1
1
  import nodePath from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
- import { generateSlideHTML } from "@mulmocast/deck";
3
+ import { generateSlideFragment, generateSlideHTML } from "@mulmocast/deck";
4
4
  import { renderHTMLToImage } from "../html_render.js";
5
5
  import { parrotingImagePath } from "./utils.js";
6
6
  import { pathToDataUrl } from "../../methods/mulmo_media_source.js";
@@ -170,6 +170,24 @@ const processSlide = async (params) => {
170
170
  await renderHTMLToImage(html, imagePath, canvasSize.width, canvasSize.height);
171
171
  return imagePath;
172
172
  };
173
+ /**
174
+ * A slide beat as markup for the export document, not as a document of its own.
175
+ *
176
+ * `generateSlideHTML` above is right for the PNG path, where the slide IS the page. Here the
177
+ * result is concatenated into one `<body>` alongside every other beat, so a full document per
178
+ * slide meant N Tailwind CDN loads, N `tailwind.config` assignments racing each other, and an
179
+ * `html, body { overflow: hidden }` escaping into the export page — which made a document of
180
+ * stacked slides unscrollable. `beat_html/slide.ts` already draws the browser path from
181
+ * `generateSlideFragment`; this is the same change for the dump path.
182
+ *
183
+ * The fragment's own css is emitted next to it because it is confined to `scopeClass`, which
184
+ * deck makes unique per call. What is SHARED — Tailwind, the chart and mermaid runtimes,
185
+ * `slideUtilityCss` — belongs to the page and is `actions/html.ts`'s job, once.
186
+ *
187
+ * The box is here rather than left to the host: the fragment is `w-full h-full`, so without
188
+ * one it collapses to nothing, and `actions/html.ts` concatenates beats without knowing which
189
+ * of them is a slide.
190
+ */
173
191
  const dumpHtml = async (params) => {
174
192
  const { beat } = params;
175
193
  if (!beat.image || beat.image.type !== imageType)
@@ -178,7 +196,8 @@ const dumpHtml = async (params) => {
178
196
  const slide = resolveSlide(params);
179
197
  const reference = beat.image.reference;
180
198
  const resolvedBranding = await resolveAndConvertBranding(params);
181
- return generateSlideHTML(theme, slide, reference, resolvedBranding);
199
+ const fragment = generateSlideFragment(theme, slide, { reference, branding: resolvedBranding });
200
+ return `<style>${fragment.css}</style>\n<div style="aspect-ratio: 16 / 9;">${fragment.html}</div>`;
182
201
  };
183
202
  export const process = processSlide;
184
203
  export const path = parrotingImagePath;
@@ -1,3 +1,3 @@
1
- import { type ImageProcessorParams, type ImageType } from "../../types/index.js";
1
+ import { BeatPathParams, type ImageProcessorParams, type ImageType } from "../../types/index.js";
2
2
  export declare const processSource: (imageType: ImageType) => (params: ImageProcessorParams) => Promise<string | undefined>;
3
- export declare const pathSource: (imageType: ImageType) => (params: ImageProcessorParams) => string | undefined;
3
+ export declare const pathSource: (imageType: ImageType) => (params: BeatPathParams) => string | undefined;
@@ -1,6 +1,6 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "textSlide";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
- export declare const path: (params: ImageProcessorParams) => string;
5
- export declare const markdown: (params: ImageProcessorParams) => string | undefined;
6
- export declare const html: (params: ImageProcessorParams) => Promise<string>;
4
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
5
+ export declare const markdown: (params: BeatRenderParams) => string | undefined;
6
+ export declare const html: (params: BeatRenderParams) => Promise<string>;
@@ -1,4 +1,4 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
2
- export declare const parrotingImagePath: (params: ImageProcessorParams) => string;
1
+ import { BeatPathParams } from "../../types/index.js";
2
+ export declare const parrotingImagePath: (params: BeatPathParams) => string;
3
3
  export declare const resolveStyle: (styleName: string | undefined, fallbackStyle: string) => string;
4
4
  export declare const generateUniqueId: (prefix: string) => string;
@@ -1,5 +1,5 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatRenderParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "vision";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
- export declare const path: (params: ImageProcessorParams) => string;
5
- export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
4
+ export declare const path: (params: import("../../types/type.js").BeatPathParams) => string;
5
+ export declare const html: (params: BeatRenderParams) => Promise<string | undefined>;
@@ -1,5 +1,5 @@
1
- import { ImageProcessorParams } from "../../types/index.js";
1
+ import { BeatPathParams, ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "voice_over";
3
3
  export declare const processBeatReference: (__: ImageProcessorParams) => Promise<undefined>;
4
4
  export declare const process: (__: ImageProcessorParams) => Promise<undefined>;
5
- export declare const path: (__: ImageProcessorParams) => undefined;
5
+ export declare const path: (__: BeatPathParams) => undefined;
@@ -67,6 +67,13 @@ export interface SwipeElement {
67
67
  * this a sound narrowing rather than a cast wearing a guard's clothes.
68
68
  */
69
69
  export declare const isSwipeElements: (value: unknown) => value is SwipeElement[];
70
+ /**
71
+ * The root the elements are positioned inside. Named so a browser host can give it a height:
72
+ * the root asks for 100% of its parent, and an inline height wins over any rule aimed at the
73
+ * root itself, so the parent is where the box has to come from. The document path has a
74
+ * viewport to fill and needs nothing.
75
+ */
76
+ export declare const SWIPE_ROOT_ATTRIBUTE = "data-mulmo-swipe-root";
70
77
  /** Generate HTML from Swipe elements */
71
78
  export declare const swipeElementsToHtml: (elements: SwipeElement[]) => string;
72
79
  /** Generate render() script from Swipe element animations */
@@ -113,10 +113,17 @@ const elementToHtml = (el, index) => {
113
113
  lines.push("</div>");
114
114
  return lines.join("\n");
115
115
  };
116
+ /**
117
+ * The root the elements are positioned inside. Named so a browser host can give it a height:
118
+ * the root asks for 100% of its parent, and an inline height wins over any rule aimed at the
119
+ * root itself, so the parent is where the box has to come from. The document path has a
120
+ * viewport to fill and needs nothing.
121
+ */
122
+ export const SWIPE_ROOT_ATTRIBUTE = "data-mulmo-swipe-root";
116
123
  /** Generate HTML from Swipe elements */
117
124
  export const swipeElementsToHtml = (elements) => {
118
125
  const html = elements.map((el, i) => elementToHtml(el, i)).join("\n");
119
- return `<div style="position:relative; width:100%; height:100%; overflow:hidden;">\n${html}\n</div>`;
126
+ return `<div ${SWIPE_ROOT_ATTRIBUTE} style="position:relative; width:100%; height:100%; overflow:hidden;">\n${html}\n</div>`;
120
127
  };
121
128
  const collectAnimations = (elements, entries, indexBase = 0) => {
122
129
  elements.forEach((el, i) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.10.0",
3
+ "version": "2.12.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -56,15 +56,17 @@
56
56
  "images": "npx tsx ./src/cli/bin.ts images",
57
57
  "preprocess": "npx tsx ./src/cli/bin.ts preprocess",
58
58
  "pdf": "npx tsx ./src/cli/bin.ts pdf",
59
- "test": "rm -f scratchpad/test*.* && npx tsx ./src/audio.ts scripts/test/test.json && npx tsx ./src/images.ts scripts/test/test.json && npx tsx ./src/movie.ts scripts/test/test.json",
59
+ "test": "rm -f scratchpad/test*.* && npx tsx ./src/cli/bin.ts audio scripts/test/test.json && npx tsx ./src/cli/bin.ts images scripts/test/test.json && npx tsx ./src/cli/bin.ts movie scripts/test/test.json",
60
60
  "ci_test": "cross-env NODE_ENV=test tsx --test --experimental-test-coverage ./test/*/test_*.ts",
61
61
  "lint": "eslint src test assets/html/js",
62
62
  "build": "tsc",
63
+ "typecheck:test": "tsc --noEmit -p tsconfig.test.json",
63
64
  "build_test": "tsc && git checkout -- lib/*",
64
65
  "cli": "npx tsx ./src/cli/bin.ts",
65
66
  "scripting": "npx tsx ./src/cli/bin.ts tool scripting",
66
67
  "prompt": "npx tsx ./src/cli/bin.ts tool prompt",
67
68
  "schema": "npx tsx ./src/cli/bin.ts tool schema",
69
+ "schema:write": "npx tsx ./src/cli/bin.ts tool schema > assets/schemas/mulmo_script.json",
68
70
  "markdown": "npx tsx ./src/cli/bin.ts markdown",
69
71
  "html": "npx tsx ./src/cli/bin.ts html",
70
72
  "story_to_script": "npx tsx ./src/cli/bin.ts tool story_to_script",
@@ -89,7 +91,7 @@
89
91
  "homepage": "https://github.com/receptron/mulmocast-cli#readme",
90
92
  "dependencies": {
91
93
  "@google-cloud/text-to-speech": "^6.4.1",
92
- "@google/genai": "^2.15.0",
94
+ "@google/genai": "^2.17.1",
93
95
  "@graphai/anthropic_agent": "^2.0.12",
94
96
  "@graphai/browserless_agent": "^2.0.3",
95
97
  "@graphai/gemini_agent": "^2.0.5",
@@ -112,10 +114,10 @@
112
114
  "graphai": "^2.0.18",
113
115
  "js-tiktoken": "^1.0.21",
114
116
  "jsdom": "^29.1.1",
115
- "marked": "^18.0.7",
117
+ "marked": "^18.0.10",
116
118
  "mulmocast-vision": "^1.0.12",
117
119
  "ora": "^9.4.1",
118
- "puppeteer": "^25.4.0",
120
+ "puppeteer": "^25.8.0",
119
121
  "replicate": "^1.4.0",
120
122
  "yaml": "^2.9.0",
121
123
  "yargs": "^18.1.0",
@@ -126,19 +128,20 @@
126
128
  "@receptron/test_utils": "^2.0.3",
127
129
  "@types/archiver": "^8.0.0",
128
130
  "@types/jsdom": "^28.0.3",
131
+ "@types/node": "^26.0.0",
129
132
  "@types/yargs": "^17.0.35",
130
133
  "cross-env": "^10.1.0",
131
134
  "esbuild": "^0.28.2",
132
- "eslint": "^10.8.0",
135
+ "eslint": "^10.8.1",
133
136
  "eslint-config-prettier": "^10.1.8",
134
137
  "eslint-plugin-import": "^2.32.0",
135
138
  "eslint-plugin-prettier": "^5.5.6",
136
139
  "eslint-plugin-sonarjs": "^4.2.0",
137
- "globals": "^17.9.0",
140
+ "globals": "^17.11.0",
138
141
  "prettier": "^3.9.6",
139
- "tsx": "^4.23.5",
142
+ "tsx": "^4.23.12",
140
143
  "typescript": "6.0.3",
141
- "typescript-eslint": "^8.66.0"
144
+ "typescript-eslint": "^8.67.0"
142
145
  },
143
146
  "engines": {
144
147
  "node": ">=22.0.0"
@@ -1,3 +0,0 @@
1
- export declare const GENAI_REQUEST_TIMEOUT_MS = 120000;
2
- export declare const REPLICATE_RUN_TIMEOUT_MS = 600000;
3
- export declare const VIDEO_POLL_TIMEOUT_MS = 1200000;