mulmocast 2.2.6 → 2.3.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.
package/README.md CHANGED
@@ -207,6 +207,51 @@ MulmoScript configuration (same as OpenAI):
207
207
 
208
208
  For detailed setup and region availability, see [Azure OpenAI Usage Guide](./docs/azure_openai_usage.md).
209
209
 
210
+ ### mulmo.config.json
211
+
212
+ Create a `mulmo.config.json` file to set project-wide defaults. The CLI searches for it in **CWD** first, then **home directory (`~/`)**.
213
+
214
+ ```json
215
+ {
216
+ "speechParams": {
217
+ "provider": "elevenlabs"
218
+ },
219
+ "imageParams": {
220
+ "provider": "google"
221
+ },
222
+ "audioParams": {
223
+ "bgm": { "kind": "path", "path": "assets/bgm.mp3" },
224
+ "bgmVolume": 0.15
225
+ }
226
+ }
227
+ ```
228
+
229
+ Top-level keys are applied as **defaults** (script values take precedence). Use the `override` key to **force** values over scripts — useful for enterprise branding or TTS provider enforcement:
230
+
231
+ ```json
232
+ {
233
+ "speechParams": {
234
+ "provider": "elevenlabs"
235
+ },
236
+ "override": {
237
+ "speechParams": {
238
+ "provider": "elevenlabs",
239
+ "model": "eleven_multilingual_v2",
240
+ "speakers": {
241
+ "Presenter": { "provider": "elevenlabs", "voiceId": "Rachel" }
242
+ }
243
+ }
244
+ }
245
+ }
246
+ ```
247
+
248
+ Priority chain: `config (defaults)` < `template/style` < `script` < `config.override` < `presentationStyle (-p)`
249
+
250
+ Verify the merged result with:
251
+ ```bash
252
+ mulmo tool info merged --script <script.json>
253
+ ```
254
+
210
255
  ## Workflow
211
256
 
212
257
  1. Create a MulmoScript JSON file with `mulmo tool scripting`
@@ -3,4 +3,6 @@ export declare const builder: (yargs: Argv) => Argv<{
3
3
  category: string | undefined;
4
4
  } & {
5
5
  format: string;
6
+ } & {
7
+ script: string | undefined;
6
8
  }>;
@@ -2,7 +2,7 @@ export const builder = (yargs) => yargs
2
2
  .positional("category", {
3
3
  describe: "Category to show info for",
4
4
  type: "string",
5
- choices: ["styles", "bgm", "templates", "voices", "images", "movies", "llm", "themes"],
5
+ choices: ["styles", "bgm", "templates", "voices", "images", "movies", "llm", "themes", "config", "merged"],
6
6
  })
7
7
  .option("format", {
8
8
  alias: "F",
@@ -10,4 +10,9 @@ export const builder = (yargs) => yargs
10
10
  type: "string",
11
11
  choices: ["text", "json", "yaml"],
12
12
  default: "text",
13
+ })
14
+ .option("script", {
15
+ alias: "S",
16
+ describe: "Script file path (required for 'merged' category)",
17
+ type: "string",
13
18
  });
@@ -1,6 +1,7 @@
1
1
  interface InfoCliArgs {
2
2
  category?: string;
3
3
  format: string;
4
+ script?: string;
4
5
  }
5
6
  export declare const handler: (argv: InfoCliArgs) => void;
6
7
  export {};
@@ -1,9 +1,12 @@
1
1
  /* eslint-disable no-console */
2
+ import path from "path";
2
3
  import { getMarkdownStyleNames, getMarkdownCategories, getMarkdownStylesByCategory } from "../../../../data/markdownStyles.js";
3
4
  import { bgmAssets } from "../../../../data/bgmAssets.js";
4
5
  import { templateDataSet } from "../../../../data/templateDataSet.js";
5
6
  import { slideThemes } from "../../../../data/slideThemes.js";
6
7
  import { provider2TTSAgent, provider2ImageAgent, provider2MovieAgent, provider2LLMAgent } from "../../../../types/provider2agent.js";
8
+ import { findConfigFile, loadMulmoConfig, mergeConfigWithScript } from "../../../../utils/mulmo_config.js";
9
+ import { readMulmoScriptFile } from "../../../../utils/file.js";
7
10
  import YAML from "yaml";
8
11
  const formatOutput = (data, format) => {
9
12
  if (format === "json") {
@@ -170,6 +173,78 @@ const printThemesText = () => {
170
173
  }
171
174
  console.log("");
172
175
  };
176
+ const getConfigInfo = () => {
177
+ const baseDirPath = process.cwd();
178
+ const configPath = findConfigFile(baseDirPath);
179
+ if (!configPath) {
180
+ return { configFile: null, config: null };
181
+ }
182
+ const config = loadMulmoConfig(baseDirPath);
183
+ return { configFile: configPath, config };
184
+ };
185
+ const printConfigText = () => {
186
+ const baseDirPath = process.cwd();
187
+ const configPath = findConfigFile(baseDirPath);
188
+ console.log("\n📄 mulmo.config.json\n");
189
+ if (!configPath) {
190
+ console.log(" No mulmo.config.json found.");
191
+ console.log(" Searched: CWD → ~\n");
192
+ return;
193
+ }
194
+ console.log(` Active config: ${configPath}\n`);
195
+ const config = loadMulmoConfig(baseDirPath);
196
+ if (config) {
197
+ console.log(JSON.stringify(config, null, 2));
198
+ }
199
+ console.log("");
200
+ };
201
+ const readScriptFile = (scriptPath) => {
202
+ const result = readMulmoScriptFile(scriptPath, `Error: File not found: ${scriptPath}`);
203
+ if (!result) {
204
+ console.error(`Error: Could not read script file: ${scriptPath}`);
205
+ process.exit(1);
206
+ }
207
+ return result.mulmoData;
208
+ };
209
+ const getMergedInfo = (scriptPath) => {
210
+ if (!scriptPath) {
211
+ console.error("Error: --script <file> is required for 'merged' category");
212
+ process.exit(1);
213
+ }
214
+ const baseDirPath = process.cwd();
215
+ const configResult = loadMulmoConfig(baseDirPath);
216
+ const script = readScriptFile(scriptPath);
217
+ if (!configResult) {
218
+ return { configFile: null, merged: script };
219
+ }
220
+ const configPath = findConfigFile(baseDirPath);
221
+ const merged = mergeConfigWithScript(configResult, script);
222
+ return { configFile: configPath, defaults: configResult.defaults, override: configResult.override, merged };
223
+ };
224
+ const printMergedText = (scriptPath) => {
225
+ if (!scriptPath) {
226
+ console.error("Error: --script <file> is required for 'merged' category");
227
+ console.error("Usage: mulmo tool info merged --script <script.json>");
228
+ process.exit(1);
229
+ }
230
+ const baseDirPath = process.cwd();
231
+ const configResult = loadMulmoConfig(baseDirPath);
232
+ const script = readScriptFile(scriptPath);
233
+ console.log("\n📋 Merged Script Result\n");
234
+ console.log(` Script: ${path.resolve(scriptPath)}`);
235
+ if (!configResult) {
236
+ console.log(" Config: (none)\n");
237
+ console.log(JSON.stringify(script, null, 2));
238
+ console.log("");
239
+ return;
240
+ }
241
+ const configPath = findConfigFile(baseDirPath);
242
+ console.log(` Config: ${configPath}`);
243
+ console.log(` Override: ${configResult.override ? "yes" : "no"}\n`);
244
+ const merged = mergeConfigWithScript(configResult, script);
245
+ console.log(JSON.stringify(merged, null, 2));
246
+ console.log("");
247
+ };
173
248
  const printAllCategories = () => {
174
249
  console.log("\n📚 Available Info Categories\n");
175
250
  console.log(" Usage: mulmo tool info <category> [--format json|yaml]\n");
@@ -181,14 +256,16 @@ const printAllCategories = () => {
181
256
  console.log(" images - Image generation providers and models");
182
257
  console.log(" movies - Movie generation providers and models");
183
258
  console.log(" llm - LLM providers and models");
184
- console.log(" themes - Slide themes and color palettes\n");
259
+ console.log(" themes - Slide themes and color palettes");
260
+ console.log(" config - Active mulmo.config.json location and contents");
261
+ console.log(" merged - Show script merged with mulmo.config.json (--script <file>)\n");
185
262
  };
186
- const validCategories = ["styles", "bgm", "templates", "voices", "images", "movies", "llm", "themes"];
263
+ const validCategories = ["styles", "bgm", "templates", "voices", "images", "movies", "llm", "themes", "config", "merged"];
187
264
  const isValidCategory = (category) => {
188
265
  return validCategories.includes(category);
189
266
  };
190
267
  export const handler = (argv) => {
191
- const { category, format = "text" } = argv;
268
+ const { category, format = "text", script: scriptPath } = argv;
192
269
  if (!category) {
193
270
  if (format === "text") {
194
271
  printAllCategories();
@@ -216,6 +293,8 @@ export const handler = (argv) => {
216
293
  movies: getMoviesInfo,
217
294
  llm: getLlmInfo,
218
295
  themes: getThemesInfo,
296
+ config: getConfigInfo,
297
+ merged: () => getMergedInfo(scriptPath),
219
298
  };
220
299
  const textPrinters = {
221
300
  styles: printStylesText,
@@ -226,6 +305,8 @@ export const handler = (argv) => {
226
305
  movies: printMoviesText,
227
306
  llm: printLlmText,
228
307
  themes: printThemesText,
308
+ config: printConfigText,
309
+ merged: () => printMergedText(scriptPath),
229
310
  };
230
311
  if (format === "text") {
231
312
  textPrinters[category]();
@@ -1,4 +1,4 @@
1
1
  export declare const command = "info [category]";
2
- export declare const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm, themes)";
2
+ export declare const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm, themes, config, merged)";
3
3
  export { builder } from "./builder.js";
4
4
  export { handler } from "./handler.js";
@@ -1,4 +1,4 @@
1
1
  export const command = "info [category]";
2
- export const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm, themes)";
2
+ export const desc = "Show available options (styles, bgm, templates, voices, images, movies, llm, themes, config, merged)";
3
3
  export { builder } from "./builder.js";
4
4
  export { handler } from "./handler.js";
@@ -1,6 +1,6 @@
1
1
  import { type ZodSafeParseResult } from "zod";
2
2
  import type { MulmoScript } from "../types/type.js";
3
- type PartialMulmoScript = Record<string, unknown>;
3
+ export type PartialMulmoScript = Record<string, unknown>;
4
4
  /**
5
5
  * Add $mulmocast version if not present
6
6
  */
@@ -17,6 +17,7 @@ export type CompleteScriptResult = ZodSafeParseResult<MulmoScript>;
17
17
  type CompleteScriptOptions = {
18
18
  templateName?: string;
19
19
  styleName?: string;
20
+ baseDirPath?: string;
20
21
  };
21
22
  /**
22
23
  * Complete a partial MulmoScript with schema defaults, optional style or template
@@ -4,6 +4,7 @@ import { mulmoScriptSchema } from "../types/schema.js";
4
4
  import { getScriptFromPromptTemplate } from "../utils/file.js";
5
5
  import { currentMulmoScriptVersion } from "../types/const.js";
6
6
  import { promptTemplates } from "../data/index.js";
7
+ import { loadMulmoConfig } from "../utils/mulmo_config.js";
7
8
  /**
8
9
  * Add $mulmocast version if not present
9
10
  */
@@ -16,7 +17,7 @@ export const addMulmocastVersion = (data) => {
16
17
  $mulmocast: { version: currentMulmoScriptVersion },
17
18
  };
18
19
  };
19
- const deepMergeKeys = ["speechParams", "imageParams", "movieParams", "audioParams"];
20
+ const deepMergeKeys = ["speechParams", "imageParams", "movieParams", "audioParams", "slideParams"];
20
21
  /**
21
22
  * Merge base with override (override takes precedence)
22
23
  */
@@ -82,11 +83,13 @@ export const getStyle = (style) => {
82
83
  * completeScript(data, { styleName: "./my-style.json" })
83
84
  */
84
85
  export const completeScript = (data, options = {}) => {
85
- const { templateName, styleName } = options;
86
+ const { templateName, styleName, baseDirPath } = options;
86
87
  // template and style are mutually exclusive
87
88
  if (templateName && styleName) {
88
89
  throw new Error("Cannot specify both templateName and styleName. They are mutually exclusive.");
89
90
  }
91
+ // Load mulmo.config.json
92
+ const configResult = baseDirPath ? loadMulmoConfig(baseDirPath) : null;
90
93
  // Get base config from template or style
91
94
  const getBase = () => {
92
95
  if (templateName) {
@@ -97,9 +100,12 @@ export const completeScript = (data, options = {}) => {
97
100
  }
98
101
  return undefined;
99
102
  };
100
- const base = getBase();
101
- // Merge base with input data (input data has highest precedence)
102
- const merged = base ? mergeScripts(base, data) : data;
103
+ const templateOrStyle = getBase();
104
+ // Merge chain: config.defaults < template/style < input data < config.override
105
+ const defaults = configResult?.defaults;
106
+ const withDefaults = defaults && templateOrStyle ? mergeScripts(defaults, templateOrStyle) : (templateOrStyle ?? defaults);
107
+ const withData = withDefaults ? mergeScripts(withDefaults, data) : data;
108
+ const merged = configResult?.override ? mergeScripts(withData, configResult.override) : withData;
103
109
  // Add version if not present
104
110
  const withVersion = addMulmocastVersion(merged);
105
111
  return mulmoScriptSchema.safeParse(withVersion);
@@ -3,6 +3,7 @@ import { readMulmoScriptFile, fetchMulmoScriptFile, isFile } from "./file.js";
3
3
  import { beatId, multiLingualObjectToArray } from "./utils.js";
4
4
  import { mulmoStudioSchema, mulmoCaptionParamsSchema, mulmoPresentationStyleSchema } from "../types/schema.js";
5
5
  import { MulmoPresentationStyleMethods, MulmoScriptMethods, MulmoStudioMultiLingualMethod } from "../methods/index.js";
6
+ import { loadMulmoConfig, mergeConfigWithScript } from "./mulmo_config.js";
6
7
  export const silentMp3 = "https://github.com/receptron/mulmocast-cli/raw/refs/heads/main/assets/audio/silent300.mp3";
7
8
  const mulmoCredit = (speaker, isPortrait) => {
8
9
  return {
@@ -117,10 +118,13 @@ export const getPresentationStyle = (presentationStylePath) => {
117
118
  };
118
119
  export const initializeContextFromFiles = async (files, raiseError, force, withBackup, captionLang, targetLang, index) => {
119
120
  const { fileName, isHttpPath, fileOrUrl, mulmoFilePath, presentationStylePath, outputMultilingualFilePath } = files;
120
- const mulmoScript = await fetchScript(isHttpPath, mulmoFilePath, fileOrUrl);
121
- if (!mulmoScript) {
121
+ const rawScript = await fetchScript(isHttpPath, mulmoFilePath, fileOrUrl);
122
+ if (!rawScript) {
122
123
  return null;
123
124
  }
125
+ // Load and merge mulmo.config.json (defaults < script < override)
126
+ const config = loadMulmoConfig(files.baseDirPath);
127
+ const mulmoScript = config ? mergeConfigWithScript(config, rawScript) : rawScript;
124
128
  // The index param is used when you want to process only a specific beat in an app, etc. This is to avoid parser errors.
125
129
  if (!isNull(index) && mulmoScript.beats[index]) {
126
130
  mulmoScript.beats = [mulmoScript.beats[index]];
@@ -1,6 +1,7 @@
1
1
  import nodePath from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { generateSlideHTML } from "../../slide/index.js";
4
+ import { slideThemes } from "../../data/slideThemes.js";
4
5
  import { renderHTMLToImage } from "../html_render.js";
5
6
  import { parrotingImagePath } from "./utils.js";
6
7
  import { pathToDataUrl } from "../../methods/mulmo_media_source.js";
@@ -95,10 +96,7 @@ const resolveTheme = (params) => {
95
96
  throw new Error("resolveTheme called on non-slide beat");
96
97
  }
97
98
  const defaultTheme = context.presentationStyle.slideParams?.theme;
98
- const theme = beat.image.theme ?? defaultTheme;
99
- if (!theme) {
100
- throw new Error("Slide theme is required: set slideParams.theme or beat.image.theme");
101
- }
99
+ const theme = beat.image.theme ?? defaultTheme ?? slideThemes.corporate;
102
100
  return theme;
103
101
  };
104
102
  const resolveSlide = (params, converter = pathToDataUrl) => {
@@ -0,0 +1,28 @@
1
+ import { type PartialMulmoScript } from "../tools/complete_script.js";
2
+ /**
3
+ * Search for mulmo.config.json in CWD → ~ order.
4
+ * Returns the first found path, or null if not found.
5
+ */
6
+ export declare const findConfigFile: (baseDirPath: string) => string | null;
7
+ /**
8
+ * Resolve all kind:"path" references in config relative to the config file directory.
9
+ */
10
+ export declare const resolveConfigPaths: (config: PartialMulmoScript, configDirPath: string) => PartialMulmoScript;
11
+ export type MulmoConfigResult = {
12
+ defaults: PartialMulmoScript;
13
+ override: PartialMulmoScript | null;
14
+ };
15
+ /**
16
+ * Load mulmo.config.json from baseDirPath or home directory.
17
+ * Resolves kind:"path" entries relative to the config file location.
18
+ * Returns { defaults, override } or null if no config file is found.
19
+ *
20
+ * - defaults: applied as low-priority base (script wins)
21
+ * - override: applied after script merge (wins over script)
22
+ */
23
+ export declare const loadMulmoConfig: (baseDirPath: string) => MulmoConfigResult | null;
24
+ /**
25
+ * Merge mulmo.config.json with a MulmoScript.
26
+ * defaults < script < override
27
+ */
28
+ export declare const mergeConfigWithScript: (configResult: MulmoConfigResult, script: PartialMulmoScript) => PartialMulmoScript;
@@ -0,0 +1,90 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import path from "path";
3
+ import os from "os";
4
+ import { GraphAILogger } from "graphai";
5
+ import { mergeScripts } from "../tools/complete_script.js";
6
+ import { getFullPath } from "./file.js";
7
+ const CONFIG_FILE_NAME = "mulmo.config.json";
8
+ /**
9
+ * Search for mulmo.config.json in CWD → ~ order.
10
+ * Returns the first found path, or null if not found.
11
+ */
12
+ export const findConfigFile = (baseDirPath) => {
13
+ const candidates = [path.resolve(baseDirPath, CONFIG_FILE_NAME), path.resolve(os.homedir(), CONFIG_FILE_NAME)];
14
+ for (const candidate of candidates) {
15
+ if (existsSync(candidate)) {
16
+ return candidate;
17
+ }
18
+ }
19
+ return null;
20
+ };
21
+ /**
22
+ * Resolve kind:"path" entries in config to absolute paths relative to config file location.
23
+ */
24
+ const resolveMediaSourcePath = (source, configDirPath) => {
25
+ if (source.kind === "path" && typeof source.path === "string") {
26
+ return { ...source, path: getFullPath(configDirPath, source.path) };
27
+ }
28
+ return source;
29
+ };
30
+ /**
31
+ * Immutably resolve a nested kind:"path" source at the given key path.
32
+ * e.g. ["audioParams", "bgm"] resolves config.audioParams.bgm
33
+ */
34
+ const resolveNestedPath = (obj, keys, configDirPath) => {
35
+ const [head, ...tail] = keys;
36
+ const child = obj[head];
37
+ if (!child || typeof child !== "object") {
38
+ return obj;
39
+ }
40
+ const childObj = child;
41
+ const resolved = tail.length === 0 ? resolveMediaSourcePath(childObj, configDirPath) : resolveNestedPath(childObj, tail, configDirPath);
42
+ return resolved === child ? obj : { ...obj, [head]: resolved };
43
+ };
44
+ /** Key paths to kind:"path" sources that need resolution */
45
+ const MEDIA_SOURCE_PATHS = [
46
+ ["audioParams", "bgm"],
47
+ ["slideParams", "branding", "logo", "source"],
48
+ ["slideParams", "branding", "backgroundImage", "source"],
49
+ ];
50
+ /**
51
+ * Resolve all kind:"path" references in config relative to the config file directory.
52
+ */
53
+ export const resolveConfigPaths = (config, configDirPath) => {
54
+ return MEDIA_SOURCE_PATHS.reduce((acc, keys) => resolveNestedPath(acc, keys, configDirPath), config);
55
+ };
56
+ /**
57
+ * Load mulmo.config.json from baseDirPath or home directory.
58
+ * Resolves kind:"path" entries relative to the config file location.
59
+ * Returns { defaults, override } or null if no config file is found.
60
+ *
61
+ * - defaults: applied as low-priority base (script wins)
62
+ * - override: applied after script merge (wins over script)
63
+ */
64
+ export const loadMulmoConfig = (baseDirPath) => {
65
+ const configPath = findConfigFile(baseDirPath);
66
+ if (!configPath) {
67
+ return null;
68
+ }
69
+ try {
70
+ const content = readFileSync(configPath, "utf-8");
71
+ const raw = JSON.parse(content);
72
+ const configDirPath = path.dirname(configPath);
73
+ const { override: rawOverride, ...rest } = raw;
74
+ const defaults = resolveConfigPaths(rest, configDirPath);
75
+ const override = rawOverride ? resolveConfigPaths(rawOverride, configDirPath) : null;
76
+ return { defaults, override };
77
+ }
78
+ catch (error) {
79
+ GraphAILogger.error(`Error loading ${configPath}: ${error.message}`);
80
+ throw error;
81
+ }
82
+ };
83
+ /**
84
+ * Merge mulmo.config.json with a MulmoScript.
85
+ * defaults < script < override
86
+ */
87
+ export const mergeConfigWithScript = (configResult, script) => {
88
+ const withDefaults = mergeScripts(configResult.defaults, script);
89
+ return configResult.override ? mergeScripts(withDefaults, configResult.override) : withDefaults;
90
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.2.6",
3
+ "version": "2.3.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "resolutions": {
27
- "minimatch": "^10.2.2"
27
+ "minimatch": "^10.2.3"
28
28
  },
29
29
  "bin": {
30
30
  "mulmo": "lib/cli/bin.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.41.0",
90
+ "@google/genai": "^1.42.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",
@@ -97,13 +97,13 @@
97
97
  "@graphai/stream_agent_filter": "^2.0.3",
98
98
  "@graphai/vanilla": "^2.0.12",
99
99
  "@graphai/vanilla_node_agents": "^2.0.4",
100
- "@inquirer/input": "^5.0.7",
101
- "@inquirer/select": "^5.0.7",
102
- "@modelcontextprotocol/sdk": "^1.26.0",
100
+ "@inquirer/input": "^5.0.8",
101
+ "@inquirer/select": "^5.1.0",
102
+ "@modelcontextprotocol/sdk": "^1.27.1",
103
103
  "@mozilla/readability": "^0.6.0",
104
104
  "@tavily/core": "^0.5.11",
105
105
  "archiver": "^7.0.1",
106
- "clipboardy": "^5.3.0",
106
+ "clipboardy": "^5.3.1",
107
107
  "dotenv": "^17.3.1",
108
108
  "fluent-ffmpeg": "^2.1.3",
109
109
  "graphai": "^2.0.16",
@@ -122,10 +122,10 @@
122
122
  "@receptron/test_utils": "^2.0.3",
123
123
  "@types/archiver": "^7.0.0",
124
124
  "@types/fluent-ffmpeg": "^2.1.28",
125
- "@types/jsdom": "^27.0.0",
125
+ "@types/jsdom": "^28.0.0",
126
126
  "@types/yargs": "^17.0.35",
127
127
  "cross-env": "^10.1.0",
128
- "eslint": "^10.0.1",
128
+ "eslint": "^10.0.2",
129
129
  "eslint-config-prettier": "^10.1.8",
130
130
  "eslint-plugin-prettier": "^5.5.5",
131
131
  "eslint-plugin-sonarjs": "^4.0.0",
@@ -133,7 +133,7 @@
133
133
  "prettier": "^3.8.1",
134
134
  "tsx": "^4.21.0",
135
135
  "typescript": "^5.9.3",
136
- "typescript-eslint": "^8.56.0"
136
+ "typescript-eslint": "^8.56.1"
137
137
  },
138
138
  "engines": {
139
139
  "node": ">=22.0.0"
@@ -273,6 +273,23 @@
273
273
  "type": "beat",
274
274
  "id": "pingpongmov"
275
275
  }
276
+ },
277
+ {
278
+ "speaker": "Presenter",
279
+ "text": "This is a slide with table layout.",
280
+ "image": {
281
+ "type": "slide",
282
+ "slide": {
283
+ "layout": "table",
284
+ "title": "Feature Comparison",
285
+ "headers": ["Feature", "Free", "Pro"],
286
+ "rows": [
287
+ ["Audio", "OpenAI", "ElevenLabs"],
288
+ ["Images", "DALL-E", "Imagen 3"],
289
+ ["Video", "-", "Veo"]
290
+ ]
291
+ }
292
+ }
276
293
  }
277
294
  ]
278
295
  }