mulmocast 2.4.6 → 2.4.8

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.
@@ -21,7 +21,7 @@
21
21
  /* Text positioned at the bottom */
22
22
  width: 80%;
23
23
  position: absolute;
24
- bottom: 0px;
24
+ bottom: ${bottomOffset}%;
25
25
  /* Enable text wrapping */
26
26
  word-wrap: break-word;
27
27
  overflow-wrap: break-word;
@@ -1,4 +1,6 @@
1
1
  import { MulmoStudioContext, PublicAPIArgs } from "../types/index.js";
2
2
  import type { GraphData } from "graphai";
3
+ export declare const stripHtmlTags: (text: string) => string;
4
+ export declare const calculateTimingRatios: (splitTexts: string[]) => number[];
3
5
  export declare const caption_graph_data: GraphData;
4
6
  export declare const captions: (context: MulmoStudioContext, args?: PublicAPIArgs) => Promise<MulmoStudioContext>;
@@ -44,13 +44,21 @@ const getSplitTexts = (text, texts, textSplit) => {
44
44
  }
45
45
  return [text];
46
46
  };
47
- // Calculate timing ratios based on text length
48
- const calculateTimingRatios = (splitTexts) => {
49
- const totalLength = splitTexts.reduce((sum, t) => sum + t.length, 0);
47
+ // HTML tags commonly used in caption texts
48
+ const CAPTION_HTML_TAGS = "span|br|b|i|em|strong|u|s|div|p|a|sub|sup|mark";
49
+ const captionTagRegex = new RegExp(`</?(?:${CAPTION_HTML_TAGS})(?:\\s[^>]*)?\\/?>`, "gi");
50
+ // Strip known HTML tags to get plain text length (for timing calculation, not sanitization)
51
+ export const stripHtmlTags = (text) => {
52
+ return text.replace(captionTagRegex, "");
53
+ };
54
+ // Calculate timing ratios based on text length (HTML tags excluded)
55
+ export const calculateTimingRatios = (splitTexts) => {
56
+ const plainTexts = splitTexts.map(stripHtmlTags);
57
+ const totalLength = plainTexts.reduce((sum, t) => sum + t.length, 0);
50
58
  if (totalLength === 0) {
51
59
  return splitTexts.map(() => 1 / splitTexts.length);
52
60
  }
53
- return splitTexts.map((t) => t.length / totalLength);
61
+ return plainTexts.map((t) => t.length / totalLength);
54
62
  };
55
63
  // Convert ratios to cumulative ratios: [0.3, 0.5, 0.2] -> [0, 0.3, 0.8, 1.0]
56
64
  const calculateCumulativeRatios = (ratios) => {
@@ -89,6 +97,7 @@ const generateBeatCaptions = async (beat, context, index) => {
89
97
  width: `${canvasSize.width}`,
90
98
  height: `${canvasSize.height}`,
91
99
  styles: (mergedCaptionParams.styles ?? []).join(";\n"),
100
+ bottomOffset: `${mergedCaptionParams.bottomOffset ?? 0}`,
92
101
  });
93
102
  await renderHTMLToImage(htmlData, imagePath, canvasSize.width, canvasSize.height, false, true);
94
103
  return {
@@ -327,6 +327,7 @@ export declare const mulmoCaptionParamsSchema: z.ZodObject<{
327
327
  type: z.ZodLiteral<"delimiters">;
328
328
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
329
329
  }, z.core.$strip>], "type">>;
330
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
330
331
  }, z.core.$strict>;
331
332
  export declare const mulmoChartMediaSchema: z.ZodObject<{
332
333
  type: z.ZodLiteral<"chart">;
@@ -6288,6 +6289,7 @@ export declare const mulmoBeatSchema: z.ZodObject<{
6288
6289
  type: z.ZodLiteral<"delimiters">;
6289
6290
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
6290
6291
  }, z.core.$strip>], "type">>;
6292
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
6291
6293
  }, z.core.$strict>>;
6292
6294
  imageNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
6293
6295
  imagePrompt: z.ZodOptional<z.ZodString>;
@@ -6747,6 +6749,7 @@ export declare const mulmoPresentationStyleSchema: z.ZodObject<{
6747
6749
  type: z.ZodLiteral<"delimiters">;
6748
6750
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
6749
6751
  }, z.core.$strip>], "type">>;
6752
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
6750
6753
  }, z.core.$strict>>;
6751
6754
  audioParams: z.ZodDefault<z.ZodObject<{
6752
6755
  padding: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -7202,6 +7205,7 @@ export declare const mulmoScriptSchema: z.ZodObject<{
7202
7205
  type: z.ZodLiteral<"delimiters">;
7203
7206
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
7204
7207
  }, z.core.$strip>], "type">>;
7208
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
7205
7209
  }, z.core.$strict>>;
7206
7210
  audioParams: z.ZodDefault<z.ZodObject<{
7207
7211
  padding: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -10083,6 +10087,7 @@ export declare const mulmoScriptSchema: z.ZodObject<{
10083
10087
  type: z.ZodLiteral<"delimiters">;
10084
10088
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
10085
10089
  }, z.core.$strip>], "type">>;
10090
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
10086
10091
  }, z.core.$strict>>;
10087
10092
  imageNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
10088
10093
  imagePrompt: z.ZodOptional<z.ZodString>;
@@ -10617,6 +10622,7 @@ export declare const mulmoStudioSchema: z.ZodObject<{
10617
10622
  type: z.ZodLiteral<"delimiters">;
10618
10623
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
10619
10624
  }, z.core.$strip>], "type">>;
10625
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
10620
10626
  }, z.core.$strict>>;
10621
10627
  audioParams: z.ZodDefault<z.ZodObject<{
10622
10628
  padding: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -13498,6 +13504,7 @@ export declare const mulmoStudioSchema: z.ZodObject<{
13498
13504
  type: z.ZodLiteral<"delimiters">;
13499
13505
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
13500
13506
  }, z.core.$strip>], "type">>;
13507
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
13501
13508
  }, z.core.$strict>>;
13502
13509
  imageNames: z.ZodOptional<z.ZodArray<z.ZodString>>;
13503
13510
  imagePrompt: z.ZodOptional<z.ZodString>;
@@ -13968,6 +13975,7 @@ export declare const mulmoPromptTemplateSchema: z.ZodObject<{
13968
13975
  type: z.ZodLiteral<"delimiters">;
13969
13976
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
13970
13977
  }, z.core.$strip>], "type">>;
13978
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
13971
13979
  }, z.core.$strict>>;
13972
13980
  audioParams: z.ZodDefault<z.ZodObject<{
13973
13981
  padding: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -14417,6 +14425,7 @@ export declare const mulmoPromptTemplateFileSchema: z.ZodObject<{
14417
14425
  type: z.ZodLiteral<"delimiters">;
14418
14426
  delimiters: z.ZodOptional<z.ZodArray<z.ZodString>>;
14419
14427
  }, z.core.$strip>], "type">>;
14428
+ bottomOffset: z.ZodOptional<z.ZodNumber>;
14420
14429
  }, z.core.$strict>>;
14421
14430
  audioParams: z.ZodDefault<z.ZodObject<{
14422
14431
  padding: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
@@ -174,6 +174,7 @@ export const mulmoCaptionParamsSchema = z
174
174
  styles: z.array(z.string()).optional(), // css styles
175
175
  captionSplit: captionSplitSchema.optional(), // how to determine caption timing
176
176
  textSplit: textSplitSchema.optional(), // how to split text into segments (default: none)
177
+ bottomOffset: z.number().min(0).max(100).optional(), // bottom offset in percentage (e.g., 20 = 20% from bottom)
177
178
  })
178
179
  .strict();
179
180
  export const mulmoChartMediaSchema = z
@@ -1960,6 +1960,7 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
1960
1960
  type: "delimiters";
1961
1961
  delimiters?: string[] | undefined;
1962
1962
  } | undefined;
1963
+ bottomOffset?: number | undefined;
1963
1964
  } | undefined;
1964
1965
  imageNames?: string[] | undefined;
1965
1966
  imagePrompt?: string | undefined;
@@ -2050,6 +2051,7 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
2050
2051
  type: "delimiters";
2051
2052
  delimiters?: string[] | undefined;
2052
2053
  } | undefined;
2054
+ bottomOffset?: number | undefined;
2053
2055
  } | undefined;
2054
2056
  title?: string | undefined;
2055
2057
  description?: string | undefined;
@@ -4052,6 +4054,7 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
4052
4054
  type: "delimiters";
4053
4055
  delimiters?: string[] | undefined;
4054
4056
  } | undefined;
4057
+ bottomOffset?: number | undefined;
4055
4058
  } | undefined;
4056
4059
  imageNames?: string[] | undefined;
4057
4060
  imagePrompt?: string | undefined;
@@ -4142,6 +4145,7 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
4142
4145
  type: "delimiters";
4143
4146
  delimiters?: string[] | undefined;
4144
4147
  } | undefined;
4148
+ bottomOffset?: number | undefined;
4145
4149
  } | undefined;
4146
4150
  title?: string | undefined;
4147
4151
  description?: string | undefined;
@@ -4534,6 +4538,7 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
4534
4538
  type: "delimiters";
4535
4539
  delimiters?: string[] | undefined;
4536
4540
  } | undefined;
4541
+ bottomOffset?: number | undefined;
4537
4542
  } | undefined;
4538
4543
  };
4539
4544
  sessionState: {
@@ -1,5 +1,15 @@
1
1
  import { ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "html_tailwind";
3
+ /**
4
+ * Resolve image:name references to file:// absolute paths using imageRefs.
5
+ * e.g., src="image:bg_office" → src="file:///abs/path/to/bg_office.png"
6
+ */
7
+ export declare const resolveImageRefs: (html: string, imageRefs: Record<string, string>) => string;
8
+ /**
9
+ * Resolve relative paths in src attributes to file:// absolute paths.
10
+ * Paths starting with http://, https://, file://, data:, image:, or / are left unchanged.
11
+ */
12
+ export declare const resolveRelativeImagePaths: (html: string, baseDirPath: string) => string;
3
13
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
14
  export declare const path: (params: ImageProcessorParams) => string;
5
15
  export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
@@ -6,12 +6,25 @@ import { renderHTMLToImage, interpolate, renderHTMLToFrames } from "../html_rend
6
6
  import { framesToVideo } from "../ffmpeg_utils.js";
7
7
  import { parrotingImagePath } from "./utils.js";
8
8
  export const imageType = "html_tailwind";
9
+ /**
10
+ * Resolve image:name references to file:// absolute paths using imageRefs.
11
+ * e.g., src="image:bg_office" → src="file:///abs/path/to/bg_office.png"
12
+ */
13
+ export const resolveImageRefs = (html, imageRefs) => {
14
+ return html.replace(/(\bsrc\s*=\s*)(["'])image:([^"']+)\2/gi, (match, prefix, quote, name) => {
15
+ const resolvedPath = imageRefs[name];
16
+ if (!resolvedPath) {
17
+ return match;
18
+ }
19
+ return `${prefix}${quote}file://${resolvedPath}${quote}`;
20
+ });
21
+ };
9
22
  /**
10
23
  * Resolve relative paths in src attributes to file:// absolute paths.
11
- * Paths starting with http://, https://, file://, data:, or / are left unchanged.
24
+ * Paths starting with http://, https://, file://, data:, image:, or / are left unchanged.
12
25
  */
13
- const resolveRelativeImagePaths = (html, baseDirPath) => {
14
- return html.replace(/(\bsrc\s*=\s*)(["'])((?!https?:\/\/|file:\/\/|data:|\/)[^"']+)\2/gi, (_, prefix, quote, relativePath) => {
26
+ export const resolveRelativeImagePaths = (html, baseDirPath) => {
27
+ return html.replace(/(\bsrc\s*=\s*)(["'])((?!https?:\/\/|file:\/\/|data:|image:|\/)[^"']+)\2/gi, (_, prefix, quote, relativePath) => {
15
28
  const absolutePath = nodePath.resolve(baseDirPath, relativePath);
16
29
  return `${prefix}${quote}file://${absolutePath}${quote}`;
17
30
  });
@@ -64,7 +77,8 @@ const processHtmlTailwindAnimated = async (params) => {
64
77
  fps: String(fps),
65
78
  custom_style: "",
66
79
  });
67
- const htmlData = resolveRelativeImagePaths(rawHtmlData, context.fileDirs.mulmoFileDirPath);
80
+ const resolvedRefs = resolveImageRefs(rawHtmlData, params.imageRefs ?? {});
81
+ const htmlData = resolveRelativeImagePaths(resolvedRefs, context.fileDirs.mulmoFileDirPath);
68
82
  // imagePath is set to the .mp4 path by imagePluginAgent for animated beats
69
83
  const videoPath = imagePath;
70
84
  // Create frames directory next to the video file
@@ -90,7 +104,8 @@ const processHtmlTailwindStatic = async (params) => {
90
104
  html_body: html,
91
105
  user_script: buildUserScript(script),
92
106
  });
93
- const htmlData = resolveRelativeImagePaths(rawHtmlData, context.fileDirs.mulmoFileDirPath);
107
+ const resolvedRefs = resolveImageRefs(rawHtmlData, params.imageRefs ?? {});
108
+ const htmlData = resolveRelativeImagePaths(resolvedRefs, context.fileDirs.mulmoFileDirPath);
94
109
  await renderHTMLToImage(htmlData, imagePath, canvasSize.width, canvasSize.height);
95
110
  return imagePath;
96
111
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.4.6",
3
+ "version": "2.4.8",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -87,7 +87,7 @@
87
87
  "homepage": "https://github.com/receptron/mulmocast-cli#readme",
88
88
  "dependencies": {
89
89
  "@google-cloud/text-to-speech": "^6.4.0",
90
- "@google/genai": "^1.42.0",
90
+ "@google/genai": "^1.44.0",
91
91
  "@graphai/anthropic_agent": "^2.0.12",
92
92
  "@graphai/browserless_agent": "^2.0.2",
93
93
  "@graphai/gemini_agent": "^2.0.5",
@@ -108,10 +108,10 @@
108
108
  "fluent-ffmpeg": "^2.1.3",
109
109
  "graphai": "^2.0.16",
110
110
  "jsdom": "^28.1.0",
111
- "marked": "^17.0.3",
111
+ "marked": "^17.0.4",
112
112
  "mulmocast-vision": "^1.0.8",
113
113
  "ora": "^9.3.0",
114
- "puppeteer": "^24.37.5",
114
+ "puppeteer": "^24.38.0",
115
115
  "replicate": "^1.4.0",
116
116
  "yaml": "^2.8.2",
117
117
  "yargs": "^18.0.0",
@@ -128,8 +128,8 @@
128
128
  "eslint": "^10.0.2",
129
129
  "eslint-config-prettier": "^10.1.8",
130
130
  "eslint-plugin-prettier": "^5.5.5",
131
- "eslint-plugin-sonarjs": "^4.0.0",
132
- "globals": "^17.3.0",
131
+ "eslint-plugin-sonarjs": "^4.0.1",
132
+ "globals": "^17.4.0",
133
133
  "prettier": "^3.8.1",
134
134
  "tsx": "^4.21.0",
135
135
  "typescript": "6.0.0-beta",
@@ -0,0 +1,84 @@
1
+ {
2
+ "$mulmocast": { "version": "1.1" },
3
+ "lang": "ja",
4
+ "canvasSize": { "width": 1080, "height": 1920 },
5
+ "title": "image: スキームテスト",
6
+ "speechParams": {
7
+ "speakers": {
8
+ "Presenter": { "voiceId": "shimmer" }
9
+ }
10
+ },
11
+ "audioParams": {
12
+ "bgm": {
13
+ "kind": "url",
14
+ "url": "https://github.com/receptron/mulmocast-media/raw/refs/heads/main/bgms/theme001.mp3"
15
+ },
16
+ "bgmVolume": 0.12
17
+ },
18
+ "imageParams": {
19
+ "provider": "google",
20
+ "model": "gemini-3.1-flash-image-preview",
21
+ "images": {
22
+ "bg_office": {
23
+ "type": "imagePrompt",
24
+ "prompt": "Empty modern tech office with abandoned desks and computer screens still glowing, dramatic morning sunlight, photorealistic, vertical composition 9:16"
25
+ },
26
+ "bg_city": {
27
+ "type": "imagePrompt",
28
+ "prompt": "Futuristic city skyline at sunset with neon lights reflecting on glass buildings, vibrant colors, photorealistic, vertical composition 9:16"
29
+ }
30
+ }
31
+ },
32
+ "beats": [
33
+ {
34
+ "text": "image:スキームのテストです。背景画像がimageParamsで生成した画像に置き換わります。",
35
+ "speaker": "Presenter",
36
+ "image": {
37
+ "type": "html_tailwind",
38
+ "html": [
39
+ "<div class='h-full w-full overflow-hidden relative bg-black'>",
40
+ " <div id='wrap' style='position:absolute;inset:0;overflow:hidden'>",
41
+ " <img src='image:bg_office' style='width:100%;height:100%;object-fit:cover;filter:brightness(0.7)' />",
42
+ " </div>",
43
+ " <div style='position:absolute;top:50%;left:40px;right:40px;transform:translateY(-50%);text-align:center'>",
44
+ " <div style='display:inline-block;background:rgba(239,68,68,0.85);padding:12px 32px;border-radius:12px'>",
45
+ " <span style='color:white;font-size:80px;font-weight:900'>image: test</span>",
46
+ " </div>",
47
+ " <div style='color:white;font-size:48px;font-weight:900;margin-top:20px;text-shadow:0 4px 16px rgba(0,0,0,0.9)'>bg_office を参照</div>",
48
+ " </div>",
49
+ "</div>"
50
+ ],
51
+ "script": [
52
+ "const animation = new MulmoAnimation();",
53
+ "animation.animate('#wrap', { scale: [1.0, 1.15] }, { start: 0, end: 'auto', easing: 'linear' });"
54
+ ],
55
+ "animation": true
56
+ }
57
+ },
58
+ {
59
+ "text": "二枚目の背景です。別のimageRefsキーを参照しています。",
60
+ "speaker": "Presenter",
61
+ "image": {
62
+ "type": "html_tailwind",
63
+ "html": [
64
+ "<div class='h-full w-full overflow-hidden relative bg-black'>",
65
+ " <div id='wrap' style='position:absolute;inset:0;overflow:hidden'>",
66
+ " <img src='image:bg_city' style='width:100%;height:100%;object-fit:cover;filter:brightness(0.6)' />",
67
+ " </div>",
68
+ " <div style='position:absolute;top:50%;left:40px;right:40px;transform:translateY(-50%);text-align:center'>",
69
+ " <div style='display:inline-block;background:rgba(59,130,246,0.85);padding:12px 32px;border-radius:12px'>",
70
+ " <span style='color:white;font-size:80px;font-weight:900'>image: test</span>",
71
+ " </div>",
72
+ " <div style='color:white;font-size:48px;font-weight:900;margin-top:20px;text-shadow:0 4px 16px rgba(0,0,0,0.9)'>bg_city を参照</div>",
73
+ " </div>",
74
+ "</div>"
75
+ ],
76
+ "script": [
77
+ "const animation = new MulmoAnimation();",
78
+ "animation.animate('#wrap', { translateX: [0, -30] }, { start: 0, end: 'auto', easing: 'linear' });"
79
+ ],
80
+ "animation": true
81
+ }
82
+ }
83
+ ]
84
+ }