mulmocast 2.14.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.
Files changed (38) hide show
  1. package/README.md +1 -7
  2. package/assets/schemas/mulmo_script.json +2 -2
  3. package/lib/actions/avatar.d.ts +2 -1
  4. package/lib/actions/avatar.js +59 -3
  5. package/lib/actions/image_references.d.ts +23 -1
  6. package/lib/actions/image_references.js +16 -9
  7. package/lib/agents/image_genai_agent.js +3 -3
  8. package/lib/agents/movie_genai_agent.js +5 -7
  9. package/lib/data/scriptTemplates.js +2 -2
  10. package/lib/data/templateDataSet.js +1 -1
  11. package/lib/index.remotion.d.ts +3 -0
  12. package/lib/index.remotion.js +2 -0
  13. package/lib/index.remotion_guide.d.ts +1 -0
  14. package/lib/index.remotion_guide.js +3 -0
  15. package/lib/methods/mulmo_presentation_style.js +3 -3
  16. package/lib/types/provider2agent.d.ts +25 -16
  17. package/lib/types/provider2agent.js +99 -63
  18. package/lib/types/schema.js +3 -1
  19. package/lib/utils/estimate_usage.js +7 -3
  20. package/lib/utils/image_plugins/remotion.js +3 -10
  21. package/lib/utils/remotion/frames.d.ts +3 -0
  22. package/lib/utils/remotion/frames.js +11 -0
  23. package/lib/utils/remotion/network_guard.d.ts +5 -0
  24. package/lib/utils/remotion/network_guard.js +48 -0
  25. package/lib/utils/remotion/network_policy.d.ts +10 -0
  26. package/lib/utils/remotion/network_policy.js +51 -0
  27. package/lib/utils/remotion/render.d.ts +1 -1
  28. package/lib/utils/remotion/render.js +50 -6
  29. package/lib/utils/remotion/render_frames.d.ts +23 -0
  30. package/lib/utils/remotion/render_frames.js +61 -0
  31. package/package.json +7 -3
  32. package/scripts/templates/html.json +1 -1
  33. package/scripts/templates/presentation.json +1 -1
  34. package/scripts/test/test_avatar.json +97 -23
  35. package/scripts/test/test_genai.json +2 -2
  36. package/scripts/test/test_gpt_image.json +1 -1
  37. package/scripts/test/test_replicate.json +3 -3
  38. package/scripts/test/test_vertexai.json +3 -3
@@ -8,7 +8,7 @@ export type RemotionRenderRequest = {
8
8
  workDir: string;
9
9
  props: RemotionSceneProps;
10
10
  videoPath?: string;
11
- stillPath: string;
11
+ stillPath?: string;
12
12
  extraStills?: {
13
13
  frame: number;
14
14
  path: string;
@@ -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
- export const renderRemotionScene = async ({ workDir, props, videoPath, stillPath, extraStills = [] }) => {
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 composition = await renderer.selectComposition({ serveUrl, id: REMOTION_COMPOSITION_ID, inputProps: props, chromiumOptions: CHROMIUM_OPTIONS });
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({ composition, serveUrl, codec: "h264", outputLocation: videoPath, inputProps: props, chromiumOptions: CHROMIUM_OPTIONS });
87
+ await renderer.renderMedia({ ...shared, ...downloadGuard, composition, codec: "h264", outputLocation: videoPath });
59
88
  }
60
- const stills = [...extraStills, { frame: composition.durationInFrames - 1, path: stillPath }];
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({ composition, serveUrl, output: still.path, frame: still.frame, inputProps: props, chromiumOptions: CHROMIUM_OPTIONS });
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
+ };
@@ -0,0 +1,23 @@
1
+ import { SceneRenderer } from "./render.js";
2
+ export type RemotionFramesRequest = {
3
+ code: string;
4
+ durationSec: number;
5
+ outDir: string;
6
+ fps?: number;
7
+ width?: number;
8
+ height?: number;
9
+ fractions?: readonly number[];
10
+ };
11
+ export type RemotionFrame = {
12
+ fraction: number;
13
+ frame: number;
14
+ path: string;
15
+ };
16
+ export type RemotionFramesDeps = {
17
+ ensurePackages: () => void;
18
+ renderScene: SceneRenderer;
19
+ makeWorkDir: () => string;
20
+ };
21
+ export declare const framePlan: (durationInFrames: number, fractions: readonly number[], outDir: string) => RemotionFrame[];
22
+ export declare const createRemotionFrameRenderer: (deps: RemotionFramesDeps) => (request: RemotionFramesRequest) => Promise<RemotionFrame[]>;
23
+ export declare const renderRemotionFrames: (request: RemotionFramesRequest) => Promise<RemotionFrame[]>;
@@ -0,0 +1,61 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { REMOTION_COMPONENT_FILE } from "./claude_prompt.js";
5
+ import { REMOTION_REVIEW_FRACTIONS, frameAtFraction, toFrameCount } from "./frames.js";
6
+ import { ensureRemotionPackages, renderRemotionScene } from "./render.js";
7
+ const DEFAULT_FPS = 30;
8
+ const DEFAULT_WIDTH = 1920;
9
+ const DEFAULT_HEIGHT = 1080;
10
+ const isValidFraction = (fraction) => Number.isFinite(fraction) && fraction >= 0 && fraction <= 1;
11
+ const checkFractions = (fractions) => {
12
+ if (fractions.length === 0)
13
+ throw new Error("remotion: fractions is empty");
14
+ const invalid = fractions.filter((fraction) => !isValidFraction(fraction));
15
+ if (invalid.length > 0)
16
+ throw new Error(`remotion: fractions must be between 0 and 1 (got ${invalid.join(", ")})`);
17
+ };
18
+ // Named by percent so a reader can tell which moment each file is; two fractions on one percent keep their order.
19
+ export const framePlan = (durationInFrames, fractions, outDir) => fractions.map((fraction, index) => {
20
+ const percent = String(Math.round(fraction * 100)).padStart(3, "0");
21
+ return { fraction, frame: frameAtFraction(durationInFrames, fraction), path: path.join(outDir, `frame_${index}_${percent}.png`) };
22
+ });
23
+ const isPositiveInteger = (value) => Number.isInteger(value) && value > 0;
24
+ const checkCanvas = (width, height) => {
25
+ if (!isPositiveInteger(width) || !isPositiveInteger(height))
26
+ throw new Error(`remotion: width and height must be positive integers (got ${width}x${height})`);
27
+ };
28
+ const errorMessage = (error) => (error instanceof Error ? error.message : String(error));
29
+ export const createRemotionFrameRenderer = (deps) => async (request) => {
30
+ const fps = request.fps ?? DEFAULT_FPS;
31
+ const props = {
32
+ durationInFrames: toFrameCount(request.durationSec, fps),
33
+ fps,
34
+ width: request.width ?? DEFAULT_WIDTH,
35
+ height: request.height ?? DEFAULT_HEIGHT,
36
+ };
37
+ const fractions = request.fractions ?? REMOTION_REVIEW_FRACTIONS;
38
+ checkFractions(fractions);
39
+ checkCanvas(props.width, props.height);
40
+ deps.ensurePackages();
41
+ const frames = framePlan(props.durationInFrames, fractions, request.outDir);
42
+ fs.mkdirSync(request.outDir, { recursive: true });
43
+ const workDir = deps.makeWorkDir();
44
+ try {
45
+ fs.writeFileSync(path.join(workDir, REMOTION_COMPONENT_FILE), request.code);
46
+ await deps.renderScene({ workDir, props, extraStills: frames.map(({ frame, path: file }) => ({ frame, path: file })) });
47
+ return frames;
48
+ }
49
+ catch (error) {
50
+ throw new Error(`remotion: failed to render the component's frames: ${errorMessage(error)}`, { cause: error });
51
+ }
52
+ finally {
53
+ fs.rmSync(workDir, { recursive: true, force: true });
54
+ }
55
+ };
56
+ // The bundle and entry files go to a temporary directory, so outDir ends up holding only the PNGs.
57
+ export const renderRemotionFrames = createRemotionFrameRenderer({
58
+ ensurePackages: ensureRemotionPackages,
59
+ renderScene: renderRemotionScene,
60
+ makeWorkDir: () => fs.mkdtempSync(path.join(os.tmpdir(), "mulmocast-remotion-frames-")),
61
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mulmocast",
3
- "version": "2.14.0",
3
+ "version": "2.16.0",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "main": "lib/index.node.js",
@@ -20,6 +20,10 @@
20
20
  "types": "./lib/index.remotion.d.ts",
21
21
  "default": "./lib/index.remotion.js"
22
22
  },
23
+ "./remotion/guide": {
24
+ "types": "./lib/index.remotion_guide.d.ts",
25
+ "default": "./lib/index.remotion_guide.js"
26
+ },
23
27
  "./assets/*": "./assets/*",
24
28
  "./scripts/*": "./scripts/*",
25
29
  "./tools/complete_script": {
@@ -145,7 +149,7 @@
145
149
  "@types/jsdom": "^28.0.3",
146
150
  "@types/node": "^26.6.4",
147
151
  "@types/yargs": "^17.0.35",
148
- "avatarscript": "^0.2.1",
152
+ "avatarscript": "^0.3.2",
149
153
  "cross-env": "^10.1.0",
150
154
  "esbuild": "^0.28.2",
151
155
  "eslint": "^10.12.0",
@@ -175,7 +179,7 @@
175
179
  "@remotion/shapes": "^4.0.533",
176
180
  "@remotion/three": "^4.0.533",
177
181
  "@remotion/transitions": "^4.0.533",
178
- "avatarscript": "^0.2.1",
182
+ "avatarscript": "^0.3.2",
179
183
  "onnxruntime-node": "^1.30.0",
180
184
  "react": "^19.2.0",
181
185
  "react-dom": "^19.2.0",
@@ -13,7 +13,7 @@
13
13
  "title": "[TITLE: Brief, engaging title for the topic]",
14
14
  "htmlImageParams": {
15
15
  "provider": "anthropic",
16
- "model": "claude-sonnet-4-5-20250929"
16
+ "model": "claude-sonnet-4-6"
17
17
  },
18
18
  "lang": "en",
19
19
  "beats": [
@@ -5,7 +5,7 @@
5
5
  },
6
6
  "htmlImageParams": {
7
7
  "provider": "anthropic",
8
- "model": "claude-sonnet-4-5-20250929"
8
+ "model": "claude-sonnet-4-6"
9
9
  },
10
10
  "title": "Sample Title",
11
11
  "references": [
@@ -1,46 +1,120 @@
1
1
  {
2
- "$mulmocast": { "version": "1.1" },
2
+ "$mulmocast": {
3
+ "version": "1.1"
4
+ },
3
5
  "lang": "ja",
4
6
  "title": "Talking avatar over slides",
5
- "description": "Needs mesh-avatar-studio checked out next to this repository (for its sample avatar), and npm packages avatarscript and onnxruntime-node.",
6
- "avatarParams": { "position": { "x": "82%", "y": "100%", "scale": "62%" } },
7
+ "description": "Loads the sample avatar ani from mulmocast-media by URL. Needs the npm packages avatarscript and onnxruntime-node.",
8
+ "avatarParams": {
9
+ "position": {
10
+ "x": "82%",
11
+ "y": "100%",
12
+ "scale": "62%"
13
+ }
14
+ },
7
15
  "speechParams": {
8
16
  "speakers": {
9
- "Miko": {
10
- "provider": "openai",
11
- "voiceId": "coral",
12
- "displayName": { "ja": "ミコ" },
13
- "avatar": { "source": "../../../mesh-avatar-studio/samples/miko-qipao" }
17
+ "Ani": {
18
+ "provider": "gemini",
19
+ "voiceId": "Leda",
20
+ "speechOptions": {
21
+ "instruction": "Speak in a slightly high-pitched, curt tone with sudden flustered shifts—like a tsundere anime girl."
22
+ },
23
+ "displayName": {
24
+ "ja": "アニ"
25
+ },
26
+ "avatar": {
27
+ "source": "https://raw.githubusercontent.com/receptron/mulmocast-media/main/avatars/ani"
28
+ }
14
29
  }
15
30
  }
16
31
  },
17
32
  "beats": [
18
33
  {
19
- "speaker": "Miko",
20
- "text": "みなさん、こんにちは!今日は、スライドの上でしゃべるアバターを紹介します。",
21
- "image": { "type": "markdown", "markdown": ["# Talking avatars", "", "- MulmoScript + AvatarScript", "- スライドの上でアバターが話す"] },
22
- "avatarParams": { "emotion": "happy", "motions": [{ "motion": "nod", "at": "こんにちは" }] }
34
+ "speaker": "Ani",
35
+ "text": "べ、別にあなたのために説明するわけじゃないんだからね!今日は、スライドの上でしゃべるアバターを紹介してあげる。",
36
+ "image": {
37
+ "type": "markdown",
38
+ "markdown": ["# Talking avatars", "", "- MulmoScript + AvatarScript", "- スライドの上でアバターが話す"]
39
+ },
40
+ "avatarParams": {
41
+ "emotion": "happy",
42
+ "motions": [
43
+ {
44
+ "motion": "nod",
45
+ "at": "紹介"
46
+ }
47
+ ]
48
+ }
49
+ },
50
+ {
51
+ "speaker": "Ani",
52
+ "text": "このアバターはね、メッシュ・アバター・スタジオで、たった一枚のイラストから作ったの。それを、マルモキャストの上で動かしてるのよ。",
53
+ "image": {
54
+ "type": "markdown",
55
+ "markdown": ["# How it's made", "", "- mesh-avatar-studio: 1枚のイラスト → メッシュアバター", "- MulmoCast + AvatarScript: 台本 → 音声 → 口パク動画"]
56
+ },
57
+ "avatarParams": {
58
+ "emotion": "relaxed",
59
+ "motions": [
60
+ {
61
+ "motion": "nod",
62
+ "at": "マルモキャスト"
63
+ }
64
+ ]
65
+ }
23
66
  },
24
67
  {
25
- "speaker": "Miko",
26
- "text": "位置と大きさは、台本全体で決めておいて、ビートごとに変えられます。このスライドでは、大きくなっています。",
27
- "image": { "type": "markdown", "markdown": ["# Position and size", "", "- 台本全体: `avatarParams.position`", "- ビートごとに上書き"] },
28
- "avatarParams": { "emotion": "relaxed", "position": { "x": "76%", "scale": "82%" }, "motions": [{ "motion": "tilt", "at": "大きく" }] }
68
+ "speaker": "Ani",
69
+ "text": "位置と大きさは、台本全体で決めておいて、ビートごとに変えられるの。ほら、大きくなったでしょ。ちゃんと見なさいよね!",
70
+ "image": {
71
+ "type": "markdown",
72
+ "markdown": ["# Position and size", "", "- 台本全体: `avatarParams.position`", "- ビートごとに上書き"]
73
+ },
74
+ "avatarParams": {
75
+ "emotion": "relaxed",
76
+ "position": {
77
+ "x": "76%",
78
+ "scale": "82%"
79
+ },
80
+ "motions": [
81
+ {
82
+ "motion": "tilt",
83
+ "at": "大きく"
84
+ }
85
+ ]
86
+ }
29
87
  },
30
88
  {
31
- "speaker": "Miko",
32
- "text": "こちらでは、左側に小さく移動しました。",
89
+ "speaker": "Ani",
90
+ "text": "ちょ、ちょっと!今度は左に小さくされちゃったんだけど!",
33
91
  "image": {
34
92
  "type": "markdown",
35
93
  "markdown": ["# Moved", "", "```json", "\"avatarParams\": {", " \"position\": { \"x\": \"18%\", \"scale\": \"48%\" }", "}", "```"]
36
94
  },
37
- "avatarParams": { "emotion": "surprised", "position": { "x": "18%", "scale": "48%" }, "motions": [{ "motion": "surprise" }] }
95
+ "avatarParams": {
96
+ "emotion": "surprised",
97
+ "position": {
98
+ "x": "18%",
99
+ "scale": "48%"
100
+ },
101
+ "motions": [
102
+ {
103
+ "motion": "surprise"
104
+ }
105
+ ]
106
+ }
38
107
  },
39
108
  {
40
- "speaker": "Miko",
41
- "text": "最後は、元の位置に戻ります。それでは、始めましょう!",
42
- "image": { "type": "markdown", "markdown": ["# Let's start"] },
43
- "avatarParams": { "emotion": "happy" }
109
+ "speaker": "Ani",
110
+ "text": "最後は元の位置に戻るわ。べ、別に、こんなことが簡単にできるって、自慢してるわけじゃないんだからね!",
111
+ "image": {
112
+ "type": "markdown",
113
+ "markdown": ["# 簡単…でしょ?"]
114
+ },
115
+ "avatarParams": {
116
+ "emotion": "angry"
117
+ }
44
118
  }
45
119
  ]
46
120
  }
@@ -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.0-generate-001"
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.0-generate-001"
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-1",
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-v1.6-pro",
100
- "text": "kwaivgi/kling-v1.6-pro with gpt4-image",
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-v1.6-pro"
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-2.0-generate-001",
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.0-generate-001",
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.0-generate-001",
104
+ "model": "veo-3.1-generate-001",
105
105
  "vertexai_project": "YOUR_PROJECT_ID",
106
106
  "vertexai_location": "us-central1"
107
107
  },