mulmocast 1.2.32 → 1.2.34

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.
@@ -45,9 +45,17 @@ const generateHtmlContent = (context, imageWidth) => {
45
45
  <title>${title}</title>
46
46
  <!-- Tailwind CSS CDN -->
47
47
  <script src="https://cdn.tailwindcss.com"></script>
48
+ <!-- Chart.js CDN -->
49
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
50
+ <!-- Mermaid CDN -->
51
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
48
52
  </head>
49
53
  <body class="min-h-screen flex flex-col">
50
54
  ${html}
55
+ <!-- Initialize Mermaid -->
56
+ <script>
57
+ mermaid.initialize({ startOnLoad: true });
58
+ </script>
51
59
  </body>
52
60
  </html>
53
61
  `;
@@ -1,3 +1,18 @@
1
1
  import type { AgentFunctionInfo } from "graphai";
2
+ import { MulmoStudioContext, MulmoBeat } from "../types/index.js";
3
+ export declare const getPadding: (context: MulmoStudioContext, beat: MulmoBeat, index: number) => number;
4
+ export declare const getTotalPadding: (padding: number, movieDuration: number, audioDuration: number, duration?: number) => number;
5
+ export type MediaDuration = {
6
+ movieDuration: number;
7
+ audioDuration: number;
8
+ hasMedia: boolean;
9
+ silenceDuration: number;
10
+ hasMovieAudio: boolean;
11
+ };
12
+ export declare const getGroupBeatDurations: (context: MulmoStudioContext, group: number[], audioDuration: number) => number[];
13
+ export declare const voiceOverProcess: (context: MulmoStudioContext, mediaDurations: MediaDuration[], movieDuration: number, beatDurations: number[], groupLength: number) => (remaining: number, idx: number, iGroup: number) => number;
14
+ export declare const spilledOverAudio: (context: MulmoStudioContext, group: number[], audioDuration: number, beatDurations: number[], mediaDurations: MediaDuration[]) => void;
15
+ export declare const noSpilledOverAudio: (context: MulmoStudioContext, beat: MulmoBeat, index: number, movieDuration: number, audioDuration: number, beatDurations: number[], mediaDurations: MediaDuration[]) => void;
16
+ export declare const updateDurations: (context: MulmoStudioContext, mediaDurations: MediaDuration[]) => number[];
2
17
  declare const combineAudioFilesAgentInfo: AgentFunctionInfo;
3
18
  export default combineAudioFilesAgentInfo;
@@ -1,17 +1,20 @@
1
1
  import { assert, GraphAILogger } from "graphai";
2
2
  import { silent60secPath } from "../utils/file.js";
3
3
  import { FfmpegContextInit, FfmpegContextGenerateOutput, FfmpegContextInputFormattedAudio, ffmpegGetMediaDuration, } from "../utils/ffmpeg_utils.js";
4
+ import { MulmoMediaSourceMethods } from "../methods/mulmo_media_source.js";
4
5
  import { userAssert } from "../utils/utils.js";
5
- const getMovieDuration = async (beat) => {
6
- if (beat.image?.type === "movie" && (beat.image.source.kind === "url" || beat.image.source.kind === "path")) {
7
- const pathOrUrl = beat.image.source.kind === "url" ? beat.image.source.url : beat.image.source.path;
8
- const speed = beat.movieParams?.speed ?? 1.0;
9
- const { duration, hasAudio } = await ffmpegGetMediaDuration(pathOrUrl);
10
- return { duration: duration / speed, hasAudio };
6
+ const getMovieDuration = async (context, beat) => {
7
+ if (beat.image?.type === "movie") {
8
+ const pathOrUrl = MulmoMediaSourceMethods.resolve(beat.image.source, context);
9
+ if (pathOrUrl) {
10
+ const speed = beat.movieParams?.speed ?? 1.0;
11
+ const { duration, hasAudio } = await ffmpegGetMediaDuration(pathOrUrl);
12
+ return { duration: duration / speed, hasAudio };
13
+ }
11
14
  }
12
15
  return { duration: 0, hasAudio: false };
13
16
  };
14
- const getPadding = (context, beat, index) => {
17
+ export const getPadding = (context, beat, index) => {
15
18
  if (beat.audioParams?.padding !== undefined) {
16
19
  return beat.audioParams.padding;
17
20
  }
@@ -21,7 +24,7 @@ const getPadding = (context, beat, index) => {
21
24
  const isClosingGap = index === context.studio.beats.length - 2;
22
25
  return isClosingGap ? context.presentationStyle.audioParams.closingPadding : context.presentationStyle.audioParams.padding;
23
26
  };
24
- const getTotalPadding = (padding, movieDuration, audioDuration, duration) => {
27
+ export const getTotalPadding = (padding, movieDuration, audioDuration, duration) => {
25
28
  if (movieDuration > 0) {
26
29
  return padding + (movieDuration - audioDuration);
27
30
  }
@@ -33,7 +36,7 @@ const getTotalPadding = (padding, movieDuration, audioDuration, duration) => {
33
36
  const getMediaDurationsOfAllBeats = (context) => {
34
37
  return Promise.all(context.studio.beats.map(async (studioBeat, index) => {
35
38
  const beat = context.studio.script.beats[index];
36
- const { duration: movieDuration, hasAudio: hasMovieAudio } = await getMovieDuration(beat);
39
+ const { duration: movieDuration, hasAudio: hasMovieAudio } = await getMovieDuration(context, beat);
37
40
  const audioDuration = studioBeat.audioFile ? (await ffmpegGetMediaDuration(studioBeat.audioFile)).duration : 0;
38
41
  return {
39
42
  movieDuration,
@@ -44,7 +47,7 @@ const getMediaDurationsOfAllBeats = (context) => {
44
47
  };
45
48
  }));
46
49
  };
47
- const getGroupBeatDurations = (context, group, audioDuration) => {
50
+ export const getGroupBeatDurations = (context, group, audioDuration) => {
48
51
  const specifiedSum = group
49
52
  .map((idx) => context.studio.script.beats[idx].duration)
50
53
  .filter((d) => d !== undefined)
@@ -79,7 +82,7 @@ const getInputIds = (context, mediaDurations, ffmpegContext, silentIds) => {
79
82
  });
80
83
  return inputIds;
81
84
  };
82
- const voiceOverProcess = (context, mediaDurations, movieDuration, beatDurations, groupLength) => {
85
+ export const voiceOverProcess = (context, mediaDurations, movieDuration, beatDurations, groupLength) => {
83
86
  return (remaining, idx, iGroup) => {
84
87
  const subBeatDurations = mediaDurations[idx];
85
88
  userAssert(subBeatDurations.audioDuration <= remaining, `Duration Overflow: At index(${idx}) audioDuration(${subBeatDurations.audioDuration}) > remaining(${remaining})`);
@@ -118,7 +121,7 @@ const getSpillOverGroup = (context, mediaDurations, index) => {
118
121
  }
119
122
  return group;
120
123
  };
121
- const spilledOverAudio = (context, group, audioDuration, beatDurations, mediaDurations) => {
124
+ export const spilledOverAudio = (context, group, audioDuration, beatDurations, mediaDurations) => {
122
125
  const groupBeatsDurations = getGroupBeatDurations(context, group, audioDuration);
123
126
  // Yes, the current beat has spilled over audio.
124
127
  const beatsTotalDuration = groupBeatsDurations.reduce((a, b) => a + b, 0);
@@ -138,7 +141,7 @@ const spilledOverAudio = (context, group, audioDuration, beatDurations, mediaDur
138
141
  }
139
142
  beatDurations.push(...groupBeatsDurations);
140
143
  };
141
- const noSpilledOverAudio = (context, beat, index, movieDuration, audioDuration, beatDurations, mediaDurations) => {
144
+ export const noSpilledOverAudio = (context, beat, index, movieDuration, audioDuration, beatDurations, mediaDurations) => {
142
145
  // padding is the amount of audio padding specified in the script.
143
146
  const padding = getPadding(context, beat, index);
144
147
  // totalPadding is the amount of audio padding to be added to the audio file.
@@ -149,11 +152,7 @@ const noSpilledOverAudio = (context, beat, index, movieDuration, audioDuration,
149
152
  mediaDurations[index].silenceDuration = totalPadding;
150
153
  }
151
154
  };
152
- const combineAudioFilesAgent = async ({ namedInputs, }) => {
153
- const { context, combinedFileName } = namedInputs;
154
- const ffmpegContext = FfmpegContextInit();
155
- // First, get the audio durations of all beats, taking advantage of multi-threading capability of ffmpeg.
156
- const mediaDurations = await getMediaDurationsOfAllBeats(context);
155
+ export const updateDurations = (context, mediaDurations) => {
157
156
  const beatDurations = [];
158
157
  context.studio.script.beats.forEach((beat, index) => {
159
158
  if (beatDurations.length > index) {
@@ -192,10 +191,18 @@ const combineAudioFilesAgent = async ({ namedInputs, }) => {
192
191
  return;
193
192
  }
194
193
  // The current beat has no audio, nor no spilled over audio
195
- const beatDuration = beat.duration ?? (movieDuration > 0 ? movieDuration : 1.0);
194
+ const beatDuration = beat.duration ?? 1.0;
196
195
  beatDurations.push(beatDuration);
197
196
  mediaDurations[index].silenceDuration = beatDuration;
198
197
  });
198
+ return beatDurations;
199
+ };
200
+ const combineAudioFilesAgent = async ({ namedInputs, }) => {
201
+ const { context, combinedFileName } = namedInputs;
202
+ // First, get the audio durations of all beats, taking advantage of multi-threading capability of ffmpeg.
203
+ const mediaDurations = await getMediaDurationsOfAllBeats(context);
204
+ const ffmpegContext = FfmpegContextInit();
205
+ const beatDurations = updateDurations(context, mediaDurations);
199
206
  assert(beatDurations.length === context.studio.beats.length, "beatDurations.length !== studio.beats.length");
200
207
  // We cannot reuse longSilentId. We need to explicitly split it for each beat.
201
208
  const silentIds = mediaDurations.filter((md) => md.silenceDuration > 0).map((_, index) => `[ls_${index}]`);
@@ -394,6 +394,10 @@ export declare const scriptTemplates: ({
394
394
  slide?: undefined;
395
395
  data?: undefined;
396
396
  style?: undefined;
397
+ chartData?: undefined;
398
+ title?: undefined;
399
+ code?: undefined;
400
+ source?: undefined;
397
401
  };
398
402
  text: string;
399
403
  } | {
@@ -404,6 +408,10 @@ export declare const scriptTemplates: ({
404
408
  slide?: undefined;
405
409
  data?: undefined;
406
410
  style?: undefined;
411
+ chartData?: undefined;
412
+ title?: undefined;
413
+ code?: undefined;
414
+ source?: undefined;
407
415
  };
408
416
  text: string;
409
417
  } | {
@@ -417,6 +425,10 @@ export declare const scriptTemplates: ({
417
425
  markdown?: undefined;
418
426
  data?: undefined;
419
427
  style?: undefined;
428
+ chartData?: undefined;
429
+ title?: undefined;
430
+ code?: undefined;
431
+ source?: undefined;
420
432
  };
421
433
  text: string;
422
434
  } | {
@@ -432,6 +444,10 @@ export declare const scriptTemplates: ({
432
444
  html?: undefined;
433
445
  markdown?: undefined;
434
446
  slide?: undefined;
447
+ chartData?: undefined;
448
+ title?: undefined;
449
+ code?: undefined;
450
+ source?: undefined;
435
451
  };
436
452
  text: string;
437
453
  } | {
@@ -447,6 +463,79 @@ export declare const scriptTemplates: ({
447
463
  html?: undefined;
448
464
  markdown?: undefined;
449
465
  slide?: undefined;
466
+ chartData?: undefined;
467
+ title?: undefined;
468
+ code?: undefined;
469
+ source?: undefined;
470
+ };
471
+ text: string;
472
+ } | {
473
+ image: {
474
+ chartData: {
475
+ data: {
476
+ datasets: {
477
+ backgroundColor: string[];
478
+ borderColor: string[];
479
+ borderWidth: number;
480
+ data: number[];
481
+ label: string;
482
+ }[];
483
+ labels: string[];
484
+ };
485
+ options: {
486
+ maintainAspectRatio: boolean;
487
+ responsive: boolean;
488
+ scales: {
489
+ y: {
490
+ beginAtZero: boolean;
491
+ };
492
+ };
493
+ };
494
+ type: string;
495
+ };
496
+ title: string;
497
+ type: string;
498
+ html?: undefined;
499
+ markdown?: undefined;
500
+ slide?: undefined;
501
+ data?: undefined;
502
+ style?: undefined;
503
+ code?: undefined;
504
+ source?: undefined;
505
+ };
506
+ text: string;
507
+ } | {
508
+ image: {
509
+ code: {
510
+ kind: string;
511
+ text: string;
512
+ };
513
+ title: string;
514
+ type: string;
515
+ html?: undefined;
516
+ markdown?: undefined;
517
+ slide?: undefined;
518
+ data?: undefined;
519
+ style?: undefined;
520
+ chartData?: undefined;
521
+ source?: undefined;
522
+ };
523
+ text: string;
524
+ } | {
525
+ image: {
526
+ source: {
527
+ kind: string;
528
+ url: string;
529
+ };
530
+ type: string;
531
+ html?: undefined;
532
+ markdown?: undefined;
533
+ slide?: undefined;
534
+ data?: undefined;
535
+ style?: undefined;
536
+ chartData?: undefined;
537
+ title?: undefined;
538
+ code?: undefined;
450
539
  };
451
540
  text: string;
452
541
  })[];
@@ -604,6 +604,63 @@ export const scriptTemplates = [
604
604
  },
605
605
  text: "agendaSlide",
606
606
  },
607
+ {
608
+ image: {
609
+ chartData: {
610
+ data: {
611
+ datasets: [
612
+ {
613
+ backgroundColor: ["rgba(255, 99, 132, 0.2)", "rgba(54, 162, 235, 0.2)", "rgba(255, 205, 86, 0.2)", "rgba(75, 192, 192, 0.2)"],
614
+ borderColor: ["rgba(255, 99, 132, 1)", "rgba(54, 162, 235, 1)", "rgba(255, 205, 86, 1)", "rgba(75, 192, 192, 1)"],
615
+ borderWidth: 1,
616
+ data: [120, 190, 300, 500],
617
+ label: "Sales ($k)",
618
+ },
619
+ ],
620
+ labels: ["Q1", "Q2", "Q3", "Q4"],
621
+ },
622
+ options: {
623
+ maintainAspectRatio: false,
624
+ responsive: true,
625
+ scales: {
626
+ y: {
627
+ beginAtZero: true,
628
+ },
629
+ },
630
+ },
631
+ type: "bar",
632
+ },
633
+ title: "Quarterly Sales Performance",
634
+ type: "chart",
635
+ },
636
+ text: "This chart shows sales data over the quarters.",
637
+ },
638
+ {
639
+ image: {
640
+ code: {
641
+ kind: "text",
642
+ text: "graph TD\n" +
643
+ " A[Start] --> B{Is it working?}\n" +
644
+ " B -->|Yes| C[Great!]\n" +
645
+ " B -->|No| D[Debug]\n" +
646
+ " D --> B\n" +
647
+ " C --> E[End]",
648
+ },
649
+ title: "Development Workflow",
650
+ type: "mermaid",
651
+ },
652
+ text: "Here's a mermaid diagram showing the workflow.",
653
+ },
654
+ {
655
+ image: {
656
+ source: {
657
+ kind: "url",
658
+ url: "https://www.w3schools.com/html/mov_bbb.mp4",
659
+ },
660
+ type: "movie",
661
+ },
662
+ text: "Sample video content demonstration.",
663
+ },
607
664
  ],
608
665
  filename: "html_sample",
609
666
  lang: "en",
@@ -23,22 +23,22 @@ const extractBlocks = (text) => {
23
23
  // greedy( {…} / […])
24
24
  const starts = [];
25
25
  for (let i = 0; i < text.length; i++) {
26
- if (text[i] === '{' || text[i] === '[') {
26
+ if (text[i] === "{" || text[i] === "[") {
27
27
  starts.push(i);
28
28
  }
29
29
  }
30
30
  for (const s of starts) {
31
31
  const open = text[s];
32
- const close = open === '{' ? '}' : ']';
32
+ const close = open === "{" ? "}" : "]";
33
33
  let depth = 0, inStr = null, prev = "";
34
34
  for (let j = s; j < text.length; j++) {
35
35
  const ch = text[j];
36
36
  if (inStr) {
37
- if (ch === inStr && prev !== '\\')
37
+ if (ch === inStr && prev !== "\\")
38
38
  inStr = null;
39
39
  }
40
40
  else {
41
- if (ch === '"' || ch === "'" || ch === '`')
41
+ if (ch === '"' || ch === "'" || ch === "`")
42
42
  inStr = ch;
43
43
  else if (ch === open)
44
44
  depth++;
@@ -2,3 +2,4 @@ import { ImageProcessorParams } from "../../types/index.js";
2
2
  export declare const imageType = "chart";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
4
  export declare const path: (params: ImageProcessorParams) => string;
5
+ export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
@@ -1,6 +1,7 @@
1
1
  import { getHTMLFile } from "../file.js";
2
2
  import { renderHTMLToImage, interpolate } from "../markdown.js";
3
3
  import { parrotingImagePath } from "./utils.js";
4
+ import nodeProcess from "node:process";
4
5
  export const imageType = "chart";
5
6
  const processChart = async (params) => {
6
7
  const { beat, imagePath, canvasSize, textSlideStyle } = params;
@@ -21,5 +22,29 @@ const processChart = async (params) => {
21
22
  await renderHTMLToImage(htmlData, imagePath, canvasSize.width, canvasSize.height);
22
23
  return imagePath;
23
24
  };
25
+ const dumpHtml = async (params) => {
26
+ const { beat } = params;
27
+ if (!beat.image || beat.image.type !== imageType)
28
+ return;
29
+ const chartData = JSON.stringify(beat.image.chartData, null, 2);
30
+ const title = beat.image.title || "Chart";
31
+ // Safe: UI-only jitter; no security or fairness implications.
32
+ // eslint-disable-next-line sonarjs/pseudo-random
33
+ const chartId = nodeProcess.env.NODE_ENV === "test" ? "id" : `chart-${Math.random().toString(36).substr(2, 9)}`;
34
+ return `
35
+ <div class="chart-container mb-6">
36
+ <h3 class="text-xl font-semibold mb-4">${title}</h3>
37
+ <div class="w-full" style="position: relative; height: 400px;">
38
+ <canvas id="${chartId}"></canvas>
39
+ </div>
40
+ <script>
41
+ (function() {
42
+ const ctx = document.getElementById('${chartId}').getContext('2d');
43
+ new Chart(ctx, ${chartData});
44
+ })();
45
+ </script>
46
+ </div>`;
47
+ };
24
48
  export const process = processChart;
25
49
  export const path = parrotingImagePath;
50
+ export const html = dumpHtml;
@@ -3,3 +3,4 @@ export declare const imageType = "mermaid";
3
3
  export declare const process: (params: ImageProcessorParams) => Promise<string | undefined>;
4
4
  export declare const path: (params: ImageProcessorParams) => string;
5
5
  export declare const markdown: (params: ImageProcessorParams) => string | undefined;
6
+ export declare const html: (params: ImageProcessorParams) => Promise<string | undefined>;
@@ -2,6 +2,7 @@ import { MulmoMediaSourceMethods } from "../../methods/index.js";
2
2
  import { getHTMLFile } from "../file.js";
3
3
  import { renderHTMLToImage, interpolate } from "../markdown.js";
4
4
  import { parrotingImagePath } from "./utils.js";
5
+ import nodeProcess from "node:process";
5
6
  export const imageType = "mermaid";
6
7
  const processMermaid = async (params) => {
7
8
  const { beat, imagePath, canvasSize, context, textSlideStyle } = params;
@@ -27,6 +28,29 @@ const dumpMarkdown = (params) => {
27
28
  return; // support only text for now
28
29
  return `\`\`\`mermaid\n${beat.image.code.text}\n\`\`\``;
29
30
  };
31
+ const dumpHtml = async (params) => {
32
+ const { beat } = params;
33
+ if (!beat.image || beat.image.type !== imageType)
34
+ return;
35
+ const diagramCode = await MulmoMediaSourceMethods.getText(beat.image.code, params.context);
36
+ if (!diagramCode)
37
+ return;
38
+ const title = beat.image.title || "Diagram";
39
+ const appendix = beat.image.appendix?.join("\n") || "";
40
+ const fullCode = `${diagramCode}\n${appendix}`.trim();
41
+ // eslint-disable-next-line sonarjs/pseudo-random
42
+ const diagramId = nodeProcess.env.NODE_ENV === "test" ? "id" : `mermaid-${Math.random().toString(36).substr(2, 9)}`;
43
+ return `
44
+ <div class="mermaid-container mb-6">
45
+ <h3 class="text-xl font-semibold mb-4">${title}</h3>
46
+ <div class="flex justify-center">
47
+ <div id="${diagramId}" class="mermaid">
48
+ ${fullCode}
49
+ </div>
50
+ </div>
51
+ </div>`;
52
+ };
30
53
  export const process = processMermaid;
31
54
  export const path = parrotingImagePath;
32
55
  export const markdown = dumpMarkdown;
56
+ export const html = dumpHtml;
@@ -1,3 +1,5 @@
1
+ import { ImageProcessorParams } from "../../types/index.js";
1
2
  export declare const imageType = "movie";
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 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>;
@@ -1,4 +1,29 @@
1
1
  import { processSource, pathSource } from "./source.js";
2
+ import { MulmoMediaSourceMethods } from "../../methods/mulmo_media_source.js";
2
3
  export const imageType = "movie";
4
+ const dumpHtml = async (params) => {
5
+ const { beat, context } = params;
6
+ if (!beat.image || beat.image.type !== imageType)
7
+ return;
8
+ const moviePathOrUrl = MulmoMediaSourceMethods.resolve(beat.image.source, context);
9
+ if (!moviePathOrUrl)
10
+ return;
11
+ return `
12
+ <div class="movie-container mb-6">
13
+ <div class="relative w-full" style="padding-bottom: 56.25%; /* 16:9 aspect ratio */">
14
+ <video
15
+ class="absolute top-0 left-0 w-full h-full rounded-lg shadow-lg"
16
+ controls
17
+ preload="metadata"
18
+ >
19
+ <source src="${moviePathOrUrl}" type="video/mp4">
20
+ <source src="${moviePathOrUrl}" type="video/webm">
21
+ <source src="${moviePathOrUrl}" type="video/ogg">
22
+ Your browser does not support the video tag.
23
+ </video>
24
+ </div>
25
+ </div>`;
26
+ };
3
27
  export const process = processSource(imageType);
4
28
  export const path = pathSource(imageType);
29
+ export const html = dumpHtml;
@@ -16,10 +16,11 @@ const processVision = async (params) => {
16
16
  return imagePath;
17
17
  };
18
18
  const dumpHtml = async (params) => {
19
- const { beat } = params;
19
+ const { beat, context } = params;
20
+ const rootDir = context.fileDirs.nodeModuleRootPath ? resolvePath(context.fileDirs.nodeModuleRootPath, "mulmocast-vision") : undefined;
20
21
  if (!beat.image || beat.image.type !== imageType)
21
22
  return;
22
- const handler = new htmlPlugin({});
23
+ const handler = new htmlPlugin({ rootDir });
23
24
  return handler.getHtml(templateNameTofunctionName(beat.image.style), beat.image.data);
24
25
  };
25
26
  export const process = processVision;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "1.2.32",
3
+ "version": "1.2.34",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -41,7 +41,7 @@
41
41
  "preprocess": "npx tsx ./src/cli/bin.ts preprocess",
42
42
  "pdf": "npx tsx ./src/cli/bin.ts pdf",
43
43
  "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",
44
- "ci_test": "NODE_ENV=test tsx --test ./test/*/test_*.ts",
44
+ "ci_test": "NODE_ENV=test tsx --test --experimental-test-coverage ./test/*/test_*.ts",
45
45
  "lint": "eslint src test",
46
46
  "build": "tsc",
47
47
  "build_test": "tsc && git checkout -- lib/*",
@@ -106,6 +106,58 @@
106
106
  ]
107
107
  }
108
108
  }
109
+ },
110
+ {
111
+ "text": "This chart shows sales data over the quarters.",
112
+ "image": {
113
+ "type": "chart",
114
+ "title": "Quarterly Sales Performance",
115
+ "chartData": {
116
+ "type": "bar",
117
+ "data": {
118
+ "labels": ["Q1", "Q2", "Q3", "Q4"],
119
+ "datasets": [
120
+ {
121
+ "label": "Sales ($k)",
122
+ "data": [120, 190, 300, 500],
123
+ "backgroundColor": ["rgba(255, 99, 132, 0.2)", "rgba(54, 162, 235, 0.2)", "rgba(255, 205, 86, 0.2)", "rgba(75, 192, 192, 0.2)"],
124
+ "borderColor": ["rgba(255, 99, 132, 1)", "rgba(54, 162, 235, 1)", "rgba(255, 205, 86, 1)", "rgba(75, 192, 192, 1)"],
125
+ "borderWidth": 1
126
+ }
127
+ ]
128
+ },
129
+ "options": {
130
+ "responsive": true,
131
+ "maintainAspectRatio": false,
132
+ "scales": {
133
+ "y": {
134
+ "beginAtZero": true
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ },
141
+ {
142
+ "text": "Here's a mermaid diagram showing the workflow.",
143
+ "image": {
144
+ "type": "mermaid",
145
+ "title": "Development Workflow",
146
+ "code": {
147
+ "kind": "text",
148
+ "text": "graph TD\n A[Start] --> B{Is it working?}\n B -->|Yes| C[Great!]\n B -->|No| D[Debug]\n D --> B\n C --> E[End]"
149
+ }
150
+ }
151
+ },
152
+ {
153
+ "text": "Sample video content demonstration.",
154
+ "image": {
155
+ "type": "movie",
156
+ "source": {
157
+ "kind": "url",
158
+ "url": "https://www.w3schools.com/html/mov_bbb.mp4"
159
+ }
160
+ }
109
161
  }
110
162
  ]
111
163
  }