mulmocast 2.7.2 → 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/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/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
|
});
|
|
@@ -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];
|