vargai 0.4.0-alpha2 → 0.4.0-alpha21

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 (69) hide show
  1. package/README.md +483 -61
  2. package/launch-videos/06-kawaii-fruits.tsx +93 -0
  3. package/launch-videos/07-ugc-weight-loss.tsx +132 -0
  4. package/launch-videos/08-talking-head-varg.tsx +107 -0
  5. package/launch-videos/09-girl.tsx +160 -0
  6. package/launch-videos/README.md +42 -0
  7. package/package.json +8 -4
  8. package/skills/varg-video-generation/SKILL.md +213 -0
  9. package/skills/varg-video-generation/references/templates.md +380 -0
  10. package/skills/varg-video-generation/scripts/setup.ts +265 -0
  11. package/src/ai-sdk/cache.ts +1 -1
  12. package/src/ai-sdk/middleware/wrap-image-model.ts +4 -21
  13. package/src/ai-sdk/middleware/wrap-music-model.ts +4 -16
  14. package/src/ai-sdk/middleware/wrap-video-model.ts +5 -17
  15. package/src/ai-sdk/providers/editly/index.ts +110 -53
  16. package/src/ai-sdk/providers/editly/types.ts +2 -0
  17. package/src/ai-sdk/providers/elevenlabs.ts +10 -2
  18. package/src/ai-sdk/providers/fal.ts +6 -1
  19. package/src/cli/commands/find.tsx +1 -0
  20. package/src/cli/commands/hello.ts +85 -0
  21. package/src/cli/commands/help.tsx +18 -30
  22. package/src/cli/commands/index.ts +9 -1
  23. package/src/cli/commands/init.tsx +412 -0
  24. package/src/cli/commands/list.tsx +1 -0
  25. package/src/cli/commands/render.tsx +292 -80
  26. package/src/cli/commands/run.tsx +1 -0
  27. package/src/cli/commands/studio.ts +47 -0
  28. package/src/cli/commands/which.tsx +1 -0
  29. package/src/cli/index.ts +20 -5
  30. package/src/cli/ui/components/Badge.tsx +1 -0
  31. package/src/cli/ui/components/DataTable.tsx +1 -0
  32. package/src/cli/ui/components/Header.tsx +1 -0
  33. package/src/cli/ui/components/HelpBlock.tsx +1 -0
  34. package/src/cli/ui/components/KeyValue.tsx +1 -0
  35. package/src/cli/ui/components/OptionRow.tsx +1 -0
  36. package/src/cli/ui/components/Separator.tsx +1 -0
  37. package/src/cli/ui/components/StatusBox.tsx +1 -0
  38. package/src/cli/ui/components/VargBox.tsx +1 -0
  39. package/src/cli/ui/components/VargProgress.tsx +1 -0
  40. package/src/cli/ui/components/VargSpinner.tsx +1 -0
  41. package/src/cli/ui/components/VargText.tsx +1 -0
  42. package/src/react/assets.ts +9 -0
  43. package/src/react/elements.ts +0 -5
  44. package/src/react/examples/branching.tsx +6 -4
  45. package/src/react/examples/character-video.tsx +13 -10
  46. package/src/react/examples/madi.tsx +13 -10
  47. package/src/react/examples/mcmeows.tsx +40 -0
  48. package/src/react/examples/music-defaults.tsx +24 -0
  49. package/src/react/examples/quickstart-test.tsx +97 -0
  50. package/src/react/index.ts +1 -2
  51. package/src/react/react.test.ts +10 -10
  52. package/src/react/renderers/clip.ts +13 -24
  53. package/src/react/renderers/context.ts +3 -0
  54. package/src/react/renderers/image.ts +4 -2
  55. package/src/react/renderers/index.ts +0 -1
  56. package/src/react/renderers/music.ts +3 -3
  57. package/src/react/renderers/progress.ts +1 -3
  58. package/src/react/renderers/render.ts +49 -63
  59. package/src/react/renderers/speech.ts +2 -2
  60. package/src/react/renderers/video.ts +46 -9
  61. package/src/react/types.ts +18 -14
  62. package/src/studio/stages.ts +4 -24
  63. package/src/studio/step-renderer.ts +0 -15
  64. package/test-sync-v2.ts +30 -0
  65. package/test-sync-v2.tsx +29 -0
  66. package/tsconfig.json +5 -3
  67. package/video.tsx +7 -0
  68. package/src/react/cli.ts +0 -52
  69. package/src/react/renderers/animate.ts +0 -59
@@ -1,6 +1,6 @@
1
1
  import { elevenlabs } from "../../ai-sdk/providers/elevenlabs";
2
2
  import { fal } from "../../ai-sdk/providers/fal";
3
- import { Animate, Clip, Image, Music, Render } from "..";
3
+ import { Clip, Image, Music, Render, Video } from "..";
4
4
 
5
5
  const MADI_REF =
6
6
  "https://s3.varg.ai/fellowers/madi/character_shots/madi_shot_03_closeup.png";
@@ -43,16 +43,19 @@ export default (
43
43
 
44
44
  {SCENES.map((scene) => (
45
45
  <Clip key={scene.prompt} duration={2}>
46
- <Animate
47
- image={Image({
48
- prompt: { text: scene.prompt, images: [MADI_REF] },
49
- model: fal.imageModel("nano-banana-pro/edit"),
50
- aspectRatio: "9:16",
51
- resize: "cover",
52
- })}
53
- motion={scene.motion}
46
+ <Video
47
+ prompt={{
48
+ text: scene.motion,
49
+ images: [
50
+ Image({
51
+ prompt: { text: scene.prompt, images: [MADI_REF] },
52
+ model: fal.imageModel("nano-banana-pro/edit"),
53
+ aspectRatio: "9:16",
54
+ resize: "cover",
55
+ }),
56
+ ],
57
+ }}
54
58
  model={fal.videoModel("wan-2.5")}
55
- duration={5}
56
59
  />
57
60
  </Clip>
58
61
  ))}
@@ -0,0 +1,40 @@
1
+ import { elevenlabs } from "../../ai-sdk/providers/elevenlabs";
2
+ import { fal } from "../../ai-sdk/providers/fal";
3
+ import { Clip, Music, Render, render, Title, Video } from "..";
4
+
5
+ export default (
6
+ <Render>
7
+ <Clip duration={4}>
8
+ <Video prompt="A sophisticated tabby cat wearing a tailored tiny business suit and small round glasses, standing upright at a McDonald's counter. One paw raised pointing assertively at the illuminated menu board above. The cat has an extremely serious, concentrated expression with furrowed brows. Cinematic lighting with warm McDonald's interior ambiance, shallow depth of field focusing on the cat's determined face, photorealistic fur texture with fine detail. Professional business atmosphere meets fast food chaos." />
9
+ </Clip>
10
+
11
+ <Clip duration={4}>
12
+ <Video prompt="Absolute mayhem - four to five cats in a chaotic pile fight. Orange tabby, black and white tuxedo cat, calico with patches, and fluffy gray cat all scrambling, paws flailing, tumbling over each other in exaggerated cartoon-style motion. They're all desperately reaching for a single perfect golden McDonald's french fry sitting on a red plastic tray in the center. Wide-eyed expressions, mouths open mid-meow, dynamic motion blur, comedic timing. Fast food restaurant background slightly blurred. Playful, over-the-top energy." />
13
+ </Clip>
14
+
15
+ <Clip duration={3}>
16
+ <Video prompt="Proud orange tabby cat wearing an official McDonald's crew member visor and name tag, standing upright behind a restaurant register counter. Front paws crossed confidently across chest, chin lifted with a smug, self-satisfied expression. Perfect posture, professional demeanor. Bright McDonald's interior lighting, red and yellow color scheme in background. The cat radiates 'employee of the month' energy. Crisp, clean, professional fast food aesthetic." />
17
+ <Title position="bottom">McMeow's: NOW HIRING</Title>
18
+ </Clip>
19
+
20
+ <Music
21
+ model={elevenlabs.musicModel()}
22
+ prompt="playful upbeat comedy music with quirky pizzicato strings and light percussion, funny corporate training video vibes"
23
+ duration={11}
24
+ />
25
+ </Render>
26
+ );
27
+
28
+ async function main() {
29
+ const component = await import("./mcmeows.tsx").then((m) => m.default);
30
+ await render(component, {
31
+ output: "output/mcmeows.mp4",
32
+ cache: ".cache/ai",
33
+ verbose: true,
34
+ defaults: {
35
+ video: fal.videoModel("wan-2.5"),
36
+ },
37
+ });
38
+ }
39
+
40
+ main().catch(console.error);
@@ -0,0 +1,24 @@
1
+ import { elevenlabs } from "../../ai-sdk/providers/elevenlabs";
2
+ import { Clip, Image, Music, Render, render } from "..";
3
+
4
+ export default (
5
+ <Render width={1920} height={1080}>
6
+ <Clip duration={5}>
7
+ <Image src="media/cyberpunk-street.png" />
8
+ </Clip>
9
+ <Music prompt="calm ambient electronic music" duration={5} />
10
+ </Render>
11
+ );
12
+
13
+ async function main() {
14
+ const component = await import("./music-defaults.tsx").then((m) => m.default);
15
+ await render(component, {
16
+ output: "output/music-defaults.mp4",
17
+ defaults: {
18
+ music: elevenlabs.musicModel(),
19
+ },
20
+ });
21
+ console.log("done! check output/music-defaults.mp4");
22
+ }
23
+
24
+ main().catch(console.error);
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Quickstart Test - Verification script for video generation setup
3
+ *
4
+ * This minimal test confirms your FAL_API_KEY is working correctly.
5
+ * It generates a simple 3-second animated image.
6
+ *
7
+ * Run: bun run src/react/examples/quickstart-test.tsx
8
+ * Output: output/quickstart-test.mp4
9
+ */
10
+
11
+ import { fal } from "../../ai-sdk/providers/fal";
12
+ import { Clip, Image, Render, render, Video } from "..";
13
+
14
+ async function main() {
15
+ console.log("=== Varg Video Generation - Setup Verification ===\n");
16
+
17
+ // Check for FAL_API_KEY
18
+ if (!process.env.FAL_API_KEY) {
19
+ console.error("ERROR: FAL_API_KEY not found in environment");
20
+ console.error("\nTo fix this:");
21
+ console.error("1. Get an API key at: https://fal.ai/dashboard/keys");
22
+ console.error("2. Add to .env file: FAL_API_KEY=fal_xxxxx");
23
+ console.error("3. Run this test again");
24
+ process.exit(1);
25
+ }
26
+
27
+ console.log("FAL_API_KEY found");
28
+
29
+ // Check for optional keys
30
+ const hasElevenLabs = !!process.env.ELEVENLABS_API_KEY;
31
+ const hasReplicate = !!process.env.REPLICATE_API_TOKEN;
32
+ const hasGroq = !!process.env.GROQ_API_KEY;
33
+
34
+ console.log("\nOptional keys:");
35
+ console.log(
36
+ ` ELEVENLABS_API_KEY: ${hasElevenLabs ? "found" : "not found (music/voice unavailable)"}`,
37
+ );
38
+ console.log(
39
+ ` REPLICATE_API_TOKEN: ${hasReplicate ? "found" : "not found (lipsync unavailable)"}`,
40
+ );
41
+ console.log(
42
+ ` GROQ_API_KEY: ${hasGroq ? "found" : "not found (transcription unavailable)"}`,
43
+ );
44
+
45
+ console.log("\n--- Running verification test ---\n");
46
+ console.log("Generating a simple 3-second animation...");
47
+ console.log("This may take 30-60 seconds on first run.\n");
48
+
49
+ const video = (
50
+ <Render width={720} height={720}>
51
+ <Clip duration={3}>
52
+ <Video
53
+ prompt={{
54
+ text: "robot waves hello, friendly gesture, slight head tilt",
55
+ images: [
56
+ Image({
57
+ prompt:
58
+ "a friendly robot waving hello, simple cartoon style, blue and white colors, clean background",
59
+ model: fal.imageModel("flux-schnell"),
60
+ aspectRatio: "1:1",
61
+ }),
62
+ ],
63
+ }}
64
+ model={fal.videoModel("wan-2.5")}
65
+ />
66
+ </Clip>
67
+ </Render>
68
+ );
69
+
70
+ try {
71
+ const buffer = await render(video, {
72
+ output: "output/quickstart-test.mp4",
73
+ cache: ".cache/ai",
74
+ });
75
+
76
+ console.log("\n=== SUCCESS ===");
77
+ console.log(
78
+ `Output: output/quickstart-test.mp4 (${(buffer.byteLength / 1024 / 1024).toFixed(2)} MB)`,
79
+ );
80
+ console.log("\nYour setup is working! You can now:");
81
+ console.log("1. Try the templates in .claude/skills/video-generation.md");
82
+ console.log(
83
+ "2. Run existing examples: bun run src/react/examples/madi.tsx",
84
+ );
85
+ console.log("3. Create your own videos using the React engine");
86
+ } catch (error) {
87
+ console.error("\n=== VERIFICATION FAILED ===");
88
+ console.error("Error:", error instanceof Error ? error.message : error);
89
+ console.error("\nCommon fixes:");
90
+ console.error("- Check FAL_API_KEY is correct (no extra spaces)");
91
+ console.error("- Ensure you have credits at fal.ai");
92
+ console.error("- Try running: bun install");
93
+ process.exit(1);
94
+ }
95
+ }
96
+
97
+ main();
@@ -1,6 +1,6 @@
1
1
  export type { SizeValue } from "../ai-sdk/providers/editly/types";
2
+ export { assets } from "./assets";
2
3
  export {
3
- Animate,
4
4
  Captions,
5
5
  Clip,
6
6
  Image,
@@ -20,7 +20,6 @@ export {
20
20
  export { Grid, SplitLayout } from "./layouts";
21
21
  export { render, renderStream } from "./render";
22
22
  export type {
23
- AnimateProps,
24
23
  CaptionsProps,
25
24
  ClipProps,
26
25
  ImageProps,
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test";
2
2
  import { existsSync, unlinkSync } from "node:fs";
3
3
  import { fal } from "../ai-sdk/providers/fal";
4
4
  import {
5
- Animate,
6
5
  Captions,
7
6
  Clip,
8
7
  Image,
@@ -80,19 +79,20 @@ describe("varg-react elements", () => {
80
79
  expect(element.children).toContain("I'M IN DANGER");
81
80
  });
82
81
 
83
- test("Animate creates correct element with nested image", () => {
82
+ test("Video creates correct element with nested image", () => {
84
83
  const image = Image({ prompt: "luigi in wheelchair" });
85
- const element = Animate({
86
- image,
84
+ const element = Video({
85
+ prompt: { text: "wheels spinning fast", images: [image] },
87
86
  model: fal.videoModel("wan-2.5"),
88
- motion: "wheels spinning fast",
89
- duration: 5,
90
87
  });
91
88
 
92
- expect(element.type).toBe("animate");
93
- expect(element.props.image).toBe(image);
94
- expect(element.props.motion).toBe("wheels spinning fast");
95
- expect(element.props.duration).toBe(5);
89
+ expect(element.type).toBe("video");
90
+ expect((element.props.prompt as { images: unknown[] }).images[0]).toBe(
91
+ image,
92
+ );
93
+ expect((element.props.prompt as { text: string }).text).toBe(
94
+ "wheels spinning fast",
95
+ );
96
96
  });
97
97
 
98
98
  test("nested composition builds correct tree", () => {
@@ -8,7 +8,6 @@ import type {
8
8
  VideoLayer,
9
9
  } from "../../ai-sdk/providers/editly/types";
10
10
  import type {
11
- AnimateProps,
12
11
  ClipProps,
13
12
  ImageProps,
14
13
  SpeechProps,
@@ -16,7 +15,6 @@ import type {
16
15
  VargNode,
17
16
  VideoProps,
18
17
  } from "../types";
19
- import { renderAnimate } from "./animate";
20
18
  import type { RenderContext } from "./context";
21
19
  import { renderImage } from "./image";
22
20
  import { renderPackshot } from "./packshot";
@@ -32,9 +30,15 @@ type PendingLayer =
32
30
  | { type: "sync"; layer: Layer }
33
31
  | { type: "async"; promise: Promise<Layer> };
34
32
 
33
+ interface ClipLayerOptions {
34
+ cutFrom?: number;
35
+ cutTo?: number;
36
+ }
37
+
35
38
  async function renderClipLayers(
36
39
  children: VargNode[],
37
40
  ctx: RenderContext,
41
+ clipOptions?: ClipLayerOptions,
38
42
  ): Promise<Layer[]> {
39
43
  const pending: PendingLayer[] = [];
40
44
 
@@ -86,8 +90,9 @@ async function renderClipLayers(
86
90
  type: "video",
87
91
  path,
88
92
  resizeMode: props.resize,
89
- cutFrom: props.cutFrom,
90
- cutTo: props.cutTo,
93
+ // Video-level cutFrom/cutTo take precedence over clip-level
94
+ cutFrom: props.cutFrom ?? clipOptions?.cutFrom,
95
+ cutTo: props.cutTo ?? clipOptions?.cutTo,
91
96
  mixVolume: props.keepAudio ? (props.volume ?? 1) : 0,
92
97
  left: props.left,
93
98
  top: props.top,
@@ -99,25 +104,6 @@ async function renderClipLayers(
99
104
  break;
100
105
  }
101
106
 
102
- case "animate": {
103
- const props = element.props as AnimateProps;
104
- pending.push({
105
- type: "async",
106
- promise: renderAnimate(element as VargElement<"animate">, ctx).then(
107
- (path) =>
108
- ({
109
- type: "video",
110
- path,
111
- left: props.left,
112
- top: props.top,
113
- width: props.width,
114
- height: props.height,
115
- }) as VideoLayer,
116
- ),
117
- });
118
- break;
119
- }
120
-
121
107
  case "title": {
122
108
  pending.push({
123
109
  type: "sync",
@@ -220,7 +206,10 @@ export async function renderClip(
220
206
  ctx: RenderContext,
221
207
  ): Promise<Clip> {
222
208
  const props = element.props as ClipProps;
223
- const layers = await renderClipLayers(element.children, ctx);
209
+ const layers = await renderClipLayers(element.children, ctx, {
210
+ cutFrom: props.cutFrom,
211
+ cutTo: props.cutTo,
212
+ });
224
213
 
225
214
  const isOverlayVideo = (l: Layer) =>
226
215
  l.type === "video" &&
@@ -1,6 +1,7 @@
1
1
  import type { generateImage } from "ai";
2
2
  import type { fileCache } from "../../ai-sdk/file-cache";
3
3
  import type { generateVideo } from "../../ai-sdk/generate-video";
4
+ import type { DefaultModels } from "../types";
4
5
  import type { ProgressTracker } from "./progress";
5
6
 
6
7
  export interface RenderContext {
@@ -14,4 +15,6 @@ export interface RenderContext {
14
15
  progress?: ProgressTracker;
15
16
  /** In-memory deduplication for concurrent renders of the same element */
16
17
  pending: Map<string, Promise<string>>;
18
+ /** Default models for elements that don't specify one */
19
+ defaults?: DefaultModels;
17
20
  }
@@ -54,9 +54,11 @@ export async function renderImage(
54
54
  throw new Error("Image element requires either 'prompt' or 'src'");
55
55
  }
56
56
 
57
- const model = props.model;
57
+ const model = props.model ?? ctx.defaults?.image;
58
58
  if (!model) {
59
- throw new Error("Image element requires 'model' prop when using prompt");
59
+ throw new Error(
60
+ "Image element requires 'model' prop (or set defaults.image in render options)",
61
+ );
60
62
  }
61
63
 
62
64
  // Compute cache key for deduplication
@@ -1,4 +1,3 @@
1
- export { renderAnimate } from "./animate";
2
1
  export { renderCaptions } from "./captions";
3
2
  export { renderClip } from "./clip";
4
3
  export type { RenderContext } from "./context";
@@ -10,9 +10,9 @@ export async function renderMusic(
10
10
  const props = element.props as MusicProps;
11
11
 
12
12
  const prompt = props.prompt;
13
- const model = props.model;
13
+ const model = props.model ?? ctx.defaults?.music;
14
14
  if (!prompt || !model) {
15
- throw new Error("Music generation requires both prompt and model");
15
+ throw new Error("Music requires prompt and model (or set defaults.music)");
16
16
  }
17
17
 
18
18
  const cacheKey = JSON.stringify({
@@ -22,7 +22,7 @@ export async function renderMusic(
22
22
  duration: props.duration,
23
23
  });
24
24
 
25
- const modelId = model.modelId;
25
+ const modelId = model.modelId ?? "music";
26
26
  const taskId = ctx.progress ? addTask(ctx.progress, "music", modelId) : null;
27
27
 
28
28
  const generateFn = async () => {
@@ -1,7 +1,6 @@
1
1
  export type GenerationType =
2
2
  | "image"
3
3
  | "video"
4
- | "animate"
5
4
  | "speech"
6
5
  | "music"
7
6
  | "editly"
@@ -11,7 +10,6 @@ export type GenerationType =
11
10
  export const TIME_ESTIMATES: Record<GenerationType, number> = {
12
11
  image: 30,
13
12
  video: 120,
14
- animate: 90,
15
13
  speech: 5,
16
14
  music: 45,
17
15
  editly: 15,
@@ -103,7 +101,7 @@ export function completeTask(tracker: ProgressTracker, id: string): void {
103
101
  }
104
102
 
105
103
  function getEstimate(task: ProgressTask): number {
106
- const modelLower = task.model.toLowerCase();
104
+ const modelLower = task.model?.toLowerCase() ?? "";
107
105
  for (const [key, estimate] of Object.entries(MODEL_TIME_ESTIMATES)) {
108
106
  if (modelLower.includes(key.toLowerCase())) {
109
107
  return estimate;
@@ -25,7 +25,6 @@ import type {
25
25
  SpeechProps,
26
26
  VargElement,
27
27
  } from "../types";
28
- import { renderAnimate } from "./animate";
29
28
  import { renderCaptions } from "./captions";
30
29
  import { renderClip } from "./clip";
31
30
  import type { RenderContext } from "./context";
@@ -54,58 +53,59 @@ export async function renderRoot(
54
53
  const props = element.props as RenderProps;
55
54
  const progress = createProgressTracker(options.quiet ?? false);
56
55
 
57
- const mode: RenderMode = options.mode ?? "default";
56
+ const mode: RenderMode = options.mode ?? "strict";
58
57
  const placeholderCount = { images: 0, videos: 0, total: 0 };
59
58
 
60
- const onFallback = (error: Error, prompt: string) => {
61
- if (!options.quiet) {
62
- console.warn(
63
- `\x1b[33m⚠ provider failed: ${error.message} → placeholder\x1b[0m`,
64
- );
65
- }
66
- };
67
-
68
59
  const trackPlaceholder = (type: "image" | "video") => {
69
60
  placeholderCount[type === "image" ? "images" : "videos"]++;
70
61
  placeholderCount.total++;
71
62
  };
72
63
 
64
+ const cachedGenerateImage = options.cache
65
+ ? withCache(generateImage, { storage: fileCache({ dir: options.cache }) })
66
+ : generateImage;
67
+
68
+ const cachedGenerateVideo = options.cache
69
+ ? withCache(generateVideo, { storage: fileCache({ dir: options.cache }) })
70
+ : generateVideo;
71
+
73
72
  const wrapGenerateImage: typeof generateImage = async (opts) => {
74
73
  if (
75
74
  typeof opts.model === "string" ||
76
75
  opts.model.specificationVersion !== "v3"
77
76
  ) {
78
- return generateImage(opts);
77
+ return cachedGenerateImage(opts);
79
78
  }
80
- const wrappedModel = wrapImageModel({
81
- model: opts.model,
82
- middleware: imagePlaceholderFallbackMiddleware({
83
- mode,
84
- onFallback: (error, prompt) => {
85
- trackPlaceholder("image");
86
- onFallback(error, prompt);
87
- },
88
- }),
89
- });
90
- const result = await generateImage({ ...opts, model: wrappedModel });
91
- if (mode === "preview") trackPlaceholder("image");
92
- return result;
79
+
80
+ if (mode === "preview") {
81
+ trackPlaceholder("image");
82
+ const wrappedModel = wrapImageModel({
83
+ model: opts.model,
84
+ middleware: imagePlaceholderFallbackMiddleware({
85
+ mode: "preview",
86
+ onFallback: () => {},
87
+ }),
88
+ });
89
+ return generateImage({ ...opts, model: wrappedModel });
90
+ }
91
+
92
+ return cachedGenerateImage(opts);
93
93
  };
94
94
 
95
95
  const wrapGenerateVideo: typeof generateVideo = async (opts) => {
96
- const wrappedModel = wrapVideoModel({
97
- model: opts.model,
98
- middleware: placeholderFallbackMiddleware({
99
- mode,
100
- onFallback: (error, prompt) => {
101
- trackPlaceholder("video");
102
- onFallback(error, prompt);
103
- },
104
- }),
105
- });
106
- const result = await generateVideo({ ...opts, model: wrappedModel });
107
- if (mode === "preview") trackPlaceholder("video");
108
- return result;
96
+ if (mode === "preview") {
97
+ trackPlaceholder("video");
98
+ const wrappedModel = wrapVideoModel({
99
+ model: opts.model,
100
+ middleware: placeholderFallbackMiddleware({
101
+ mode: "preview",
102
+ onFallback: () => {},
103
+ }),
104
+ });
105
+ return generateVideo({ ...opts, model: wrappedModel });
106
+ }
107
+
108
+ return cachedGenerateVideo(opts);
109
109
  };
110
110
 
111
111
  const ctx: RenderContext = {
@@ -113,19 +113,12 @@ export async function renderRoot(
113
113
  height: props.height ?? 1080,
114
114
  fps: props.fps ?? 30,
115
115
  cache: options.cache ? fileCache({ dir: options.cache }) : undefined,
116
- generateImage: options.cache
117
- ? withCache(wrapGenerateImage, {
118
- storage: fileCache({ dir: options.cache }),
119
- })
120
- : wrapGenerateImage,
121
- generateVideo: options.cache
122
- ? withCache(wrapGenerateVideo, {
123
- storage: fileCache({ dir: options.cache }),
124
- })
125
- : wrapGenerateVideo,
116
+ generateImage: wrapGenerateImage,
117
+ generateVideo: wrapGenerateVideo,
126
118
  tempFiles: [],
127
119
  progress,
128
120
  pending: new Map(),
121
+ defaults: options.defaults,
129
122
  };
130
123
 
131
124
  const clipElements: VargElement<"clip">[] = [];
@@ -177,13 +170,10 @@ export async function renderRoot(
177
170
  const childElement = child as VargElement;
178
171
 
179
172
  let path: string | undefined;
180
- const isVideo =
181
- childElement.type === "video" || childElement.type === "animate";
173
+ const isVideo = childElement.type === "video";
182
174
 
183
175
  if (childElement.type === "video") {
184
176
  path = await renderVideo(childElement as VargElement<"video">, ctx);
185
- } else if (childElement.type === "animate") {
186
- path = await renderAnimate(childElement as VargElement<"animate">, ctx);
187
177
  } else if (childElement.type === "image") {
188
178
  path = await renderImage(childElement as VargElement<"image">, ctx);
189
179
  }
@@ -255,11 +245,11 @@ export async function renderRoot(
255
245
  let path: string;
256
246
  if (musicProps.src) {
257
247
  path = resolvePath(musicProps.src);
258
- } else if (musicProps.prompt && musicProps.model) {
248
+ } else if (musicProps.prompt) {
259
249
  const result = await renderMusic(musicElement, ctx);
260
250
  path = result.path;
261
251
  } else {
262
- throw new Error("Music requires either src or prompt+model");
252
+ throw new Error("Music requires either src or prompt");
263
253
  }
264
254
 
265
255
  audioTracks.push({
@@ -287,6 +277,8 @@ export async function renderRoot(
287
277
  fps: ctx.fps,
288
278
  clips,
289
279
  audioTracks: audioTracks.length > 0 ? audioTracks : undefined,
280
+ shortest: props.shortest,
281
+ verbose: options.verbose,
290
282
  });
291
283
 
292
284
  completeTask(progress, editlyTaskId);
@@ -302,16 +294,10 @@ export async function renderRoot(
302
294
  completeTask(progress, captionsTaskId);
303
295
  }
304
296
 
305
- if (!options.quiet && placeholderCount.total > 0) {
306
- if (mode === "preview") {
307
- console.log(
308
- `\x1b[36mℹ preview mode: ${placeholderCount.total} placeholders used (${placeholderCount.images} images, ${placeholderCount.videos} videos)\x1b[0m`,
309
- );
310
- } else {
311
- console.warn(
312
- `\x1b[33m⚠ ${placeholderCount.total} elements used placeholders - run with --strict for production\x1b[0m`,
313
- );
314
- }
297
+ if (!options.quiet && mode === "preview" && placeholderCount.total > 0) {
298
+ console.log(
299
+ `\x1b[36mℹ preview mode: ${placeholderCount.total} placeholders used (${placeholderCount.images} images, ${placeholderCount.videos} videos)\x1b[0m`,
300
+ );
315
301
  }
316
302
 
317
303
  const result = await Bun.file(finalOutPath).arrayBuffer();
@@ -21,9 +21,9 @@ export async function renderSpeech(
21
21
  throw new Error("Speech element requires text content");
22
22
  }
23
23
 
24
- const model = props.model;
24
+ const model = props.model ?? ctx.defaults?.speech;
25
25
  if (!model) {
26
- throw new Error("Speech element requires 'model' prop");
26
+ throw new Error("Speech requires 'model' prop (or set defaults.speech)");
27
27
  }
28
28
 
29
29
  const cacheKey = computeCacheKey(element);