mulmocast 2.15.0 → 2.16.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 +1 -7
- package/lib/actions/image_references.d.ts +23 -1
- package/lib/actions/image_references.js +16 -9
- package/lib/agents/image_genai_agent.js +3 -3
- package/lib/agents/movie_genai_agent.js +5 -7
- 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 +25 -16
- package/lib/types/provider2agent.js +99 -63
- package/lib/utils/estimate_usage.js +7 -3
- 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 +1 -1
- 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
package/README.md
CHANGED
|
@@ -103,11 +103,6 @@ Create a `.env` file in your project directory with the following API keys:
|
|
|
103
103
|
OPENAI_API_KEY=your_openai_api_key
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
-
#### (Optional) For the advanced image generation model
|
|
107
|
-
```bash
|
|
108
|
-
DEFAULT_OPENAI_IMAGE_MODEL=gpt-image-1 # for the advanced image generation model
|
|
109
|
-
```
|
|
110
|
-
|
|
111
106
|
#### (Optional) For Google's image and TTS.
|
|
112
107
|
```bash
|
|
113
108
|
GEMINI_API_KEY=your_google_gemini_api_key
|
|
@@ -298,10 +293,9 @@ Optionally, you can specify *__clipboard* as the script file name to paste the s
|
|
|
298
293
|
Verify your `.env` file contains:
|
|
299
294
|
```bash
|
|
300
295
|
OPENAI_API_KEY=your_openai_api_key
|
|
301
|
-
DEFAULT_OPENAI_IMAGE_MODEL=gpt-image-1 # required for high-quality Ghibli-style images
|
|
302
296
|
```
|
|
303
297
|
|
|
304
|
-
> **Note:** Ensure your OpenAI organization is verified to access the
|
|
298
|
+
> **Note:** Ensure your OpenAI organization is verified to access the GPT Image models. Visit https://platform.openai.com/settings/organization/general and complete the "Verifications" section.
|
|
305
299
|
|
|
306
300
|
### Step 2: Generate a Ghibli-style MulmoScript
|
|
307
301
|
```bash
|
|
@@ -1,6 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MulmoPresentationStyleMethods } from "../methods/index.js";
|
|
2
|
+
import { MulmoStudioContext, MulmoBeat, MulmoImagePromptMedia, MulmoCanvasDimension } from "../types/index.js";
|
|
2
3
|
import type { ResolvedImageReference } from "../methods/mulmo_image_prompt_media.js";
|
|
3
4
|
export type { ResolvedImageReference } from "../methods/mulmo_image_prompt_media.js";
|
|
5
|
+
type ImageAgentInfo = ReturnType<typeof MulmoPresentationStyleMethods.getImageAgentInfo>;
|
|
6
|
+
type MovieAgentInfo = ReturnType<typeof MulmoPresentationStyleMethods.getMovieAgentInfo>;
|
|
7
|
+
export declare const referenceImageGraphParams: (imageAgentInfo: ImageAgentInfo, canvasSize: MulmoCanvasDimension) => {
|
|
8
|
+
model: string | undefined;
|
|
9
|
+
canvasSize: {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
vertexai_project: string | undefined;
|
|
14
|
+
vertexai_location: string | undefined;
|
|
15
|
+
};
|
|
16
|
+
export declare const referenceMovieGraphParams: (movieAgentInfo: MovieAgentInfo, canvasSize: MulmoCanvasDimension) => {
|
|
17
|
+
model: string | undefined;
|
|
18
|
+
canvasSize: {
|
|
19
|
+
width: number;
|
|
20
|
+
height: number;
|
|
21
|
+
};
|
|
22
|
+
generateAudio: boolean | undefined;
|
|
23
|
+
vertexai_project: string | undefined;
|
|
24
|
+
vertexai_location: string | undefined;
|
|
25
|
+
};
|
|
4
26
|
export declare const generateReferenceImage: (inputs: {
|
|
5
27
|
context: MulmoStudioContext;
|
|
6
28
|
key: string;
|
|
@@ -6,6 +6,20 @@ import { MulmoPresentationStyleMethods, MulmoMediaSourceMethods, MulmoImagePromp
|
|
|
6
6
|
import { ImageMediaType, } from "../types/index.js";
|
|
7
7
|
import { imageOpenaiAgent, mediaMockAgent, imageGenAIAgent, imageReplicateAgent, movieGenAIAgent, movieReplicateAgent } from "../agents/index.js";
|
|
8
8
|
import { agentGenerationError, imageReferenceAction, imageFileTarget, movieFileTarget } from "../utils/error_cause.js";
|
|
9
|
+
// Reference media are generated outside the beat graphs, so they must carry the same Vertex AI target the beats do.
|
|
10
|
+
export const referenceImageGraphParams = (imageAgentInfo, canvasSize) => ({
|
|
11
|
+
model: imageAgentInfo.imageParams.model,
|
|
12
|
+
canvasSize,
|
|
13
|
+
vertexai_project: imageAgentInfo.imageParams.vertexai_project,
|
|
14
|
+
vertexai_location: imageAgentInfo.imageParams.vertexai_location,
|
|
15
|
+
});
|
|
16
|
+
export const referenceMovieGraphParams = (movieAgentInfo, canvasSize) => ({
|
|
17
|
+
model: movieAgentInfo.movieParams.model,
|
|
18
|
+
canvasSize,
|
|
19
|
+
generateAudio: movieAgentInfo.movieParams.generateAudio,
|
|
20
|
+
vertexai_project: movieAgentInfo.movieParams.vertexai_project,
|
|
21
|
+
vertexai_location: movieAgentInfo.movieParams.vertexai_location,
|
|
22
|
+
});
|
|
9
23
|
// Resolve the normalized reference list to local file paths (with labels), in reference order.
|
|
10
24
|
// imageKey is the imageRefs key of the image being generated (used for warnings and url
|
|
11
25
|
// download file naming). A named reference that is missing from imageRefs warns and is
|
|
@@ -62,10 +76,7 @@ export const generateReferenceImage = async (inputs) => {
|
|
|
62
76
|
sessionType: "imageReference",
|
|
63
77
|
},
|
|
64
78
|
},
|
|
65
|
-
params:
|
|
66
|
-
model: imageAgentInfo.imageParams.model,
|
|
67
|
-
canvasSize: image.canvasSize ?? context.presentationStyle.canvasSize,
|
|
68
|
-
},
|
|
79
|
+
params: referenceImageGraphParams(imageAgentInfo, image.canvasSize ?? context.presentationStyle.canvasSize),
|
|
69
80
|
},
|
|
70
81
|
},
|
|
71
82
|
};
|
|
@@ -145,11 +156,7 @@ const generateReferenceMovie = async (inputs) => {
|
|
|
145
156
|
sessionType: "imageReference",
|
|
146
157
|
},
|
|
147
158
|
},
|
|
148
|
-
params:
|
|
149
|
-
model: movieAgentInfo.movieParams.model,
|
|
150
|
-
canvasSize: context.presentationStyle.canvasSize,
|
|
151
|
-
generateAudio: movieAgentInfo.movieParams.generateAudio,
|
|
152
|
-
},
|
|
159
|
+
params: referenceMovieGraphParams(movieAgentInfo, context.presentationStyle.canvasSize),
|
|
153
160
|
},
|
|
154
161
|
},
|
|
155
162
|
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import { GraphAILogger } from "graphai";
|
|
3
|
-
import { provider2ImageAgent, deprecatedGoogleImageModelHints, vertexAIGlobalOnlyImageModels, } from "../types/provider2agent.js";
|
|
3
|
+
import { provider2ImageAgent, deprecatedGoogleImageModelHints, vertexAIGlobalOnlyImageModels, isGeminiImageModel, vertexImageLocation, } from "../types/provider2agent.js";
|
|
4
4
|
import { apiKeyMissingError, agentIncorrectAPIKeyError, agentGenerationError, agentInvalidResponseError, imageAction, imageFileTarget, unsupportedModelTarget, hasCause, getGenAIErrorReason, resultify, } from "../utils/error_cause.js";
|
|
5
5
|
import { getAspectRatio } from "../utils/utils.js";
|
|
6
6
|
import { ASPECT_RATIOS, PRO_ASPECT_RATIOS } from "../types/const.js";
|
|
@@ -85,7 +85,7 @@ export const imageGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
85
85
|
const apiKey = config?.apiKey;
|
|
86
86
|
const ai = params.vertexai_project
|
|
87
87
|
? (() => {
|
|
88
|
-
const location = params.vertexai_location
|
|
88
|
+
const location = vertexImageLocation(model, params.vertexai_location);
|
|
89
89
|
if (vertexAIGlobalOnlyImageModels.has(model) && location !== "global") {
|
|
90
90
|
GraphAILogger.warn(`imageGenAIAgent: model "${model}" on Vertex AI is only available in location "global", but got "${location}". Set imageParams.vertexai_location to "global".`);
|
|
91
91
|
}
|
|
@@ -99,7 +99,7 @@ export const imageGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
99
99
|
}
|
|
100
100
|
return new GoogleGenAI({ apiKey, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
101
101
|
})();
|
|
102
|
-
if (model
|
|
102
|
+
if (isGeminiImageModel(model)) {
|
|
103
103
|
const contentParams = (() => {
|
|
104
104
|
const contents = getGeminiContents(prompt, referenceImages);
|
|
105
105
|
return {
|
|
@@ -5,7 +5,7 @@ import { apiKeyMissingError, agentGenerationError, agentInvalidResponseError, im
|
|
|
5
5
|
import { getAspectRatio } from "../utils/utils.js";
|
|
6
6
|
import { ffmpegGetMediaDuration } from "../utils/ffmpeg_utils.js";
|
|
7
7
|
import { ASPECT_RATIOS } from "../types/const.js";
|
|
8
|
-
import { getModelDuration, provider2MovieAgent, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS } from "../types/provider2agent.js";
|
|
8
|
+
import { getModelDuration, provider2MovieAgent, AUDIO_MODE_NEVER, AUDIO_MODE_ALWAYS, defaultMovieModel, veoExtensionCount, VEO_EXTENSION_MODEL, VEO_SEGMENT_SEC, } from "../types/provider2agent.js";
|
|
9
9
|
// Per-request timeout so a stalled GenAI video API call rejects instead of hanging.
|
|
10
10
|
const GENAI_REQUEST_TIMEOUT_MS = 120_000;
|
|
11
11
|
// Wall-clock cap on the long-running video operation poll loop (Veo runs minutes).
|
|
@@ -89,14 +89,12 @@ const createVeo31Payload = (model, prompt, aspectRatio, source) => ({
|
|
|
89
89
|
...source,
|
|
90
90
|
});
|
|
91
91
|
const generateExtendedVideo = async (ai, model, prompt, aspectRatio, imagePath, requestedDuration, movieFile, isVertexAI) => {
|
|
92
|
-
const
|
|
93
|
-
const maxExtensionDuration = 8;
|
|
94
|
-
const extensionsNeeded = Math.ceil((requestedDuration - initialDuration) / maxExtensionDuration);
|
|
92
|
+
const extensionsNeeded = veoExtensionCount(requestedDuration);
|
|
95
93
|
GraphAILogger.info(`Veo 3.1 video extension: ${extensionsNeeded} extensions needed for ${requestedDuration}s target`);
|
|
96
94
|
const generateIteration = async (iteration, accumulatedDuration, previousVideo) => {
|
|
97
95
|
const isInitial = iteration === 0;
|
|
98
96
|
const remainingDuration = requestedDuration - accumulatedDuration;
|
|
99
|
-
const extensionDuration = isInitial ?
|
|
97
|
+
const extensionDuration = isInitial ? VEO_SEGMENT_SEC : (getModelDuration("google", model, remainingDuration) ?? VEO_SEGMENT_SEC);
|
|
100
98
|
const getSource = () => {
|
|
101
99
|
if (isInitial)
|
|
102
100
|
return imagePath ? { image: loadImageAsBase64(imagePath) } : undefined;
|
|
@@ -172,7 +170,7 @@ const generateStandardVideo = async (ai, model, prompt, aspectRatio, imagePath,
|
|
|
172
170
|
export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
173
171
|
const { prompt, imagePath, lastFrameImagePath, referenceImages, movieFile } = namedInputs;
|
|
174
172
|
const aspectRatio = getAspectRatio(params.canvasSize, ASPECT_RATIOS);
|
|
175
|
-
const model = params.model ??
|
|
173
|
+
const model = params.model ?? defaultMovieModel("google", params);
|
|
176
174
|
const apiKey = config?.apiKey;
|
|
177
175
|
try {
|
|
178
176
|
const requestedDuration = params.duration ?? 8;
|
|
@@ -211,7 +209,7 @@ export const movieGenAIAgent = async ({ namedInputs, params, config, }) => {
|
|
|
211
209
|
return new GoogleGenAI({ apiKey, httpOptions: { timeout: GENAI_REQUEST_TIMEOUT_MS } });
|
|
212
210
|
})();
|
|
213
211
|
// Veo 3.1: Video extension mode for videos longer than 8s
|
|
214
|
-
if (model ===
|
|
212
|
+
if (model === VEO_EXTENSION_MODEL && requestedDuration > VEO_SEGMENT_SEC && params.canvasSize) {
|
|
215
213
|
return generateExtendedVideo(ai, model, prompt, aspectRatio, imagePath, requestedDuration, movieFile, isVertexAI);
|
|
216
214
|
}
|
|
217
215
|
// Standard mode
|
|
@@ -484,7 +484,7 @@ export const scriptTemplates = [
|
|
|
484
484
|
],
|
|
485
485
|
filename: "html",
|
|
486
486
|
htmlImageParams: {
|
|
487
|
-
model: "claude-sonnet-4-
|
|
487
|
+
model: "claude-sonnet-4-6",
|
|
488
488
|
provider: "anthropic",
|
|
489
489
|
},
|
|
490
490
|
lang: "en",
|
|
@@ -987,7 +987,7 @@ export const scriptTemplates = [
|
|
|
987
987
|
],
|
|
988
988
|
filename: "presentation",
|
|
989
989
|
htmlImageParams: {
|
|
990
|
-
model: "claude-sonnet-4-
|
|
990
|
+
model: "claude-sonnet-4-6",
|
|
991
991
|
provider: "anthropic",
|
|
992
992
|
},
|
|
993
993
|
lang: "en",
|
|
@@ -49,7 +49,7 @@ export const templateDataSet = {
|
|
|
49
49
|
"```",
|
|
50
50
|
html: "Another LLM will generate actual slides from the prompt and data for each beat. Adding optional data would help it to generate more compelling slide. Mention the reference in one of beats, if it exists. The valid type of reference is 'article', 'paper', 'image', 'video', 'audio'. Use the JSON below as a template.\n" +
|
|
51
51
|
"```JSON\n" +
|
|
52
|
-
'{"$mulmocast":{"version":"1.1","credit":"closing"},"references":[{"url":"https://www.somegreatwebsite.com/article/123","title":"Title of the article we are referencing","type":"[TYPE OF ARTICLE: article, paper, image, video, audio]"}],"title":"[TITLE: Brief, engaging title for the topic]","htmlImageParams":{"provider":"anthropic","model":"claude-sonnet-4-
|
|
52
|
+
'{"$mulmocast":{"version":"1.1","credit":"closing"},"references":[{"url":"https://www.somegreatwebsite.com/article/123","title":"Title of the article we are referencing","type":"[TYPE OF ARTICLE: article, paper, image, video, audio]"}],"title":"[TITLE: Brief, engaging title for the topic]","htmlImageParams":{"provider":"anthropic","model":"claude-sonnet-4-6"},"lang":"en","beats":[{"text":"[NARRATION: Narration for the beat.]","htmlPrompt":{"prompt":"[PROMPT to create appropriate HTML page for the beat.]"}},{"text":"[NARRATION: Narration for the beat.]","htmlPrompt":{"prompt":"[PROMPT to create appropriate HTML page for the beat with the data.]","data":{"description":"DATA TO BE PRESENTED IN THIS BEAT (in any format)]","net_income":{"Q2 FY2024":320,"Q3 FY2024":333,"Q4 FY2024":350},"unit":"USD (Million)"}}}],"canvasSize":{"width":1536,"height":1024}}\n' +
|
|
53
53
|
"```",
|
|
54
54
|
image_prompt: "Another AI will generate images for each beat based on the image prompt of that beat. Movie prompts must be written in English. Mention the reference in one of beats, if it exists. Use the JSON below as a template.\n" +
|
|
55
55
|
"```JSON\n" +
|
|
@@ -8,7 +8,7 @@ import { userAssert } from "../utils/utils.js";
|
|
|
8
8
|
import { slideThemes } from "../data/slideThemes.js";
|
|
9
9
|
import { text2ImageProviderSchema, text2HtmlImageProviderSchema, text2MovieProviderSchema, text2SpeechProviderSchema, mulmoCanvasDimensionSchema, mulmoTransitionSchema, mulmoFillOptionSchema, } from "../types/schema.js";
|
|
10
10
|
import { MulmoBeatMethods } from "./mulmo_beat.js";
|
|
11
|
-
import { provider2ImageAgent, provider2MovieAgent, provider2LLMAgent, provider2TTSAgent, provider2SoundEffectAgent, provider2LipSyncAgent, defaultProviders, getModelAudio, AUDIO_MODE_ALWAYS, } from "../types/provider2agent.js";
|
|
11
|
+
import { provider2ImageAgent, provider2MovieAgent, defaultMovieModel, provider2LLMAgent, provider2TTSAgent, provider2SoundEffectAgent, provider2LipSyncAgent, defaultProviders, getModelAudio, AUDIO_MODE_ALWAYS, } from "../types/provider2agent.js";
|
|
12
12
|
const defaultTextSlideStyles = [
|
|
13
13
|
'*,*::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}}',
|
|
14
14
|
"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 }",
|
|
@@ -160,7 +160,7 @@ export const MulmoPresentationStyleMethods = {
|
|
|
160
160
|
const imageParams = { ...presentationStyle.imageParams, ...beat?.imageParams };
|
|
161
161
|
const provider = MulmoPresentationStyleMethods.getText2ImageProvider(imageParams?.provider) ?? defaultProviders.text2image;
|
|
162
162
|
const agentInfo = provider2ImageAgent[provider];
|
|
163
|
-
// The default text2image model is gpt-image-
|
|
163
|
+
// The default text2image model is gpt-image-2.5-sunburst from OpenAI.
|
|
164
164
|
const defaultImageParams = {
|
|
165
165
|
provider,
|
|
166
166
|
model: agentInfo.defaultModel,
|
|
@@ -189,7 +189,7 @@ export const MulmoPresentationStyleMethods = {
|
|
|
189
189
|
return true;
|
|
190
190
|
}
|
|
191
191
|
const provider = text2MovieProviderSchema.parse(movieParams?.provider ?? defaultProviders.text2movie);
|
|
192
|
-
const model = movieParams?.model ??
|
|
192
|
+
const model = movieParams?.model ?? defaultMovieModel(provider, movieParams);
|
|
193
193
|
return getModelAudio(provider, model)?.mode === AUDIO_MODE_ALWAYS;
|
|
194
194
|
},
|
|
195
195
|
getSoundEffectAgentInfo(presentationStyle, beat) {
|
|
@@ -43,20 +43,22 @@ export declare const provider2TTSAgent: {
|
|
|
43
43
|
};
|
|
44
44
|
export declare const gptImages: string[];
|
|
45
45
|
export declare const deprecatedOpenAIImageModelHints: {
|
|
46
|
-
readonly "dall-e-2": "Use 'gpt-image-
|
|
47
|
-
readonly "dall-e-3": "Use 'gpt-image-
|
|
46
|
+
readonly "dall-e-2": "Use 'gpt-image-2.5-sunburst' or another supported model.";
|
|
47
|
+
readonly "dall-e-3": "Use 'gpt-image-2.5-sunburst' or another supported model.";
|
|
48
48
|
};
|
|
49
49
|
export type DeprecatedOpenAIImageModel = keyof typeof deprecatedOpenAIImageModelHints;
|
|
50
50
|
export declare const deprecatedGoogleImageModelHints: {
|
|
51
|
-
readonly "imagen-3.0-generate-002": "Use 'gemini-
|
|
52
|
-
readonly "imagen-4.0-generate-001": "Use 'gemini-
|
|
53
|
-
readonly "imagen-4.0-ultra-generate-001": "Use 'gemini-
|
|
54
|
-
readonly "imagen-4.0-fast-generate-001": "Use 'gemini-
|
|
55
|
-
readonly "imagen-4.0-generate-preview-06-06": "Use 'gemini-
|
|
56
|
-
readonly "imagen-4.0-ultra-generate-preview-06-06": "Use 'gemini-
|
|
51
|
+
readonly "imagen-3.0-generate-002": "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
52
|
+
readonly "imagen-4.0-generate-001": "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
53
|
+
readonly "imagen-4.0-ultra-generate-001": "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
54
|
+
readonly "imagen-4.0-fast-generate-001": "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
55
|
+
readonly "imagen-4.0-generate-preview-06-06": "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
56
|
+
readonly "imagen-4.0-ultra-generate-preview-06-06": "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
57
57
|
};
|
|
58
58
|
export type DeprecatedGoogleImageModel = keyof typeof deprecatedGoogleImageModelHints;
|
|
59
|
+
export declare const isGeminiImageModel: (model: string) => boolean;
|
|
59
60
|
export declare const vertexAIGlobalOnlyImageModels: ReadonlySet<string>;
|
|
61
|
+
export declare const vertexImageLocation: (model: string, location?: string) => string;
|
|
60
62
|
export declare const provider2ImageAgent: {
|
|
61
63
|
openai: {
|
|
62
64
|
agentName: string;
|
|
@@ -169,34 +171,34 @@ export declare const provider2LipSyncAgent: {
|
|
|
169
171
|
export declare const provider2LLMAgent: {
|
|
170
172
|
readonly openai: {
|
|
171
173
|
readonly agentName: "openAIAgent";
|
|
172
|
-
readonly defaultModel: "gpt-5";
|
|
174
|
+
readonly defaultModel: "gpt-5.6-sol";
|
|
173
175
|
readonly keyName: "OPENAI_API_KEY";
|
|
174
176
|
readonly baseURLKeyName: "OPENAI_BASE_URL";
|
|
175
177
|
readonly apiVersionKeyName: "OPENAI_API_VERSION";
|
|
176
178
|
readonly max_tokens: 8192;
|
|
177
|
-
readonly models: readonly ["gpt-5", "gpt-5-nano", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "o3", "o3-mini", "o3-pro", "o1", "o1-pro", "gpt-4o", "gpt-4o-mini"];
|
|
179
|
+
readonly models: readonly ["gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5", "gpt-5-nano", "gpt-5-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano", "o3", "o3-mini", "o3-pro", "o1", "o1-pro", "gpt-4o", "gpt-4o-mini"];
|
|
178
180
|
};
|
|
179
181
|
readonly anthropic: {
|
|
180
182
|
readonly agentName: "anthropicAgent";
|
|
181
|
-
readonly defaultModel: "claude-sonnet-4-
|
|
183
|
+
readonly defaultModel: "claude-sonnet-4-6";
|
|
182
184
|
readonly max_tokens: 8192;
|
|
183
|
-
readonly models: readonly ["claude-
|
|
185
|
+
readonly models: readonly ["claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"];
|
|
184
186
|
readonly keyName: "ANTHROPIC_API_KEY";
|
|
185
187
|
readonly apiKeyNameOverride: "ANTHROPIC_API_TOKEN";
|
|
186
188
|
};
|
|
187
189
|
readonly gemini: {
|
|
188
190
|
readonly agentName: "geminiAgent";
|
|
189
|
-
readonly defaultModel: "gemini-
|
|
191
|
+
readonly defaultModel: "gemini-3.8-flash";
|
|
190
192
|
readonly max_tokens: 8192;
|
|
191
|
-
readonly models: readonly ["gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.5-flash-lite"];
|
|
193
|
+
readonly models: readonly ["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"];
|
|
192
194
|
readonly keyName: "GEMINI_API_KEY";
|
|
193
195
|
};
|
|
194
196
|
readonly groq: {
|
|
195
197
|
readonly agentName: "groqAgent";
|
|
196
|
-
readonly defaultModel: "
|
|
198
|
+
readonly defaultModel: "openai/gpt-oss-20b";
|
|
197
199
|
readonly keyName: "GROQ_API_KEY";
|
|
198
200
|
readonly max_tokens: 4096;
|
|
199
|
-
readonly models: readonly ["
|
|
201
|
+
readonly models: readonly ["openai/gpt-oss-20b", "openai/gpt-oss-120b"];
|
|
200
202
|
};
|
|
201
203
|
readonly mock: {
|
|
202
204
|
readonly agentName: "mediaMockAgent";
|
|
@@ -229,6 +231,13 @@ export declare const ProviderName: {
|
|
|
229
231
|
export declare const isImageProvider: (provider: string | undefined) => provider is keyof typeof provider2ImageAgent;
|
|
230
232
|
export declare const getMaxImageReferenceImages: (provider: string | undefined, model: string) => number | undefined;
|
|
231
233
|
export declare const getModelAudio: (provider: keyof typeof provider2MovieAgent, model: string) => MovieAudioSpec | undefined;
|
|
234
|
+
export declare const defaultMovieModel: (provider: keyof typeof provider2MovieAgent, movieParams?: {
|
|
235
|
+
vertexai_project?: string;
|
|
236
|
+
}) => string;
|
|
237
|
+
export declare const VEO_EXTENSION_MODEL = "veo-3.1-generate-preview";
|
|
238
|
+
export declare const VEO_SEGMENT_SEC = 8;
|
|
239
|
+
export declare const veoExtensionCount: (requestedSec: number) => number;
|
|
240
|
+
export declare const veoExtendedSeconds: (requestedSec: number) => number;
|
|
232
241
|
export declare const getModelDuration: (provider: keyof typeof provider2MovieAgent, model: string, movieDuration?: number) => number | undefined;
|
|
233
242
|
export type ModelPricing = {
|
|
234
243
|
unit: "tokens" | "chars" | "seconds" | "images";
|
|
@@ -44,13 +44,13 @@ export const provider2TTSAgent = {
|
|
|
44
44
|
models: ["mock-model"],
|
|
45
45
|
},
|
|
46
46
|
};
|
|
47
|
-
export const gptImages = ["gpt-image-2", "gpt-image-1.5", "gpt-image-1", "gpt-image-1-mini"];
|
|
48
|
-
const supportedOpenAIImageReplacementHint = "Use 'gpt-image-
|
|
47
|
+
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"];
|
|
48
|
+
const supportedOpenAIImageReplacementHint = "Use 'gpt-image-2.5-sunburst' or another supported model.";
|
|
49
49
|
export const deprecatedOpenAIImageModelHints = {
|
|
50
50
|
"dall-e-2": supportedOpenAIImageReplacementHint,
|
|
51
51
|
"dall-e-3": supportedOpenAIImageReplacementHint,
|
|
52
52
|
};
|
|
53
|
-
const supportedGoogleImageReplacementHint = "Use 'gemini-
|
|
53
|
+
const supportedGoogleImageReplacementHint = "Use 'gemini-3.1-flash-lite-image' or 'gemini-3-pro-image' instead.";
|
|
54
54
|
export const deprecatedGoogleImageModelHints = {
|
|
55
55
|
"imagen-3.0-generate-002": supportedGoogleImageReplacementHint,
|
|
56
56
|
"imagen-4.0-generate-001": supportedGoogleImageReplacementHint,
|
|
@@ -59,11 +59,21 @@ export const deprecatedGoogleImageModelHints = {
|
|
|
59
59
|
"imagen-4.0-generate-preview-06-06": supportedGoogleImageReplacementHint,
|
|
60
60
|
"imagen-4.0-ultra-generate-preview-06-06": supportedGoogleImageReplacementHint,
|
|
61
61
|
};
|
|
62
|
+
// Gemini image models are called through generateContent; anything else is treated as an Imagen model (generateImages).
|
|
63
|
+
export const isGeminiImageModel = (model) => model.startsWith("gemini-");
|
|
62
64
|
// Google image models that on Vertex AI are only published under location "global"
|
|
63
65
|
// (regional endpoints like us-central1 return 404 NOT_FOUND).
|
|
64
66
|
// See https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/3-pro-image
|
|
65
67
|
// and https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/3-1-flash-image
|
|
66
|
-
export const vertexAIGlobalOnlyImageModels = new Set([
|
|
68
|
+
export const vertexAIGlobalOnlyImageModels = new Set([
|
|
69
|
+
"gemini-3.1-flash-lite-image",
|
|
70
|
+
"gemini-3-pro-image",
|
|
71
|
+
"gemini-3-pro-image-preview",
|
|
72
|
+
"gemini-3.1-flash-image-preview",
|
|
73
|
+
]);
|
|
74
|
+
const VERTEX_DEFAULT_IMAGE_LOCATION = "us-central1";
|
|
75
|
+
// A global-only model has no regional endpoint, so an omitted location means "global" for it.
|
|
76
|
+
export const vertexImageLocation = (model, location) => location ?? (vertexAIGlobalOnlyImageModels.has(model) ? "global" : VERTEX_DEFAULT_IMAGE_LOCATION);
|
|
67
77
|
// Per-model reference image limits (image_input array). Only verified entries; unlisted models are not truncated.
|
|
68
78
|
const replicateImageModelParams = {
|
|
69
79
|
"bytedance/seedream-4": { maxReferenceImages: 10 },
|
|
@@ -71,7 +81,9 @@ const replicateImageModelParams = {
|
|
|
71
81
|
export const provider2ImageAgent = {
|
|
72
82
|
openai: {
|
|
73
83
|
agentName: "imageOpenaiAgent",
|
|
74
|
-
|
|
84
|
+
// gpt-image-1 shuts down on 2026-10-23; OpenAI names gpt-image-2.5-sunburst or -flare as its successor.
|
|
85
|
+
// https://developers.openai.com/api/docs/deprecations
|
|
86
|
+
defaultModel: "gpt-image-2.5-sunburst",
|
|
75
87
|
models: [...gptImages],
|
|
76
88
|
keyName: "OPENAI_API_KEY",
|
|
77
89
|
baseURLKeyName: "OPENAI_BASE_URL",
|
|
@@ -79,8 +91,16 @@ export const provider2ImageAgent = {
|
|
|
79
91
|
},
|
|
80
92
|
google: {
|
|
81
93
|
agentName: "imageGenAIAgent",
|
|
82
|
-
|
|
83
|
-
|
|
94
|
+
// gemini-2.5-flash-image is deprecated; Google names gemini-3.1-flash-lite-image as its successor.
|
|
95
|
+
defaultModel: "gemini-3.1-flash-lite-image",
|
|
96
|
+
models: [
|
|
97
|
+
"gemini-3.1-flash-lite-image",
|
|
98
|
+
"gemini-3.1-flash-image",
|
|
99
|
+
"gemini-3-pro-image",
|
|
100
|
+
"gemini-2.5-flash-image",
|
|
101
|
+
"gemini-3.1-flash-image-preview",
|
|
102
|
+
"gemini-3-pro-image-preview",
|
|
103
|
+
],
|
|
84
104
|
keyName: "GEMINI_API_KEY",
|
|
85
105
|
},
|
|
86
106
|
replicate: {
|
|
@@ -126,7 +146,6 @@ export const provider2MovieAgent = {
|
|
|
126
146
|
"bytedance/seedance-1-pro",
|
|
127
147
|
"bytedance/seedance-2.0",
|
|
128
148
|
"bytedance/seedance-2.0-fast",
|
|
129
|
-
"kwaivgi/kling-v1.6-pro",
|
|
130
149
|
"kwaivgi/kling-v2.1",
|
|
131
150
|
"kwaivgi/kling-v2.1-master",
|
|
132
151
|
"google/veo-2",
|
|
@@ -173,20 +192,14 @@ export const provider2MovieAgent = {
|
|
|
173
192
|
start_image: "image",
|
|
174
193
|
last_image: "last_frame_image",
|
|
175
194
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
176
|
-
price_per_sec: 0.
|
|
195
|
+
price_per_sec: 0.18,
|
|
177
196
|
},
|
|
178
197
|
"bytedance/seedance-2.0-fast": {
|
|
179
198
|
durations: [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
180
199
|
start_image: "image",
|
|
181
200
|
last_image: "last_frame_image",
|
|
182
201
|
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,
|
|
202
|
+
price_per_sec: 0.15,
|
|
190
203
|
},
|
|
191
204
|
"kwaivgi/kling-v2.1": {
|
|
192
205
|
durations: [5, 10],
|
|
@@ -210,7 +223,7 @@ export const provider2MovieAgent = {
|
|
|
210
223
|
durations: [8],
|
|
211
224
|
start_image: "image",
|
|
212
225
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
213
|
-
price_per_sec: 0.
|
|
226
|
+
price_per_sec: 0.2,
|
|
214
227
|
},
|
|
215
228
|
"google/veo-3.1": {
|
|
216
229
|
durations: [4, 6, 8],
|
|
@@ -218,14 +231,14 @@ export const provider2MovieAgent = {
|
|
|
218
231
|
last_image: "last_frame_image",
|
|
219
232
|
reference_images_param: "reference_images",
|
|
220
233
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
221
|
-
price_per_sec: 0.
|
|
234
|
+
price_per_sec: 0.2,
|
|
222
235
|
},
|
|
223
236
|
"google/veo-3.1-fast": {
|
|
224
237
|
durations: [4, 6, 8],
|
|
225
238
|
start_image: "image",
|
|
226
239
|
last_image: "last_frame_image",
|
|
227
240
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
228
|
-
price_per_sec: 0.
|
|
241
|
+
price_per_sec: 0.1,
|
|
229
242
|
},
|
|
230
243
|
"google/veo-3.1-lite": {
|
|
231
244
|
durations: [4, 6, 8],
|
|
@@ -238,13 +251,13 @@ export const provider2MovieAgent = {
|
|
|
238
251
|
durations: [8],
|
|
239
252
|
start_image: "image",
|
|
240
253
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
241
|
-
price_per_sec: 0.
|
|
254
|
+
price_per_sec: 0.1,
|
|
242
255
|
},
|
|
243
256
|
"minimax/video-01": {
|
|
244
257
|
durations: [6],
|
|
245
258
|
start_image: "first_frame_image",
|
|
246
259
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
247
|
-
price_per_sec: 0.
|
|
260
|
+
price_per_sec: 0.083,
|
|
248
261
|
},
|
|
249
262
|
"minimax/hailuo-02": {
|
|
250
263
|
durations: [6], // NOTE: 10 for only 720p
|
|
@@ -264,7 +277,7 @@ export const provider2MovieAgent = {
|
|
|
264
277
|
start_image: "image",
|
|
265
278
|
last_image: "last_frame_image",
|
|
266
279
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "sound_effect_switch" },
|
|
267
|
-
price_per_sec: 0.
|
|
280
|
+
price_per_sec: 0.06,
|
|
268
281
|
},
|
|
269
282
|
"wan-video/wan-2.2-i2v-fast": {
|
|
270
283
|
// No duration input: length is num_frames (default 81) / frames_per_second (default 16) ≈ 5s.
|
|
@@ -279,26 +292,26 @@ export const provider2MovieAgent = {
|
|
|
279
292
|
durations: [5],
|
|
280
293
|
start_image: undefined,
|
|
281
294
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
282
|
-
price_per_sec: 0.
|
|
295
|
+
price_per_sec: 0.01,
|
|
283
296
|
},
|
|
284
297
|
"xai/grok-imagine-video": {
|
|
285
298
|
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
286
299
|
start_image: "image",
|
|
287
300
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
288
|
-
price_per_sec: 0.
|
|
301
|
+
price_per_sec: 0.05,
|
|
289
302
|
},
|
|
290
303
|
"xai/grok-imagine-r2v": {
|
|
291
304
|
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
292
305
|
start_image: undefined,
|
|
293
306
|
reference_images_param: "reference_images",
|
|
294
307
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
295
|
-
price_per_sec: 0.
|
|
308
|
+
price_per_sec: 0.05,
|
|
296
309
|
},
|
|
297
310
|
"runwayml/gen-4.5": {
|
|
298
311
|
durations: [5, 10],
|
|
299
312
|
start_image: "image",
|
|
300
313
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
301
|
-
price_per_sec: 0.
|
|
314
|
+
price_per_sec: 0.12,
|
|
302
315
|
},
|
|
303
316
|
"kwaivgi/kling-v3-omni-video": {
|
|
304
317
|
durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
@@ -306,7 +319,7 @@ export const provider2MovieAgent = {
|
|
|
306
319
|
last_image: "end_image",
|
|
307
320
|
reference_images_param: "reference_images",
|
|
308
321
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
309
|
-
price_per_sec: 0.
|
|
322
|
+
price_per_sec: 0.224,
|
|
310
323
|
},
|
|
311
324
|
"kwaivgi/kling-v3-video": {
|
|
312
325
|
durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
@@ -314,7 +327,7 @@ export const provider2MovieAgent = {
|
|
|
314
327
|
last_image: "end_image",
|
|
315
328
|
reference_images_param: "reference_images",
|
|
316
329
|
audio: { mode: AUDIO_MODE_OPTIONAL, param: "generate_audio" },
|
|
317
|
-
price_per_sec: 0.
|
|
330
|
+
price_per_sec: 0.224,
|
|
318
331
|
},
|
|
319
332
|
// TODO: price_per_sec for the models below is a coarse approximation.
|
|
320
333
|
// Actual Replicate pricing varies by resolution / duration / quality and
|
|
@@ -324,27 +337,27 @@ export const provider2MovieAgent = {
|
|
|
324
337
|
durations: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
|
|
325
338
|
start_image: "image",
|
|
326
339
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
327
|
-
price_per_sec: 0.
|
|
340
|
+
price_per_sec: 0.14,
|
|
328
341
|
},
|
|
329
342
|
"minimax/hailuo-2.3": {
|
|
330
343
|
durations: [6, 10],
|
|
331
344
|
start_image: "first_frame_image",
|
|
332
345
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
333
|
-
price_per_sec: 0.
|
|
346
|
+
price_per_sec: 0.047,
|
|
334
347
|
},
|
|
335
348
|
"minimax/hailuo-2.3-fast": {
|
|
336
349
|
durations: [6, 10],
|
|
337
350
|
start_image: "first_frame_image",
|
|
338
351
|
start_image_required: true,
|
|
339
352
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
340
|
-
price_per_sec: 0.
|
|
353
|
+
price_per_sec: 0.032,
|
|
341
354
|
},
|
|
342
355
|
"pixverse/pixverse-v5": {
|
|
343
356
|
durations: [5, 8],
|
|
344
357
|
start_image: "image",
|
|
345
358
|
last_image: "last_frame_image",
|
|
346
359
|
audio: { mode: AUDIO_MODE_NEVER },
|
|
347
|
-
price_per_sec: 0.
|
|
360
|
+
price_per_sec: 0.06,
|
|
348
361
|
},
|
|
349
362
|
"prunaai/p-video": {
|
|
350
363
|
durations: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
|
|
@@ -364,8 +377,9 @@ export const provider2MovieAgent = {
|
|
|
364
377
|
},
|
|
365
378
|
google: {
|
|
366
379
|
agentName: "movieGenAIAgent",
|
|
367
|
-
|
|
368
|
-
|
|
380
|
+
// 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.
|
|
381
|
+
defaultModel: "veo-3.1-generate-preview",
|
|
382
|
+
models: ["veo-3.1-generate-preview", "veo-3.1-lite-generate-preview", "veo-3.1-generate-001"],
|
|
369
383
|
keyName: "GEMINI_API_KEY",
|
|
370
384
|
modelParams: {
|
|
371
385
|
"veo-3.1-lite-generate-preview": {
|
|
@@ -376,7 +390,8 @@ export const provider2MovieAgent = {
|
|
|
376
390
|
supportsPersonGeneration: false,
|
|
377
391
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
378
392
|
},
|
|
379
|
-
|
|
393
|
+
// Vertex AI only: the GA name of Veo 3.1 there, where the preview IDs are retired.
|
|
394
|
+
"veo-3.1-generate-001": {
|
|
380
395
|
durations: [4, 6, 8],
|
|
381
396
|
supportsDuration: true,
|
|
382
397
|
supportsLastFrame: true,
|
|
@@ -384,22 +399,14 @@ export const provider2MovieAgent = {
|
|
|
384
399
|
supportsPersonGeneration: false,
|
|
385
400
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
386
401
|
},
|
|
387
|
-
"veo-3.
|
|
388
|
-
durations: [8],
|
|
389
|
-
supportsDuration:
|
|
390
|
-
supportsLastFrame:
|
|
391
|
-
supportsReferenceImages:
|
|
402
|
+
"veo-3.1-generate-preview": {
|
|
403
|
+
durations: [4, 6, 8],
|
|
404
|
+
supportsDuration: true,
|
|
405
|
+
supportsLastFrame: true,
|
|
406
|
+
supportsReferenceImages: true,
|
|
392
407
|
supportsPersonGeneration: false,
|
|
393
408
|
audio: { mode: AUDIO_MODE_ALWAYS },
|
|
394
409
|
},
|
|
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
410
|
},
|
|
404
411
|
},
|
|
405
412
|
mock: {
|
|
@@ -471,12 +478,16 @@ export const provider2LipSyncAgent = {
|
|
|
471
478
|
export const provider2LLMAgent = {
|
|
472
479
|
openai: {
|
|
473
480
|
agentName: "openAIAgent",
|
|
474
|
-
|
|
481
|
+
// gpt-5 (gpt-5-2025-08-07) shuts down on 2026-12-11; OpenAI names gpt-5.6-sol as its successor.
|
|
482
|
+
defaultModel: "gpt-5.6-sol",
|
|
475
483
|
keyName: "OPENAI_API_KEY",
|
|
476
484
|
baseURLKeyName: "OPENAI_BASE_URL",
|
|
477
485
|
apiVersionKeyName: "OPENAI_API_VERSION",
|
|
478
486
|
max_tokens: 8192,
|
|
479
487
|
models: [
|
|
488
|
+
"gpt-5.6-sol",
|
|
489
|
+
"gpt-5.6-terra",
|
|
490
|
+
"gpt-5.6-luna",
|
|
480
491
|
"gpt-5",
|
|
481
492
|
"gpt-5-nano",
|
|
482
493
|
"gpt-5-mini",
|
|
@@ -494,26 +505,31 @@ export const provider2LLMAgent = {
|
|
|
494
505
|
},
|
|
495
506
|
anthropic: {
|
|
496
507
|
agentName: "anthropicAgent",
|
|
497
|
-
|
|
508
|
+
// claude-sonnet-4-5 retires on 2026-11-30. Its named successor, claude-sonnet-5-5 (like claude-opus-4-8),
|
|
509
|
+
// rejects the temperature @graphai/anthropic_agent always sends, so the newest model that answers is used.
|
|
510
|
+
defaultModel: "claude-sonnet-4-6",
|
|
498
511
|
max_tokens: 8192,
|
|
499
|
-
models: ["claude-
|
|
512
|
+
models: ["claude-sonnet-4-6", "claude-sonnet-4-5-20250929", "claude-haiku-4-5-20251001"],
|
|
500
513
|
keyName: "ANTHROPIC_API_KEY",
|
|
501
514
|
apiKeyNameOverride: "ANTHROPIC_API_TOKEN",
|
|
502
515
|
// GraphAI is currently using ANTHROPIC_API_KEY, but the official name is ANTHROPIC_API_TOKEN.
|
|
503
516
|
},
|
|
504
517
|
gemini: {
|
|
505
518
|
agentName: "geminiAgent",
|
|
506
|
-
|
|
519
|
+
// Google limits the 2.5 models to users who have used them before and recommends 3.8 Flash.
|
|
520
|
+
defaultModel: "gemini-3.8-flash",
|
|
507
521
|
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"],
|
|
522
|
+
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
523
|
keyName: "GEMINI_API_KEY",
|
|
510
524
|
},
|
|
511
525
|
groq: {
|
|
512
526
|
agentName: "groqAgent",
|
|
513
|
-
|
|
527
|
+
// The Llama and DeepSeek models were shut down; Groq names openai/gpt-oss-20b as the 8B model's successor.
|
|
528
|
+
// https://console.groq.com/docs/deprecations
|
|
529
|
+
defaultModel: "openai/gpt-oss-20b",
|
|
514
530
|
keyName: "GROQ_API_KEY",
|
|
515
531
|
max_tokens: 4096,
|
|
516
|
-
models: ["
|
|
532
|
+
models: ["openai/gpt-oss-20b", "openai/gpt-oss-120b"],
|
|
517
533
|
},
|
|
518
534
|
mock: {
|
|
519
535
|
agentName: "mediaMockAgent",
|
|
@@ -562,6 +578,14 @@ export const getModelAudio = (provider, model) => {
|
|
|
562
578
|
const modelParams = provider2MovieAgent[provider]?.modelParams;
|
|
563
579
|
return modelParams?.[model]?.audio;
|
|
564
580
|
};
|
|
581
|
+
const GOOGLE_VERTEX_DEFAULT_MOVIE_MODEL = "veo-3.1-generate-001";
|
|
582
|
+
// The default model is shared by the Gemini API and Vertex AI, but Vertex retired the Veo 3.1 preview IDs.
|
|
583
|
+
export const defaultMovieModel = (provider, movieParams) => provider === "google" && movieParams?.vertexai_project ? GOOGLE_VERTEX_DEFAULT_MOVIE_MODEL : provider2MovieAgent[provider].defaultModel;
|
|
584
|
+
// Veo 3.1 (preview) builds clips longer than one segment by extending an 8-second first segment 8 seconds at a time.
|
|
585
|
+
export const VEO_EXTENSION_MODEL = "veo-3.1-generate-preview";
|
|
586
|
+
export const VEO_SEGMENT_SEC = 8;
|
|
587
|
+
export const veoExtensionCount = (requestedSec) => Math.ceil((requestedSec - VEO_SEGMENT_SEC) / VEO_SEGMENT_SEC);
|
|
588
|
+
export const veoExtendedSeconds = (requestedSec) => VEO_SEGMENT_SEC * (1 + veoExtensionCount(requestedSec));
|
|
565
589
|
export const getModelDuration = (provider, model, movieDuration) => {
|
|
566
590
|
const modelParams = provider2MovieAgent[provider]?.modelParams;
|
|
567
591
|
const { durations } = modelParams[model];
|
|
@@ -582,7 +606,8 @@ export const gptImageOutputTokens = {
|
|
|
582
606
|
// Spot-checked against replicate.com model pages on this date (seedance-1-lite, kling-v2.1, omni-human).
|
|
583
607
|
// Some replicate prices vary by resolution/variant; price_per_sec holds the rate for the typical configuration
|
|
584
608
|
// (e.g. seedance-1-lite at 720p, kling-v2.1 standard).
|
|
585
|
-
|
|
609
|
+
// Optional-audio models are priced without audio, since mulmocast generates them silent by default.
|
|
610
|
+
const REPLICATE_PRICING_AS_OF = "2026-10-07";
|
|
586
611
|
const replicateMoviePricing = Object.fromEntries(Object.entries(provider2MovieAgent.replicate.modelParams).map(([model, params]) => [
|
|
587
612
|
model,
|
|
588
613
|
{ unit: "seconds", perSecUSD: params.price_per_sec, asOf: REPLICATE_PRICING_AS_OF },
|
|
@@ -601,9 +626,15 @@ export const modelPricing = {
|
|
|
601
626
|
"gpt-image-1.5": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 32, asOf: "2026-07-03" },
|
|
602
627
|
"gpt-image-2": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 30, asOf: "2026-07-03" },
|
|
603
628
|
"gpt-image-1-mini": { unit: "tokens", inputPerMTokensUSD: 2, outputPerMTokensUSD: 8, asOf: "2026-07-03" },
|
|
629
|
+
// https://developers.openai.com/api/docs/pricing (text input $5/1M, image output $30/1M; image input $8/1M is not modeled)
|
|
630
|
+
"gpt-image-2.5-sunburst": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 30, asOf: "2026-10-07" },
|
|
631
|
+
"gpt-image-2.5-flare": { unit: "tokens", inputPerMTokensUSD: 5, outputPerMTokensUSD: 30, asOf: "2026-10-07" },
|
|
604
632
|
// https://developers.openai.com/api/docs/models/gpt-5 etc.
|
|
605
633
|
"gpt-5": { unit: "tokens", inputPerMTokensUSD: 1.25, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
606
634
|
"gpt-5-mini": { unit: "tokens", inputPerMTokensUSD: 0.25, outputPerMTokensUSD: 2, asOf: "2026-07-03" },
|
|
635
|
+
// https://developers.openai.com/api/docs/models/gpt-5.6-sol (promotional price, "at least through November 21, 2026")
|
|
636
|
+
"gpt-5.6-sol": { unit: "tokens", inputPerMTokensUSD: 4, outputPerMTokensUSD: 20, asOf: "2026-10-07" },
|
|
637
|
+
"gpt-5.6-terra": { unit: "tokens", inputPerMTokensUSD: 2, outputPerMTokensUSD: 12, asOf: "2026-10-07" },
|
|
607
638
|
"gpt-4o": { unit: "tokens", inputPerMTokensUSD: 2.5, outputPerMTokensUSD: 10, asOf: "2026-07-03" },
|
|
608
639
|
},
|
|
609
640
|
gemini: {
|
|
@@ -614,9 +645,13 @@ export const modelPricing = {
|
|
|
614
645
|
google: {
|
|
615
646
|
// https://ai.google.dev/gemini-api/docs/pricing ($0.039/image ≒ 1290 output tokens at $30/1M)
|
|
616
647
|
"gemini-2.5-flash-image": { unit: "images", inputPerMTokensUSD: 0.3, perImageUSD: 0.039, asOf: "2026-07-03" },
|
|
648
|
+
// $0.0336 per 1K-resolution image
|
|
649
|
+
"gemini-3.1-flash-lite-image": { unit: "images", inputPerMTokensUSD: 0.25, perImageUSD: 0.0336, asOf: "2026-10-07" },
|
|
617
650
|
// Veo per second of generated video (720p). veo-2.0-generate-001 and veo-3.0-generate-001
|
|
618
651
|
// were shut down on 2026-06-30, so they intentionally have no price.
|
|
619
652
|
"veo-3.1-generate-preview": { unit: "seconds", perSecUSD: 0.4, asOf: "2026-07-03" },
|
|
653
|
+
// Vertex AI: https://cloud.google.com/vertex-ai/generative-ai/pricing (video + audio, 720p/1080p)
|
|
654
|
+
"veo-3.1-generate-001": { unit: "seconds", perSecUSD: 0.4, asOf: "2026-10-07" },
|
|
620
655
|
"veo-3.1-lite-generate-preview": { unit: "seconds", perSecUSD: 0.05, asOf: "2026-07-03" },
|
|
621
656
|
// Google Cloud Text-to-Speech per 1M characters, keyed by voice tier.
|
|
622
657
|
// https://cloud.google.com/text-to-speech/pricing
|
|
@@ -629,15 +664,16 @@ export const modelPricing = {
|
|
|
629
664
|
anthropic: {
|
|
630
665
|
// https://platform.claude.com/docs/en/docs/about-claude/pricing
|
|
631
666
|
"claude-sonnet-4-5-20250929": { unit: "tokens", inputPerMTokensUSD: 3, outputPerMTokensUSD: 15, asOf: "2026-07-03" },
|
|
667
|
+
"claude-sonnet-4-6": { unit: "tokens", inputPerMTokensUSD: 3, outputPerMTokensUSD: 15, asOf: "2026-10-07" },
|
|
632
668
|
},
|
|
633
669
|
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:
|
|
670
|
+
// https://elevenlabs.io/pricing/api ($0.08 per 1k chars for multilingual/v3, $0.04 for flash/turbo)
|
|
671
|
+
eleven_v3: { unit: "chars", perMCharsUSD: 80, asOf: "2026-10-07" },
|
|
672
|
+
eleven_multilingual_v2: { unit: "chars", perMCharsUSD: 80, asOf: "2026-10-07" },
|
|
673
|
+
eleven_turbo_v2_5: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
674
|
+
eleven_turbo_v2: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
675
|
+
eleven_flash_v2_5: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
676
|
+
eleven_flash_v2: { unit: "chars", perMCharsUSD: 40, asOf: "2026-10-07" },
|
|
641
677
|
},
|
|
642
678
|
replicate: {
|
|
643
679
|
...replicateMoviePricing,
|
|
@@ -4,7 +4,7 @@
|
|
|
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, 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";
|
|
@@ -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,48 @@
|
|
|
1
|
+
import { WEBRTC_REMOVAL_SCRIPT, isAllowedSceneRequest } from "./network_policy.js";
|
|
2
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
3
|
+
const isPausedRequest = (value) => isRecord(value) && typeof value.requestId === "string" && isRecord(value.request) && typeof value.request.url === "string";
|
|
4
|
+
const isAttachedTarget = (value) => isRecord(value) && typeof value.sessionId === "string" && isRecord(value.targetInfo) && typeof value.targetInfo.type === "string";
|
|
5
|
+
// Remotion types only the CDP commands it uses itself; Fetch and Runtime.runIfWaitingForDebugger
|
|
6
|
+
// are sent through the same send(), which resolves on the reply and rejects on a CDP error.
|
|
7
|
+
const sendCommand = async (channel, method, params = {}) => {
|
|
8
|
+
const send = Reflect.get(channel, "send");
|
|
9
|
+
if (typeof send !== "function")
|
|
10
|
+
throw new Error(`remotion: cannot send ${method}: the browser connection has no send()`);
|
|
11
|
+
await send.call(channel, method, params);
|
|
12
|
+
};
|
|
13
|
+
const FRAME_TARGET_TYPES = ["page", "iframe"];
|
|
14
|
+
const answerRequest = async (connection, paused, serverPort, onBlocked) => {
|
|
15
|
+
if (isAllowedSceneRequest({ url: paused.request.url, isDocument: paused.resourceType === "Document" }, serverPort)) {
|
|
16
|
+
await sendCommand(connection, "Fetch.continueRequest", { requestId: paused.requestId });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
onBlocked(paused.request.url);
|
|
20
|
+
await sendCommand(connection, "Fetch.failRequest", { requestId: paused.requestId, errorReason: "BlockedByClient" });
|
|
21
|
+
};
|
|
22
|
+
// A target that could not be guarded stays paused, so its render fails instead of running unguarded.
|
|
23
|
+
const guardTarget = async (browser, attached) => {
|
|
24
|
+
const session = browser.connection.session(attached.sessionId);
|
|
25
|
+
if (!session)
|
|
26
|
+
throw new Error(`remotion: no CDP session for a new ${attached.targetInfo.type}`);
|
|
27
|
+
if (FRAME_TARGET_TYPES.includes(attached.targetInfo.type)) {
|
|
28
|
+
await session.send("Page.enable");
|
|
29
|
+
await session.send("Page.addScriptToEvaluateOnNewDocument", { source: WEBRTC_REMOVAL_SCRIPT });
|
|
30
|
+
}
|
|
31
|
+
await sendCommand(session, "Runtime.runIfWaitingForDebugger");
|
|
32
|
+
};
|
|
33
|
+
export const guardSceneNetwork = async (browser, serverPort, onBlocked) => {
|
|
34
|
+
const guard = { failures: [] };
|
|
35
|
+
const fail = (error) => guard.failures.push(error instanceof Error ? error.message : String(error));
|
|
36
|
+
const connection = browser.connection;
|
|
37
|
+
connection.on("Fetch.requestPaused", (event) => {
|
|
38
|
+
if (isPausedRequest(event))
|
|
39
|
+
answerRequest(connection, event, serverPort, onBlocked).catch(fail);
|
|
40
|
+
});
|
|
41
|
+
connection.on("Target.attachedToTarget", (event) => {
|
|
42
|
+
if (isAttachedTarget(event))
|
|
43
|
+
guardTarget(browser, event).catch(fail);
|
|
44
|
+
});
|
|
45
|
+
await sendCommand(connection, "Fetch.enable", { patterns: [{ urlPattern: "*" }] });
|
|
46
|
+
await connection.send("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: true, flatten: true });
|
|
47
|
+
return guard;
|
|
48
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type SceneRequest = {
|
|
2
|
+
url: string;
|
|
3
|
+
isDocument: boolean;
|
|
4
|
+
};
|
|
5
|
+
export declare const isAllowedSceneRequest: ({ url, isDocument }: SceneRequest, serverPort: number) => boolean;
|
|
6
|
+
export declare const isAllowedMediaDownload: (src: string) => boolean;
|
|
7
|
+
export declare const REMOTION_CONTENT_SECURITY_POLICY = "connect-src 'self' data: blob:";
|
|
8
|
+
export declare const withContentSecurityPolicy: (html: string) => string;
|
|
9
|
+
export declare const WEBRTC_GLOBALS: readonly ["RTCPeerConnection", "webkitRTCPeerConnection", "RTCDataChannel", "RTCIceCandidate", "RTCSessionDescription"];
|
|
10
|
+
export declare const WEBRTC_REMOVAL_SCRIPT: string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// What a remotion scene's browser may reach: only the bundle server started for that render, and
|
|
2
|
+
// in-memory URLs. Kept free of I/O so the rules can be tested without a browser.
|
|
3
|
+
const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "[::1]"];
|
|
4
|
+
const IN_MEMORY_SCHEMES = ["data:", "blob:"];
|
|
5
|
+
const parseUrl = (url) => {
|
|
6
|
+
try {
|
|
7
|
+
return new URL(url);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
// Remotion's /proxy makes the bundle server download its `src` from Node, on the scene's behalf.
|
|
14
|
+
const REMOTION_PROXY_PATH = "/proxy";
|
|
15
|
+
// The only page that carries the CSP; any other HTML the server returns (a directory listing, a 404)
|
|
16
|
+
// would run without it, so no other path may be loaded as a document.
|
|
17
|
+
const SCENE_DOCUMENT_PATHS = ["/", "/index.html"];
|
|
18
|
+
// Another port on localhost is another service, so only the render's own port counts as local.
|
|
19
|
+
const isBundleServer = (parsed, serverPort) => parsed.protocol === "http:" && LOOPBACK_HOSTS.includes(parsed.hostname) && parsed.port === String(serverPort);
|
|
20
|
+
export const isAllowedSceneRequest = ({ url, isDocument }, serverPort) => {
|
|
21
|
+
const parsed = parseUrl(url);
|
|
22
|
+
if (!parsed)
|
|
23
|
+
return false;
|
|
24
|
+
if (IN_MEMORY_SCHEMES.includes(parsed.protocol))
|
|
25
|
+
return true;
|
|
26
|
+
if (!isBundleServer(parsed, serverPort) || parsed.pathname.startsWith(REMOTION_PROXY_PATH))
|
|
27
|
+
return false;
|
|
28
|
+
return !isDocument || SCENE_DOCUMENT_PATHS.includes(parsed.pathname);
|
|
29
|
+
};
|
|
30
|
+
// Remotion downloads <Audio> / <Video> sources from Node while rendering, outside the browser;
|
|
31
|
+
// a scene uses no media files, so only inline data survives.
|
|
32
|
+
export const isAllowedMediaDownload = (src) => parseUrl(src)?.protocol === "data:";
|
|
33
|
+
// The browser-level request block does not see WebSocket or EventSource connections; this does.
|
|
34
|
+
export const REMOTION_CONTENT_SECURITY_POLICY = "connect-src 'self' data: blob:";
|
|
35
|
+
const HEAD_TAG = /<head(\s[^>]*)?>/i;
|
|
36
|
+
export const withContentSecurityPolicy = (html) => {
|
|
37
|
+
const match = HEAD_TAG.exec(html);
|
|
38
|
+
if (!match)
|
|
39
|
+
throw new Error("remotion: the bundle's index.html has no <head>, so the network policy cannot be applied");
|
|
40
|
+
const meta = `<meta http-equiv="Content-Security-Policy" content="${REMOTION_CONTENT_SECURITY_POLICY}">`;
|
|
41
|
+
const insertAt = match.index + match[0].length;
|
|
42
|
+
return [html.slice(0, insertAt), meta, html.slice(insertAt)].join("");
|
|
43
|
+
};
|
|
44
|
+
export const WEBRTC_GLOBALS = ["RTCPeerConnection", "webkitRTCPeerConnection", "RTCDataChannel", "RTCIceCandidate", "RTCSessionDescription"];
|
|
45
|
+
// Run before any page script in every frame: WebRTC opens UDP connections that neither the request
|
|
46
|
+
// block nor CSP covers. Non-configurable, so scene code cannot put the constructors back.
|
|
47
|
+
export const WEBRTC_REMOVAL_SCRIPT = [
|
|
48
|
+
`for (const name of ${JSON.stringify(WEBRTC_GLOBALS)}) {`,
|
|
49
|
+
" try { Object.defineProperty(globalThis, name, { value: undefined, writable: false, configurable: false }); } catch (_) {}",
|
|
50
|
+
"}",
|
|
51
|
+
].join("\n");
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import net from "node:net";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
5
|
+
import { GraphAILogger } from "graphai";
|
|
4
6
|
import { REMOTION_COMPOSITION_ID, REMOTION_ENTRY_FILE, buildEntrySource } from "./claude_prompt.js";
|
|
5
7
|
import { missingRemotionPackages, remotionInstallCommand } from "./packages.js";
|
|
8
|
+
import { isAllowedMediaDownload, withContentSecurityPolicy } from "./network_policy.js";
|
|
9
|
+
import { guardSceneNetwork } from "./network_guard.js";
|
|
6
10
|
// WebGL (three.js scenes) needs the ANGLE backend; the default one cannot create a context headless.
|
|
7
11
|
const CHROMIUM_OPTIONS = { gl: "angle" };
|
|
12
|
+
const MAX_LOGGED_URL_LENGTH = 200;
|
|
8
13
|
const loadRemotion = async () => {
|
|
9
14
|
try {
|
|
10
15
|
const [bundler, renderer] = await Promise.all([import("@remotion/bundler"), import("@remotion/renderer")]);
|
|
@@ -40,8 +45,7 @@ const remotionNodeModulesDir = () => {
|
|
|
40
45
|
const remotionPackageJson = createRequire(import.meta.url).resolve("remotion/package.json");
|
|
41
46
|
return path.dirname(path.dirname(remotionPackageJson));
|
|
42
47
|
};
|
|
43
|
-
|
|
44
|
-
const { bundle, renderer } = await loadRemotion();
|
|
48
|
+
const bundleScene = async (bundle, workDir) => {
|
|
45
49
|
const entryPoint = path.join(workDir, REMOTION_ENTRY_FILE);
|
|
46
50
|
fs.writeFileSync(entryPoint, buildEntrySource());
|
|
47
51
|
const nodeModulesDir = remotionNodeModulesDir();
|
|
@@ -53,13 +57,53 @@ export const renderRemotionScene = async ({ workDir, props, videoPath, stillPath
|
|
|
53
57
|
resolve: { ...config.resolve, modules: [...(config.resolve?.modules ?? ["node_modules"]), nodeModulesDir] },
|
|
54
58
|
}),
|
|
55
59
|
});
|
|
56
|
-
const
|
|
60
|
+
const indexHtml = path.join(serveUrl, "index.html");
|
|
61
|
+
fs.writeFileSync(indexHtml, withContentSecurityPolicy(fs.readFileSync(indexHtml, "utf8")));
|
|
62
|
+
return serveUrl;
|
|
63
|
+
};
|
|
64
|
+
const findFreePort = () => new Promise((resolve, reject) => {
|
|
65
|
+
const server = net.createServer();
|
|
66
|
+
server.once("error", reject);
|
|
67
|
+
server.listen(0, "127.0.0.1", () => {
|
|
68
|
+
const address = server.address();
|
|
69
|
+
const port = typeof address === "object" && address ? address.port : 0;
|
|
70
|
+
server.close(() => (port > 0 ? resolve(port) : reject(new Error("remotion: could not pick a port for the bundle server"))));
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
const logBlockedRequest = (url) => GraphAILogger.info(`remotion: blocked a network request from the scene: ${url}`);
|
|
74
|
+
// Throwing here stops remotion before it fetches, and fails the render.
|
|
75
|
+
const rejectMediaDownload = (src) => {
|
|
76
|
+
if (!isAllowedMediaDownload(src))
|
|
77
|
+
throw new Error(`remotion: a scene may not load media files (${src.slice(0, MAX_LOGGED_URL_LENGTH)})`);
|
|
78
|
+
return undefined;
|
|
79
|
+
};
|
|
80
|
+
// The bundle server listens on the port given here, the only local origin the guard lets the scene reach.
|
|
81
|
+
const renderOutputs = async (renderer, browser, serveUrl, port, outputs) => {
|
|
82
|
+
const { props, videoPath, stillPath, extraStills = [] } = outputs;
|
|
83
|
+
const shared = { serveUrl, inputProps: props, chromiumOptions: CHROMIUM_OPTIONS, puppeteerInstance: browser, port };
|
|
84
|
+
const downloadGuard = { onDownload: rejectMediaDownload };
|
|
85
|
+
const composition = await renderer.selectComposition({ ...shared, id: REMOTION_COMPOSITION_ID });
|
|
57
86
|
if (videoPath) {
|
|
58
|
-
await renderer.renderMedia({
|
|
87
|
+
await renderer.renderMedia({ ...shared, ...downloadGuard, composition, codec: "h264", outputLocation: videoPath });
|
|
59
88
|
}
|
|
60
89
|
const stills = stillPath ? [...extraStills, { frame: composition.durationInFrames - 1, path: stillPath }] : extraStills;
|
|
61
90
|
await stills.reduce(async (previous, still) => {
|
|
62
91
|
await previous;
|
|
63
|
-
await renderer.renderStill({
|
|
92
|
+
await renderer.renderStill({ ...shared, ...downloadGuard, composition, output: still.path, frame: still.frame });
|
|
64
93
|
}, Promise.resolve());
|
|
65
94
|
};
|
|
95
|
+
export const renderRemotionScene = async ({ workDir, ...outputs }) => {
|
|
96
|
+
const { bundle, renderer } = await loadRemotion();
|
|
97
|
+
const serveUrl = await bundleScene(bundle, workDir);
|
|
98
|
+
const port = await findFreePort();
|
|
99
|
+
const browser = await renderer.openBrowser("chrome", { chromiumOptions: CHROMIUM_OPTIONS });
|
|
100
|
+
try {
|
|
101
|
+
const guard = await guardSceneNetwork(browser, port, logBlockedRequest);
|
|
102
|
+
await renderOutputs(renderer, browser, serveUrl, port, outputs);
|
|
103
|
+
if (guard.failures.length > 0)
|
|
104
|
+
throw new Error(`remotion: the network guard failed: ${guard.failures.join("; ")}`);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await browser.close({ silent: true });
|
|
108
|
+
}
|
|
109
|
+
};
|
package/package.json
CHANGED
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"text": "movie generated by veo3",
|
|
60
60
|
"moviePrompt": "a woman is walking through a busy Tokyo street at night, she is wearing dark sunglasses",
|
|
61
61
|
"movieParams": {
|
|
62
|
-
"model": "veo-3.
|
|
62
|
+
"model": "veo-3.1-lite-generate-preview"
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
{
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
},
|
|
81
81
|
"moviePrompt": "a woman is walking through a busy Tokyo street at night, she is wearing dark sunglasses",
|
|
82
82
|
"movieParams": {
|
|
83
|
-
"model": "veo-3.
|
|
83
|
+
"model": "veo-3.1-lite-generate-preview"
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
]
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"title": "Testing OpenAI's new Image Generation",
|
|
8
8
|
"description": "Hello",
|
|
9
9
|
"imageParams": {
|
|
10
|
-
"model": "gpt-image-
|
|
10
|
+
"model": "gpt-image-2.5-sunburst",
|
|
11
11
|
"style": "<style>Japanese animation with soft watercolor backgrounds, characters with simple rounded faces, large expressive eyes, small nose and mouth, soft jawlines, minimalist facial features, pastel color palette, detailed natural environments, whimsical magical elements, hand-drawn aesthetic, gentle lighting, flowing movement in hair and clothing, nostalgic countryside scenery with fantasy elements."
|
|
12
12
|
},
|
|
13
13
|
"speechParams": {
|
|
@@ -96,12 +96,12 @@
|
|
|
96
96
|
}
|
|
97
97
|
},
|
|
98
98
|
{
|
|
99
|
-
"id": "kling-
|
|
100
|
-
"text": "kwaivgi/kling-
|
|
99
|
+
"id": "kling-v2.1-start-image",
|
|
100
|
+
"text": "kwaivgi/kling-v2.1 with gpt4-image",
|
|
101
101
|
"imagePrompt": "a woman is walking through a busy Tokyo street at night, she is wearing dark sunglasses",
|
|
102
102
|
"moviePrompt": "a woman is walking through a busy Tokyo street at night, she is wearing dark sunglasses",
|
|
103
103
|
"movieParams": {
|
|
104
|
-
"model": "kwaivgi/kling-
|
|
104
|
+
"model": "kwaivgi/kling-v2.1"
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
{
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"movieParams": {
|
|
26
26
|
"provider": "google",
|
|
27
|
-
"model": "veo-
|
|
27
|
+
"model": "veo-3.1-generate-001",
|
|
28
28
|
"vertexai_project": "YOUR_PROJECT_ID",
|
|
29
29
|
"vertexai_location": "us-central1"
|
|
30
30
|
},
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"text": "TTS: Gemini 2.5 Pro TTS Kore. Movie: Veo 3 override.",
|
|
77
77
|
"moviePrompt": "Ocean waves crashing on a beach at sunset",
|
|
78
78
|
"movieParams": {
|
|
79
|
-
"model": "veo-3.
|
|
79
|
+
"model": "veo-3.1-generate-001",
|
|
80
80
|
"vertexai_project": "YOUR_PROJECT_ID",
|
|
81
81
|
"vertexai_location": "us-central1"
|
|
82
82
|
}
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
},
|
|
102
102
|
"moviePrompt": "The robot turns its head and waves at the camera",
|
|
103
103
|
"movieParams": {
|
|
104
|
-
"model": "veo-3.
|
|
104
|
+
"model": "veo-3.1-generate-001",
|
|
105
105
|
"vertexai_project": "YOUR_PROJECT_ID",
|
|
106
106
|
"vertexai_location": "us-central1"
|
|
107
107
|
},
|