mulmocast 2.15.0 → 2.17.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/README.md +2 -8
- package/lib/actions/image_references.d.ts +25 -1
- package/lib/actions/image_references.js +18 -9
- package/lib/agents/image_genai_agent.js +3 -3
- package/lib/agents/image_openai_agent.js +4 -3
- package/lib/agents/movie_genai_agent.js +5 -7
- package/lib/agents/tts_gemini_agent.d.ts +56 -0
- package/lib/agents/tts_gemini_agent.js +43 -38
- package/lib/data/scriptTemplates.js +2 -2
- package/lib/data/templateDataSet.js +1 -1
- package/lib/methods/mulmo_presentation_style.js +3 -3
- package/lib/types/provider2agent.d.ts +27 -16
- package/lib/types/provider2agent.js +110 -65
- package/lib/types/schema.d.ts +1 -1
- package/lib/utils/estimate_usage.js +13 -9
- package/lib/utils/gemini_tts.d.ts +45 -0
- package/lib/utils/gemini_tts.js +35 -0
- package/lib/utils/remotion/network_guard.d.ts +5 -0
- package/lib/utils/remotion/network_guard.js +48 -0
- package/lib/utils/remotion/network_policy.d.ts +10 -0
- package/lib/utils/remotion/network_policy.js +51 -0
- package/lib/utils/remotion/render.js +49 -5
- package/package.json +2 -2
- package/scripts/templates/html.json +1 -1
- package/scripts/templates/presentation.json +1 -1
- package/scripts/test/test_genai.json +2 -2
- package/scripts/test/test_gpt_image.json +1 -1
- package/scripts/test/test_replicate.json +3 -3
- package/scripts/test/test_vertexai.json +3 -3
|
@@ -16,9 +16,10 @@ export const provider2TTSAgent = {
|
|
|
16
16
|
gemini: {
|
|
17
17
|
agentName: "ttsGeminiAgent",
|
|
18
18
|
hasLimitedConcurrency: false,
|
|
19
|
-
|
|
19
|
+
// The 2.5 preview TTS models shut down on 2026-11-17; Google names gemini-3.8-flash-tts as their successor.
|
|
20
|
+
defaultModel: "gemini-3.8-flash-tts",
|
|
20
21
|
defaultVoice: "Kore",
|
|
21
|
-
models: ["gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"],
|
|
22
|
+
models: ["gemini-3.8-flash-tts", "gemini-3.8-flash-lite-tts", "gemini-2.5-flash-preview-tts", "gemini-2.5-pro-preview-tts"],
|
|
22
23
|
keyName: "GEMINI_API_KEY",
|
|
23
24
|
},
|
|
24
25
|
elevenlabs: {
|
|
@@ -44,13 +45,18 @@ export const provider2TTSAgent = {
|
|
|
44
45
|
models: ["mock-model"],
|
|
45
46
|
},
|
|
46
47
|
};
|
|
47
|
-
export const gptImages = ["gpt-image-2", "gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini"];
|
|
48
|
-
|
|
48
|
+
export const gptImages = ["gpt-image-2.5-sunburst", "gpt-image-2.5-flare", "gpt-image-2", "gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini"];
|
|
49
|
+
// The 2.5 models render "auto" (their default) at "low", where gpt-image-1 rendered it at "high".
|
|
50
|
+
export const isGptImage25Model = (model) => model.startsWith("gpt-image-2.5");
|
|
51
|
+
// An unset quality on 2.5 is sent as "high" to keep the look scripts had before the default model changed;
|
|
52
|
+
// an explicit "auto" is the script's choice and is sent as is.
|
|
53
|
+
export const gptImageQuality = (model, quality) => quality ?? (isGptImage25Model(model) ? "high" : undefined);
|
|
54
|
+
const supportedOpenAIImageReplacementHint = "Use 'gpt-image-2.5-sunburst' or another supported model.";
|
|
49
55
|
export const deprecatedOpenAIImageModelHints = {
|
|
50
56
|
"dall-e-2": supportedOpenAIImageReplacementHint,
|
|
51
57
|
"dall-e-3": supportedOpenAIImageReplacementHint,
|
|
52
58
|
};
|
|
53
|
-
const supportedGoogleImageReplacementHint = "Use 'gemini-
|
|
59
|
+
const supportedGoogleImageReplacementHint = "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
54
60
|
export const deprecatedGoogleImageModelHints = {
|
|
55
61
|
"imagen-3.0-generate-002": supportedGoogleImageReplacementHint,
|
|
56
62
|
"imagen-4.0-generate-001": supportedGoogleImageReplacementHint,
|
|
@@ -59,11 +65,21 @@ export const deprecatedGoogleImageModelHints = {
|
|
|
59
65
|
"imagen-4.0-generate-preview-06-06": supportedGoogleImageReplacementHint,
|
|
60
66
|
"imagen-4.0-ultra-generate-preview-06-06": supportedGoogleImageReplacementHint,
|
|
61
67
|
};
|
|
68
|
+
// Gemini image models are called through generateContent; anything else is treated as an Imagen model (generateImages).
|
|
69
|
+
export const isGeminiImageModel = (model) => model.startsWith("gemini-");
|
|
62
70
|
// Google image models that on Vertex AI are only published under location "global"
|
|
63
71
|
// (regional endpoints like us-central1 return 404 NOT_FOUND).
|
|
64
72
|
// See https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/3-pro-image
|
|
65
73
|
// and https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/3-1-flash-image
|
|
66
|
-
export const vertexAIGlobalOnlyImageModels = new Set([
|
|
74
|
+
export const vertexAIGlobalOnlyImageModels = new Set([
|
|
75
|
+
"gemini-3.1-flash-lite-image",
|
|
76
|
+
"gemini-3-pro-image",
|
|
77
|
+
"gemini-3-pro-image-preview",
|
|
78
|
+
"gemini-3.1-flash-image-preview",
|
|
79
|
+
]);
|
|
80
|
+
const VERTEX_DEFAULT_IMAGE_LOCATION = "us-central1";
|
|
81
|
+
// A global-only model has no regional endpoint, so an omitted location means "global" for it.
|
|
82
|
+
export const vertexImageLocation = (model, location) => location ?? (vertexAIGlobalOnlyImageModels.has(model) ? "global" : VERTEX_DEFAULT_IMAGE_LOCATION);
|
|
67
83
|
// Per-model reference image limits (image_input array). Only verified entries; unlisted models are not truncated.
|
|
68
84
|
const replicateImageModelParams = {
|
|
69
85
|
"bytedance/seedream-4": { maxReferenceImages: 10 },
|
|
@@ -71,7 +87,9 @@ const replicateImageModelParams = {
|
|
|
71
87
|
export const provider2ImageAgent = {
|
|
72
88
|
openai: {
|
|
73
89
|
agentName: "imageOpenaiAgent",
|
|
74
|
-
|
|
90
|
+
// gpt-image-1 shuts down on 2026-10-23; OpenAI names gpt-image-2.5-sunburst or -flare as its successor.
|
|
91
|
+
// https://developers.openai.com/api/docs/deprecations
|
|
92
|
+
defaultModel: "gpt-image-2.5-sunburst",
|
|
75
93
|
models: [...gptImages],
|
|
76
94
|
keyName: "OPENAI_API_KEY",
|
|
77
95
|
baseURLKeyName: "OPENAI_BASE_URL",
|
|
@@ -79,8 +97,16 @@ export const provider2ImageAgent = {
|
|
|
79
97
|
},
|
|
80
98
|
google: {
|
|
81
99
|
agentName: "imageGenAIAgent",
|
|
82
|
-
|
|
83
|
-
|
|
100
|
+
// gemini-2.5-flash-image is deprecated; Google names gemini-3.1-flash-lite-image as its successor.
|
|
101
|
+
defaultModel: "gemini-3.1-flash-lite-image",
|
|
102
|
+
models: [
|
|
103
|
+
"gemini-3.1-flash-lite-image",
|
|
104
|
+
"gemini-3.1-flash-image",
|
|
105
|
+
"gemini-3-pro-image",
|
|
106
|
+
"gemini-2.5-flash-image",
|
|
107
|
+
"gemini-3.1-flash-image-preview",
|
|
108
|
+
"gemini-3-pro-image-preview",
|
|
109
|
+
],
|
|
84
110
|
keyName: "GEMINI_API_KEY",
|
|
85
111
|
},
|
|
86
112
|
replicate: {
|
|
@@ -126,7 +152,6 @@ export const provider2MovieAgent = {
|
|
|
126
152
|
"bytedance/seedance-1-pro",
|
|
127
153
|
"bytedance/seedance-2.0",
|
|
128
154
|
"bytedance/seedance-2.0-fast",
|
|
129
|
-
"kwaivgi/kling-v1.6-pro",
|
|
130
155
|
"kwaivgi/kling-v2.1",
|
|
131
156
|
"kwaivgi/kling-v2.1-master",
|
|
132
157
|
"google/veo-2",
|
|
@@ -173,20 +198,14 @@ export const provider2MovieAgent = {
|
|
|
173
198
|
start_image: "image",
|
|
174
199
|
last_image: "last_frame_image",
|
|
175
200
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
176
|
-
price_per_sec: 0.
|
|
201
|
+
price_per_sec: 0.18,
|
|
177
202
|
},
|
|
178
203
|
"bytedance/seedance-2.0-fast": {
|
|
179
204
|
durations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
180
205
|
start_image: "image",
|
|
181
206
|
last_image: "last_frame_image",
|
|
182
207
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
183
|
-
price_per_sec: 0.
|
|
184
|
-
},
|
|
185
|
-
"kwaivgi/kling-v1.6-pro": {
|
|
186
|
-
durations: [5, 10],
|
|
187
|
-
start_image: "start_image",
|
|
188
|
-
audio: { mode: AUDIO_MODE_NEVER },
|
|
189
|
-
price_per_sec: 0.095,
|
|
208
|
+
price_per_sec: 0.15,
|
|
190
209
|
},
|
|
191
210
|
"kwaivgi/kling-v2.1": {
|
|
192
211
|
durations: [5, 10],
|
|
@@ -210,7 +229,7 @@ export const provider2MovieAgent = {
|
|
|
210
229
|
durations: [8],
|
|
211
230
|
start_image: "image",
|
|
212
231
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
213
|
-
price_per_sec: 0.
|
|
232
|
+
price_per_sec: 0.2,
|
|
214
233
|
},
|
|
215
234
|
"google/veo-3.1": {
|
|
216
235
|
durations: [4, 6, 8],
|
|
@@ -218,14 +237,14 @@ export const provider2MovieAgent = {
|
|
|
218
237
|
last_image: "last_frame_image",
|
|
219
238
|
reference_images_param: "reference_images",
|
|
220
239
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
221
|
-
price_per_sec: 0.
|
|
240
|
+
price_per_sec: 0.2,
|
|
222
241
|
},
|
|
223
242
|
"google/veo-3.1-fast": {
|
|
224
243
|
durations: [4, 6, 8],
|
|
225
244
|
start_image: "image",
|
|
226
245
|
last_image: "last_frame_image",
|
|
227
246
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
228
|
-
price_per_sec: 0.
|
|
247
|
+
price_per_sec: 0.1,
|
|
229
248
|
},
|
|
230
249
|
"google/veo-3.1-lite": {
|
|
231
250
|
durations: [4, 6, 8],
|
|
@@ -238,13 +257,13 @@ export const provider2MovieAgent = {
|
|
|
238
257
|
durations: [8],
|
|
239
258
|
start_image: "image",
|
|
240
259
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
241
|
-
price_per_sec: 0.
|
|
260
|
+
price_per_sec: 0.1,
|
|
242
261
|
},
|
|
243
262
|
"minimax/video-01": {
|
|
244
263
|
durations: [6],
|
|
245
264
|
start_image: "first_frame_image",
|
|
246
265
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
247
|
-
price_per_sec: 0.
|
|
266
|
+
price_per_sec: 0.083,
|
|
248
267
|
},
|
|
249
268
|
"minimax/hailuo-02": {
|
|
250
269
|
durations: [6], // NOTE: 10 for only 720p
|
|
@@ -264,7 +283,7 @@ export const provider2MovieAgent = {
|
|
|
264
283
|
start_image: "image",
|
|
265
284
|
last_image: "last_frame_image",
|
|
266
285
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "sound_effect_switch" },
|
|
267
|
-
price_per_sec: 0.
|
|
286
|
+
price_per_sec: 0.06,
|
|
268
287
|
},
|
|
269
288
|
"wan-video/wan-2.2-i2v-fast": {
|
|
270
289
|
// No duration input: length is num_frames (default 81) / frames_per_second (default 16) ≈ 5s.
|
|
@@ -279,26 +298,26 @@ export const provider2MovieAgent = {
|
|
|
279
298
|
durations: [5],
|
|
280
299
|
start_image: undefined,
|
|
281
300
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
282
|
-
price_per_sec: 0.
|
|
301
|
+
price_per_sec: 0.01,
|
|
283
302
|
},
|
|
284
303
|
"xai/grok-imagine-video": {
|
|
285
304
|
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
286
305
|
start_image: "image",
|
|
287
306
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
288
|
-
price_per_sec: 0.
|
|
307
|
+
price_per_sec: 0.05,
|
|
289
308
|
},
|
|
290
309
|
"xai/grok-imagine-r2v": {
|
|
291
310
|
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
292
311
|
start_image: undefined,
|
|
293
312
|
reference_images_param: "reference_images",
|
|
294
313
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
295
|
-
price_per_sec: 0.
|
|
314
|
+
price_per_sec: 0.05,
|
|
296
315
|
},
|
|
297
316
|
"runwayml/gen-4.5": {
|
|
298
317
|
durations: [5, 10],
|
|
299
318
|
start_image: "image",
|
|
300
319
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
301
|
-
price_per_sec: 0.
|
|
320
|
+
price_per_sec: 0.12,
|
|
302
321
|
},
|
|
303
322
|
"kwaivgi/kling-v3-omni-video": {
|
|
304
323
|
durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
@@ -306,7 +325,7 @@ export const provider2MovieAgent = {
|
|
|
306
325
|
last_image: "end_image",
|
|
307
326
|
reference_images_param: "reference_images",
|
|
308
327
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
309
|
-
price_per_sec: 0.
|
|
328
|
+
price_per_sec: 0.224,
|
|
310
329
|
},
|
|
311
330
|
"kwaivgi/kling-v3-video": {
|
|
312
331
|
durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
@@ -314,7 +333,7 @@ export const provider2MovieAgent = {
|
|
|
314
333
|
last_image: "end_image",
|
|
315
334
|
reference_images_param: "reference_images",
|
|
316
335
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
317
|
-
price_per_sec: 0.
|
|
336
|
+
price_per_sec: 0.224,
|
|
318
337
|
},
|
|
319
338
|
// TODO: price_per_sec for the models below is a coarse approximation.
|
|
320
339
|
// Actual Replicate pricing varies by resolution / duration / quality and
|
|
@@ -324,27 +343,27 @@ export const provider2MovieAgent = {
|
|
|
324
343
|
durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
325
344
|
start_image: "image",
|
|
326
345
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
327
|
-
price_per_sec: 0.
|
|
346
|
+
price_per_sec: 0.14,
|
|
328
347
|
},
|
|
329
348
|
"minimax/hailuo-2.3": {
|
|
330
349
|
durations: [6, 10],
|
|
331
350
|
start_image: "first_frame_image",
|
|
332
351
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
333
|
-
price_per_sec: 0.
|
|
352
|
+
price_per_sec: 0.047,
|
|
334
353
|
},
|
|
335
354
|
"minimax/hailuo-2.3-fast": {
|
|
336
355
|
durations: [6, 10],
|
|
337
356
|
start_image: "first_frame_image",
|
|
338
357
|
start_image_required: true,
|
|
339
358
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
340
|
-
price_per_sec: 0.
|
|
359
|
+
price_per_sec: 0.032,
|
|
341
360
|
},
|
|
342
361
|
"pixverse/pixverse-v5": {
|
|
343
362
|
durations: [5, 8],
|
|
344
363
|
start_image: "image",
|
|
345
364
|
last_image: "last_frame_image",
|
|
346
365
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
347
|
-
price_per_sec: 0.
|
|
366
|
+
price_per_sec: 0.06,
|
|
348
367
|
},
|
|
349
368
|
"prunaai/p-video": {
|
|
350
369
|
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
|
|
@@ -364,8 +383,9 @@ export const provider2MovieAgent = {
|
|
|
364
383
|
},
|
|
365
384
|
google: {
|
|
366
385
|
agentName: "movieGenAIAgent",
|
|
367
|
-
|
|
368
|
-
|
|
386
|
+
// veo-2.0 and veo-3.0 were shut down on 2026-06-30; the Gemini API now lists only the Veo 3.1 previews.
|
|
387
|
+
defaultModel: "veo-3.1-generate-preview",
|
|
388
|
+
models: ["veo-3.1-generate-preview", "veo-3.1-lite-generate-preview", "veo-3.1-generate-001"],
|
|
369
389
|
keyName: "GEMINI_API_KEY",
|
|
370
390
|
modelParams: {
|
|
371
391
|
"veo-3.1-lite-generate-preview": {
|
|
@@ -376,7 +396,8 @@ export const provider2MovieAgent = {
|
|
|
376
396
|
supportsPersonGeneration: false,
|
|
377
397
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
378
398
|
},
|
|
379
|
-
|
|
399
|
+
// Vertex AI only: the GA name of Veo 3.1 there, where the preview IDs are retired.
|
|
400
|
+
"veo-3.1-generate-001": {
|
|
380
401
|
durations: [4, 6, 8],
|
|
381
402
|
supportsDuration: true,
|
|
382
403
|
supportsLastFrame: true,
|
|
@@ -384,22 +405,14 @@ export const provider2MovieAgent = {
|
|
|
384
405
|
supportsPersonGeneration: false,
|
|
385
406
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
386
407
|
},
|
|
387
|
-
"veo-3.
|
|
388
|
-
durations: [8],
|
|
389
|
-
supportsDuration:
|
|
390
|
-
supportsLastFrame:
|
|
391
|
-
supportsReferenceImages:
|
|
408
|
+
"veo-3.1-generate-preview": {
|
|
409
|
+
durations: [4, 6, 8],
|
|
410
|
+
supportsDuration: true,
|
|
411
|
+
supportsLastFrame: true,
|
|
412
|
+
supportsReferenceImages: true,
|
|
392
413
|
supportsPersonGeneration: false,
|
|
393
414
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
394
415
|
},
|
|
395
|
-
"veo-2.0-generate-001": {
|
|
396
|
-
durations: [5, 6, 8],
|
|
397
|
-
supportsDuration: true,
|
|
398
|
-
supportsLastFrame: false, // Vertex AI only
|
|
399
|
-
supportsReferenceImages: false,
|
|
400
|
-
supportsPersonGeneration: true,
|
|
401
|
-
audio: { mode: AUDIO_MODE_NEVER },
|
|
402
|
-
},
|
|
403
416
|
},
|
|
404
417
|
},
|
|
405
418
|
mock: {
|
|
@@ -471,12 +484,16 @@ export const provider2LipSyncAgent = {
|
|
|
471
484
|
export const provider2LLMAgent = {
|
|
472
485
|
openai: {
|
|
473
486
|
agentName: "openAIAgent",
|
|
474
|
-
|
|
487
|
+
// gpt-5 (gpt-5-2025-08-07) shuts down on 2026-12-11; OpenAI names gpt-5.6-sol as its successor.
|
|
488
|
+
defaultModel: "gpt-5.6-sol",
|
|
475
489
|
keyName: "OPENAI_API_KEY",
|
|
476
490
|
baseURLKeyName: "OPENAI_BASE_URL",
|
|
477
491
|
apiVersionKeyName: "OPENAI_API_VERSION",
|
|
478
492
|
max_tokens: 8192,
|
|
479
493
|
models: [
|
|
494
|
+
"gpt-5.6-sol",
|
|
495
|
+
"gpt-5.6-terra",
|
|
496
|
+
"gpt-5.6-luna",
|
|
480
497
|
"gpt-5",
|
|
481
498
|
"gpt-5-nano",
|
|
482
499
|
"gpt-5-mini",
|
|
@@ -494,26 +511,31 @@ export const provider2LLMAgent = {
|
|
|
494
511
|
},
|
|
495
512
|
anthropic: {
|
|
496
513
|
agentName: "anthropicAgent",
|
|
497
|
-
|
|
514
|
+
// claude-sonnet-4-5 retires on 2026-11-30. Its named successor, claude-sonnet-5-5 (like claude-opus-4-8),
|
|
515
|
+
// rejects the temperature @graphai/anthropic_agent always sends, so the newest model that answers is used.
|
|
516
|
+
defaultModel: "claude-sonnet-4-6",
|
|
498
517
|
max_tokens: 8192,
|
|
499
|
-
models: ["claude-
|
|
518
|
+
models: ["claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"],
|
|
500
519
|
keyName: "ANTHROPIC_API_KEY",
|
|
501
520
|
apiKeyNameOverride: "ANTHROPIC_API_TOKEN",
|
|
502
521
|
// GraphAI is currently using ANTHROPIC_API_KEY, but the official name is ANTHROPIC_API_TOKEN.
|
|
503
522
|
},
|
|
504
523
|
gemini: {
|
|
505
524
|
agentName: "geminiAgent",
|
|
506
|
-
|
|
525
|
+
// Google limits the 2.5 models to users who have used them before and recommends 3.8 Flash.
|
|
526
|
+
defaultModel: "gemini-3.8-flash",
|
|
507
527
|
max_tokens: 8192,
|
|
508
|
-
models: ["gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"],
|
|
528
|
+
models: ["gemini-3.8-flash", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"],
|
|
509
529
|
keyName: "GEMINI_API_KEY",
|
|
510
530
|
},
|
|
511
531
|
groq: {
|
|
512
532
|
agentName: "groqAgent",
|
|
513
|
-
|
|
533
|
+
// The Llama and DeepSeek models were shut down; Groq names openai/gpt-oss-20b as the 8B model's successor.
|
|
534
|
+
// https://console.groq.com/docs/deprecations
|
|
535
|
+
defaultModel: "openai/gpt-oss-20b",
|
|
514
536
|
keyName: "GROQ_API_KEY",
|
|
515
537
|
max_tokens: 4096,
|
|
516
|
-
models: ["
|
|
538
|
+
models: ["openai/gpt-oss-20b", "openai/gpt-oss-120b"],
|
|
517
539
|
},
|
|
518
540
|
mock: {
|
|
519
541
|
agentName: "mediaMockAgent",
|
|
@@ -562,6 +584,14 @@ export const getModelAudio = (provider, model) => {
|
|
|
562
584
|
const modelParams = provider2MovieAgent[provider]?.modelParams;
|
|
563
585
|
return modelParams?.[model]?.audio;
|
|
564
586
|
};
|
|
587
|
+
const GOOGLE_VERTEX_DEFAULT_MOVIE_MODEL = "veo-3.1-generate-001";
|
|
588
|
+
// The default model is shared by the Gemini API and Vertex AI, but Vertex retired the Veo 3.1 preview IDs.
|
|
589
|
+
export const defaultMovieModel = (provider, movieParams) => provider === "google" && movieParams?.vertexai_project ? GOOGLE_VERTEX_DEFAULT_MOVIE_MODEL : provider2MovieAgent[provider].defaultModel;
|
|
590
|
+
// Veo 3.1 (preview) builds clips longer than one segment by extending an 8-second first segment 8 seconds at a time.
|
|
591
|
+
export const VEO_EXTENSION_MODEL = "veo-3.1-generate-preview";
|
|
592
|
+
export const VEO_SEGMENT_SEC = 8;
|
|
593
|
+
export const veoExtensionCount = (requestedSec) => Math.ceil((requestedSec - VEO_SEGMENT_SEC) / VEO_SEGMENT_SEC);
|
|
594
|
+
export const veoExtendedSeconds = (requestedSec) => VEO_SEGMENT_SEC * (1 + veoExtensionCount(requestedSec));
|
|
565
595
|
export const getModelDuration = (provider, model, movieDuration) => {
|
|
566
596
|
const modelParams = provider2MovieAgent[provider]?.modelParams;
|
|
567
597
|
const { durations } = modelParams[model];
|
|
@@ -582,7 +612,8 @@ export const gptImageOutputTokens = {
|
|
|
582
612
|
// Spot-checked against replicate.com model pages on this date (seedance-1-lite, kling-v2.1, omni-human).
|
|
583
613
|
// Some replicate prices vary by resolution/variant; price_per_sec holds the rate for the typical configuration
|
|
584
614
|
// (e.g. seedance-1-lite at 720p, kling-v2.1 standard).
|
|
585
|
-
|
|
615
|
+
// Optional-audio models are priced without audio, since mulmocast generates them silent by default.
|
|
616
|
+
const REPLICATE_PRICING_AS_OF = "2026-10-07";
|
|
586
617
|
const replicateMoviePricing = Object.fromEntries(Object.entries(provider2MovieAgent.replicate.modelParams).map(([model, params]) => [
|
|
587
618
|
model,
|
|
588
619
|
{ unit: "seconds", perSecUSD: params.price_per_sec, asOf: REPLICATE_PRICING_AS_OF },
|
|
@@ -601,22 +632,35 @@ export const modelPricing = {
|
|
|
601
632
|
"gpt-image-1.5": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 32, asOf: "2026-07-03" },
|
|
602
633
|
"gpt-image-2": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 30, asOf: "2026-07-03" },
|
|
603
634
|
"gpt-image-1-mini": { unit: "tokens", inputPerMTokensUSD: 2, outputPerMTokensUSD: 8, asOf: "2026-07-03" },
|
|
635
|
+
// https://developers.openai.com/api/docs/pricing (text input $5/1M, image output $30/1M; image input $8/1M is not modeled)
|
|
636
|
+
"gpt-image-2.5-sunburst": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 30, asOf: "2026-10-07" },
|
|
637
|
+
"gpt-image-2.5-flare": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 30, asOf: "2026-10-07" },
|
|
604
638
|
// https://developers.openai.com/api/docs/models/gpt-5 etc.
|
|
605
639
|
"gpt-5": { unit: "tokens", inputPerMTokensUSD: 1.25, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
606
640
|
"gpt-5-mini": { unit: "tokens", inputPerMTokensUSD: 0.25, outputPerMTokensUSD: 2, asOf: "2026-07-03" },
|
|
641
|
+
// https://developers.openai.com/api/docs/models/gpt-5.6-sol (promotional price, "at least through November 21, 2026")
|
|
642
|
+
"gpt-5.6-sol": { unit: "tokens", inputPerMTokensUSD: 4, outputPerMTokensUSD: 20, asOf: "2026-10-07" },
|
|
643
|
+
"gpt-5.6-terra": { unit: "tokens", inputPerMTokensUSD: 2, outputPerMTokensUSD: 12, asOf: "2026-10-07" },
|
|
607
644
|
"gpt-4o": { unit: "tokens", inputPerMTokensUSD: 2.5, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
608
645
|
},
|
|
609
646
|
gemini: {
|
|
610
647
|
// https://ai.google.dev/gemini-api/docs/pricing
|
|
611
648
|
"gemini-2.5-flash-preview-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
612
649
|
"gemini-2.5-pro-preview-tts": { unit: "tokens", inputPerMTokensUSD: 1, outputPerMTokensUSD: 20, asOf: "2026-07-03" },
|
|
650
|
+
// through 2026-12-31; $1 / $18 (flash) and $1 / $12 (lite) from 2027-01-01
|
|
651
|
+
"gemini-3.8-flash-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 9, asOf: "2026-10-07" },
|
|
652
|
+
"gemini-3.8-flash-lite-tts": { unit: "tokens", inputPerMTokensUSD: 0.5, outputPerMTokensUSD: 6, asOf: "2026-10-07" },
|
|
613
653
|
},
|
|
614
654
|
google: {
|
|
615
655
|
// https://ai.google.dev/gemini-api/docs/pricing ($0.039/image ≒ 1290 output tokens at $30/1M)
|
|
616
656
|
"gemini-2.5-flash-image": { unit: "images", inputPerMTokensUSD: 0.3, perImageUSD: 0.039, asOf: "2026-07-03" },
|
|
657
|
+
// $0.0336 per 1K-resolution image
|
|
658
|
+
"gemini-3.1-flash-lite-image": { unit: "images", inputPerMTokensUSD: 0.25, perImageUSD: 0.0336, asOf: "2026-10-07" },
|
|
617
659
|
// Veo per second of generated video (720p). veo-2.0-generate-001 and veo-3.0-generate-001
|
|
618
660
|
// were shut down on 2026-06-30, so they intentionally have no price.
|
|
619
661
|
"veo-3.1-generate-preview": { unit: "seconds", perSecUSD: 0.4, asOf: "2026-07-03" },
|
|
662
|
+
// Vertex AI: https://cloud.google.com/vertex-ai/generative-ai/pricing (video + audio, 720p/1080p)
|
|
663
|
+
"veo-3.1-generate-001": { unit: "seconds", perSecUSD: 0.4, asOf: "2026-10-07" },
|
|
620
664
|
"veo-3.1-lite-generate-preview": { unit: "seconds", perSecUSD: 0.05, asOf: "2026-07-03" },
|
|
621
665
|
// Google Cloud Text-to-Speech per 1M characters, keyed by voice tier.
|
|
622
666
|
// https://cloud.google.com/text-to-speech/pricing
|
|
@@ -629,15 +673,16 @@ export const modelPricing = {
|
|
|
629
673
|
anthropic: {
|
|
630
674
|
// https://platform.claude.com/docs/en/docs/about-claude/pricing
|
|
631
675
|
"claude-sonnet-4-5-20250929": { unit: "tokens", inputPerMTokensUSD: 3, outputPerMTokensUSD: 15, asOf: "2026-07-03" },
|
|
676
|
+
"claude-sonnet-4-6": { unit: "tokens", inputPerMTokensUSD: 3, outputPerMTokensUSD: 15, asOf: "2026-10-07" },
|
|
632
677
|
},
|
|
633
678
|
elevenlabs: {
|
|
634
|
-
// https://elevenlabs.io/pricing/api ($0.
|
|
635
|
-
eleven_v3: { unit: "chars", perMCharsUSD:
|
|
636
|
-
eleven_multilingual_v2: { unit: "chars", perMCharsUSD:
|
|
637
|
-
eleven_turbo_v2_5: { unit: "chars", perMCharsUSD:
|
|
638
|
-
eleven_turbo_v2: { unit: "chars", perMCharsUSD:
|
|
639
|
-
eleven_flash_v2_5: { unit: "chars", perMCharsUSD:
|
|
640
|
-
eleven_flash_v2: { unit: "chars", perMCharsUSD:
|
|
679
|
+
// https://elevenlabs.io/pricing/api ($0.08 per 1k chars for multilingual/v3, $0.04 for flash/turbo)
|
|
680
|
+
eleven_v3: { unit: "chars", perMCharsUSD: 80, asOf: "2026-10-07" },
|
|
681
|
+
eleven_multilingual_v2: { unit: "chars", perMCharsUSD: 80, asOf: "2026-10-07" },
|
|
682
|
+
eleven_turbo_v2_5: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
683
|
+
eleven_turbo_v2: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
684
|
+
eleven_flash_v2_5: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
685
|
+
eleven_flash_v2: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
641
686
|
},
|
|
642
687
|
replicate: {
|
|
643
688
|
...replicateMoviePricing,
|
package/lib/types/schema.d.ts
CHANGED
|
@@ -4466,8 +4466,8 @@ export declare const mulmoOpenAIImageModelSchema: z.ZodObject<{
|
|
|
4466
4466
|
[x: string]: string;
|
|
4467
4467
|
}>>;
|
|
4468
4468
|
quality: z.ZodOptional<z.ZodEnum<{
|
|
4469
|
-
low: "low";
|
|
4470
4469
|
high: "high";
|
|
4470
|
+
low: "low";
|
|
4471
4471
|
auto: "auto";
|
|
4472
4472
|
medium: "medium";
|
|
4473
4473
|
}>>;
|
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
// assume every asset is generated fresh.
|
|
5
5
|
import { Tiktoken } from "js-tiktoken/lite";
|
|
6
6
|
import o200k_base from "js-tiktoken/ranks/o200k_base";
|
|
7
|
-
import { provider2TTSAgent, provider2MovieAgent, gptImageOutputTokens, getModelDuration, getModelPricing, defaultProviders, } from "../types/provider2agent.js";
|
|
7
|
+
import { provider2TTSAgent, provider2MovieAgent, gptImageOutputTokens, isGptImage25Model, getModelDuration, defaultMovieModel, veoExtendedSeconds, VEO_EXTENSION_MODEL, VEO_SEGMENT_SEC, getModelPricing, defaultProviders, } from "../types/provider2agent.js";
|
|
8
8
|
import { text2SpeechProviderSchema, text2MovieProviderSchema } from "../types/schema.js";
|
|
9
9
|
import { MulmoPresentationStyleMethods } from "../methods/mulmo_presentation_style.js";
|
|
10
10
|
import { MulmoBeatMethods } from "../methods/mulmo_beat.js";
|
|
11
|
+
import { geminiTtsInputText } from "./gemini_tts.js";
|
|
11
12
|
import { imagePrompt, htmlImageSystemPrompt, translateSystemPrompt, translatePrompts } from "./prompt.js";
|
|
12
13
|
const MILLION = 1_000_000;
|
|
13
14
|
const LATIN_CHARS_PER_TOKEN = 4;
|
|
@@ -92,15 +93,14 @@ const buildOpenAITts = ({ speaker, text, textIsFinal, beatIndex, lang }) => {
|
|
|
92
93
|
return { process: "tts", beatIndex, lang, provider: "openai", model, inputChars: metric(text.length, textIsFinal), ...tokenMetrics };
|
|
93
94
|
};
|
|
94
95
|
const buildGeminiTts = ({ speaker, speechOptions, text, beatIndex, lang }) => {
|
|
95
|
-
|
|
96
|
-
const prompt = speechOptions?.instruction ? `### DIRECTOR'S NOTES\n${speechOptions.instruction}\n\n#### TRANSCRIPT\n${text}` : text;
|
|
96
|
+
const model = speaker.model ?? provider2TTSAgent.gemini.defaultModel;
|
|
97
97
|
return {
|
|
98
98
|
process: "tts",
|
|
99
99
|
beatIndex,
|
|
100
100
|
lang,
|
|
101
101
|
provider: "gemini",
|
|
102
|
-
model
|
|
103
|
-
inputTokens: estimated(countHeuristicTokens(
|
|
102
|
+
model,
|
|
103
|
+
inputTokens: estimated(countHeuristicTokens(geminiTtsInputText(model, text, speechOptions?.instruction))),
|
|
104
104
|
outputTokens: estimated(estimateSpeechSec(text) * GEMINI_TTS_AUDIO_TOKENS_PER_SEC),
|
|
105
105
|
};
|
|
106
106
|
};
|
|
@@ -162,8 +162,8 @@ const gptImageSize = (canvasSize) => {
|
|
|
162
162
|
const gptImageOutputMetric = (model, canvasSize, quality) => {
|
|
163
163
|
const table = gptImageOutputTokens[gptImageSize(canvasSize)];
|
|
164
164
|
const knownQuality = quality === "low" || quality === "medium" || quality === "high" ? quality : undefined;
|
|
165
|
-
//
|
|
166
|
-
const tokens = table[knownQuality ?? "high"];
|
|
165
|
+
// Anything else renders at "high" (the upper bound), except an explicit "auto" on 2.5, which renders at "low".
|
|
166
|
+
const tokens = table[knownQuality ?? (quality === "auto" && isGptImage25Model(model) ? "low" : "high")];
|
|
167
167
|
return metric(tokens, GPT_IMAGE_FIXED_TOKEN_MODELS.includes(model) && knownQuality !== undefined);
|
|
168
168
|
};
|
|
169
169
|
const buildImageRecord = ({ process, provider, model, prompt, canvasSize, quality, beatIndex, refKey }) => {
|
|
@@ -217,6 +217,8 @@ const beatDurationMetric = (beat) => {
|
|
|
217
217
|
}
|
|
218
218
|
return estimated(beat.text ? estimateSpeechSec(beat.text) : DEFAULT_MOVIE_DURATION_SEC);
|
|
219
219
|
};
|
|
220
|
+
// The movie agent extends these clips past one segment instead of capping them, and Google bills every generated second.
|
|
221
|
+
const isVeoExtension = (provider, model, duration) => provider === "google" && model === VEO_EXTENSION_MODEL && duration.value > VEO_SEGMENT_SEC;
|
|
220
222
|
const snapMovieDuration = (provider, model, duration) => {
|
|
221
223
|
if (!isKeyOf(provider2MovieAgent[provider].modelParams, model)) {
|
|
222
224
|
return duration;
|
|
@@ -230,9 +232,11 @@ const estimateMovieGeneration = (style, beat, beatIndex, refKey) => {
|
|
|
230
232
|
if (!isKeyOf(provider2MovieAgent, provider) || provider === "mock") {
|
|
231
233
|
return undefined;
|
|
232
234
|
}
|
|
233
|
-
const model = info.movieParams?.model ??
|
|
235
|
+
const model = info.movieParams?.model ?? defaultMovieModel(provider, info.movieParams);
|
|
234
236
|
const duration = beat ? beatDurationMetric(beat) : estimated(DEFAULT_MOVIE_DURATION_SEC);
|
|
235
|
-
const predictSec =
|
|
237
|
+
const predictSec = isVeoExtension(provider, model, duration)
|
|
238
|
+
? metric(veoExtendedSeconds(duration.value), duration.precision === "exact")
|
|
239
|
+
: snapMovieDuration(provider, model, duration);
|
|
236
240
|
return attachCost({ process: refKey === undefined ? "movie" : "movieReference", beatIndex, refKey, provider, model, predictSec });
|
|
237
241
|
};
|
|
238
242
|
const estimateSoundEffect = (style, beat, beatIndex) => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export declare const GEMINI_INTERACTIONS_TTS_MODELS: ReadonlySet<string>;
|
|
2
|
+
export declare const GEMINI_TTS_SAMPLE_RATE = 24000;
|
|
3
|
+
export declare const geminiDirectorsNotesPrompt: (text: string, instructions?: string) => string;
|
|
4
|
+
export declare const geminiTtsInputText: (model: string, text: string, instructions?: string) => string;
|
|
5
|
+
export declare const usesGeminiInteractionsTts: (model: string) => boolean;
|
|
6
|
+
export type GeminiInteractionsTtsInput = {
|
|
7
|
+
model: string;
|
|
8
|
+
text: string;
|
|
9
|
+
voice: string;
|
|
10
|
+
instructions?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const buildGeminiInteractionsTtsRequest: ({ model, text, voice, instructions }: GeminiInteractionsTtsInput) => {
|
|
13
|
+
model: string;
|
|
14
|
+
input: {
|
|
15
|
+
type: "user_input";
|
|
16
|
+
content: {
|
|
17
|
+
annotations?: {
|
|
18
|
+
type: "speech_metadata";
|
|
19
|
+
style: string;
|
|
20
|
+
}[] | undefined;
|
|
21
|
+
type: "text";
|
|
22
|
+
text: string;
|
|
23
|
+
}[];
|
|
24
|
+
}[];
|
|
25
|
+
response_format: {
|
|
26
|
+
type: "audio";
|
|
27
|
+
mime_type: string;
|
|
28
|
+
sample_rate: number;
|
|
29
|
+
};
|
|
30
|
+
generation_config: {
|
|
31
|
+
speech_config: {
|
|
32
|
+
voice: string;
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
type InteractionAudio = {
|
|
37
|
+
data?: string;
|
|
38
|
+
mime_type?: string;
|
|
39
|
+
sample_rate?: number;
|
|
40
|
+
};
|
|
41
|
+
export declare const pcmFromInteractionAudio: (audio: InteractionAudio | undefined) => {
|
|
42
|
+
rawPcm: Buffer;
|
|
43
|
+
sampleRate: number;
|
|
44
|
+
};
|
|
45
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Gemini 3.8 TTS takes the Interactions API: the text is read verbatim, so speaker direction goes in a
|
|
2
|
+
// speech_metadata annotation instead of the prompt, and raw PCM must be asked for (the unary default is WAV).
|
|
3
|
+
export const GEMINI_INTERACTIONS_TTS_MODELS = new Set(["gemini-3.8-flash-tts", "gemini-3.8-flash-lite-tts"]);
|
|
4
|
+
export const GEMINI_TTS_SAMPLE_RATE = 24000;
|
|
5
|
+
// 2.5 TTS has no field for direction, so it goes in the prompt: https://ai.google.dev/gemini-api/docs/speech-generation#controllable
|
|
6
|
+
export const geminiDirectorsNotesPrompt = (text, instructions) => instructions ? ["### DIRECTOR'S NOTES", instructions, "", "#### TRANSCRIPT", text].join("\n") : text;
|
|
7
|
+
// The text the model is given for one line: 3.8 gets the transcript plus the style annotation, 2.5 the wrapped prompt.
|
|
8
|
+
export const geminiTtsInputText = (model, text, instructions) => usesGeminiInteractionsTts(model) ? [text, instructions].filter(Boolean).join("\n") : geminiDirectorsNotesPrompt(text, instructions);
|
|
9
|
+
export const usesGeminiInteractionsTts = (model) => GEMINI_INTERACTIONS_TTS_MODELS.has(model);
|
|
10
|
+
export const buildGeminiInteractionsTtsRequest = ({ model, text, voice, instructions }) => ({
|
|
11
|
+
model,
|
|
12
|
+
input: [
|
|
13
|
+
{
|
|
14
|
+
type: "user_input",
|
|
15
|
+
content: [{ type: "text", text, ...(instructions ? { annotations: [{ type: "speech_metadata", style: instructions }] } : {}) }],
|
|
16
|
+
},
|
|
17
|
+
],
|
|
18
|
+
response_format: { type: "audio", mime_type: "audio/l16", sample_rate: GEMINI_TTS_SAMPLE_RATE },
|
|
19
|
+
generation_config: { speech_config: [{ voice }] },
|
|
20
|
+
});
|
|
21
|
+
const sampleRateOf = (audio) => {
|
|
22
|
+
if (typeof audio.sample_rate === "number" && audio.sample_rate > 0)
|
|
23
|
+
return audio.sample_rate;
|
|
24
|
+
const match = audio.mime_type?.match(/rate=(\d+)/);
|
|
25
|
+
return match ? Number(match[1]) : GEMINI_TTS_SAMPLE_RATE;
|
|
26
|
+
};
|
|
27
|
+
// Anything but headerless 16-bit PCM would be fed to the PCM encoder as noise, so it is an error, not a guess.
|
|
28
|
+
export const pcmFromInteractionAudio = (audio) => {
|
|
29
|
+
if (!audio?.data)
|
|
30
|
+
throw new Error("No audio data returned");
|
|
31
|
+
if (!audio.mime_type?.toLowerCase().startsWith("audio/l16")) {
|
|
32
|
+
throw new Error(`Gemini TTS returned ${audio.mime_type ?? "audio of an unknown type"}, expected audio/l16`);
|
|
33
|
+
}
|
|
34
|
+
return { rawPcm: Buffer.from(audio.data, "base64"), sampleRate: sampleRateOf(audio) };
|
|
35
|
+
};
|