mulmocast 2.7.1 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/agents/combine_audio_files_agent.js +17 -2
- package/lib/agents/movie_replicate_agent.js +7 -4
- package/lib/methods/index.d.ts +1 -1
- package/lib/methods/index.js +3 -1
- package/lib/methods/mulmo_beat.d.ts +0 -7
- package/lib/methods/mulmo_beat.js +0 -8
- package/lib/methods/mulmo_beat_node.d.ts +22 -0
- package/lib/methods/mulmo_beat_node.js +10 -0
- package/lib/methods/mulmo_presentation_style.d.ts +1 -0
- package/lib/methods/mulmo_presentation_style.js +12 -1
- package/lib/types/provider2agent.d.ts +2 -1
- package/lib/types/provider2agent.js +26 -3
- package/lib/types/schema.js +10 -2
- package/lib/utils/image_plugins/index.d.ts +8 -1
- package/lib/utils/image_plugins/index.js +7 -0
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { assert, GraphAILogger } from "graphai";
|
|
|
2
2
|
import { silent60secPath, isFile } from "../utils/file.js";
|
|
3
3
|
import { FfmpegContextInit, FfmpegContextGenerateOutput, FfmpegContextInputFormattedAudio, ffmpegGetMediaDuration, } from "../utils/ffmpeg_utils.js";
|
|
4
4
|
import { MulmoMediaSourceMethods } from "../methods/mulmo_media_source.js";
|
|
5
|
-
import { MulmoBeatMethods } from "../methods/index.js";
|
|
5
|
+
import { MulmoBeatMethods, MulmoPresentationStyleMethods } from "../methods/index.js";
|
|
6
6
|
import { userAssert } from "../utils/utils.js";
|
|
7
7
|
import { getAudioInputIdsError } from "../utils/error_cause.js";
|
|
8
8
|
const getMovieDuration = async (context, beat) => {
|
|
@@ -126,7 +126,22 @@ const getVoiceOverGroup = (context, index) => {
|
|
|
126
126
|
};
|
|
127
127
|
const getSpillOverGroup = (context, mediaDurations, index) => {
|
|
128
128
|
const group = [index];
|
|
129
|
-
for (let i = index + 1; i < context.studio.beats.length
|
|
129
|
+
for (let i = index + 1; i < context.studio.beats.length; i++) {
|
|
130
|
+
const media = mediaDurations[i];
|
|
131
|
+
// An empty-text beat with only a moviePrompt (no generated audio requested) continues
|
|
132
|
+
// the preceding narration (spill-over). Generated movies don't exist yet at this point,
|
|
133
|
+
// so movieDuration === 0; imported movies and animated html_tailwind beats have
|
|
134
|
+
// movieDuration > 0 and keep their own timeline, ending the group as before.
|
|
135
|
+
const beat = context.studio.script.beats[i];
|
|
136
|
+
const isSilentContinuation = !beat.text?.trim() &&
|
|
137
|
+
media.audioDuration === 0 &&
|
|
138
|
+
media.movieDuration === 0 &&
|
|
139
|
+
Boolean(beat.moviePrompt) &&
|
|
140
|
+
!beat.soundEffectPrompt &&
|
|
141
|
+
!MulmoPresentationStyleMethods.generatedMovieHasAudio(context.presentationStyle, beat);
|
|
142
|
+
if (media.hasMedia && !isSilentContinuation) {
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
130
145
|
group.push(i);
|
|
131
146
|
}
|
|
132
147
|
return group;
|
|
@@ -81,11 +81,14 @@ async function generateMovie(model, apiKey, prompt, imagePath, lastFrameImagePat
|
|
|
81
81
|
}
|
|
82
82
|
// Add generate_audio if the model supports it
|
|
83
83
|
const audio = provider2MovieAgent.replicate.modelParams[model].audio;
|
|
84
|
+
if (audio.mode === AUDIO_MODE_OPTIONAL) {
|
|
85
|
+
// Always send the flag: several models (e.g. seedance-2.0's generate_audio, p-video's
|
|
86
|
+
// save_audio) default it to true, which silently embeds generated audio.
|
|
87
|
+
// Silent unless explicitly requested.
|
|
88
|
+
input[audio.param] = generateAudio ?? false;
|
|
89
|
+
}
|
|
84
90
|
if (generateAudio !== undefined) {
|
|
85
|
-
if (audio.mode ===
|
|
86
|
-
input[audio.param] = generateAudio;
|
|
87
|
-
}
|
|
88
|
-
else if (audio.mode === AUDIO_MODE_NEVER && generateAudio === true) {
|
|
91
|
+
if (audio.mode === AUDIO_MODE_NEVER && generateAudio === true) {
|
|
89
92
|
throw new Error(`Model ${model} does not support audio generation`, {
|
|
90
93
|
cause: agentGenerationError("movieReplicateAgent", imageAction, unsupportedModelTarget),
|
|
91
94
|
});
|
package/lib/methods/index.d.ts
CHANGED
package/lib/methods/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export * from "./mulmo_presentation_style.js";
|
|
2
2
|
export * from "./mulmo_studio_context.js";
|
|
3
3
|
export * from "./mulmo_media_source.js";
|
|
4
|
-
|
|
4
|
+
// Node-only MulmoBeatMethods (includes getPlugin). The browser-safe subset lives in
|
|
5
|
+
// ./mulmo_beat.js and is re-exported by ../index.common.js for mulmocast/browser.
|
|
6
|
+
export * from "./mulmo_beat_node.js";
|
|
5
7
|
export * from "./mulmo_script.js";
|
|
@@ -9,13 +9,6 @@ export declare const MulmoBeatMethods: {
|
|
|
9
9
|
isAnimatedHtmlTailwind: (beat: MulmoBeat) => boolean;
|
|
10
10
|
isMovieMode: (animation: unknown) => boolean;
|
|
11
11
|
getHtmlPrompt(beat: MulmoBeat): string | undefined;
|
|
12
|
-
getPlugin(beat: MulmoBeat): {
|
|
13
|
-
imageType: string;
|
|
14
|
-
process: (params: import("../types/type.js").ImageProcessorParams) => Promise<string | undefined> | void;
|
|
15
|
-
path: (params: import("../types/type.js").ImageProcessorParams) => string | undefined;
|
|
16
|
-
markdown?: (params: import("../types/type.js").ImageProcessorParams) => string | undefined;
|
|
17
|
-
html?: (params: import("../types/type.js").ImageProcessorParams) => Promise<string | undefined>;
|
|
18
|
-
};
|
|
19
12
|
getImageReferenceForImageGenerator(beat: MulmoBeat, imageRefs: Record<string, string>): string[];
|
|
20
13
|
};
|
|
21
14
|
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { findImagePlugin } from "../utils/image_plugins/index.js";
|
|
2
1
|
/** Type guard: checks if animation value is an object config like { fps: 30 } */
|
|
3
2
|
const isAnimationObject = (animation) => {
|
|
4
3
|
return typeof animation === "object" && animation !== null && !Array.isArray(animation);
|
|
@@ -29,13 +28,6 @@ export const MulmoBeatMethods = {
|
|
|
29
28
|
}
|
|
30
29
|
return beat?.htmlPrompt?.prompt;
|
|
31
30
|
},
|
|
32
|
-
getPlugin(beat) {
|
|
33
|
-
const plugin = findImagePlugin(beat?.image?.type);
|
|
34
|
-
if (!plugin) {
|
|
35
|
-
throw new Error(`invalid beat image type: ${beat.image}`); // TODO: cause
|
|
36
|
-
}
|
|
37
|
-
return plugin;
|
|
38
|
-
},
|
|
39
31
|
getImageReferenceForImageGenerator(beat, imageRefs) {
|
|
40
32
|
const imageNames = beat.imageNames ?? Object.keys(imageRefs); // use all images if imageNames is not specified
|
|
41
33
|
const sources = imageNames.map((name) => imageRefs[name]);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MulmoBeat } from "../types/index.js";
|
|
2
|
+
export declare const MulmoBeatMethods: {
|
|
3
|
+
getPlugin(beat: MulmoBeat): {
|
|
4
|
+
imageType: string;
|
|
5
|
+
process: (params: import("../types/type.js").ImageProcessorParams) => Promise<string | undefined> | void;
|
|
6
|
+
path: (params: import("../types/type.js").ImageProcessorParams) => string | undefined;
|
|
7
|
+
markdown?: (params: import("../types/type.js").ImageProcessorParams) => string | undefined;
|
|
8
|
+
html?: (params: import("../types/type.js").ImageProcessorParams) => Promise<string | undefined>;
|
|
9
|
+
};
|
|
10
|
+
isAnimationEnabled: (animation: unknown) => animation is true | {
|
|
11
|
+
fps?: number;
|
|
12
|
+
movie?: boolean;
|
|
13
|
+
};
|
|
14
|
+
isAnimationObject: (animation: unknown) => animation is {
|
|
15
|
+
fps?: number;
|
|
16
|
+
movie?: boolean;
|
|
17
|
+
};
|
|
18
|
+
isAnimatedHtmlTailwind: (beat: MulmoBeat) => boolean;
|
|
19
|
+
isMovieMode: (animation: unknown) => boolean;
|
|
20
|
+
getHtmlPrompt(beat: MulmoBeat): string | undefined;
|
|
21
|
+
getImageReferenceForImageGenerator(beat: MulmoBeat, imageRefs: Record<string, string>): string[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { MulmoBeatMethods as MulmoBeatMethodsBrowser } from "./mulmo_beat.js";
|
|
2
|
+
import { getBeatPlugin } from "../utils/image_plugins/index.js";
|
|
3
|
+
// Node-only extension of MulmoBeatMethods. Kept out of methods/mulmo_beat.ts so
|
|
4
|
+
// browser bundles (mulmocast/browser) do not pull in image_plugins → mulmocast-vision → puppeteer.
|
|
5
|
+
export const MulmoBeatMethods = {
|
|
6
|
+
...MulmoBeatMethodsBrowser,
|
|
7
|
+
getPlugin(beat) {
|
|
8
|
+
return getBeatPlugin(beat);
|
|
9
|
+
},
|
|
10
|
+
};
|
|
@@ -204,6 +204,7 @@ export declare const MulmoPresentationStyleMethods: {
|
|
|
204
204
|
};
|
|
205
205
|
keyName: string;
|
|
206
206
|
};
|
|
207
|
+
generatedMovieHasAudio(presentationStyle: MulmoPresentationStyle, beat: MulmoBeat): boolean;
|
|
207
208
|
getSoundEffectAgentInfo(presentationStyle: MulmoPresentationStyle, beat: MulmoBeat): {
|
|
208
209
|
agentName: string;
|
|
209
210
|
defaultModel: import("../types/provider2agent.js").ReplicateModel;
|
|
@@ -7,7 +7,7 @@ import { isNull } from "graphai";
|
|
|
7
7
|
import { userAssert } from "../utils/utils.js";
|
|
8
8
|
import { slideThemes } from "../data/slideThemes.js";
|
|
9
9
|
import { text2ImageProviderSchema, text2HtmlImageProviderSchema, text2MovieProviderSchema, text2SpeechProviderSchema, mulmoCanvasDimensionSchema, mulmoTransitionSchema, } from "../types/schema.js";
|
|
10
|
-
import { provider2ImageAgent, provider2MovieAgent, provider2LLMAgent, provider2TTSAgent, provider2SoundEffectAgent, provider2LipSyncAgent, defaultProviders, } from "../types/provider2agent.js";
|
|
10
|
+
import { provider2ImageAgent, provider2MovieAgent, provider2LLMAgent, provider2TTSAgent, provider2SoundEffectAgent, provider2LipSyncAgent, defaultProviders, getModelAudio, AUDIO_MODE_ALWAYS, } from "../types/provider2agent.js";
|
|
11
11
|
const defaultTextSlideStyles = [
|
|
12
12
|
'*,*::before,*::after{box-sizing:border-box}body,h1,h2,h3,h4,p,figure,blockquote,dl,dd{margin:0}ul[role="list"],ol[role="list"]{list-style:none}html:focus-within{scroll-behavior:smooth}body{min-height:100vh;text-rendering:optimizeSpeed;line-height:1.5}a:not([class]){text-decoration-skip-ink:auto}img,picture{max-width:100%;display:block}input,button,textarea,select{font:inherit}@media(prefers-reduced-motion:reduce){html:focus-within{scroll-behavior:auto}*,*::before,*::after{animation-duration:.01ms !important;animation-iteration-count:1 !important;transition-duration:.01ms !important;scroll-behavior:auto !important}}',
|
|
13
13
|
"body { margin: 60px; margin-top: 40px; color:#333; background-color:#fff; font-size: 30px; font-family: Arial, sans-serif; box-sizing: border-box; height: 100vh }",
|
|
@@ -131,6 +131,17 @@ export const MulmoPresentationStyleMethods = {
|
|
|
131
131
|
keyName: agentInfo.keyName,
|
|
132
132
|
};
|
|
133
133
|
},
|
|
134
|
+
// True when the movie generated for this beat will carry an audio track:
|
|
135
|
+
// the model always embeds audio, or audio generation is explicitly requested.
|
|
136
|
+
generatedMovieHasAudio(presentationStyle, beat) {
|
|
137
|
+
const movieParams = { ...presentationStyle.movieParams, ...beat.movieParams };
|
|
138
|
+
if (movieParams?.generateAudio === true) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
const provider = text2MovieProviderSchema.parse(movieParams?.provider ?? defaultProviders.text2movie);
|
|
142
|
+
const model = movieParams?.model ?? provider2MovieAgent[provider].defaultModel;
|
|
143
|
+
return getModelAudio(provider, model)?.mode === AUDIO_MODE_ALWAYS;
|
|
144
|
+
},
|
|
134
145
|
getSoundEffectAgentInfo(presentationStyle, beat) {
|
|
135
146
|
const soundEffectProvider = (beat.soundEffectParams?.provider ??
|
|
136
147
|
presentationStyle.soundEffectParams?.provider ??
|
|
@@ -88,7 +88,7 @@ export type ReplicateModel = `${string}/${string}`;
|
|
|
88
88
|
export declare const AUDIO_MODE_NEVER: "never";
|
|
89
89
|
export declare const AUDIO_MODE_ALWAYS: "always";
|
|
90
90
|
export declare const AUDIO_MODE_OPTIONAL: "optional";
|
|
91
|
-
type MovieAudioSpec = {
|
|
91
|
+
export type MovieAudioSpec = {
|
|
92
92
|
mode: typeof AUDIO_MODE_NEVER;
|
|
93
93
|
} | {
|
|
94
94
|
mode: typeof AUDIO_MODE_ALWAYS;
|
|
@@ -213,6 +213,7 @@ export declare const defaultProviders: {
|
|
|
213
213
|
export declare const llm: (keyof typeof provider2LLMAgent)[];
|
|
214
214
|
export type LLM = keyof typeof provider2LLMAgent;
|
|
215
215
|
export declare const htmlLLMProvider: string[];
|
|
216
|
+
export declare const getModelAudio: (provider: keyof typeof provider2MovieAgent, model: string) => MovieAudioSpec | undefined;
|
|
216
217
|
export declare const getModelDuration: (provider: keyof typeof provider2MovieAgent, model: string, movieDuration?: number) => number | undefined;
|
|
217
218
|
export type ModelPricing = {
|
|
218
219
|
unit: "tokens" | "chars" | "seconds" | "images";
|
|
@@ -144,10 +144,12 @@ export const provider2MovieAgent = {
|
|
|
144
144
|
"minimax/hailuo-2.3",
|
|
145
145
|
"minimax/hailuo-2.3-fast",
|
|
146
146
|
"pixverse/pixverse-v5",
|
|
147
|
+
"prunaai/p-video",
|
|
148
|
+
"xai/grok-imagine-video-1.5",
|
|
147
149
|
],
|
|
148
150
|
modelParams: {
|
|
149
151
|
"bytedance/seedance-1-lite": {
|
|
150
|
-
durations: [5, 10],
|
|
152
|
+
durations: [4, 5, 6, 7, 8, 9, 10, 11, 12],
|
|
151
153
|
start_image: "image",
|
|
152
154
|
last_image: "last_frame_image",
|
|
153
155
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
@@ -259,10 +261,13 @@ export const provider2MovieAgent = {
|
|
|
259
261
|
price_per_sec: 0.12,
|
|
260
262
|
},
|
|
261
263
|
"wan-video/wan-2.2-i2v-fast": {
|
|
264
|
+
// No duration input: length is num_frames (default 81) / frames_per_second (default 16) ≈ 5s.
|
|
262
265
|
durations: [5],
|
|
263
266
|
start_image: "image",
|
|
267
|
+
start_image_required: true,
|
|
268
|
+
last_image: "last_image",
|
|
264
269
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
265
|
-
price_per_sec: 0.
|
|
270
|
+
price_per_sec: 0.01, // $0.05 per 81-frame 480p video; up to $0.145 at 720p
|
|
266
271
|
},
|
|
267
272
|
"wan-video/wan-2.2-t2v-fast": {
|
|
268
273
|
durations: [5],
|
|
@@ -295,7 +300,7 @@ export const provider2MovieAgent = {
|
|
|
295
300
|
last_image: "end_image",
|
|
296
301
|
reference_images_param: "reference_images",
|
|
297
302
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
298
|
-
price_per_sec: 0.
|
|
303
|
+
price_per_sec: 0.28, // 'pro' (1080p, default); 'standard' $0.168, '4k' $0.42
|
|
299
304
|
},
|
|
300
305
|
"kwaivgi/kling-v3-video": {
|
|
301
306
|
durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
@@ -335,6 +340,20 @@ export const provider2MovieAgent = {
|
|
|
335
340
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
336
341
|
price_per_sec: 0.12,
|
|
337
342
|
},
|
|
343
|
+
"prunaai/p-video": {
|
|
344
|
+
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
|
|
345
|
+
start_image: "image",
|
|
346
|
+
last_image: "last_frame_image",
|
|
347
|
+
audio: { mode: AUDIO_MODE_OPTIONAL, param: "save_audio" },
|
|
348
|
+
price_per_sec: 0.02, // 720p, draft mode off ($0.04 at 1080p)
|
|
349
|
+
},
|
|
350
|
+
"xai/grok-imagine-video-1.5": {
|
|
351
|
+
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
352
|
+
start_image: "image",
|
|
353
|
+
start_image_required: true,
|
|
354
|
+
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
355
|
+
price_per_sec: 0.08,
|
|
356
|
+
},
|
|
338
357
|
},
|
|
339
358
|
},
|
|
340
359
|
google: {
|
|
@@ -508,6 +527,10 @@ export const defaultProviders = {
|
|
|
508
527
|
};
|
|
509
528
|
export const llm = Object.keys(provider2LLMAgent);
|
|
510
529
|
export const htmlLLMProvider = ["openai", "anthropic", "mock"];
|
|
530
|
+
export const getModelAudio = (provider, model) => {
|
|
531
|
+
const modelParams = provider2MovieAgent[provider]?.modelParams;
|
|
532
|
+
return modelParams?.[model]?.audio;
|
|
533
|
+
};
|
|
511
534
|
export const getModelDuration = (provider, model, movieDuration) => {
|
|
512
535
|
const modelParams = provider2MovieAgent[provider]?.modelParams;
|
|
513
536
|
const { durations } = modelParams[model];
|
package/lib/types/schema.js
CHANGED
|
@@ -423,8 +423,16 @@ export const audioParamsSchema = z
|
|
|
423
423
|
.object({
|
|
424
424
|
padding: z.number().optional().default(0.3).describe("Padding between beats"), // seconds
|
|
425
425
|
introPadding: z.number().optional().default(1.0).describe("Padding at the beginning of the audio"), // seconds
|
|
426
|
-
closingPadding: z
|
|
427
|
-
|
|
426
|
+
closingPadding: z
|
|
427
|
+
.number()
|
|
428
|
+
.optional()
|
|
429
|
+
.default(0.8)
|
|
430
|
+
.describe("Padding before the last beat (typically the closing credit). For padding after the last beat, use outroPadding"), // seconds
|
|
431
|
+
outroPadding: z
|
|
432
|
+
.number()
|
|
433
|
+
.optional()
|
|
434
|
+
.default(1.0)
|
|
435
|
+
.describe("Padding after the last beat, at the very end of the audio where the BGM fades out (distinct from closingPadding, which comes before the last beat)"), // seconds
|
|
428
436
|
bgm: mediaSourceSchema.optional(),
|
|
429
437
|
bgmVolume: z.number().optional().default(0.2).describe("Volume of the background music"),
|
|
430
438
|
audioVolume: z.number().optional().default(1.0).describe("Volume of the audio"),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ImageProcessorParams } from "../../types/index.js";
|
|
1
|
+
import { ImageProcessorParams, MulmoBeat } from "../../types/index.js";
|
|
2
2
|
export declare const findImagePlugin: (imageType?: string) => {
|
|
3
3
|
imageType: string;
|
|
4
4
|
process: (params: ImageProcessorParams) => Promise<string | undefined> | void;
|
|
@@ -6,3 +6,10 @@ export declare const findImagePlugin: (imageType?: string) => {
|
|
|
6
6
|
markdown?: (params: ImageProcessorParams) => string | undefined;
|
|
7
7
|
html?: (params: ImageProcessorParams) => Promise<string | undefined>;
|
|
8
8
|
} | undefined;
|
|
9
|
+
export declare const getBeatPlugin: (beat: MulmoBeat) => {
|
|
10
|
+
imageType: string;
|
|
11
|
+
process: (params: ImageProcessorParams) => Promise<string | undefined> | void;
|
|
12
|
+
path: (params: ImageProcessorParams) => string | undefined;
|
|
13
|
+
markdown?: (params: ImageProcessorParams) => string | undefined;
|
|
14
|
+
html?: (params: ImageProcessorParams) => Promise<string | undefined>;
|
|
15
|
+
};
|
|
@@ -25,3 +25,10 @@ const imagePlugins = [
|
|
|
25
25
|
export const findImagePlugin = (imageType) => {
|
|
26
26
|
return imagePlugins.find((plugin) => plugin.imageType === imageType);
|
|
27
27
|
};
|
|
28
|
+
export const getBeatPlugin = (beat) => {
|
|
29
|
+
const plugin = findImagePlugin(beat?.image?.type);
|
|
30
|
+
if (!plugin) {
|
|
31
|
+
throw new Error(`invalid beat image type: ${beat.image}`);
|
|
32
|
+
}
|
|
33
|
+
return plugin;
|
|
34
|
+
};
|