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.
@@ -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 && !mediaDurations[i].hasMedia; i++) {
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 === AUDIO_MODE_OPTIONAL) {
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.012,
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.3,
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.7.2",
3
+ "version": "2.8.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",