mulmocast 2.6.0 → 2.6.2
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/image_agents.d.ts +1 -0
- package/lib/actions/image_agents.js +2 -2
- package/lib/actions/image_references.d.ts +6 -0
- package/lib/actions/image_references.js +14 -2
- package/lib/actions/images.d.ts +4 -0
- package/lib/actions/images.js +8 -2
- package/lib/agents/combine_audio_files_agent.js +2 -1
- package/lib/types/schema.d.ts +109 -0
- package/lib/types/schema.js +2 -2
- package/lib/types/type.d.ts +3 -1
- package/lib/utils/context.d.ts +60 -24
- package/lib/utils/html_render.js +65 -0
- package/lib/utils/image_plugins/html_tailwind.d.ts +5 -0
- package/lib/utils/image_plugins/html_tailwind.js +19 -4
- package/lib/utils/image_plugins/markdown.js +4 -1
- package/package.json +1 -1
- package/scripts/test/test_markdown_image_refs.json +59 -0
- package/scripts/test/test_movie_refs.json +126 -0
|
@@ -60,6 +60,7 @@ export declare const imagePluginAgent: (namedInputs: {
|
|
|
60
60
|
beat: MulmoBeat;
|
|
61
61
|
index: number;
|
|
62
62
|
imageRefs?: Record<string, string>;
|
|
63
|
+
movieRefs?: Record<string, string>;
|
|
63
64
|
}) => Promise<void>;
|
|
64
65
|
export declare const htmlImageGeneratorAgent: (namedInputs: {
|
|
65
66
|
file: string;
|
|
@@ -115,7 +115,7 @@ export const imagePreprocessAgent = async (namedInputs) => {
|
|
|
115
115
|
return { ...returnValue, imagePath, referenceImageForMovie: imagePath, imageAgentInfo, prompt, referenceImages };
|
|
116
116
|
};
|
|
117
117
|
export const imagePluginAgent = async (namedInputs) => {
|
|
118
|
-
const { context, beat, index, imageRefs } = namedInputs;
|
|
118
|
+
const { context, beat, index, imageRefs, movieRefs } = namedInputs;
|
|
119
119
|
const { imagePath } = getBeatPngImagePath(context, index);
|
|
120
120
|
const plugin = MulmoBeatMethods.getPlugin(beat);
|
|
121
121
|
// For animated html_tailwind, use the .mp4 path so the plugin writes video there
|
|
@@ -125,7 +125,7 @@ export const imagePluginAgent = async (namedInputs) => {
|
|
|
125
125
|
MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, true);
|
|
126
126
|
const studioBeat = context.studio.beats[index];
|
|
127
127
|
const beatDuration = beat.duration ?? studioBeat?.duration;
|
|
128
|
-
const processorParams = { beat, context, imagePath: effectiveImagePath, imageRefs, beatDuration, ...htmlStyle(context, beat) };
|
|
128
|
+
const processorParams = { beat, context, imagePath: effectiveImagePath, imageRefs, movieRefs, beatDuration, ...htmlStyle(context, beat) };
|
|
129
129
|
await plugin.process(processorParams);
|
|
130
130
|
MulmoStudioContextMethods.setBeatSessionState(context, "image", index, beat.id, false);
|
|
131
131
|
}
|
|
@@ -6,4 +6,10 @@ export declare const generateReferenceImage: (inputs: {
|
|
|
6
6
|
image: MulmoImagePromptMedia;
|
|
7
7
|
force?: boolean;
|
|
8
8
|
}) => Promise<string>;
|
|
9
|
+
export type MediaRefs = {
|
|
10
|
+
imageRefs: Record<string, string>;
|
|
11
|
+
movieRefs: Record<string, string>;
|
|
12
|
+
};
|
|
13
|
+
export declare const getMediaRefs: (context: MulmoStudioContext) => Promise<MediaRefs>;
|
|
14
|
+
/** @deprecated Use getMediaRefs instead */
|
|
9
15
|
export declare const getImageRefs: (context: MulmoStudioContext) => Promise<Record<string, string>>;
|
|
@@ -51,12 +51,13 @@ export const generateReferenceImage = async (inputs) => {
|
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
53
|
};
|
|
54
|
-
export const
|
|
54
|
+
export const getMediaRefs = async (context) => {
|
|
55
55
|
const images = context.presentationStyle.imageParams?.images;
|
|
56
56
|
if (!images) {
|
|
57
|
-
return {};
|
|
57
|
+
return { imageRefs: {}, movieRefs: {} };
|
|
58
58
|
}
|
|
59
59
|
const imageRefs = {};
|
|
60
|
+
const movieRefs = {};
|
|
60
61
|
await Promise.all(Object.keys(images)
|
|
61
62
|
.sort()
|
|
62
63
|
.map(async (key, index) => {
|
|
@@ -67,6 +68,17 @@ export const getImageRefs = async (context) => {
|
|
|
67
68
|
else if (image.type === "image") {
|
|
68
69
|
imageRefs[key] = await MulmoMediaSourceMethods.imageReference(image.source, context, key);
|
|
69
70
|
}
|
|
71
|
+
else if (image.type === "movie") {
|
|
72
|
+
movieRefs[key] = await resolveMovieReference(image, context, key);
|
|
73
|
+
}
|
|
70
74
|
}));
|
|
75
|
+
return { imageRefs, movieRefs };
|
|
76
|
+
};
|
|
77
|
+
const resolveMovieReference = async (movie, context, key) => {
|
|
78
|
+
return MulmoMediaSourceMethods.imageReference(movie.source, context, key);
|
|
79
|
+
};
|
|
80
|
+
/** @deprecated Use getMediaRefs instead */
|
|
81
|
+
export const getImageRefs = async (context) => {
|
|
82
|
+
const { imageRefs } = await getMediaRefs(context);
|
|
71
83
|
return imageRefs;
|
|
72
84
|
};
|
package/lib/actions/images.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare const beat_graph_data: {
|
|
|
7
7
|
context: {};
|
|
8
8
|
htmlImageAgentInfo: {};
|
|
9
9
|
imageRefs: {};
|
|
10
|
+
movieRefs: {};
|
|
10
11
|
beat: {};
|
|
11
12
|
__mapIndex: {};
|
|
12
13
|
forceMovie: {
|
|
@@ -154,6 +155,7 @@ export declare const beat_graph_data: {
|
|
|
154
155
|
beat: string;
|
|
155
156
|
index: string;
|
|
156
157
|
imageRefs: string;
|
|
158
|
+
movieRefs: string;
|
|
157
159
|
};
|
|
158
160
|
};
|
|
159
161
|
imagePlugin: {
|
|
@@ -164,12 +166,14 @@ export declare const beat_graph_data: {
|
|
|
164
166
|
beat: import("../types/type.js").MulmoBeat;
|
|
165
167
|
index: number;
|
|
166
168
|
imageRefs?: Record<string, string>;
|
|
169
|
+
movieRefs?: Record<string, string>;
|
|
167
170
|
}) => Promise<void>;
|
|
168
171
|
inputs: {
|
|
169
172
|
context: string;
|
|
170
173
|
beat: string;
|
|
171
174
|
index: string;
|
|
172
175
|
imageRefs: string;
|
|
176
|
+
movieRefs: string;
|
|
173
177
|
onComplete: string[];
|
|
174
178
|
};
|
|
175
179
|
};
|
package/lib/actions/images.js
CHANGED
|
@@ -14,7 +14,7 @@ import { fileCacheAgentFilter } from "../utils/filters.js";
|
|
|
14
14
|
import { settings2GraphAIConfig } from "../utils/utils.js";
|
|
15
15
|
import { audioCheckerError } from "../utils/error_cause.js";
|
|
16
16
|
import { extractImageFromMovie, ffmpegGetMediaDuration, trimMusic } from "../utils/ffmpeg_utils.js";
|
|
17
|
-
import {
|
|
17
|
+
import { getMediaRefs } from "./image_references.js";
|
|
18
18
|
import { imagePreprocessAgent, imagePluginAgent, htmlImageGeneratorAgent } from "./image_agents.js";
|
|
19
19
|
const vanillaAgents = vanilla.default ?? vanilla;
|
|
20
20
|
const imageAgents = {
|
|
@@ -52,6 +52,7 @@ export const beat_graph_data = {
|
|
|
52
52
|
context: {},
|
|
53
53
|
htmlImageAgentInfo: {},
|
|
54
54
|
imageRefs: {},
|
|
55
|
+
movieRefs: {},
|
|
55
56
|
beat: {},
|
|
56
57
|
__mapIndex: {},
|
|
57
58
|
forceMovie: { value: false },
|
|
@@ -66,6 +67,7 @@ export const beat_graph_data = {
|
|
|
66
67
|
beat: ":beat",
|
|
67
68
|
index: ":__mapIndex",
|
|
68
69
|
imageRefs: ":imageRefs",
|
|
70
|
+
movieRefs: ":movieRefs",
|
|
69
71
|
},
|
|
70
72
|
},
|
|
71
73
|
imagePlugin: {
|
|
@@ -77,6 +79,7 @@ export const beat_graph_data = {
|
|
|
77
79
|
beat: ":beat",
|
|
78
80
|
index: ":__mapIndex",
|
|
79
81
|
imageRefs: ":imageRefs",
|
|
82
|
+
movieRefs: ":movieRefs",
|
|
80
83
|
onComplete: [":preprocessor"],
|
|
81
84
|
},
|
|
82
85
|
},
|
|
@@ -334,6 +337,7 @@ export const images_graph_data = {
|
|
|
334
337
|
htmlImageAgentInfo: {},
|
|
335
338
|
outputStudioFilePath: {},
|
|
336
339
|
imageRefs: {},
|
|
340
|
+
movieRefs: {},
|
|
337
341
|
map: {
|
|
338
342
|
agent: "mapAgent",
|
|
339
343
|
inputs: {
|
|
@@ -341,6 +345,7 @@ export const images_graph_data = {
|
|
|
341
345
|
context: ":context",
|
|
342
346
|
htmlImageAgentInfo: ":htmlImageAgentInfo",
|
|
343
347
|
imageRefs: ":imageRefs",
|
|
348
|
+
movieRefs: ":movieRefs",
|
|
344
349
|
},
|
|
345
350
|
isResult: true,
|
|
346
351
|
params: {
|
|
@@ -436,13 +441,14 @@ const prepareGenerateImages = async (context) => {
|
|
|
436
441
|
mkdir(imageProjectDirPath);
|
|
437
442
|
const provider = MulmoPresentationStyleMethods.getText2ImageProvider(context.presentationStyle.imageParams?.provider);
|
|
438
443
|
const htmlImageAgentInfo = MulmoPresentationStyleMethods.getHtmlImageAgentInfo(context.presentationStyle);
|
|
439
|
-
const imageRefs = await
|
|
444
|
+
const { imageRefs, movieRefs } = await getMediaRefs(context);
|
|
440
445
|
GraphAILogger.info(`text2image: provider=${provider} model=${context.presentationStyle.imageParams?.model}`);
|
|
441
446
|
const injections = {
|
|
442
447
|
context,
|
|
443
448
|
htmlImageAgentInfo,
|
|
444
449
|
outputStudioFilePath: getOutputStudioFilePath(outDirPath, fileName),
|
|
445
450
|
imageRefs,
|
|
451
|
+
movieRefs,
|
|
446
452
|
};
|
|
447
453
|
return injections;
|
|
448
454
|
};
|
|
@@ -44,10 +44,11 @@ const getMediaDurationsOfAllBeats = (context) => {
|
|
|
44
44
|
const beat = context.studio.script.beats[index];
|
|
45
45
|
const { duration: movieDuration, hasAudio: hasMovieAudio } = await getMovieDuration(context, beat);
|
|
46
46
|
const audioDuration = studioBeat.audioFile ? (await ffmpegGetMediaDuration(studioBeat.audioFile)).duration : 0;
|
|
47
|
+
const hasMoviePrompt = Boolean(beat.moviePrompt);
|
|
47
48
|
return {
|
|
48
49
|
movieDuration,
|
|
49
50
|
audioDuration,
|
|
50
|
-
hasMedia: movieDuration + audioDuration > 0,
|
|
51
|
+
hasMedia: movieDuration + audioDuration > 0 || hasMoviePrompt,
|
|
51
52
|
silenceDuration: 0,
|
|
52
53
|
hasMovieAudio,
|
|
53
54
|
};
|
package/lib/types/schema.d.ts
CHANGED
|
@@ -276,6 +276,19 @@ export declare const mulmoImageMediaSchema: z.ZodObject<{
|
|
|
276
276
|
path: z.ZodString;
|
|
277
277
|
}, z.core.$strict>], "kind">;
|
|
278
278
|
}, z.core.$strict>;
|
|
279
|
+
export declare const mulmoMovieMediaSchema: z.ZodObject<{
|
|
280
|
+
type: z.ZodLiteral<"movie">;
|
|
281
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
282
|
+
kind: z.ZodLiteral<"url">;
|
|
283
|
+
url: z.ZodURL;
|
|
284
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
285
|
+
kind: z.ZodLiteral<"base64">;
|
|
286
|
+
data: z.ZodString;
|
|
287
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
288
|
+
kind: z.ZodLiteral<"path">;
|
|
289
|
+
path: z.ZodString;
|
|
290
|
+
}, z.core.$strict>], "kind">;
|
|
291
|
+
}, z.core.$strict>;
|
|
279
292
|
export declare const mulmoTextSlideMediaSchema: z.ZodObject<{
|
|
280
293
|
type: z.ZodLiteral<"textSlide">;
|
|
281
294
|
slide: z.ZodObject<{
|
|
@@ -2984,6 +2997,18 @@ export declare const mulmoImageParamsImagesValueSchema: z.ZodUnion<readonly [z.Z
|
|
|
2984
2997
|
width: z.ZodNumber;
|
|
2985
2998
|
height: z.ZodNumber;
|
|
2986
2999
|
}, z.core.$strict>>;
|
|
3000
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3001
|
+
type: z.ZodLiteral<"movie">;
|
|
3002
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3003
|
+
kind: z.ZodLiteral<"url">;
|
|
3004
|
+
url: z.ZodURL;
|
|
3005
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3006
|
+
kind: z.ZodLiteral<"base64">;
|
|
3007
|
+
data: z.ZodString;
|
|
3008
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3009
|
+
kind: z.ZodLiteral<"path">;
|
|
3010
|
+
path: z.ZodString;
|
|
3011
|
+
}, z.core.$strict>], "kind">;
|
|
2987
3012
|
}, z.core.$strict>]>;
|
|
2988
3013
|
export declare const mulmoImageParamsImagesSchema: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodObject<{
|
|
2989
3014
|
type: z.ZodLiteral<"image">;
|
|
@@ -3004,6 +3029,18 @@ export declare const mulmoImageParamsImagesSchema: z.ZodRecord<z.ZodString, z.Zo
|
|
|
3004
3029
|
width: z.ZodNumber;
|
|
3005
3030
|
height: z.ZodNumber;
|
|
3006
3031
|
}, z.core.$strict>>;
|
|
3032
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3033
|
+
type: z.ZodLiteral<"movie">;
|
|
3034
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3035
|
+
kind: z.ZodLiteral<"url">;
|
|
3036
|
+
url: z.ZodURL;
|
|
3037
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3038
|
+
kind: z.ZodLiteral<"base64">;
|
|
3039
|
+
data: z.ZodString;
|
|
3040
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3041
|
+
kind: z.ZodLiteral<"path">;
|
|
3042
|
+
path: z.ZodString;
|
|
3043
|
+
}, z.core.$strict>], "kind">;
|
|
3007
3044
|
}, z.core.$strict>]>>;
|
|
3008
3045
|
export declare const mulmoFillOptionSchema: z.ZodObject<{
|
|
3009
3046
|
style: z.ZodDefault<z.ZodOptional<z.ZodEnum<{
|
|
@@ -3076,6 +3113,18 @@ export declare const mulmoImageParamsSchema: z.ZodObject<{
|
|
|
3076
3113
|
width: z.ZodNumber;
|
|
3077
3114
|
height: z.ZodNumber;
|
|
3078
3115
|
}, z.core.$strict>>;
|
|
3116
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3117
|
+
type: z.ZodLiteral<"movie">;
|
|
3118
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
3119
|
+
kind: z.ZodLiteral<"url">;
|
|
3120
|
+
url: z.ZodURL;
|
|
3121
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3122
|
+
kind: z.ZodLiteral<"base64">;
|
|
3123
|
+
data: z.ZodString;
|
|
3124
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
3125
|
+
kind: z.ZodLiteral<"path">;
|
|
3126
|
+
path: z.ZodString;
|
|
3127
|
+
}, z.core.$strict>], "kind">;
|
|
3079
3128
|
}, z.core.$strict>]>>>;
|
|
3080
3129
|
backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
3081
3130
|
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -6429,6 +6478,18 @@ export declare const mulmoPresentationStyleSchema: z.ZodObject<{
|
|
|
6429
6478
|
width: z.ZodNumber;
|
|
6430
6479
|
height: z.ZodNumber;
|
|
6431
6480
|
}, z.core.$strict>>;
|
|
6481
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6482
|
+
type: z.ZodLiteral<"movie">;
|
|
6483
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
6484
|
+
kind: z.ZodLiteral<"url">;
|
|
6485
|
+
url: z.ZodURL;
|
|
6486
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6487
|
+
kind: z.ZodLiteral<"base64">;
|
|
6488
|
+
data: z.ZodString;
|
|
6489
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6490
|
+
kind: z.ZodLiteral<"path">;
|
|
6491
|
+
path: z.ZodString;
|
|
6492
|
+
}, z.core.$strict>], "kind">;
|
|
6432
6493
|
}, z.core.$strict>]>>>;
|
|
6433
6494
|
backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
6434
6495
|
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -6889,6 +6950,18 @@ export declare const mulmoScriptSchema: z.ZodObject<{
|
|
|
6889
6950
|
width: z.ZodNumber;
|
|
6890
6951
|
height: z.ZodNumber;
|
|
6891
6952
|
}, z.core.$strict>>;
|
|
6953
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6954
|
+
type: z.ZodLiteral<"movie">;
|
|
6955
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
6956
|
+
kind: z.ZodLiteral<"url">;
|
|
6957
|
+
url: z.ZodURL;
|
|
6958
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6959
|
+
kind: z.ZodLiteral<"base64">;
|
|
6960
|
+
data: z.ZodString;
|
|
6961
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
6962
|
+
kind: z.ZodLiteral<"path">;
|
|
6963
|
+
path: z.ZodString;
|
|
6964
|
+
}, z.core.$strict>], "kind">;
|
|
6892
6965
|
}, z.core.$strict>]>>>;
|
|
6893
6966
|
backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
6894
6967
|
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -10312,6 +10385,18 @@ export declare const mulmoStudioSchema: z.ZodObject<{
|
|
|
10312
10385
|
width: z.ZodNumber;
|
|
10313
10386
|
height: z.ZodNumber;
|
|
10314
10387
|
}, z.core.$strict>>;
|
|
10388
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
10389
|
+
type: z.ZodLiteral<"movie">;
|
|
10390
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
10391
|
+
kind: z.ZodLiteral<"url">;
|
|
10392
|
+
url: z.ZodURL;
|
|
10393
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
10394
|
+
kind: z.ZodLiteral<"base64">;
|
|
10395
|
+
data: z.ZodString;
|
|
10396
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
10397
|
+
kind: z.ZodLiteral<"path">;
|
|
10398
|
+
path: z.ZodString;
|
|
10399
|
+
}, z.core.$strict>], "kind">;
|
|
10315
10400
|
}, z.core.$strict>]>>>;
|
|
10316
10401
|
backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
10317
10402
|
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -13671,6 +13756,18 @@ export declare const mulmoPromptTemplateSchema: z.ZodObject<{
|
|
|
13671
13756
|
width: z.ZodNumber;
|
|
13672
13757
|
height: z.ZodNumber;
|
|
13673
13758
|
}, z.core.$strict>>;
|
|
13759
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
13760
|
+
type: z.ZodLiteral<"movie">;
|
|
13761
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
13762
|
+
kind: z.ZodLiteral<"url">;
|
|
13763
|
+
url: z.ZodURL;
|
|
13764
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
13765
|
+
kind: z.ZodLiteral<"base64">;
|
|
13766
|
+
data: z.ZodString;
|
|
13767
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
13768
|
+
kind: z.ZodLiteral<"path">;
|
|
13769
|
+
path: z.ZodString;
|
|
13770
|
+
}, z.core.$strict>], "kind">;
|
|
13674
13771
|
}, z.core.$strict>]>>>;
|
|
13675
13772
|
backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
13676
13773
|
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
@@ -14125,6 +14222,18 @@ export declare const mulmoPromptTemplateFileSchema: z.ZodObject<{
|
|
|
14125
14222
|
width: z.ZodNumber;
|
|
14126
14223
|
height: z.ZodNumber;
|
|
14127
14224
|
}, z.core.$strict>>;
|
|
14225
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
14226
|
+
type: z.ZodLiteral<"movie">;
|
|
14227
|
+
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
14228
|
+
kind: z.ZodLiteral<"url">;
|
|
14229
|
+
url: z.ZodURL;
|
|
14230
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
14231
|
+
kind: z.ZodLiteral<"base64">;
|
|
14232
|
+
data: z.ZodString;
|
|
14233
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
14234
|
+
kind: z.ZodLiteral<"path">;
|
|
14235
|
+
path: z.ZodString;
|
|
14236
|
+
}, z.core.$strict>], "kind">;
|
|
14128
14237
|
}, z.core.$strict>]>>>;
|
|
14129
14238
|
backgroundImage: z.ZodOptional<z.ZodNullable<z.ZodUnion<readonly [z.ZodString, z.ZodObject<{
|
|
14130
14239
|
source: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
package/lib/types/schema.js
CHANGED
|
@@ -142,7 +142,7 @@ const mulmoSvgMediaSchema = z
|
|
|
142
142
|
source: mediaSourceSchema,
|
|
143
143
|
})
|
|
144
144
|
.strict();
|
|
145
|
-
const mulmoMovieMediaSchema = z
|
|
145
|
+
export const mulmoMovieMediaSchema = z
|
|
146
146
|
.object({
|
|
147
147
|
type: z.literal("movie"),
|
|
148
148
|
source: mediaSourceSchema,
|
|
@@ -337,7 +337,7 @@ export const mulmoImagePromptMediaSchema = z
|
|
|
337
337
|
canvasSize: z.object({ width: z.number(), height: z.number() }).strict().optional(),
|
|
338
338
|
})
|
|
339
339
|
.strict();
|
|
340
|
-
export const mulmoImageParamsImagesValueSchema = z.union([mulmoImageMediaSchema, mulmoImagePromptMediaSchema]);
|
|
340
|
+
export const mulmoImageParamsImagesValueSchema = z.union([mulmoImageMediaSchema, mulmoImagePromptMediaSchema, mulmoMovieMediaSchema]);
|
|
341
341
|
export const mulmoImageParamsImagesSchema = z.record(imageIdSchema, mulmoImageParamsImagesValueSchema);
|
|
342
342
|
export const mulmoFillOptionSchema = z
|
|
343
343
|
.object({
|
package/lib/types/type.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CallbackFunction } from "graphai";
|
|
2
|
-
import { langSchema, localizedTextSchema, mulmoBeatSchema, mulmoScriptSchema, mulmoStudioSchema, mulmoStudioBeatSchema, mulmoStoryboardSchema, mulmoStoryboardSceneSchema, mulmoStudioMultiLingualSchema, mulmoStudioMultiLingualArraySchema, mulmoStudioMultiLingualDataSchema, mulmoStudioMultiLingualFileSchema, speakerDictionarySchema, speakerSchema, mulmoSpeechParamsSchema, mulmoImageParamsSchema, mulmoImageParamsImagesValueSchema, mulmoImageParamsImagesSchema, mulmoFillOptionSchema, mulmoTransitionSchema, mulmoVideoFilterSchema, mulmoMovieParamsSchema, mulmoSoundEffectParamsSchema, mulmoLipSyncParamsSchema, textSlideParamsSchema, speechOptionsSchema, speakerDataSchema, mulmoCanvasDimensionSchema, mulmoPromptTemplateSchema, mulmoPromptTemplateFileSchema, text2ImageProviderSchema, text2HtmlImageProviderSchema, text2MovieProviderSchema, text2SpeechProviderSchema, mulmoPresentationStyleSchema, multiLingualTextsSchema, mulmoImageAssetSchema, mulmoMermaidMediaSchema, mulmoTextSlideMediaSchema, mulmoMarkdownMediaSchema, mulmoImageMediaSchema, mulmoChartMediaSchema, mediaSourceSchema, mediaSourceMermaidSchema, backgroundImageSchema, backgroundImageSourceSchema, mulmoSessionStateSchema, mulmoOpenAIImageModelSchema, mulmoGoogleImageModelSchema, mulmoGoogleMovieModelSchema, mulmoReplicateMovieModelSchema, mulmoImagePromptMediaSchema, markdownLayoutSchema, row2Schema, grid2x2Schema } from "./schema.js";
|
|
2
|
+
import { langSchema, localizedTextSchema, mulmoBeatSchema, mulmoScriptSchema, mulmoStudioSchema, mulmoStudioBeatSchema, mulmoStoryboardSchema, mulmoStoryboardSceneSchema, mulmoStudioMultiLingualSchema, mulmoStudioMultiLingualArraySchema, mulmoStudioMultiLingualDataSchema, mulmoStudioMultiLingualFileSchema, speakerDictionarySchema, speakerSchema, mulmoSpeechParamsSchema, mulmoImageParamsSchema, mulmoImageParamsImagesValueSchema, mulmoImageParamsImagesSchema, mulmoFillOptionSchema, mulmoTransitionSchema, mulmoVideoFilterSchema, mulmoMovieParamsSchema, mulmoSoundEffectParamsSchema, mulmoLipSyncParamsSchema, textSlideParamsSchema, speechOptionsSchema, speakerDataSchema, mulmoCanvasDimensionSchema, mulmoPromptTemplateSchema, mulmoPromptTemplateFileSchema, text2ImageProviderSchema, text2HtmlImageProviderSchema, text2MovieProviderSchema, text2SpeechProviderSchema, mulmoPresentationStyleSchema, multiLingualTextsSchema, mulmoImageAssetSchema, mulmoMermaidMediaSchema, mulmoTextSlideMediaSchema, mulmoMarkdownMediaSchema, mulmoImageMediaSchema, mulmoChartMediaSchema, mediaSourceSchema, mediaSourceMermaidSchema, backgroundImageSchema, backgroundImageSourceSchema, mulmoSessionStateSchema, mulmoOpenAIImageModelSchema, mulmoGoogleImageModelSchema, mulmoGoogleMovieModelSchema, mulmoReplicateMovieModelSchema, mulmoImagePromptMediaSchema, mulmoMovieMediaSchema, markdownLayoutSchema, row2Schema, grid2x2Schema } from "./schema.js";
|
|
3
3
|
import { pdf_modes, pdf_sizes, storyToScriptGenerateMode } from "./const.js";
|
|
4
4
|
import type { LLM } from "./provider2agent.js";
|
|
5
5
|
import { z } from "zod";
|
|
@@ -55,6 +55,7 @@ export type MulmoImageAsset = z.infer<typeof mulmoImageAssetSchema>;
|
|
|
55
55
|
export type MulmoTextSlideMedia = z.infer<typeof mulmoTextSlideMediaSchema>;
|
|
56
56
|
export type MulmoMarkdownMedia = z.infer<typeof mulmoMarkdownMediaSchema>;
|
|
57
57
|
export type MulmoImageMedia = z.infer<typeof mulmoImageMediaSchema>;
|
|
58
|
+
export type MulmoMovieMedia = z.infer<typeof mulmoMovieMediaSchema>;
|
|
58
59
|
export type MulmoChartMedia = z.infer<typeof mulmoChartMediaSchema>;
|
|
59
60
|
export type MulmoMermaidMedia = z.infer<typeof mulmoMermaidMediaSchema>;
|
|
60
61
|
export type MulmoSessionState = z.infer<typeof mulmoSessionStateSchema>;
|
|
@@ -84,6 +85,7 @@ export type ImageProcessorParams = {
|
|
|
84
85
|
textSlideStyle: string;
|
|
85
86
|
canvasSize: MulmoCanvasDimension;
|
|
86
87
|
imageRefs?: Record<string, string>;
|
|
88
|
+
movieRefs?: Record<string, string>;
|
|
87
89
|
beatDuration?: number;
|
|
88
90
|
};
|
|
89
91
|
export type PDFMode = (typeof pdf_modes)[number];
|
package/lib/utils/context.d.ts
CHANGED
|
@@ -66,6 +66,18 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
|
|
|
66
66
|
kind: "path";
|
|
67
67
|
path: string;
|
|
68
68
|
};
|
|
69
|
+
} | {
|
|
70
|
+
type: "movie";
|
|
71
|
+
source: {
|
|
72
|
+
kind: "url";
|
|
73
|
+
url: string;
|
|
74
|
+
} | {
|
|
75
|
+
kind: "base64";
|
|
76
|
+
data: string;
|
|
77
|
+
} | {
|
|
78
|
+
kind: "path";
|
|
79
|
+
path: string;
|
|
80
|
+
};
|
|
69
81
|
} | {
|
|
70
82
|
type: "imagePrompt";
|
|
71
83
|
prompt: string;
|
|
@@ -1599,6 +1611,18 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
|
|
|
1599
1611
|
kind: "path";
|
|
1600
1612
|
path: string;
|
|
1601
1613
|
};
|
|
1614
|
+
} | {
|
|
1615
|
+
type: "movie";
|
|
1616
|
+
source: {
|
|
1617
|
+
kind: "url";
|
|
1618
|
+
url: string;
|
|
1619
|
+
} | {
|
|
1620
|
+
kind: "base64";
|
|
1621
|
+
data: string;
|
|
1622
|
+
} | {
|
|
1623
|
+
kind: "path";
|
|
1624
|
+
path: string;
|
|
1625
|
+
};
|
|
1602
1626
|
} | {
|
|
1603
1627
|
type: "markdown";
|
|
1604
1628
|
markdown: string | string[] | ({
|
|
@@ -1653,18 +1677,6 @@ export declare const createStudioData: (_mulmoScript: MulmoScript, fileName: str
|
|
|
1653
1677
|
kind: "path";
|
|
1654
1678
|
path: string;
|
|
1655
1679
|
};
|
|
1656
|
-
} | {
|
|
1657
|
-
type: "movie";
|
|
1658
|
-
source: {
|
|
1659
|
-
kind: "url";
|
|
1660
|
-
url: string;
|
|
1661
|
-
} | {
|
|
1662
|
-
kind: "base64";
|
|
1663
|
-
data: string;
|
|
1664
|
-
} | {
|
|
1665
|
-
kind: "path";
|
|
1666
|
-
path: string;
|
|
1667
|
-
};
|
|
1668
1680
|
} | {
|
|
1669
1681
|
type: "textSlide";
|
|
1670
1682
|
slide: {
|
|
@@ -2166,6 +2178,18 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
|
|
|
2166
2178
|
kind: "path";
|
|
2167
2179
|
path: string;
|
|
2168
2180
|
};
|
|
2181
|
+
} | {
|
|
2182
|
+
type: "movie";
|
|
2183
|
+
source: {
|
|
2184
|
+
kind: "url";
|
|
2185
|
+
url: string;
|
|
2186
|
+
} | {
|
|
2187
|
+
kind: "base64";
|
|
2188
|
+
data: string;
|
|
2189
|
+
} | {
|
|
2190
|
+
kind: "path";
|
|
2191
|
+
path: string;
|
|
2192
|
+
};
|
|
2169
2193
|
} | {
|
|
2170
2194
|
type: "imagePrompt";
|
|
2171
2195
|
prompt: string;
|
|
@@ -3699,6 +3723,18 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
|
|
|
3699
3723
|
kind: "path";
|
|
3700
3724
|
path: string;
|
|
3701
3725
|
};
|
|
3726
|
+
} | {
|
|
3727
|
+
type: "movie";
|
|
3728
|
+
source: {
|
|
3729
|
+
kind: "url";
|
|
3730
|
+
url: string;
|
|
3731
|
+
} | {
|
|
3732
|
+
kind: "base64";
|
|
3733
|
+
data: string;
|
|
3734
|
+
} | {
|
|
3735
|
+
kind: "path";
|
|
3736
|
+
path: string;
|
|
3737
|
+
};
|
|
3702
3738
|
} | {
|
|
3703
3739
|
type: "markdown";
|
|
3704
3740
|
markdown: string | string[] | ({
|
|
@@ -3753,18 +3789,6 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
|
|
|
3753
3789
|
kind: "path";
|
|
3754
3790
|
path: string;
|
|
3755
3791
|
};
|
|
3756
|
-
} | {
|
|
3757
|
-
type: "movie";
|
|
3758
|
-
source: {
|
|
3759
|
-
kind: "url";
|
|
3760
|
-
url: string;
|
|
3761
|
-
} | {
|
|
3762
|
-
kind: "base64";
|
|
3763
|
-
data: string;
|
|
3764
|
-
} | {
|
|
3765
|
-
kind: "path";
|
|
3766
|
-
path: string;
|
|
3767
|
-
};
|
|
3768
3792
|
} | {
|
|
3769
3793
|
type: "textSlide";
|
|
3770
3794
|
slide: {
|
|
@@ -4273,6 +4297,18 @@ export declare const initializeContextFromFiles: (files: FileObject, raiseError:
|
|
|
4273
4297
|
kind: "path";
|
|
4274
4298
|
path: string;
|
|
4275
4299
|
};
|
|
4300
|
+
} | {
|
|
4301
|
+
type: "movie";
|
|
4302
|
+
source: {
|
|
4303
|
+
kind: "url";
|
|
4304
|
+
url: string;
|
|
4305
|
+
} | {
|
|
4306
|
+
kind: "base64";
|
|
4307
|
+
data: string;
|
|
4308
|
+
} | {
|
|
4309
|
+
kind: "path";
|
|
4310
|
+
path: string;
|
|
4311
|
+
};
|
|
4276
4312
|
} | {
|
|
4277
4313
|
type: "imagePrompt";
|
|
4278
4314
|
prompt: string;
|
package/lib/utils/html_render.js
CHANGED
|
@@ -4,7 +4,58 @@ import nodePath from "node:path";
|
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
import { marked } from "marked";
|
|
6
6
|
import puppeteer from "puppeteer";
|
|
7
|
+
import { GraphAILogger } from "graphai";
|
|
7
8
|
const isCI = process.env.CI === "true";
|
|
9
|
+
const VIDEO_LOAD_TIMEOUT_MS = 15000;
|
|
10
|
+
const VIDEO_SEEK_TIMEOUT_MS = 3000;
|
|
11
|
+
/** Wait for all <video> elements on the page to be ready for playback */
|
|
12
|
+
const waitForVideosReady = async (page) => {
|
|
13
|
+
const hasVideos = await page.evaluate(() => document.querySelectorAll("video").length > 0);
|
|
14
|
+
if (!hasVideos)
|
|
15
|
+
return;
|
|
16
|
+
GraphAILogger.info("Waiting for video elements to load...");
|
|
17
|
+
await page.evaluate((timeout_ms) => {
|
|
18
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
19
|
+
const pending = videos.filter((v) => v.readyState < 3);
|
|
20
|
+
if (pending.length === 0)
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
/* eslint-disable sonarjs/no-nested-functions -- inside page.evaluate serialization boundary */
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
let remaining = pending.length;
|
|
25
|
+
const timer = setTimeout(() => resolve(), timeout_ms);
|
|
26
|
+
pending.forEach((v) => v.addEventListener("canplaythrough", () => {
|
|
27
|
+
remaining--;
|
|
28
|
+
if (remaining <= 0) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
resolve();
|
|
31
|
+
}
|
|
32
|
+
}, { once: true }));
|
|
33
|
+
});
|
|
34
|
+
/* eslint-enable sonarjs/no-nested-functions */
|
|
35
|
+
}, VIDEO_LOAD_TIMEOUT_MS);
|
|
36
|
+
};
|
|
37
|
+
/** Seek all <video> elements to the specified frame time and wait for seek to complete */
|
|
38
|
+
const syncVideosToFrame = async (page, frameIndex, fps) => {
|
|
39
|
+
const time = frameIndex / fps;
|
|
40
|
+
await page.evaluate((seekTime, seekTimeout) => {
|
|
41
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
42
|
+
if (videos.length === 0)
|
|
43
|
+
return;
|
|
44
|
+
videos.forEach((v) => {
|
|
45
|
+
v.pause();
|
|
46
|
+
v.currentTime = seekTime;
|
|
47
|
+
});
|
|
48
|
+
/* eslint-disable sonarjs/no-nested-functions -- unavoidable inside page.evaluate serialization boundary */
|
|
49
|
+
return Promise.all(videos.map((v) => new Promise((r) => {
|
|
50
|
+
const timer = setTimeout(() => r(), seekTimeout);
|
|
51
|
+
v.addEventListener("seeked", () => {
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
r();
|
|
54
|
+
}, { once: true });
|
|
55
|
+
})));
|
|
56
|
+
/* eslint-enable sonarjs/no-nested-functions */
|
|
57
|
+
}, time, VIDEO_SEEK_TIMEOUT_MS);
|
|
58
|
+
};
|
|
8
59
|
/** Scale the page content so it fits inside the viewport without overflow */
|
|
9
60
|
const scaleContentToFit = async (page, viewportWidth, viewportHeight) => {
|
|
10
61
|
await page.evaluate(({ targetWidth, targetHeight }) => {
|
|
@@ -115,9 +166,11 @@ export const renderHTMLToFrames = async (html, outputDir, width, height, totalFr
|
|
|
115
166
|
await page.addStyleTag({ content: "html{height:100%;margin:0;padding:0;overflow:hidden}" });
|
|
116
167
|
// Scale content to fit viewport (same logic as renderHTMLToImage)
|
|
117
168
|
await scaleContentToFit(page, width, height);
|
|
169
|
+
await waitForVideosReady(page);
|
|
118
170
|
const framePaths = [];
|
|
119
171
|
for (let frame = 0; frame < totalFrames; frame++) {
|
|
120
172
|
// Update frame state and call render() — await in case it returns a Promise
|
|
173
|
+
// Update frame state and call render()
|
|
121
174
|
await page.evaluate(async ({ frameIndex, totalFrameCount, framesPerSecond }) => {
|
|
122
175
|
const mulmoWindow = window;
|
|
123
176
|
mulmoWindow.__MULMO.frame = frameIndex;
|
|
@@ -125,6 +178,8 @@ export const renderHTMLToFrames = async (html, outputDir, width, height, totalFr
|
|
|
125
178
|
await mulmoWindow.render(frameIndex, totalFrameCount, framesPerSecond);
|
|
126
179
|
}
|
|
127
180
|
}, { frameIndex: frame, totalFrameCount: totalFrames, framesPerSecond: fps });
|
|
181
|
+
// Sync all <video> elements to the current frame time
|
|
182
|
+
await syncVideosToFrame(page, frame, fps);
|
|
128
183
|
const framePath = nodePath.join(outputDir, `frame_${String(frame).padStart(5, "0")}.png`);
|
|
129
184
|
await page.screenshot({ path: framePath });
|
|
130
185
|
framePaths.push(framePath);
|
|
@@ -151,6 +206,16 @@ export const renderHTMLToVideo = async (html, videoPath, width, height, totalFra
|
|
|
151
206
|
await page.setViewport({ width, height });
|
|
152
207
|
await page.addStyleTag({ content: "html{height:100%;margin:0;padding:0;overflow:hidden}" });
|
|
153
208
|
await scaleContentToFit(page, width, height);
|
|
209
|
+
await waitForVideosReady(page);
|
|
210
|
+
// Reset all videos to start and begin playback
|
|
211
|
+
await page.evaluate(() => {
|
|
212
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
213
|
+
return Promise.all(videos.map((v) => {
|
|
214
|
+
v.muted = true;
|
|
215
|
+
v.currentTime = 0;
|
|
216
|
+
return v.play().catch(() => { });
|
|
217
|
+
}));
|
|
218
|
+
});
|
|
154
219
|
const recorder = await page.screencast({
|
|
155
220
|
path: videoPath,
|
|
156
221
|
format: "mp4",
|
|
@@ -5,6 +5,11 @@ export declare const imageType = "html_tailwind";
|
|
|
5
5
|
* e.g., src="image:bg_office" → src="file:///abs/path/to/bg_office.png"
|
|
6
6
|
*/
|
|
7
7
|
export declare const resolveImageRefs: (html: string, imageRefs: Record<string, string>) => string;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve movie:name references to file:// absolute paths using movieRefs.
|
|
10
|
+
* e.g., src="movie:office_pan" → src="file:///abs/path/to/office_pan.mp4"
|
|
11
|
+
*/
|
|
12
|
+
export declare const resolveMovieRefs: (html: string, movieRefs: Record<string, string>) => string;
|
|
8
13
|
/**
|
|
9
14
|
* Resolve relative paths in src attributes to file:// absolute paths.
|
|
10
15
|
* Paths starting with http://, https://, file://, data:, image:, or / are left unchanged.
|
|
@@ -20,6 +20,19 @@ export const resolveImageRefs = (html, imageRefs) => {
|
|
|
20
20
|
return `${prefix}${quote}file://${resolvedPath}${quote}`;
|
|
21
21
|
});
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Resolve movie:name references to file:// absolute paths using movieRefs.
|
|
25
|
+
* e.g., src="movie:office_pan" → src="file:///abs/path/to/office_pan.mp4"
|
|
26
|
+
*/
|
|
27
|
+
export const resolveMovieRefs = (html, movieRefs) => {
|
|
28
|
+
return html.replace(/(\bsrc\s*=\s*)(["'])movie:([^"']+)\2/gi, (match, prefix, quote, name) => {
|
|
29
|
+
const resolvedPath = movieRefs[name];
|
|
30
|
+
if (!resolvedPath) {
|
|
31
|
+
return match;
|
|
32
|
+
}
|
|
33
|
+
return `${prefix}${quote}file://${resolvedPath}${quote}`;
|
|
34
|
+
});
|
|
35
|
+
};
|
|
23
36
|
/**
|
|
24
37
|
* Resolve relative paths in src attributes to file:// absolute paths.
|
|
25
38
|
* Paths starting with http://, https://, file://, data:, image:, or / are left unchanged.
|
|
@@ -100,8 +113,9 @@ const processHtmlTailwindAnimated = async (params) => {
|
|
|
100
113
|
fps: String(fps),
|
|
101
114
|
custom_style: "",
|
|
102
115
|
});
|
|
103
|
-
const
|
|
104
|
-
const
|
|
116
|
+
const resolvedImageRefs = resolveImageRefs(rawHtmlData, params.imageRefs ?? {});
|
|
117
|
+
const resolvedAllRefs = resolveMovieRefs(resolvedImageRefs, params.movieRefs ?? {});
|
|
118
|
+
const htmlData = resolveRelativeImagePaths(resolvedAllRefs, context.fileDirs.mulmoFileDirPath);
|
|
105
119
|
// imagePath is set to the .mp4 path by imagePluginAgent for animated beats
|
|
106
120
|
const videoPath = imagePath;
|
|
107
121
|
if (animConfig.movie) {
|
|
@@ -133,8 +147,9 @@ const processHtmlTailwindStatic = async (params) => {
|
|
|
133
147
|
html_body: html,
|
|
134
148
|
user_script: buildUserScript(script),
|
|
135
149
|
});
|
|
136
|
-
const
|
|
137
|
-
const
|
|
150
|
+
const resolvedImageRefs = resolveImageRefs(rawHtmlData, params.imageRefs ?? {});
|
|
151
|
+
const resolvedAllRefs = resolveMovieRefs(resolvedImageRefs, params.movieRefs ?? {});
|
|
152
|
+
const htmlData = resolveRelativeImagePaths(resolvedAllRefs, context.fileDirs.mulmoFileDirPath);
|
|
138
153
|
await renderHTMLToImage(htmlData, imagePath, canvasSize.width, canvasSize.height);
|
|
139
154
|
return imagePath;
|
|
140
155
|
};
|
|
@@ -3,6 +3,7 @@ import { renderHTMLToImage, interpolate } from "../html_render.js";
|
|
|
3
3
|
import { parrotingImagePath } from "./utils.js";
|
|
4
4
|
import { resolveCombinedStyle } from "./bg_image_util.js";
|
|
5
5
|
import { generateLayoutHtml, layoutToMarkdown, toMarkdownString, parseMarkdown } from "./markdown_layout.js";
|
|
6
|
+
import { resolveImageRefs, resolveMovieRefs } from "./html_tailwind.js";
|
|
6
7
|
import { isObject } from "graphai";
|
|
7
8
|
export const imageType = "markdown";
|
|
8
9
|
// Type guard for object (data) format
|
|
@@ -60,7 +61,9 @@ const processMarkdown = async (params) => {
|
|
|
60
61
|
const { beat, imagePath, canvasSize } = params;
|
|
61
62
|
if (!beat.image || beat.image.type !== imageType)
|
|
62
63
|
return;
|
|
63
|
-
const
|
|
64
|
+
const rawHtml = await generateHtml(params);
|
|
65
|
+
const resolvedImages = resolveImageRefs(rawHtml, params.imageRefs ?? {});
|
|
66
|
+
const html = resolveMovieRefs(resolvedImages, params.movieRefs ?? {});
|
|
64
67
|
const hasMermaid = containsMermaid(beat.image.markdown);
|
|
65
68
|
await renderHTMLToImage(html, imagePath, canvasSize.width, canvasSize.height, hasMermaid);
|
|
66
69
|
return imagePath;
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$mulmocast": { "version": "1.1" },
|
|
3
|
+
"lang": "en",
|
|
4
|
+
"canvasSize": { "width": 1920, "height": 1080 },
|
|
5
|
+
"title": "Markdown image:refs test",
|
|
6
|
+
"audioParams": {
|
|
7
|
+
"padding": 0,
|
|
8
|
+
"introPadding": 0,
|
|
9
|
+
"closingPadding": 0,
|
|
10
|
+
"outroPadding": 0
|
|
11
|
+
},
|
|
12
|
+
"imageParams": {
|
|
13
|
+
"images": {
|
|
14
|
+
"qaLandscape": {
|
|
15
|
+
"type": "image",
|
|
16
|
+
"source": {
|
|
17
|
+
"kind": "path",
|
|
18
|
+
"path": "images/qa_landscape.jpg"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"qaPortrait": {
|
|
22
|
+
"type": "image",
|
|
23
|
+
"source": {
|
|
24
|
+
"kind": "path",
|
|
25
|
+
"path": "images/qa_portrait.png"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"beats": [
|
|
31
|
+
{
|
|
32
|
+
"id": "markdown_text_image_ref",
|
|
33
|
+
"duration": 3,
|
|
34
|
+
"image": {
|
|
35
|
+
"type": "markdown",
|
|
36
|
+
"markdown": [
|
|
37
|
+
"# Markdown image:refs test",
|
|
38
|
+
"",
|
|
39
|
+
"",
|
|
40
|
+
"",
|
|
41
|
+
"This image is resolved from `imageParams.images` via the `image:` scheme."
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "markdown_layout_image_ref",
|
|
47
|
+
"duration": 3,
|
|
48
|
+
"image": {
|
|
49
|
+
"type": "markdown",
|
|
50
|
+
"markdown": {
|
|
51
|
+
"row-2": [
|
|
52
|
+
["# Landscape", ""],
|
|
53
|
+
["# Portrait", ""]
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$mulmocast": { "version": "1.1" },
|
|
3
|
+
"lang": "ja",
|
|
4
|
+
"canvasSize": { "width": 1080, "height": 1920 },
|
|
5
|
+
"title": "movie: スキームテスト",
|
|
6
|
+
"speechParams": {
|
|
7
|
+
"provider": "kotodama",
|
|
8
|
+
"speakers": {
|
|
9
|
+
"Presenter": { "provider": "kotodama", "voiceId": "jikkyo_baby" }
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"imageParams": {
|
|
13
|
+
"images": {
|
|
14
|
+
"sample_video": {
|
|
15
|
+
"type": "movie",
|
|
16
|
+
"source": {
|
|
17
|
+
"kind": "path",
|
|
18
|
+
"path": "../../test/assets/hello.mp4"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"beats": [
|
|
24
|
+
{
|
|
25
|
+
"text": "Beat1。動画を全画面背景として表示するパターンです。テキストオーバーレイ付き。",
|
|
26
|
+
"speaker": "Presenter",
|
|
27
|
+
"image": {
|
|
28
|
+
"type": "html_tailwind",
|
|
29
|
+
"html": [
|
|
30
|
+
"<div class='h-full w-full overflow-hidden relative bg-black'>",
|
|
31
|
+
" <div style='position:absolute;inset:0;overflow:hidden'>",
|
|
32
|
+
" <video src='movie:sample_video' autoplay muted loop style='width:100%;height:100%;object-fit:cover;filter:brightness(0.7)'></video>",
|
|
33
|
+
" </div>",
|
|
34
|
+
" <div style='position:absolute;top:50%;left:40px;right:40px;transform:translateY(-50%);text-align:center'>",
|
|
35
|
+
" <div style='display:inline-block;background:rgba(59,130,246,0.85);padding:12px 32px;border-radius:12px'>",
|
|
36
|
+
" <span style='color:white;font-size:80px;font-weight:900'>全画面背景</span>",
|
|
37
|
+
" </div>",
|
|
38
|
+
" <div style='color:white;font-size:48px;font-weight:900;margin-top:20px;text-shadow:0 4px 16px rgba(0,0,0,0.9)'>動画をフルスクリーン表示</div>",
|
|
39
|
+
" </div>",
|
|
40
|
+
"</div>"
|
|
41
|
+
],
|
|
42
|
+
"animation": { "movie": true }
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"text": "Beat2。画面の一部にだけ動画を表示するパターン。上半分に動画、下半分にテキストカードを置いています。",
|
|
47
|
+
"speaker": "Presenter",
|
|
48
|
+
"image": {
|
|
49
|
+
"type": "html_tailwind",
|
|
50
|
+
"html": [
|
|
51
|
+
"<div class='h-full w-full flex flex-col bg-gray-900'>",
|
|
52
|
+
" <div style='flex:1;overflow:hidden;border-bottom:4px solid #3B82F6'>",
|
|
53
|
+
" <video src='movie:sample_video' autoplay muted loop style='width:100%;height:100%;object-fit:cover'></video>",
|
|
54
|
+
" </div>",
|
|
55
|
+
" <div style='flex:1;display:flex;align-items:center;justify-content:center;padding:40px'>",
|
|
56
|
+
" <div style='text-align:center'>",
|
|
57
|
+
" <div style='color:#F59E0B;font-size:64px;font-weight:900;margin-bottom:16px'>上半分に動画</div>",
|
|
58
|
+
" <div style='color:white;font-size:44px;line-height:1.5'>下半分はテキストカード。<br>動画は画面の一部にだけ表示。</div>",
|
|
59
|
+
" </div>",
|
|
60
|
+
" </div>",
|
|
61
|
+
"</div>"
|
|
62
|
+
],
|
|
63
|
+
"animation": { "movie": true }
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"text": "Beat3。角丸の小さなウィンドウで動画をピクチャーインピクチャー風に表示するパターンです。",
|
|
68
|
+
"speaker": "Presenter",
|
|
69
|
+
"image": {
|
|
70
|
+
"type": "html_tailwind",
|
|
71
|
+
"html": [
|
|
72
|
+
"<div class='h-full w-full relative' style='background:linear-gradient(135deg,#1E293B 0%,#0F172A 100%)'>",
|
|
73
|
+
" <div style='position:absolute;top:80px;right:40px;width:400px;height:300px;border-radius:24px;overflow:hidden;border:3px solid rgba(255,255,255,0.3);box-shadow:0 8px 32px rgba(0,0,0,0.5)'>",
|
|
74
|
+
" <video src='movie:sample_video' autoplay muted loop style='width:100%;height:100%;object-fit:cover'></video>",
|
|
75
|
+
" </div>",
|
|
76
|
+
" <div style='position:absolute;top:50%;left:40px;right:40px;transform:translateY(-50%);text-align:left'>",
|
|
77
|
+
" <div style='color:#60A5FA;font-size:40px;font-weight:700;margin-bottom:12px'>PiP スタイル</div>",
|
|
78
|
+
" <div style='color:white;font-size:72px;font-weight:900;line-height:1.2'>右上に小さな<br>動画ウィンドウ</div>",
|
|
79
|
+
" <div style='color:rgba(255,255,255,0.6);font-size:36px;margin-top:20px'>角丸 + ボーダー + シャドウで浮遊感</div>",
|
|
80
|
+
" </div>",
|
|
81
|
+
"</div>"
|
|
82
|
+
],
|
|
83
|
+
"animation": { "movie": true }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
"text": "Beat4。円形にマスクした動画を中央に配置するパターン。プロフィール動画やアバター風の見せ方です。",
|
|
88
|
+
"speaker": "Presenter",
|
|
89
|
+
"image": {
|
|
90
|
+
"type": "html_tailwind",
|
|
91
|
+
"html": [
|
|
92
|
+
"<div class='h-full w-full flex flex-col items-center justify-center' style='background:linear-gradient(180deg,#312E81 0%,#1E1B4B 100%)'>",
|
|
93
|
+
" <div style='width:500px;height:500px;border-radius:50%;overflow:hidden;border:6px solid #A78BFA;box-shadow:0 0 60px rgba(167,139,250,0.4)'>",
|
|
94
|
+
" <video src='movie:sample_video' autoplay muted loop style='width:100%;height:100%;object-fit:cover'></video>",
|
|
95
|
+
" </div>",
|
|
96
|
+
" <div style='color:white;font-size:64px;font-weight:900;margin-top:40px'>円形マスク</div>",
|
|
97
|
+
" <div style='color:#C4B5FD;font-size:40px;margin-top:12px'>アバター / プロフィール風</div>",
|
|
98
|
+
"</div>"
|
|
99
|
+
],
|
|
100
|
+
"animation": { "movie": true }
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"text": "Beat5。左右分割で、左に動画、右にテキストを並べるスプリットレイアウトです。",
|
|
105
|
+
"speaker": "Presenter",
|
|
106
|
+
"image": {
|
|
107
|
+
"type": "html_tailwind",
|
|
108
|
+
"html": [
|
|
109
|
+
"<div class='h-full w-full flex flex-row bg-black'>",
|
|
110
|
+
" <div style='flex:1;overflow:hidden'>",
|
|
111
|
+
" <video src='movie:sample_video' autoplay muted loop style='width:100%;height:100%;object-fit:cover'></video>",
|
|
112
|
+
" </div>",
|
|
113
|
+
" <div style='flex:1;display:flex;align-items:center;justify-content:center;padding:32px;background:linear-gradient(180deg,#1E3A5F 0%,#0D1B2A 100%)'>",
|
|
114
|
+
" <div style='text-align:center'>",
|
|
115
|
+
" <div style='color:#38BDF8;font-size:56px;font-weight:900;margin-bottom:20px'>左右分割</div>",
|
|
116
|
+
" <div style='color:white;font-size:40px;line-height:1.5'>左半分が動画<br>右半分がテキスト</div>",
|
|
117
|
+
" <div style='margin-top:24px;width:120px;height:4px;background:#38BDF8;border-radius:2px;margin-left:auto;margin-right:auto'></div>",
|
|
118
|
+
" </div>",
|
|
119
|
+
" </div>",
|
|
120
|
+
"</div>"
|
|
121
|
+
],
|
|
122
|
+
"animation": { "movie": true }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|