vargai 0.4.0-alpha18 → 0.4.0-alpha19

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 CHANGED
@@ -1,115 +1,537 @@
1
1
  # varg
2
2
 
3
- AI video generation from your terminal.
3
+ ai video generation sdk. jsx for videos, built on vercel ai sdk.
4
4
 
5
- ## Quick Start
5
+ ## quickstart
6
6
 
7
7
  ```bash
8
8
  bun install vargai ai
9
9
  ```
10
10
 
11
- ### SDK Usage
11
+ set your api key:
12
+
13
+ ```bash
14
+ export FAL_API_KEY=fal_xxx # required
15
+ export ELEVENLABS_API_KEY=xxx # optional, for voice/music
16
+ ```
17
+
18
+ create `hello.tsx`:
19
+
20
+ ```tsx
21
+ import { render, Render, Clip, Image, Video } from "vargai/react";
22
+ import { fal } from "vargai/ai";
23
+
24
+ const fruit = Image({
25
+ prompt: "cute kawaii fluffy orange fruit character, round plush body, small black dot eyes, tiny smile, Pixar style",
26
+ model: fal.imageModel("nano-banana-pro"),
27
+ aspectRatio: "9:16",
28
+ });
29
+
30
+ await render(
31
+ <Render width={1080} height={1920}>
32
+ <Clip duration={3}>
33
+ <Video
34
+ prompt={{
35
+ text: "character waves hello enthusiastically, bounces up and down, eyes squint with joy",
36
+ images: [fruit],
37
+ }}
38
+ model={fal.videoModel("kling-v2.5")}
39
+ />
40
+ </Clip>
41
+ </Render>,
42
+ { output: "output/hello.mp4" }
43
+ );
44
+ ```
45
+
46
+ run it:
47
+
48
+ ```bash
49
+ bun run hello.tsx
50
+ ```
51
+
52
+ ## installation
53
+
54
+ ```bash
55
+ # with bun (recommended)
56
+ bun install vargai ai
57
+
58
+ # with npm
59
+ npm install vargai ai
60
+ ```
61
+
62
+ ## ai sdk
63
+
64
+ varg extends vercel's ai sdk with video, music, and lipsync. use familiar patterns:
12
65
 
13
66
  ```typescript
14
67
  import { generateImage } from "ai";
15
- import { File, fal, generateElement, generateVideo, scene } from "vargai";
68
+ import { generateVideo, generateMusic, generateElement, scene, fal, elevenlabs } from "vargai/ai";
69
+
70
+ // generate image
71
+ const { image } = await generateImage({
72
+ model: fal.imageModel("flux-schnell"),
73
+ prompt: "cyberpunk cityscape at night",
74
+ aspectRatio: "16:9",
75
+ });
76
+
77
+ // animate to video
78
+ const { video } = await generateVideo({
79
+ model: fal.videoModel("kling-v2.5"),
80
+ prompt: {
81
+ images: [image.uint8Array],
82
+ text: "camera slowly pans across the city",
83
+ },
84
+ duration: 5,
85
+ });
86
+
87
+ // generate music
88
+ const { audio } = await generateMusic({
89
+ model: elevenlabs.musicModel(),
90
+ prompt: "cyberpunk ambient music, electronic",
91
+ duration: 10,
92
+ });
93
+
94
+ // save output
95
+ await Bun.write("output/city.mp4", video.uint8Array);
96
+ ```
97
+
98
+ ### character consistency with elements
99
+
100
+ create reusable elements for consistent generation across scenes:
101
+
102
+ ```typescript
103
+ import { generateElement, scene, fal } from "vargai/ai";
104
+ import { generateImage, generateVideo } from "ai";
16
105
 
17
- // generate a character from reference image
106
+ // create character from reference
18
107
  const { element: character } = await generateElement({
19
108
  model: fal.imageModel("nano-banana-pro/edit"),
20
109
  type: "character",
21
110
  prompt: {
22
- text: "cartoon character, simple style",
23
- images: [await File.fromPath("media/reference.jpg").arrayBuffer()],
111
+ text: "woman in her 30s, brown hair, green eyes",
112
+ images: [referenceImageData],
24
113
  },
25
114
  });
26
115
 
27
- // generate scene with character
28
- const { image: frame } = await generateImage({
116
+ // use in scenes - same character every time
117
+ const { image: frame1 } = await generateImage({
29
118
  model: fal.imageModel("nano-banana-pro"),
30
- prompt: scene`${character} walks through a forest`,
119
+ prompt: scene`${character} waves hello`,
31
120
  });
32
121
 
33
- // animate the frame
34
- const { video } = await generateVideo({
35
- model: fal.videoModel("wan-2.5"),
122
+ const { image: frame2 } = await generateImage({
123
+ model: fal.imageModel("nano-banana-pro"),
124
+ prompt: scene`${character} gives thumbs up`,
125
+ });
126
+ ```
127
+
128
+ ### file handling
129
+
130
+ ```typescript
131
+ import { File } from "vargai/ai";
132
+
133
+ // load from disk
134
+ const file = File.fromPath("media/portrait.jpg");
135
+
136
+ // load from url
137
+ const file = await File.fromUrl("https://example.com/video.mp4");
138
+
139
+ // load from buffer
140
+ const file = File.fromBuffer(uint8Array, "image/png");
141
+
142
+ // get contents
143
+ const buffer = await file.arrayBuffer();
144
+ const base64 = await file.base64();
145
+ ```
146
+
147
+ ## jsx / react
148
+
149
+ compose videos declaratively with jsx. everything is cached - same props = instant cache hit.
150
+
151
+ ```tsx
152
+ import { render, Render, Clip, Image, Video, Music } from "vargai/react";
153
+ import { fal, elevenlabs } from "vargai/ai";
154
+
155
+ // kawaii fruit characters
156
+ const CHARACTERS = [
157
+ { name: "orange", prompt: "cute kawaii fluffy orange fruit character, round plush body, Pixar style" },
158
+ { name: "strawberry", prompt: "cute kawaii fluffy strawberry fruit character, round plush body, Pixar style" },
159
+ { name: "lemon", prompt: "cute kawaii fluffy lemon fruit character, round plush body, Pixar style" },
160
+ ];
161
+
162
+ const characterImages = CHARACTERS.map(char =>
163
+ Image({
164
+ prompt: char.prompt,
165
+ model: fal.imageModel("nano-banana-pro"),
166
+ aspectRatio: "9:16",
167
+ })
168
+ );
169
+
170
+ await render(
171
+ <Render width={1080} height={1920}>
172
+ <Music prompt="cute baby song, playful xylophone, kawaii vibes" model={elevenlabs.musicModel()} />
173
+
174
+ {CHARACTERS.map((char, i) => (
175
+ <Clip key={char.name} duration={2.5}>
176
+ <Video
177
+ prompt={{
178
+ text: "character waves hello, bounces up and down, eyes squint with joy",
179
+ images: [characterImages[i]],
180
+ }}
181
+ model={fal.videoModel("kling-v2.5")}
182
+ />
183
+ </Clip>
184
+ ))}
185
+ </Render>,
186
+ { output: "output/kawaii-fruits.mp4" }
187
+ );
188
+ ```
189
+
190
+ ### components
191
+
192
+ | component | purpose | key props |
193
+ |-----------|---------|-----------|
194
+ | `<Render>` | root container | `width`, `height`, `fps` |
195
+ | `<Clip>` | time segment | `duration`, `transition`, `cutFrom`, `cutTo` |
196
+ | `<Image>` | ai or static image | `prompt`, `src`, `model`, `zoom`, `aspectRatio`, `resize` |
197
+ | `<Video>` | ai or source video | `prompt`, `src`, `model`, `volume`, `cutFrom`, `cutTo` |
198
+ | `<Speech>` | text-to-speech | `voice`, `model`, `volume`, `children` |
199
+ | `<Music>` | background music | `prompt`, `src`, `model`, `volume`, `loop`, `ducking` |
200
+ | `<Title>` | text overlay | `position`, `color`, `start`, `end` |
201
+ | `<Subtitle>` | subtitle text | `backgroundColor` |
202
+ | `<Captions>` | auto-generated subs | `src`, `srt`, `style`, `color`, `activeColor` |
203
+ | `<Overlay>` | positioned layer | `left`, `top`, `width`, `height`, `keepAudio` |
204
+ | `<Split>` | side-by-side | `direction` |
205
+ | `<Slider>` | before/after reveal | `direction` |
206
+ | `<Swipe>` | tinder-style cards | `direction`, `interval` |
207
+ | `<TalkingHead>` | animated character | `character`, `src`, `voice`, `model`, `lipsyncModel` |
208
+ | `<Packshot>` | end card with cta | `background`, `logo`, `cta`, `blinkCta` |
209
+
210
+ ### layout helpers
211
+
212
+ ```tsx
213
+ import { Grid, SplitLayout } from "vargai/react";
214
+
215
+ // grid layout
216
+ <Grid columns={2}>
217
+ <Video prompt="scene 1" />
218
+ <Video prompt="scene 2" />
219
+ </Grid>
220
+
221
+ // split layout (before/after)
222
+ <SplitLayout left={beforeVideo} right={afterVideo} />
223
+ ```
224
+
225
+ ### transitions
226
+
227
+ 67 gl-transitions available:
228
+
229
+ ```tsx
230
+ <Clip transition={{ name: "fade", duration: 0.5 }}>
231
+ <Clip transition={{ name: "crossfade", duration: 0.5 }}>
232
+ <Clip transition={{ name: "wipeleft", duration: 0.5 }}>
233
+ <Clip transition={{ name: "cube", duration: 0.8 }}>
234
+ ```
235
+
236
+ ### caption styles
237
+
238
+ ```tsx
239
+ <Captions src={voiceover} style="tiktok" /> // word-by-word highlight
240
+ <Captions src={voiceover} style="karaoke" /> // fill left-to-right
241
+ <Captions src={voiceover} style="bounce" /> // words bounce in
242
+ <Captions src={voiceover} style="typewriter" /> // typing effect
243
+ ```
244
+
245
+ ### talking head with lipsync
246
+
247
+ ```tsx
248
+ import { render, Render, Clip, Image, Video, Speech, Captions, Music } from "vargai/react";
249
+ import { fal, elevenlabs, higgsfield } from "vargai/ai";
250
+
251
+ const voiceover = Speech({
252
+ model: elevenlabs.speechModel("eleven_v3"),
253
+ voice: "5l5f8iK3YPeGga21rQIX",
254
+ children: "With varg, you can create any videos at scale!",
255
+ });
256
+
257
+ // base character with higgsfield soul (realistic)
258
+ const baseCharacter = Image({
259
+ prompt: "beautiful East Asian woman, sleek black bob hair, fitted black t-shirt, iPhone selfie, minimalist bedroom",
260
+ model: higgsfield.imageModel("soul", { styleId: higgsfield.styles.REALISTIC }),
261
+ aspectRatio: "9:16",
262
+ });
263
+
264
+ // animate the character
265
+ const animatedCharacter = Video({
36
266
  prompt: {
37
- text: `${character.text} walks through a forest`,
38
- images: [frame.base64],
267
+ text: "woman speaking naturally, subtle head movements, friendly expression",
268
+ images: [baseCharacter],
39
269
  },
40
- duration: 5,
270
+ model: fal.videoModel("kling-v2.5"),
41
271
  });
42
272
 
43
- await Bun.write("output/scene.mp4", video.uint8Array);
273
+ await render(
274
+ <Render width={1080} height={1920}>
275
+ <Music prompt="modern tech ambient, subtle electronic" model={elevenlabs.musicModel()} volume={0.1} />
276
+
277
+ <Clip duration={5}>
278
+ {/* lipsync: animated video + speech audio -> sync-v2 */}
279
+ <Video
280
+ prompt={{ video: animatedCharacter, audio: voiceover }}
281
+ model={fal.videoModel("sync-v2-pro")}
282
+ />
283
+ </Clip>
284
+
285
+ <Captions src={voiceover} style="tiktok" color="#ffffff" />
286
+ </Render>,
287
+ { output: "output/talking-head.mp4" }
288
+ );
44
289
  ```
45
290
 
46
- ### CLI Usage
291
+ ### ugc transformation video
292
+
293
+ ```tsx
294
+ import { render, Render, Clip, Image, Video, Speech, Captions, Music, Title, SplitLayout } from "vargai/react";
295
+ import { fal, elevenlabs, higgsfield } from "vargai/ai";
296
+
297
+ const CHARACTER = "woman in her 30s, brown hair, green eyes";
298
+
299
+ // before: generated with higgsfield soul
300
+ const beforeImage = Image({
301
+ prompt: `${CHARACTER}, overweight, tired expression, loose grey t-shirt, bathroom mirror selfie`,
302
+ model: higgsfield.imageModel("soul", { styleId: higgsfield.styles.REALISTIC }),
303
+ aspectRatio: "9:16",
304
+ });
305
+
306
+ // after: edit with nano-banana-pro using before as reference
307
+ const afterImage = Image({
308
+ prompt: {
309
+ text: `${CHARACTER}, fit slim, confident smile, fitted black tank top, same bathroom, same woman 40 pounds lighter`,
310
+ images: [beforeImage]
311
+ },
312
+ model: fal.imageModel("nano-banana-pro/edit"),
313
+ aspectRatio: "9:16",
314
+ });
315
+
316
+ const beforeVideo = Video({
317
+ prompt: { text: "woman looks down sadly, sighs, tired expression", images: [beforeImage] },
318
+ model: fal.videoModel("kling-v2.5"),
319
+ });
320
+
321
+ const afterVideo = Video({
322
+ prompt: { text: "woman smiles confidently, touches hair, proud expression", images: [afterImage] },
323
+ model: fal.videoModel("kling-v2.5"),
324
+ });
325
+
326
+ const voiceover = Speech({
327
+ model: elevenlabs.speechModel("eleven_multilingual_v2"),
328
+ children: "With this technique I lost 40 pounds in just 3 months!",
329
+ });
330
+
331
+ await render(
332
+ <Render width={1080 * 2} height={1920}>
333
+ <Music prompt="upbeat motivational pop, inspiring transformation" model={elevenlabs.musicModel()} volume={0.15} />
334
+
335
+ <Clip duration={5}>
336
+ <SplitLayout direction="horizontal" left={beforeVideo} right={afterVideo} />
337
+ <Title position="top" color="#ffffff">My 3-Month Transformation</Title>
338
+ </Clip>
339
+
340
+ <Captions src={voiceover} style="tiktok" color="#ffffff" />
341
+ </Render>,
342
+ { output: "output/transformation.mp4" }
343
+ );
344
+ ```
345
+
346
+ ### render options
347
+
348
+ ```tsx
349
+ // save to file
350
+ await render(<Render>...</Render>, { output: "output/video.mp4" });
351
+
352
+ // with cache directory
353
+ await render(<Render>...</Render>, {
354
+ output: "output/video.mp4",
355
+ cache: ".cache/ai"
356
+ });
357
+
358
+ // get buffer directly
359
+ const buffer = await render(<Render>...</Render>);
360
+ await Bun.write("video.mp4", buffer);
361
+ ```
362
+
363
+ ## studio
364
+
365
+ visual editor for video workflows. write code or use node-based interface.
47
366
 
48
367
  ```bash
49
- varg run image --prompt "cyberpunk cityscape at night"
50
- varg run video --prompt "camera flies through clouds" --duration 5
51
- varg run voice --text "Hello world" --voice rachel
368
+ bun run studio
369
+ # opens http://localhost:8282
370
+ ```
371
+
372
+ features:
373
+ - monaco code editor with typescript support
374
+ - node graph visualization of workflow
375
+ - step-by-step execution with previews
376
+ - cache viewer for generated media
377
+
378
+ ## skills
379
+
380
+ skills are multi-step workflows that combine actions into pipelines. located in `skills/` directory.
381
+
382
+ ## supported providers
383
+
384
+ ### fal (primary)
385
+
386
+ ```typescript
387
+ import { fal } from "vargai/ai";
388
+
389
+ // image models
390
+ fal.imageModel("flux-schnell") // fast generation
391
+ fal.imageModel("flux-pro") // high quality
392
+ fal.imageModel("flux-dev") // development
393
+ fal.imageModel("nano-banana-pro") // versatile
394
+ fal.imageModel("nano-banana-pro/edit") // image-to-image editing
395
+ fal.imageModel("recraft-v3") // alternative
396
+
397
+ // video models
398
+ fal.videoModel("kling-v2.5") // high quality video
399
+ fal.videoModel("kling-v2.1") // previous version
400
+ fal.videoModel("wan-2.5") // good for characters
401
+ fal.videoModel("minimax") // alternative
402
+
403
+ // lipsync models
404
+ fal.videoModel("sync-v2") // lip sync
405
+ fal.videoModel("sync-v2-pro") // pro lip sync
406
+
407
+ // transcription
408
+ fal.transcriptionModel("whisper")
52
409
  ```
53
410
 
54
- ## Commands
411
+ ### elevenlabs
412
+
413
+ ```typescript
414
+ import { elevenlabs } from "vargai/ai";
55
415
 
56
- | Command | Description |
57
- |---------|-------------|
58
- | `varg run <action>` | Run an action |
59
- | `varg list` | List all available actions |
60
- | `varg find <query>` | Search actions by keyword |
61
- | `varg which <action>` | Show action details and options |
62
- | `varg help` | Show help |
416
+ // speech models
417
+ elevenlabs.speechModel("eleven_turbo_v2") // fast tts (default)
418
+ elevenlabs.speechModel("eleven_multilingual_v2") // multilingual
63
419
 
64
- ## Actions
420
+ // music model
421
+ elevenlabs.musicModel() // music generation
65
422
 
66
- | Action | Description | Example |
67
- |--------|-------------|---------|
68
- | `image` | Generate image from text | `varg run image --prompt "sunset"` |
69
- | `video` | Generate video from text or image | `varg run video --prompt "ocean waves" --image ./photo.jpg` |
70
- | `voice` | Text-to-speech | `varg run voice --text "Hello" --voice sam` |
71
- | `music` | Generate music | `varg run music --prompt "upbeat electronic"` |
72
- | `transcribe` | Audio to text/subtitles | `varg run transcribe --audio ./speech.mp3` |
73
- | `captions` | Add subtitles to video | `varg run captions --video ./clip.mp4` |
74
- | `sync` | Lipsync audio to video | `varg run sync --video ./face.mp4 --audio ./voice.mp3` |
75
- | `trim` | Trim video | `varg run trim --input ./video.mp4 --start 0 --end 10` |
76
- | `cut` | Remove section from video | `varg run cut --input ./video.mp4 --start 5 --end 8` |
77
- | `merge` | Combine videos | `varg run merge --inputs ./a.mp4 ./b.mp4` |
78
- | `split` | Split video at timestamps | `varg run split --input ./video.mp4 --timestamps 10,20,30` |
79
- | `fade` | Add fade in/out | `varg run fade --input ./video.mp4 --type both` |
80
- | `transition` | Add transitions between clips | `varg run transition --inputs ./a.mp4 ./b.mp4` |
81
- | `upload` | Upload file to S3 | `varg run upload --file ./video.mp4` |
423
+ // available voices: rachel, adam, bella, josh, sam, antoni, elli, arnold, domi
424
+ ```
82
425
 
83
- Use `varg run <action> --help` for all options.
426
+ ### higgsfield
84
427
 
85
- ## Environment Variables
428
+ ```typescript
429
+ import { higgsfield } from "vargai/ai";
86
430
 
87
- <details>
88
- <summary>Required API keys</summary>
431
+ // character-focused image generation with 100+ styles
432
+ higgsfield.imageModel("soul")
433
+ higgsfield.imageModel("soul", {
434
+ styleId: higgsfield.styles.REALISTIC,
435
+ quality: "1080p"
436
+ })
437
+
438
+ // styles include: REALISTIC, ANIME, EDITORIAL_90S, Y2K, GRUNGE, etc.
439
+ ```
440
+
441
+ ### openai
442
+
443
+ ```typescript
444
+ import { openai } from "vargai/ai";
445
+
446
+ // sora video generation
447
+ openai.videoModel("sora-2")
448
+ openai.videoModel("sora-2-pro")
449
+
450
+ // also supports all standard openai models via @ai-sdk/openai
451
+ ```
452
+
453
+ ### replicate
454
+
455
+ ```typescript
456
+ import { replicate } from "vargai/ai";
457
+
458
+ // background removal
459
+ replicate.imageModel("851-labs/background-remover")
460
+
461
+ // any replicate model
462
+ replicate.imageModel("owner/model-name")
463
+ ```
464
+
465
+ ## supported models
466
+
467
+ ### video generation
468
+
469
+ | model | provider | capabilities |
470
+ |-------|----------|--------------|
471
+ | kling-v2.5 | fal | text-to-video, image-to-video |
472
+ | kling-v2.1 | fal | text-to-video, image-to-video |
473
+ | wan-2.5 | fal | image-to-video, good for characters |
474
+ | minimax | fal | text-to-video, image-to-video |
475
+ | sora-2 | openai | text-to-video, image-to-video |
476
+ | sync-v2-pro | fal | lipsync (video + audio input) |
477
+
478
+ ### image generation
479
+
480
+ | model | provider | capabilities |
481
+ |-------|----------|--------------|
482
+ | flux-schnell | fal | fast text-to-image |
483
+ | flux-pro | fal | high quality text-to-image |
484
+ | nano-banana-pro | fal | text-to-image, versatile |
485
+ | nano-banana-pro/edit | fal | image-to-image editing |
486
+ | recraft-v3 | fal | text-to-image |
487
+ | soul | higgsfield | character-focused, 100+ styles |
488
+
489
+ ### audio
490
+
491
+ | model | provider | capabilities |
492
+ |-------|----------|--------------|
493
+ | eleven_turbo_v2 | elevenlabs | fast text-to-speech |
494
+ | eleven_multilingual_v2 | elevenlabs | multilingual tts |
495
+ | music_v1 | elevenlabs | text-to-music |
496
+ | whisper | fal | speech-to-text |
497
+
498
+ ## environment variables
89
499
 
90
500
  ```bash
91
- # AI Providers
501
+ # required
92
502
  FAL_API_KEY=fal_xxx
93
- REPLICATE_API_TOKEN=r8_xxx
94
- ELEVENLABS_API_KEY=xxx
95
- GROQ_API_KEY=gsk_xxx
96
- FIREWORKS_API_KEY=fw_xxx
97
- HIGGSFIELD_API_KEY=hf_xxx
503
+
504
+ # optional - enable additional features
505
+ ELEVENLABS_API_KEY=xxx # voice and music
506
+ REPLICATE_API_TOKEN=r8_xxx # background removal, other models
507
+ OPENAI_API_KEY=sk_xxx # sora video
508
+ HIGGSFIELD_API_KEY=hf_xxx # soul character images
98
509
  HIGGSFIELD_SECRET=secret_xxx
510
+ GROQ_API_KEY=gsk_xxx # fast transcription
99
511
 
100
- # Storage (Cloudflare R2)
512
+ # storage (for upload)
101
513
  CLOUDFLARE_R2_API_URL=https://xxx.r2.cloudflarestorage.com
102
514
  CLOUDFLARE_ACCESS_KEY_ID=xxx
103
515
  CLOUDFLARE_ACCESS_SECRET=xxx
104
516
  CLOUDFLARE_R2_BUCKET=bucket-name
105
517
  ```
106
518
 
107
- </details>
519
+ ## cli
108
520
 
109
- ## Contributing
521
+ ```bash
522
+ varg run image --prompt "sunset over mountains"
523
+ varg run video --prompt "ocean waves" --duration 5
524
+ varg run voice --text "Hello world" --voice rachel
525
+ varg list # list all actions
526
+ varg studio # open visual editor
527
+ ```
528
+
529
+ ## contributing
110
530
 
111
- See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup.
531
+ see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup.
112
532
 
113
- ## License
533
+ ## license
114
534
 
115
535
  Apache-2.0 — see [LICENSE.md](LICENSE.md)
536
+
537
+
@@ -69,8 +69,8 @@ export default (
69
69
  <Clip key={char.name} duration={2.5}>
70
70
  <Video
71
71
  prompt={{
72
- text: "character waves hello e nthusiastically, bounces up and down slightly, eyes squint with joy, tiny feet wiggle",
73
- images: [characterImages[i]],
72
+ text: "character waves hello enthusiastically, bounces up and down slightly, eyes squint with joy, tiny feet wiggle",
73
+ images: [characterImages[i]!],
74
74
  }}
75
75
  model={fal.videoModel("kling-v2.5")}
76
76
  duration={5}
package/package.json CHANGED
@@ -66,7 +66,7 @@
66
66
  "vargai": "^0.4.0-alpha11",
67
67
  "zod": "^4.2.1"
68
68
  },
69
- "version": "0.4.0-alpha18",
69
+ "version": "0.4.0-alpha19",
70
70
  "exports": {
71
71
  ".": "./src/index.ts",
72
72
  "./ai": "./src/ai-sdk/index.ts",
@@ -15,12 +15,12 @@ export interface ImagePlaceholderFallbackOptions {
15
15
  export function imagePlaceholderFallbackMiddleware(
16
16
  options: ImagePlaceholderFallbackOptions,
17
17
  ): ImageModelV3Middleware {
18
- const { mode, onFallback } = options;
18
+ const { mode } = options;
19
19
 
20
20
  return {
21
21
  specificationVersion: "v3",
22
22
  wrapGenerate: async ({ doGenerate, params, model }) => {
23
- const createPlaceholderResult = async () => {
23
+ if (mode === "preview") {
24
24
  const [width, height] = (params.size?.split("x").map(Number) ?? [
25
25
  1024, 1024,
26
26
  ]) as [number, number];
@@ -42,7 +42,7 @@ export function imagePlaceholderFallbackMiddleware(
42
42
  warnings: [
43
43
  {
44
44
  type: "other" as const,
45
- message: "placeholder: provider skipped or failed",
45
+ message: "placeholder: preview mode",
46
46
  },
47
47
  ],
48
48
  response: {
@@ -51,26 +51,9 @@ export function imagePlaceholderFallbackMiddleware(
51
51
  headers: undefined,
52
52
  },
53
53
  };
54
- };
55
-
56
- if (mode === "preview") {
57
- return createPlaceholderResult();
58
54
  }
59
55
 
60
- try {
61
- return await doGenerate();
62
- } catch (e) {
63
- if (mode === "strict") throw e;
64
-
65
- const error = e instanceof Error ? e : new Error(String(e));
66
- const promptText =
67
- typeof params.prompt === "string"
68
- ? params.prompt
69
- : ((params.prompt as { text?: string } | undefined)?.text ??
70
- "placeholder");
71
- onFallback?.(error, promptText);
72
- return createPlaceholderResult();
73
- }
56
+ return doGenerate();
74
57
  },
75
58
  };
76
59
  }
@@ -53,11 +53,11 @@ export interface MusicPlaceholderFallbackOptions {
53
53
  export function musicPlaceholderFallbackMiddleware(
54
54
  options: MusicPlaceholderFallbackOptions,
55
55
  ): MusicModelMiddleware {
56
- const { mode, onFallback } = options;
56
+ const { mode } = options;
57
57
 
58
58
  return {
59
59
  wrapGenerate: async ({ doGenerate, params, model }) => {
60
- const createPlaceholderResult = async () => {
60
+ if (mode === "preview") {
61
61
  const placeholder = await generatePlaceholder({
62
62
  type: "audio",
63
63
  prompt: params.prompt,
@@ -69,7 +69,7 @@ export function musicPlaceholderFallbackMiddleware(
69
69
  warnings: [
70
70
  {
71
71
  type: "other" as const,
72
- message: "placeholder: provider skipped or failed",
72
+ message: "placeholder: preview mode",
73
73
  },
74
74
  ],
75
75
  response: {
@@ -78,21 +78,9 @@ export function musicPlaceholderFallbackMiddleware(
78
78
  headers: undefined,
79
79
  },
80
80
  };
81
- };
82
-
83
- if (mode === "preview") {
84
- return createPlaceholderResult();
85
81
  }
86
82
 
87
- try {
88
- return await doGenerate();
89
- } catch (e) {
90
- if (mode === "strict") throw e;
91
-
92
- const error = e instanceof Error ? e : new Error(String(e));
93
- onFallback?.(error, params.prompt);
94
- return createPlaceholderResult();
95
- }
83
+ return doGenerate();
96
84
  },
97
85
  };
98
86
  }
@@ -1,7 +1,7 @@
1
1
  import type { VideoModelV3, VideoModelV3CallOptions } from "../video-model";
2
2
  import { generatePlaceholder } from "./placeholder";
3
3
 
4
- export type RenderMode = "strict" | "default" | "preview";
4
+ export type RenderMode = "strict" | "preview";
5
5
 
6
6
  export interface VideoModelMiddleware {
7
7
  transformParams?: (options: {
@@ -55,11 +55,11 @@ export interface PlaceholderFallbackOptions {
55
55
  export function placeholderFallbackMiddleware(
56
56
  options: PlaceholderFallbackOptions,
57
57
  ): VideoModelMiddleware {
58
- const { mode, onFallback } = options;
58
+ const { mode } = options;
59
59
 
60
60
  return {
61
61
  wrapGenerate: async ({ doGenerate, params, model }) => {
62
- const createPlaceholderResult = async () => {
62
+ if (mode === "preview") {
63
63
  const [width, height] = (params.resolution?.split("x").map(Number) ?? [
64
64
  1080, 1920,
65
65
  ]) as [number, number];
@@ -76,7 +76,7 @@ export function placeholderFallbackMiddleware(
76
76
  warnings: [
77
77
  {
78
78
  type: "other" as const,
79
- message: "placeholder: provider skipped or failed",
79
+ message: "placeholder: preview mode",
80
80
  },
81
81
  ],
82
82
  response: {
@@ -85,21 +85,9 @@ export function placeholderFallbackMiddleware(
85
85
  headers: undefined,
86
86
  },
87
87
  };
88
- };
89
-
90
- if (mode === "preview") {
91
- return createPlaceholderResult();
92
88
  }
93
89
 
94
- try {
95
- return await doGenerate();
96
- } catch (e) {
97
- if (mode === "strict") throw e;
98
-
99
- const error = e instanceof Error ? e : new Error(String(e));
100
- onFallback?.(error, params.prompt);
101
- return createPlaceholderResult();
102
- }
90
+ return doGenerate();
103
91
  },
104
92
  };
105
93
  }
@@ -58,7 +58,12 @@ const IMAGE_MODELS: Record<string, string> = {
58
58
  };
59
59
 
60
60
  // Models that use image_size instead of aspect_ratio
61
- const IMAGE_SIZE_MODELS = new Set(["seedream-v4.5/edit"]);
61
+ const IMAGE_SIZE_MODELS = new Set([
62
+ "flux-schnell",
63
+ "flux-dev",
64
+ "flux-pro",
65
+ "seedream-v4.5/edit",
66
+ ]);
62
67
 
63
68
  // Map aspect ratio strings to image_size enum values
64
69
  const ASPECT_RATIO_TO_IMAGE_SIZE: Record<string, string> = {
@@ -21,44 +21,37 @@ function CommandRow({ name, description }: CommandRowProps) {
21
21
 
22
22
  function HelpView() {
23
23
  const examples = [
24
- {
25
- command: 'varg run video --prompt "ocean waves"',
26
- description: "generate a video from text",
27
- },
28
- {
29
- command: 'varg run video --prompt "person talking" --image photo.jpg',
30
- description: "generate video from image",
31
- },
32
- {
33
- command: 'varg run voice --text "hello world" --voice sam',
34
- description: "text to speech",
35
- },
36
- { command: "varg list", description: "see all available" },
37
- {
38
- command: "varg which video",
39
- description: "inspect an action or model",
40
- },
24
+ { command: "varg init", description: "create hello.tsx starter" },
25
+ { command: "varg render hello.tsx", description: "render jsx to video" },
26
+ { command: "varg preview hello.tsx", description: "fast preview mode" },
41
27
  ];
42
28
 
43
29
  return (
44
30
  <VargBox title="varg">
45
31
  <Box marginBottom={1}>
46
- <Text>ai video infrastructure from your terminal</Text>
32
+ <Text>ai video generation sdk. jsx for videos.</Text>
47
33
  </Box>
48
34
 
49
35
  <Header>COMMANDS</Header>
50
36
  <Box flexDirection="column" marginY={1}>
51
- <CommandRow name="run" description="run a model, action, or skill" />
52
- <CommandRow name="list" description="discover what's available" />
53
37
  <CommandRow
54
- name="find"
55
- description="search for models, actions, skills"
38
+ name="init"
39
+ description="create hello.tsx starter project"
40
+ />
41
+ <CommandRow name="render" description="render jsx component to video" />
42
+ <CommandRow name="preview" description="fast preview (placeholders)" />
43
+ <CommandRow
44
+ name="studio"
45
+ description="visual editor at localhost:8282"
46
+ />
47
+ <CommandRow name="run" description="run a single model or action" />
48
+ <CommandRow
49
+ name="list"
50
+ description="discover models, actions, skills"
56
51
  />
57
- <CommandRow name="which" description="inspect a specific item" />
58
- <CommandRow name="help" description="show this help" />
59
52
  </Box>
60
53
 
61
- <Header>EXAMPLES</Header>
54
+ <Header>QUICKSTART</Header>
62
55
  <Box marginTop={1}>
63
56
  <HelpBlock examples={examples} />
64
57
  </Box>
@@ -1,7 +1,13 @@
1
1
  export { findCmd, showFindHelp } from "./find.tsx";
2
2
  export { helpCmd, showHelp } from "./help.tsx";
3
+ export { initCmd, showInitHelp } from "./init.tsx";
3
4
  export { listCmd, showListHelp } from "./list.tsx";
4
- export { fastCmd, previewCmd, renderCmd } from "./render.ts";
5
+ export {
6
+ previewCmd,
7
+ renderCmd,
8
+ showPreviewHelp,
9
+ showRenderHelp,
10
+ } from "./render.tsx";
5
11
  export { runCmd, showRunHelp, showTargetHelp } from "./run.tsx";
6
12
  export { studioCmd } from "./studio.ts";
7
13
  export { showWhichHelp, whichCmd } from "./which.tsx";
@@ -0,0 +1,116 @@
1
+ /** @jsxImportSource react */
2
+
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import { defineCommand } from "citty";
5
+ import { Box, Text } from "ink";
6
+ import { Header, HelpBlock, VargBox, VargText } from "../ui/index.ts";
7
+ import { renderStatic } from "../ui/render.ts";
8
+
9
+ const HELLO_TEMPLATE = `import { Render, Clip, Image, Video, assets } from "vargai/react";
10
+ import { fal } from "vargai/ai";
11
+
12
+ const girl = Image({
13
+ prompt: {
14
+ text: \`Using the attached reference images, generate a photorealistic three-quarter editorial portrait of the exact same character — maintain identical face, hairstyle, and proportions from Image 1.
15
+
16
+ Framing: Head and shoulders, cropped at upper chest. Direct eye contact with camera.
17
+
18
+ Natural confident expression, relaxed shoulders.
19
+ Preserve the outfit neckline and visible clothing details from reference.
20
+
21
+ Background: Deep black with two contrasting orange gradient accents matching Reference 2. Soft gradient bleed, no hard edges.
22
+
23
+ Shot on 85mm f/1.4 lens, shallow depth of field. Clean studio lighting — soft key light on face, subtle rim light on hair and shoulders for separation. High-end fashion editorial aesthetic.\`,
24
+ images: [assets.characters.orangeGirl, assets.backgrounds.orangeGradient],
25
+ },
26
+ model: fal.imageModel("nano-banana-pro/edit"),
27
+ aspectRatio: "9:16",
28
+ });
29
+
30
+ export default (
31
+ <Render width={1080} height={1920}>
32
+ <Clip duration={5}>
33
+ <Video
34
+ prompt={{
35
+ text: "She waves hello warmly, natural smile, friendly expression. Studio lighting, authentic confident slightly playful atmosphere. Camera static. Intense orange lighting.",
36
+ images: [girl],
37
+ }}
38
+ model={fal.videoModel("kling-v2.5")}
39
+ />
40
+ </Clip>
41
+ </Render>
42
+ );
43
+ `;
44
+
45
+ function InitHelpView() {
46
+ const examples = [
47
+ { command: "varg init", description: "create hello.tsx in current dir" },
48
+ { command: "varg init my-project", description: "create in my-project/" },
49
+ ];
50
+
51
+ return (
52
+ <VargBox title="varg init">
53
+ <Box marginBottom={1}>
54
+ <Text>initialize a new varg project with hello.tsx template.</Text>
55
+ </Box>
56
+
57
+ <Header>USAGE</Header>
58
+ <Box paddingLeft={2} marginBottom={1}>
59
+ <VargText variant="accent">varg init [directory]</VargText>
60
+ </Box>
61
+
62
+ <Header>EXAMPLES</Header>
63
+ <Box marginTop={1}>
64
+ <HelpBlock examples={examples} />
65
+ </Box>
66
+ </VargBox>
67
+ );
68
+ }
69
+
70
+ export function showInitHelp() {
71
+ renderStatic(<InitHelpView />);
72
+ }
73
+
74
+ export const initCmd = defineCommand({
75
+ meta: {
76
+ name: "init",
77
+ description: "initialize project with hello.tsx",
78
+ },
79
+ args: {
80
+ directory: {
81
+ type: "positional",
82
+ description: "project directory (default: current)",
83
+ required: false,
84
+ },
85
+ },
86
+ async run({ args }) {
87
+ const dir = (args.directory as string) || ".";
88
+ const outputDir = `${dir}/output`;
89
+ const cacheDir = `${dir}/.cache/ai`;
90
+ const helloPath = `${dir}/hello.tsx`;
91
+
92
+ if (!existsSync(dir) && dir !== ".") {
93
+ mkdirSync(dir, { recursive: true });
94
+ console.log(`created ${dir}/`);
95
+ }
96
+
97
+ if (!existsSync(outputDir)) {
98
+ mkdirSync(outputDir, { recursive: true });
99
+ console.log(`created ${outputDir}/`);
100
+ }
101
+
102
+ if (!existsSync(cacheDir)) {
103
+ mkdirSync(cacheDir, { recursive: true });
104
+ console.log(`created ${cacheDir}/`);
105
+ }
106
+
107
+ if (existsSync(helloPath)) {
108
+ console.log(`hello.tsx already exists, skipping`);
109
+ } else {
110
+ await Bun.write(helloPath, HELLO_TEMPLATE);
111
+ console.log(`created ${helloPath}`);
112
+ }
113
+
114
+ console.log(`\ndone! run: bunx vargai render hello.tsx`);
115
+ },
116
+ });
@@ -1,8 +1,13 @@
1
+ /** @jsxImportSource react */
2
+
1
3
  import { existsSync, mkdirSync } from "node:fs";
2
4
  import { resolve } from "node:path";
3
5
  import { defineCommand } from "citty";
6
+ import { Box, Text } from "ink";
4
7
  import { render } from "../../react/render";
5
8
  import type { DefaultModels, RenderMode, VargElement } from "../../react/types";
9
+ import { Header, HelpBlock, VargBox, VargText } from "../ui/index.ts";
10
+ import { renderStatic } from "../ui/render.ts";
6
11
 
7
12
  const AUTO_IMPORTS = `/** @jsxImportSource vargai */
8
13
  import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid, SplitLayout } from "vargai/react";
@@ -127,8 +132,7 @@ async function runRender(
127
132
  const outputPath = (args.output as string) ?? `output/${basename}.mp4`;
128
133
 
129
134
  if (!args.quiet) {
130
- const modeLabel =
131
- mode === "preview" ? " (fast)" : mode === "strict" ? "" : " (preview)";
135
+ const modeLabel = mode === "preview" ? " (fast)" : "";
132
136
  console.log(`rendering ${file} → ${outputPath}${modeLabel}`);
133
137
  }
134
138
 
@@ -163,21 +167,140 @@ export const renderCmd = defineCommand({
163
167
  export const previewCmd = defineCommand({
164
168
  meta: {
165
169
  name: "preview",
166
- description: "render with fallback placeholders on errors",
167
- },
168
- args: sharedArgs,
169
- async run({ args }) {
170
- await runRender(args, "default", "preview");
171
- },
172
- });
173
-
174
- export const fastCmd = defineCommand({
175
- meta: {
176
- name: "fast",
177
170
  description: "render with all placeholders (no generation)",
178
171
  },
179
172
  args: sharedArgs,
180
173
  async run({ args }) {
181
- await runRender(args, "preview", "fast");
174
+ await runRender(args, "preview", "preview");
182
175
  },
183
176
  });
177
+
178
+ function RenderHelpView() {
179
+ const examples = [
180
+ {
181
+ command: "varg render video.tsx",
182
+ description: "render component to output/video.mp4",
183
+ },
184
+ {
185
+ command: "varg render video.tsx -o my-video.mp4",
186
+ description: "custom output path",
187
+ },
188
+ {
189
+ command: "varg preview video.tsx",
190
+ description: "fast preview with placeholders",
191
+ },
192
+ ];
193
+
194
+ return (
195
+ <VargBox title="varg render">
196
+ <Box marginBottom={1}>
197
+ <Text>
198
+ render jsx components to video. the react engine for ai video.
199
+ </Text>
200
+ </Box>
201
+
202
+ <Header>USAGE</Header>
203
+ <Box paddingLeft={2} marginBottom={1}>
204
+ <VargText variant="accent">
205
+ varg render {"<file.tsx>"} [options]
206
+ </VargText>
207
+ </Box>
208
+
209
+ <Header>OPTIONS</Header>
210
+ <Box flexDirection="column" paddingLeft={2} marginBottom={1}>
211
+ <Text>
212
+ <VargText variant="accent">-o, --output </VargText>output path
213
+ (default: output/{"<name>"}.mp4)
214
+ </Text>
215
+ <Text>
216
+ <VargText variant="accent">-c, --cache </VargText>cache directory
217
+ (default: .cache/ai)
218
+ </Text>
219
+ <Text>
220
+ <VargText variant="accent">--no-cache </VargText>disable cache
221
+ </Text>
222
+ <Text>
223
+ <VargText variant="accent">-q, --quiet </VargText>minimal output
224
+ </Text>
225
+ <Text>
226
+ <VargText variant="accent">-v, --verbose </VargText>show ffmpeg
227
+ commands
228
+ </Text>
229
+ </Box>
230
+
231
+ <Header>COMPONENTS</Header>
232
+ <Box flexDirection="column" paddingLeft={2} marginBottom={1}>
233
+ <Text>{"<Render>"} root container (width, height, fps)</Text>
234
+ <Text>{"<Clip>"} time segment with duration</Text>
235
+ <Text>{"<Video>"} ai-generated or source video</Text>
236
+ <Text>{"<Image>"} ai-generated or static image</Text>
237
+ <Text>{"<Speech>"} text-to-speech audio</Text>
238
+ <Text>{"<Music>"} background music</Text>
239
+ <Text>{"<Captions>"} auto-generated subtitles</Text>
240
+ </Box>
241
+
242
+ <Header>EXAMPLES</Header>
243
+ <Box marginTop={1}>
244
+ <HelpBlock examples={examples} />
245
+ </Box>
246
+ </VargBox>
247
+ );
248
+ }
249
+
250
+ function PreviewHelpView() {
251
+ const examples = [
252
+ {
253
+ command: "varg preview video.tsx",
254
+ description: "quick test without ai calls",
255
+ },
256
+ {
257
+ command: "varg preview video.tsx -o test.mp4",
258
+ description: "preview to custom path",
259
+ },
260
+ ];
261
+
262
+ return (
263
+ <VargBox title="varg preview">
264
+ <Box marginBottom={1}>
265
+ <Text>
266
+ fast preview mode - uses placeholders instead of ai generation.
267
+ </Text>
268
+ </Box>
269
+
270
+ <Header>USAGE</Header>
271
+ <Box paddingLeft={2} marginBottom={1}>
272
+ <VargText variant="accent">
273
+ varg preview {"<file.tsx>"} [options]
274
+ </VargText>
275
+ </Box>
276
+
277
+ <Header>OPTIONS</Header>
278
+ <Box flexDirection="column" paddingLeft={2} marginBottom={1}>
279
+ <Text>
280
+ <VargText variant="accent">-o, --output </VargText>output path
281
+ (default: output/{"<name>"}.mp4)
282
+ </Text>
283
+ <Text>
284
+ <VargText variant="accent">-q, --quiet </VargText>minimal output
285
+ </Text>
286
+ <Text>
287
+ <VargText variant="accent">-v, --verbose </VargText>show ffmpeg
288
+ commands
289
+ </Text>
290
+ </Box>
291
+
292
+ <Header>EXAMPLES</Header>
293
+ <Box marginTop={1}>
294
+ <HelpBlock examples={examples} />
295
+ </Box>
296
+ </VargBox>
297
+ );
298
+ }
299
+
300
+ export function showRenderHelp() {
301
+ renderStatic(<RenderHelpView />);
302
+ }
303
+
304
+ export function showPreviewHelp() {
305
+ renderStatic(<PreviewHelpView />);
306
+ }
package/src/cli/index.ts CHANGED
@@ -12,16 +12,19 @@ import { defineCommand, runMain } from "citty";
12
12
  import { registry } from "../core/registry";
13
13
  import { allDefinitions } from "../definitions";
14
14
  import {
15
- fastCmd,
16
15
  findCmd,
17
16
  helpCmd,
17
+ initCmd,
18
18
  listCmd,
19
19
  previewCmd,
20
20
  renderCmd,
21
21
  runCmd,
22
22
  showFindHelp,
23
23
  showHelp,
24
+ showInitHelp,
24
25
  showListHelp,
26
+ showPreviewHelp,
27
+ showRenderHelp,
25
28
  showRunHelp,
26
29
  showTargetHelp,
27
30
  showWhichHelp,
@@ -47,9 +50,11 @@ for (const provider of providers.all()) {
47
50
  const args = process.argv.slice(2);
48
51
  const hasHelp = args.includes("--help") || args.includes("-h");
49
52
 
50
- // Map subcommands to their help functions
51
53
  const subcommandHelp: Record<string, () => void> = {
52
54
  run: showRunHelp,
55
+ render: showRenderHelp,
56
+ preview: showPreviewHelp,
57
+ init: showInitHelp,
53
58
  list: showListHelp,
54
59
  ls: showListHelp,
55
60
  find: showFindHelp,
@@ -104,11 +109,11 @@ const main = defineCommand({
104
109
  description: "ai video infrastructure from your terminal",
105
110
  },
106
111
  subCommands: {
107
- run: runCmd,
112
+ init: initCmd,
108
113
  render: renderCmd,
109
114
  preview: previewCmd,
110
- fast: fastCmd,
111
115
  studio: studioCmd,
116
+ run: runCmd,
112
117
  list: listCmd,
113
118
  ls: listCmd,
114
119
  find: findCmd,
@@ -0,0 +1,9 @@
1
+ export const assets = {
2
+ characters: {
3
+ orangeGirl: "https://s3.varg.ai/uploads/images/1_0475e227.png",
4
+ },
5
+ backgrounds: {
6
+ orangeGradient:
7
+ "https://s3.varg.ai/uploads/images/xyearp51qvve-zi3nrcve-zbno2hfgt5gergjrof_995f553d.png",
8
+ },
9
+ } as const;
@@ -1,4 +1,5 @@
1
1
  export type { SizeValue } from "../ai-sdk/providers/editly/types";
2
+ export { assets } from "./assets";
2
3
  export {
3
4
  Captions,
4
5
  Clip,
@@ -53,17 +53,9 @@ export async function renderRoot(
53
53
  const props = element.props as RenderProps;
54
54
  const progress = createProgressTracker(options.quiet ?? false);
55
55
 
56
- const mode: RenderMode = options.mode ?? "default";
56
+ const mode: RenderMode = options.mode ?? "strict";
57
57
  const placeholderCount = { images: 0, videos: 0, total: 0 };
58
58
 
59
- const onFallback = (error: Error, prompt: string) => {
60
- if (!options.quiet) {
61
- console.warn(
62
- `\x1b[33m⚠ provider failed: ${error.message} → placeholder\x1b[0m`,
63
- );
64
- }
65
- };
66
-
67
59
  const trackPlaceholder = (type: "image" | "video") => {
68
60
  placeholderCount[type === "image" ? "images" : "videos"]++;
69
61
  placeholderCount.total++;
@@ -87,14 +79,6 @@ export async function renderRoot(
87
79
 
88
80
  if (mode === "preview") {
89
81
  trackPlaceholder("image");
90
- }
91
-
92
- try {
93
- return await cachedGenerateImage(opts);
94
- } catch (error) {
95
- if (mode === "strict") throw error;
96
- trackPlaceholder("image");
97
- onFallback(error as Error, String(opts.prompt));
98
82
  const wrappedModel = wrapImageModel({
99
83
  model: opts.model,
100
84
  middleware: imagePlaceholderFallbackMiddleware({
@@ -104,19 +88,13 @@ export async function renderRoot(
104
88
  });
105
89
  return generateImage({ ...opts, model: wrappedModel });
106
90
  }
91
+
92
+ return cachedGenerateImage(opts);
107
93
  };
108
94
 
109
95
  const wrapGenerateVideo: typeof generateVideo = async (opts) => {
110
96
  if (mode === "preview") {
111
97
  trackPlaceholder("video");
112
- }
113
-
114
- try {
115
- return await cachedGenerateVideo(opts);
116
- } catch (error) {
117
- if (mode === "strict") throw error;
118
- trackPlaceholder("video");
119
- onFallback(error as Error, String(opts.prompt));
120
98
  const wrappedModel = wrapVideoModel({
121
99
  model: opts.model,
122
100
  middleware: placeholderFallbackMiddleware({
@@ -126,6 +104,8 @@ export async function renderRoot(
126
104
  });
127
105
  return generateVideo({ ...opts, model: wrappedModel });
128
106
  }
107
+
108
+ return cachedGenerateVideo(opts);
129
109
  };
130
110
 
131
111
  const ctx: RenderContext = {
@@ -314,16 +294,10 @@ export async function renderRoot(
314
294
  completeTask(progress, captionsTaskId);
315
295
  }
316
296
 
317
- if (!options.quiet && placeholderCount.total > 0) {
318
- if (mode === "preview") {
319
- console.log(
320
- `\x1b[36mℹ preview mode: ${placeholderCount.total} placeholders used (${placeholderCount.images} images, ${placeholderCount.videos} videos)\x1b[0m`,
321
- );
322
- } else {
323
- console.warn(
324
- `\x1b[33m⚠ ${placeholderCount.total} elements used placeholders - run with --strict for production\x1b[0m`,
325
- );
326
- }
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
+ );
327
301
  }
328
302
 
329
303
  const result = await Bun.file(finalOutPath).arrayBuffer();
@@ -145,7 +145,8 @@ export async function renderVideo(
145
145
  const { video } = await ctx.generateVideo({
146
146
  model,
147
147
  prompt: resolvedPrompt,
148
- duration: 5,
148
+ duration: props.duration ?? 5,
149
+ aspectRatio: props.aspectRatio,
149
150
  cacheKey,
150
151
  } as Parameters<typeof generateVideo>[0]);
151
152
 
@@ -118,6 +118,7 @@ export type VideoProps = BaseProps &
118
118
  src?: string;
119
119
  model?: VideoModelV3;
120
120
  resize?: ResizeMode;
121
+ aspectRatio?: `${number}:${number}`;
121
122
  };
122
123
 
123
124
  export interface SpeechProps extends BaseProps, VolumeProps {
@@ -201,7 +202,7 @@ export interface PackshotProps extends BaseProps {
201
202
  duration?: number;
202
203
  }
203
204
 
204
- export type RenderMode = "strict" | "default" | "preview";
205
+ export type RenderMode = "strict" | "preview";
205
206
 
206
207
  export interface DefaultModels {
207
208
  image?: ImageModelV3;
package/tsconfig.json CHANGED
@@ -35,7 +35,7 @@
35
35
  "vargai/jsx-dev-runtime": ["./src/react/runtime/jsx-dev-runtime.ts"]
36
36
  }
37
37
  },
38
- "include": ["src/**/*"],
38
+ "include": ["src/**/*", "launch-videos/**/*"],
39
39
  "exclude": [
40
40
  "node_modules",
41
41
  "action",