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.
@@ -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 getImageRefs = async (context) => {
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
  };
@@ -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
  };
@@ -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 { getImageRefs } from "./image_references.js";
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 getImageRefs(context);
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
  };
@@ -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<{
@@ -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({
@@ -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];
@@ -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;
@@ -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 resolvedRefs = resolveImageRefs(rawHtmlData, params.imageRefs ?? {});
104
- const htmlData = resolveRelativeImagePaths(resolvedRefs, context.fileDirs.mulmoFileDirPath);
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 resolvedRefs = resolveImageRefs(rawHtmlData, params.imageRefs ?? {});
137
- const htmlData = resolveRelativeImagePaths(resolvedRefs, context.fileDirs.mulmoFileDirPath);
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 html = await generateHtml(params);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.6.0",
3
+ "version": "2.6.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -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
+ "![Landscape photo](image:qaLandscape)",
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", "![Landscape](image:qaLandscape)"],
53
+ ["# Portrait", "![Portrait](image:qaPortrait)"]
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
+ }