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 +483 -61
- package/launch-videos/06-kawaii-fruits.tsx +2 -2
- package/package.json +1 -1
- package/src/ai-sdk/middleware/wrap-image-model.ts +4 -21
- package/src/ai-sdk/middleware/wrap-music-model.ts +4 -16
- package/src/ai-sdk/middleware/wrap-video-model.ts +5 -17
- package/src/ai-sdk/providers/fal.ts +6 -1
- package/src/cli/commands/help.tsx +18 -25
- package/src/cli/commands/index.ts +7 -1
- package/src/cli/commands/init.tsx +116 -0
- package/src/cli/commands/{render.ts → render.tsx} +137 -14
- package/src/cli/index.ts +9 -4
- package/src/react/assets.ts +9 -0
- package/src/react/index.ts +1 -0
- package/src/react/renderers/render.ts +9 -35
- package/src/react/renderers/video.ts +2 -1
- package/src/react/types.ts +2 -1
- package/tsconfig.json +1 -1
package/README.md
CHANGED
|
@@ -1,115 +1,537 @@
|
|
|
1
1
|
# varg
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ai video generation sdk. jsx for videos, built on vercel ai sdk.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## quickstart
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
bun install vargai ai
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
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 {
|
|
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
|
-
//
|
|
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: "
|
|
23
|
-
images: [
|
|
111
|
+
text: "woman in her 30s, brown hair, green eyes",
|
|
112
|
+
images: [referenceImageData],
|
|
24
113
|
},
|
|
25
114
|
});
|
|
26
115
|
|
|
27
|
-
//
|
|
28
|
-
const { image:
|
|
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}
|
|
119
|
+
prompt: scene`${character} waves hello`,
|
|
31
120
|
});
|
|
32
121
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
38
|
-
images: [
|
|
267
|
+
text: "woman speaking naturally, subtle head movements, friendly expression",
|
|
268
|
+
images: [baseCharacter],
|
|
39
269
|
},
|
|
40
|
-
|
|
270
|
+
model: fal.videoModel("kling-v2.5"),
|
|
41
271
|
});
|
|
42
272
|
|
|
43
|
-
await
|
|
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
|
-
###
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
411
|
+
### elevenlabs
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { elevenlabs } from "vargai/ai";
|
|
55
415
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
420
|
+
// music model
|
|
421
|
+
elevenlabs.musicModel() // music generation
|
|
65
422
|
|
|
66
|
-
|
|
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
|
-
|
|
426
|
+
### higgsfield
|
|
84
427
|
|
|
85
|
-
|
|
428
|
+
```typescript
|
|
429
|
+
import { higgsfield } from "vargai/ai";
|
|
86
430
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
#
|
|
501
|
+
# required
|
|
92
502
|
FAL_API_KEY=fal_xxx
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
519
|
+
## cli
|
|
108
520
|
|
|
109
|
-
|
|
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
|
-
|
|
531
|
+
see [CONTRIBUTING.md](CONTRIBUTING.md) for development setup.
|
|
112
532
|
|
|
113
|
-
##
|
|
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
|
|
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
|
@@ -15,12 +15,12 @@ export interface ImagePlaceholderFallbackOptions {
|
|
|
15
15
|
export function imagePlaceholderFallbackMiddleware(
|
|
16
16
|
options: ImagePlaceholderFallbackOptions,
|
|
17
17
|
): ImageModelV3Middleware {
|
|
18
|
-
const { mode
|
|
18
|
+
const { mode } = options;
|
|
19
19
|
|
|
20
20
|
return {
|
|
21
21
|
specificationVersion: "v3",
|
|
22
22
|
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
23
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
56
|
+
const { mode } = options;
|
|
57
57
|
|
|
58
58
|
return {
|
|
59
59
|
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
60
|
-
|
|
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:
|
|
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
|
-
|
|
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" | "
|
|
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
|
|
58
|
+
const { mode } = options;
|
|
59
59
|
|
|
60
60
|
return {
|
|
61
61
|
wrapGenerate: async ({ doGenerate, params, model }) => {
|
|
62
|
-
|
|
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:
|
|
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
|
-
|
|
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([
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
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="
|
|
55
|
-
description="
|
|
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>
|
|
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 {
|
|
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", "
|
|
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
|
-
|
|
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,
|
package/src/react/index.ts
CHANGED
|
@@ -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 ?? "
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
package/src/react/types.ts
CHANGED
|
@@ -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" | "
|
|
205
|
+
export type RenderMode = "strict" | "preview";
|
|
205
206
|
|
|
206
207
|
export interface DefaultModels {
|
|
207
208
|
image?: ImageModelV3;
|