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.
- package/lib/actions/html.js +8 -0
- package/lib/agents/combine_audio_files_agent.d.ts +15 -0
- package/lib/agents/combine_audio_files_agent.js +26 -19
- package/lib/data/scriptTemplates.d.ts +89 -0
- package/lib/data/scriptTemplates.js +57 -0
- package/lib/utils/greedy-json-lite.js +4 -4
- package/lib/utils/image_plugins/chart.d.ts +1 -0
- package/lib/utils/image_plugins/chart.js +25 -0
- package/lib/utils/image_plugins/mermaid.d.ts +1 -0
- package/lib/utils/image_plugins/mermaid.js +24 -0
- package/lib/utils/image_plugins/movie.d.ts +4 -2
- package/lib/utils/image_plugins/movie.js +25 -0
- package/lib/utils/image_plugins/vision.js +3 -2
- package/package.json +2 -2
- package/scripts/templates/html_sample.json +52 -0
package/lib/actions/html.js
CHANGED
|
@@ -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"
|
|
7
|
-
const pathOrUrl =
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
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 ??
|
|
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] ===
|
|
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:
|
|
3
|
-
export declare const path: (params:
|
|
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.
|
|
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
|
}
|