vargai 0.3.2 → 0.4.0-alpha10
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/biome.json +6 -1
- package/docs/index.html +1130 -0
- package/docs/prompting.md +326 -0
- package/docs/react.md +834 -0
- package/package.json +14 -5
- package/src/cli/commands/index.ts +2 -4
- package/src/cli/commands/render.ts +136 -0
- package/src/cli/commands/studio.ts +47 -0
- package/src/cli/index.ts +6 -1
- package/src/react/elements.ts +146 -0
- package/src/react/examples/branching.tsx +66 -0
- package/src/react/examples/captions-demo.tsx +37 -0
- package/src/react/examples/character-video.tsx +84 -0
- package/src/react/examples/grid.tsx +53 -0
- package/src/react/examples/layouts-demo.tsx +57 -0
- package/src/react/examples/madi.tsx +60 -0
- package/src/react/examples/music-test.tsx +35 -0
- package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
- package/src/react/examples/orange-portrait.tsx +41 -0
- package/src/react/examples/split-element-demo.tsx +60 -0
- package/src/react/examples/split-layout-demo.tsx +60 -0
- package/src/react/examples/split.tsx +41 -0
- package/src/react/examples/video-grid.tsx +46 -0
- package/src/react/index.ts +43 -0
- package/src/react/layouts/grid.tsx +28 -0
- package/src/react/layouts/index.ts +2 -0
- package/src/react/layouts/split.tsx +20 -0
- package/src/react/react.test.ts +309 -0
- package/src/react/render.ts +21 -0
- package/src/react/renderers/animate.ts +59 -0
- package/src/react/renderers/captions.ts +297 -0
- package/src/react/renderers/clip.ts +258 -0
- package/src/react/renderers/context.ts +17 -0
- package/src/react/renderers/image.ts +109 -0
- package/src/react/renderers/index.ts +22 -0
- package/src/react/renderers/music.ts +60 -0
- package/src/react/renderers/packshot.ts +84 -0
- package/src/react/renderers/progress.ts +173 -0
- package/src/react/renderers/render.ts +319 -0
- package/src/react/renderers/slider.ts +69 -0
- package/src/react/renderers/speech.ts +53 -0
- package/src/react/renderers/split.ts +91 -0
- package/src/react/renderers/subtitle.ts +16 -0
- package/src/react/renderers/swipe.ts +75 -0
- package/src/react/renderers/title.ts +17 -0
- package/src/react/renderers/utils.ts +124 -0
- package/src/react/renderers/video.ts +127 -0
- package/src/react/runtime/jsx-dev-runtime.ts +43 -0
- package/src/react/runtime/jsx-runtime.ts +35 -0
- package/src/react/types.ts +239 -0
- package/src/studio/index.ts +26 -0
- package/src/studio/scanner.ts +102 -0
- package/src/studio/server.ts +554 -0
- package/src/studio/stages.ts +251 -0
- package/src/studio/step-renderer.ts +279 -0
- package/src/studio/types.ts +60 -0
- package/src/studio/ui/cache.html +303 -0
- package/src/studio/ui/index.html +1820 -0
- package/tsconfig.cli.json +8 -0
- package/tsconfig.json +6 -2
- package/bun.lock +0 -1255
- package/docs/plan.md +0 -66
- package/docs/todo.md +0 -14
- /package/docs/{varg-sdk.md → sdk.md} +0 -0
package/docs/react.md
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
# varg-react
|
|
2
|
+
|
|
3
|
+
declarative video rendering with ai generation. jsx for videos.
|
|
4
|
+
|
|
5
|
+
## quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun install @vargai/react
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { render, Render, Clip, Image, Title } from "@vargai/react";
|
|
13
|
+
|
|
14
|
+
await render(
|
|
15
|
+
<Render width={1280} height={720}>
|
|
16
|
+
<Clip duration={5}>
|
|
17
|
+
<Image prompt="sunset over ocean, cinematic" />
|
|
18
|
+
<Title position="bottom">beautiful sunset</Title>
|
|
19
|
+
</Clip>
|
|
20
|
+
</Render>,
|
|
21
|
+
{ output: "output/sunset.mp4" }
|
|
22
|
+
);
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## core concepts
|
|
26
|
+
|
|
27
|
+
### everything is cached
|
|
28
|
+
|
|
29
|
+
every element computes a cache key from its props. same props = cache hit.
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
// first run: generates image (~3s)
|
|
33
|
+
// second run: instant cache hit
|
|
34
|
+
<Image prompt="cyberpunk cityscape at night" />
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
change any prop and it regenerates:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<Image prompt="cyberpunk cityscape at night" aspectRatio="9:16" />
|
|
41
|
+
// different aspectRatio = new cache key = regenerates
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### clips and layers
|
|
45
|
+
|
|
46
|
+
clips are sequential. layers within a clip are stacked.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<Render width={1080} height={1920} fps={30}>
|
|
50
|
+
{/* first clip */}
|
|
51
|
+
<Clip duration={5}>
|
|
52
|
+
<Image prompt="coffee shop interior" />
|
|
53
|
+
</Clip>
|
|
54
|
+
|
|
55
|
+
{/* second clip with transition */}
|
|
56
|
+
<Clip duration={3} transition={{ name: "fade", duration: 0.5 }}>
|
|
57
|
+
<Image prompt="park with autumn leaves" />
|
|
58
|
+
</Clip>
|
|
59
|
+
</Render>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### transitions
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
<Clip transition={{ name: "fade", duration: 0.3 }}>
|
|
66
|
+
<Clip transition={{ name: "wipeleft", duration: 0.5 }}>
|
|
67
|
+
<Clip transition={{ name: "slideright", duration: 0.4 }}>
|
|
68
|
+
<Clip transition={{ name: "crossfade", duration: 0.5 }}>
|
|
69
|
+
<Clip transition={{ name: "cube", duration: 0.8 }}>
|
|
70
|
+
<Clip transition={{ name: "directionalwipe", duration: 0.5 }}>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
67 gl-transitions available: `fade`, `crossfade`, `wipeleft`, `wiperight`, `wipeup`, `wipedown`, `slideleft`, `slideright`, `slideup`, `slidedown`, `cube`, `directionalwipe`, `dreamy`, `squareswire`, `radial`, `pixelize`, and more.
|
|
74
|
+
|
|
75
|
+
## rendering images
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { Image } from "@vargai/react";
|
|
79
|
+
|
|
80
|
+
// basic
|
|
81
|
+
<Image prompt="ralph wiggum eating glue, simpsons style" />
|
|
82
|
+
|
|
83
|
+
// with aspect ratio
|
|
84
|
+
<Image prompt="fat tiger lying on couch" aspectRatio="1:1" />
|
|
85
|
+
<Image prompt="luigi in wheelchair racing down hill" aspectRatio="16:9" />
|
|
86
|
+
<Image prompt="south park cartman screaming" aspectRatio="9:16" />
|
|
87
|
+
|
|
88
|
+
// zoom animation (ken burns)
|
|
89
|
+
<Image prompt="epic mountain landscape" zoom="in" />
|
|
90
|
+
<Image prompt="white-teeth black athletic guy flexing" zoom="out" />
|
|
91
|
+
<Image prompt="forest path mysterious" zoom="left" />
|
|
92
|
+
<Image prompt="ocean waves crashing" zoom="right" />
|
|
93
|
+
|
|
94
|
+
// contain with blur (no black bars)
|
|
95
|
+
<Image prompt="fat tiger square photo" resize="contain-blur" />
|
|
96
|
+
|
|
97
|
+
// remove background
|
|
98
|
+
<Image prompt="ralph wiggum transparent" removeBackground />
|
|
99
|
+
|
|
100
|
+
// image-to-image edit
|
|
101
|
+
<Image
|
|
102
|
+
src="./photo.png"
|
|
103
|
+
prompt="make it look like south park style"
|
|
104
|
+
/>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## rendering video
|
|
108
|
+
|
|
109
|
+
### text-to-video generation
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { Video } from "@vargai/react";
|
|
113
|
+
|
|
114
|
+
// generate video from prompt
|
|
115
|
+
<Video prompt="ocean waves crashing on beach, cinematic" model={fal.videoModel("wan-2.5")} />
|
|
116
|
+
|
|
117
|
+
// use existing video file
|
|
118
|
+
<Video src="./interview.mp4" />
|
|
119
|
+
|
|
120
|
+
// with audio and trimming
|
|
121
|
+
<Video src="./clip.mp4" keepAudio volume={0.8} cutFrom={5} cutTo={15} />
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### image-to-video animation
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { Animate } from "@vargai/react";
|
|
128
|
+
|
|
129
|
+
// animate an image
|
|
130
|
+
<Animate
|
|
131
|
+
image={<Image prompt="fat tiger on couch" />}
|
|
132
|
+
motion="fat tiger breathing heavily, belly jiggles"
|
|
133
|
+
duration={5}
|
|
134
|
+
/>
|
|
135
|
+
|
|
136
|
+
// from existing file
|
|
137
|
+
<Animate
|
|
138
|
+
src="./luigi.png"
|
|
139
|
+
motion="wheelchair spinning in circles"
|
|
140
|
+
duration={5}
|
|
141
|
+
/>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## rendering speech
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { Speech } from "@vargai/react";
|
|
148
|
+
|
|
149
|
+
<Clip>
|
|
150
|
+
<Image prompt="ralph wiggum at podium, simpsons style" />
|
|
151
|
+
<Speech voice="adam">
|
|
152
|
+
I'm in danger. Also I'm the CEO now.
|
|
153
|
+
</Speech>
|
|
154
|
+
</Clip>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
voices: `rachel`, `adam`, `bella`, `sam`, `josh`
|
|
158
|
+
|
|
159
|
+
## talking heads
|
|
160
|
+
|
|
161
|
+
compose Image, Animate, and Speech to create talking characters:
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
import { Clip, Image, Animate, Speech } from "@vargai/react";
|
|
165
|
+
import { fal, elevenlabs } from "@vargai/sdk";
|
|
166
|
+
|
|
167
|
+
// define a reusable TalkingHead component
|
|
168
|
+
const TalkingHead = ({ character, voice, children }: {
|
|
169
|
+
character: string;
|
|
170
|
+
voice: string;
|
|
171
|
+
children: string;
|
|
172
|
+
}) => (
|
|
173
|
+
<>
|
|
174
|
+
<Animate
|
|
175
|
+
image={<Image prompt={character} model={fal.imageModel("flux-schnell")} />}
|
|
176
|
+
model={fal.videoModel("wan-2.5")}
|
|
177
|
+
motion="subtle talking, head movements, blinking"
|
|
178
|
+
/>
|
|
179
|
+
<Speech voice={voice} model={elevenlabs.speechModel("turbo")}>
|
|
180
|
+
{children}
|
|
181
|
+
</Speech>
|
|
182
|
+
</>
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
// use it
|
|
186
|
+
<Clip duration="auto">
|
|
187
|
+
<TalkingHead character="south park cartman, angry face" voice="adam">
|
|
188
|
+
Screw you guys, I'm going home. But first let me tell you
|
|
189
|
+
about our sponsor, NordVPN.
|
|
190
|
+
</TalkingHead>
|
|
191
|
+
</Clip>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
this internally:
|
|
195
|
+
1. generates character image from prompt
|
|
196
|
+
2. generates speech audio from text
|
|
197
|
+
3. animates image to video
|
|
198
|
+
4. sets clip duration to match audio length
|
|
199
|
+
|
|
200
|
+
all steps cached independently.
|
|
201
|
+
|
|
202
|
+
### with existing image
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
const TalkingHead = ({ src, voice, children }) => (
|
|
206
|
+
<>
|
|
207
|
+
<Animate
|
|
208
|
+
src={src}
|
|
209
|
+
model={fal.videoModel("wan-2.5")}
|
|
210
|
+
motion="talking naturally"
|
|
211
|
+
/>
|
|
212
|
+
<Speech voice={voice} model={elevenlabs.speechModel("turbo")}>
|
|
213
|
+
{children}
|
|
214
|
+
</Speech>
|
|
215
|
+
</>
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
<Clip duration="auto">
|
|
219
|
+
<TalkingHead src="./fat-tiger.png" voice="josh">
|
|
220
|
+
I'm not fat, I'm cultivating mass.
|
|
221
|
+
</TalkingHead>
|
|
222
|
+
</Clip>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## overlays and pip
|
|
226
|
+
|
|
227
|
+
layer children stack on top of each other.
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
<Clip duration={5}>
|
|
231
|
+
{/* base layer - full frame */}
|
|
232
|
+
<Image prompt="luigi wheelchair racing track" />
|
|
233
|
+
|
|
234
|
+
{/* picture-in-picture overlay */}
|
|
235
|
+
<TalkingHead
|
|
236
|
+
character="white-teeth black athletic guy, sports commentator"
|
|
237
|
+
voice="josh"
|
|
238
|
+
position={{ right: "5%", bottom: "5%" }}
|
|
239
|
+
size={{ width: "25%", height: "25%" }}
|
|
240
|
+
>
|
|
241
|
+
And he's approaching the final turn! Incredible speed!
|
|
242
|
+
</TalkingHead>
|
|
243
|
+
</Clip>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### position presets
|
|
247
|
+
|
|
248
|
+
```tsx
|
|
249
|
+
<Image position="top-left" />
|
|
250
|
+
<Image position="top" />
|
|
251
|
+
<Image position="top-right" />
|
|
252
|
+
<Image position="left" />
|
|
253
|
+
<Image position="center" />
|
|
254
|
+
<Image position="right" />
|
|
255
|
+
<Image position="bottom-left" />
|
|
256
|
+
<Image position="bottom" />
|
|
257
|
+
<Image position="bottom-right" />
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## text overlays
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
import { Title, Subtitle } from "@vargai/react";
|
|
264
|
+
|
|
265
|
+
<Clip duration={5}>
|
|
266
|
+
<Image prompt="ralph wiggum staring blankly" />
|
|
267
|
+
|
|
268
|
+
{/* centered title */}
|
|
269
|
+
<Title>I'M IN DANGER</Title>
|
|
270
|
+
|
|
271
|
+
{/* positioned title */}
|
|
272
|
+
<Title position="top" color="#ffffff">
|
|
273
|
+
Episode 1
|
|
274
|
+
</Title>
|
|
275
|
+
|
|
276
|
+
{/* subtitle with background */}
|
|
277
|
+
<Subtitle>the beginning of something special</Subtitle>
|
|
278
|
+
</Clip>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### text timing
|
|
282
|
+
|
|
283
|
+
text appears and disappears within the clip:
|
|
284
|
+
|
|
285
|
+
```tsx
|
|
286
|
+
<Clip duration={10}>
|
|
287
|
+
<Image prompt="fat tiger sleeping timelapse" />
|
|
288
|
+
|
|
289
|
+
{/* appears at 2s, disappears at 5s */}
|
|
290
|
+
<Title start={2} end={5}>
|
|
291
|
+
Hour 1: Still sleeping
|
|
292
|
+
</Title>
|
|
293
|
+
|
|
294
|
+
{/* appears at 6s, stays until clip ends */}
|
|
295
|
+
<Title start={6}>
|
|
296
|
+
Hour 47: Still sleeping
|
|
297
|
+
</Title>
|
|
298
|
+
</Clip>
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
## captions (tiktok-style)
|
|
302
|
+
|
|
303
|
+
word-by-word animated captions synced to speech:
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
import { Captions } from "@vargai/react";
|
|
307
|
+
|
|
308
|
+
<Clip>
|
|
309
|
+
<Video src="./cartman-rant.mp4" />
|
|
310
|
+
<Captions
|
|
311
|
+
src="./cartman-rant.mp4"
|
|
312
|
+
style="tiktok"
|
|
313
|
+
color="#ffffff"
|
|
314
|
+
activeColor="#ffff00"
|
|
315
|
+
fontSize={48}
|
|
316
|
+
/>
|
|
317
|
+
</Clip>
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
or feed it a speech element directly:
|
|
321
|
+
|
|
322
|
+
```tsx
|
|
323
|
+
<Clip>
|
|
324
|
+
<Image prompt="ralph wiggum at podium" />
|
|
325
|
+
<Speech voice="adam" id="ralph-speech">
|
|
326
|
+
I'm in danger. The danger is me.
|
|
327
|
+
</Speech>
|
|
328
|
+
<Captions
|
|
329
|
+
src={ralph-speech}
|
|
330
|
+
style="tiktok"
|
|
331
|
+
/>
|
|
332
|
+
</Clip>
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### caption styles
|
|
336
|
+
|
|
337
|
+
```tsx
|
|
338
|
+
<Captions src="./audio.mp3" style="tiktok" /> // word-by-word highlight
|
|
339
|
+
<Captions src="./audio.mp3" style="karaoke" /> // fill left-to-right
|
|
340
|
+
<Captions src="./audio.mp3" style="bounce" /> // words bounce in
|
|
341
|
+
<Captions src="./audio.mp3" style="typewriter" /> // typing effect
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### with custom transcript
|
|
345
|
+
|
|
346
|
+
```tsx
|
|
347
|
+
<Clip>
|
|
348
|
+
<Video src="./video.mp4" />
|
|
349
|
+
<Captions srt="./captions.srt" style="tiktok" />
|
|
350
|
+
</Clip>
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## split screen
|
|
354
|
+
|
|
355
|
+
```tsx
|
|
356
|
+
import { Split } from "@vargai/react";
|
|
357
|
+
|
|
358
|
+
<Clip duration={5}>
|
|
359
|
+
<Split direction="horizontal">
|
|
360
|
+
<Animate
|
|
361
|
+
image={<Image prompt="fat tiger on couch, lazy" />}
|
|
362
|
+
motion="breathing heavily, not moving"
|
|
363
|
+
/>
|
|
364
|
+
<Animate
|
|
365
|
+
image={<Image prompt="fat tiger slightly less fat, still on couch" />}
|
|
366
|
+
motion="breathing slightly less heavily"
|
|
367
|
+
/>
|
|
368
|
+
</Split>
|
|
369
|
+
<Title position="bottom">WEEK 1 / WEEK 52</Title>
|
|
370
|
+
</Clip>
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## slider (before/after reveal)
|
|
374
|
+
|
|
375
|
+
animated wipe reveal between two images:
|
|
376
|
+
|
|
377
|
+
```tsx
|
|
378
|
+
import { Slider } from "@vargai/react";
|
|
379
|
+
|
|
380
|
+
<Clip duration={5}>
|
|
381
|
+
<Slider direction="horizontal">
|
|
382
|
+
<Image prompt="luigi standing normally" />
|
|
383
|
+
<Image prompt="luigi in wheelchair, looking defeated" />
|
|
384
|
+
</Slider>
|
|
385
|
+
<Title position="top">What racing does to a mf</Title>
|
|
386
|
+
</Clip>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
the slider animates from left to right, revealing the second image.
|
|
390
|
+
|
|
391
|
+
## swipe animation
|
|
392
|
+
|
|
393
|
+
tinder-style card swipes:
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
import { Swipe } from "@vargai/react";
|
|
397
|
+
|
|
398
|
+
<Clip duration={6}>
|
|
399
|
+
<Swipe direction="right" interval={1.5}>
|
|
400
|
+
<Image prompt="ralph wiggum dating profile, eating paste" />
|
|
401
|
+
<Image prompt="fat tiger dating profile, lying down" />
|
|
402
|
+
<Image prompt="luigi wheelchair dating profile, sad eyes" />
|
|
403
|
+
<Image prompt="white-teeth guy dating profile, perfect smile" />
|
|
404
|
+
</Swipe>
|
|
405
|
+
</Clip>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## packshot (end card)
|
|
409
|
+
|
|
410
|
+
end card with call-to-action button:
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
import { Packshot } from "@vargai/react";
|
|
414
|
+
|
|
415
|
+
<Clip duration={4}>
|
|
416
|
+
<Packshot
|
|
417
|
+
background={<Image prompt="south park style gradient" />}
|
|
418
|
+
logo="./cartman-enterprises.png"
|
|
419
|
+
cta="Respect My Authority"
|
|
420
|
+
ctaColor="#ff5500"
|
|
421
|
+
blinkCta
|
|
422
|
+
/>
|
|
423
|
+
</Clip>
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## audio
|
|
427
|
+
|
|
428
|
+
### background music
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
<Render>
|
|
432
|
+
{/* loops to match video duration */}
|
|
433
|
+
<Music prompt="epic orchestral, hero music" loop />
|
|
434
|
+
|
|
435
|
+
<Clip duration={5}>
|
|
436
|
+
<Image prompt="luigi wheelchair training montage" />
|
|
437
|
+
</Clip>
|
|
438
|
+
|
|
439
|
+
<Clip duration={5}>
|
|
440
|
+
<Image prompt="luigi wheelchair racing final lap" />
|
|
441
|
+
</Clip>
|
|
442
|
+
</Render>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
### preserve source audio
|
|
446
|
+
|
|
447
|
+
```tsx
|
|
448
|
+
<Clip>
|
|
449
|
+
<Video src="./interview.mp4" keepAudio volume={0.8} />
|
|
450
|
+
</Clip>
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### mix multiple audio
|
|
454
|
+
|
|
455
|
+
```tsx
|
|
456
|
+
<Clip duration={10}>
|
|
457
|
+
<Video src="./fat-tiger-sleeping.mp4" keepAudio volume={0.3} />
|
|
458
|
+
<Speech voice="sam" volume={1.0}>
|
|
459
|
+
Day 47. He still hasn't moved. Scientists are baffled.
|
|
460
|
+
</Speech>
|
|
461
|
+
</Clip>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### audio ducking
|
|
465
|
+
|
|
466
|
+
automatically lower music when speech plays:
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
<Render>
|
|
470
|
+
<Music prompt="dramatic documentary music" loop ducking />
|
|
471
|
+
|
|
472
|
+
<Clip duration={5}>
|
|
473
|
+
<Image prompt="ralph wiggum walking into frame" />
|
|
474
|
+
</Clip>
|
|
475
|
+
|
|
476
|
+
<Clip duration={10}>
|
|
477
|
+
<Image prompt="ralph wiggum close up, confused" />
|
|
478
|
+
<Speech voice="josh">
|
|
479
|
+
This is ralph. He doesn't know where he is. Neither do we.
|
|
480
|
+
</Speech>
|
|
481
|
+
</Clip>
|
|
482
|
+
</Render>
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### audio normalization
|
|
486
|
+
|
|
487
|
+
```tsx
|
|
488
|
+
<Render normalize>
|
|
489
|
+
{/* all audio levels balanced automatically */}
|
|
490
|
+
<Clip>
|
|
491
|
+
<Video src="./loud-clip.mp4" keepAudio />
|
|
492
|
+
</Clip>
|
|
493
|
+
<Clip>
|
|
494
|
+
<Video src="./quiet-clip.mp4" keepAudio />
|
|
495
|
+
</Clip>
|
|
496
|
+
</Render>
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
## character consistency
|
|
500
|
+
|
|
501
|
+
reuse the same image reference for consistency:
|
|
502
|
+
|
|
503
|
+
```tsx
|
|
504
|
+
const luigi = <Image prompt="luigi in wheelchair, determined face, mario kart style" />;
|
|
505
|
+
|
|
506
|
+
<Render>
|
|
507
|
+
<Clip duration={3}>
|
|
508
|
+
{luigi}
|
|
509
|
+
<Title>ORIGIN STORY</Title>
|
|
510
|
+
</Clip>
|
|
511
|
+
|
|
512
|
+
<Clip duration={5}>
|
|
513
|
+
<Animate image={luigi} motion="wheelchair wheels spinning, wind in mustache" />
|
|
514
|
+
</Clip>
|
|
515
|
+
|
|
516
|
+
<Clip duration={3}>
|
|
517
|
+
{luigi}
|
|
518
|
+
<Title>HE NEVER RECOVERED</Title>
|
|
519
|
+
</Clip>
|
|
520
|
+
</Render>
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
same `luigi` reference = same cache key = same generated image.
|
|
524
|
+
|
|
525
|
+
## elements and scene composition
|
|
526
|
+
|
|
527
|
+
define reusable elements for consistent generation:
|
|
528
|
+
|
|
529
|
+
```tsx
|
|
530
|
+
import { Element, scene } from "@vargai/react";
|
|
531
|
+
|
|
532
|
+
// define a character element
|
|
533
|
+
const tiger = Element({
|
|
534
|
+
type: "character",
|
|
535
|
+
prompt: "fat tiger, orange stripes, chubby cheeks, sleepy eyes",
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
// define a style element
|
|
539
|
+
const style = Element({
|
|
540
|
+
type: "style",
|
|
541
|
+
prompt: "pixar animation style, soft lighting",
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
// use in scene template
|
|
545
|
+
<Image prompt={scene`${tiger} lying on couch, ${style}`} />
|
|
546
|
+
<Image prompt={scene`${tiger} attempting to exercise, ${style}`} />
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
elements ensure visual consistency across multiple generations.
|
|
550
|
+
|
|
551
|
+
## slideshow
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
const SCENES = [
|
|
555
|
+
"at the gym, confused by equipment",
|
|
556
|
+
"eating entire pizza, no regrets",
|
|
557
|
+
"attempting yoga, stuck",
|
|
558
|
+
"napping on treadmill",
|
|
559
|
+
];
|
|
560
|
+
|
|
561
|
+
const character = "fat tiger, chubby, adorable";
|
|
562
|
+
|
|
563
|
+
<Render width={1280} height={720}>
|
|
564
|
+
<Music prompt="motivational gym music, ironic" loop />
|
|
565
|
+
|
|
566
|
+
{SCENES.map((scene, i) => (
|
|
567
|
+
<Clip key={i} duration={3} transition={{ name: "fade", duration: 0.3 }}>
|
|
568
|
+
<Image prompt={`${character}, ${scene}`} zoom="in" />
|
|
569
|
+
</Clip>
|
|
570
|
+
))}
|
|
571
|
+
</Render>
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## character grid
|
|
575
|
+
|
|
576
|
+
```tsx
|
|
577
|
+
const CHARACTERS = [
|
|
578
|
+
{ name: "Ralph", prompt: "ralph wiggum, eating glue, happy" },
|
|
579
|
+
{ name: "Fat Tiger", prompt: "fat tiger, lying down, exhausted" },
|
|
580
|
+
{ name: "Luigi", prompt: "luigi in wheelchair, sad but determined" },
|
|
581
|
+
{ name: "The Smile", prompt: "white-teeth black athletic guy, perfect smile, shiny" },
|
|
582
|
+
];
|
|
583
|
+
|
|
584
|
+
<Render width={720} height={720}>
|
|
585
|
+
{CHARACTERS.map(({ name, prompt }) => (
|
|
586
|
+
<Clip key={name} duration={2} transition={{ name: "fade", duration: 0.3 }}>
|
|
587
|
+
<Image prompt={`${prompt}, portrait, meme style`} aspectRatio="1:1" />
|
|
588
|
+
<Title position="bottom" color="#ffffff">{name}</Title>
|
|
589
|
+
</Clip>
|
|
590
|
+
))}
|
|
591
|
+
</Render>
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
## conditional rendering
|
|
595
|
+
|
|
596
|
+
standard jsx conditionals:
|
|
597
|
+
|
|
598
|
+
```tsx
|
|
599
|
+
<Render>
|
|
600
|
+
<Clip duration={5}>
|
|
601
|
+
<Image prompt="south park cartman main scene" />
|
|
602
|
+
</Clip>
|
|
603
|
+
|
|
604
|
+
{includeOutro && (
|
|
605
|
+
<Clip duration={3}>
|
|
606
|
+
<Image prompt="cartman waving goodbye" />
|
|
607
|
+
<Title>Screw you guys!</Title>
|
|
608
|
+
</Clip>
|
|
609
|
+
)}
|
|
610
|
+
|
|
611
|
+
{sponsor && (
|
|
612
|
+
<Clip duration={5}>
|
|
613
|
+
<Image prompt="cartman holding sponsor product" />
|
|
614
|
+
<Speech voice="adam">{sponsor.script}</Speech>
|
|
615
|
+
</Clip>
|
|
616
|
+
)}
|
|
617
|
+
</Render>
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## render options
|
|
621
|
+
|
|
622
|
+
```tsx
|
|
623
|
+
import { render } from "@vargai/react";
|
|
624
|
+
|
|
625
|
+
// save to file
|
|
626
|
+
await render(<Render>...</Render>, {
|
|
627
|
+
output: "output/video.mp4"
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// with cache directory
|
|
631
|
+
await render(<Render>...</Render>, {
|
|
632
|
+
output: "output/video.mp4",
|
|
633
|
+
cache: ".cache/ai"
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// get buffer
|
|
637
|
+
const buffer = await render(<Render>...</Render>);
|
|
638
|
+
await Bun.write("video.mp4", buffer);
|
|
639
|
+
|
|
640
|
+
// stream progress
|
|
641
|
+
const stream = render.stream(<Render>...</Render>);
|
|
642
|
+
for await (const event of stream) {
|
|
643
|
+
console.log(`${event.type}: ${event.progress}%`);
|
|
644
|
+
// "generating image: 45%"
|
|
645
|
+
// "generating speech: 100%"
|
|
646
|
+
// "rendering clip: 30%"
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
## full example: ralph explains crypto
|
|
651
|
+
|
|
652
|
+
```tsx
|
|
653
|
+
import { render, Render, Clip, Image, Animate, Speech, Title } from "@vargai/react";
|
|
654
|
+
import { fal, elevenlabs } from "@vargai/sdk";
|
|
655
|
+
|
|
656
|
+
// TalkingHead is just a composition of primitives
|
|
657
|
+
const TalkingHead = ({ character, voice, children }: {
|
|
658
|
+
character: string;
|
|
659
|
+
voice: string;
|
|
660
|
+
children: string;
|
|
661
|
+
}) => (
|
|
662
|
+
<Clip duration="auto">
|
|
663
|
+
<Animate
|
|
664
|
+
image={<Image prompt={character} model={fal.imageModel("flux-schnell")} />}
|
|
665
|
+
model={fal.videoModel("wan-2.5")}
|
|
666
|
+
motion="subtle head movements, blinking, mouth moving"
|
|
667
|
+
/>
|
|
668
|
+
<Speech voice={voice} model={elevenlabs.speechModel("turbo")}>
|
|
669
|
+
{children}
|
|
670
|
+
</Speech>
|
|
671
|
+
</Clip>
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
const script = `Hi, I'm Ralph! My cat's breath smells like cat food.
|
|
675
|
+
Also I invested my lunch money in dogecoin.
|
|
676
|
+
Now I live in a box. But it's a nice box!
|
|
677
|
+
It has a window. The window is a hole.`;
|
|
678
|
+
|
|
679
|
+
await render(
|
|
680
|
+
<Render width={1080} height={1920}>
|
|
681
|
+
<TalkingHead
|
|
682
|
+
character="ralph wiggum, simpsons style, innocent smile, slightly confused"
|
|
683
|
+
voice="adam"
|
|
684
|
+
>
|
|
685
|
+
{script}
|
|
686
|
+
</TalkingHead>
|
|
687
|
+
|
|
688
|
+
<Clip duration={2} transition={{ name: "fade", duration: 0.5 }}>
|
|
689
|
+
<Image
|
|
690
|
+
prompt="ralph wiggum in cardboard box, happy, simpsons style"
|
|
691
|
+
model={fal.imageModel("flux-schnell")}
|
|
692
|
+
zoom="in"
|
|
693
|
+
/>
|
|
694
|
+
<Title position="bottom">@RalphInvests</Title>
|
|
695
|
+
</Clip>
|
|
696
|
+
</Render>,
|
|
697
|
+
{
|
|
698
|
+
output: "output/ralph-crypto.mp4",
|
|
699
|
+
cache: ".cache/ai"
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
## full example: wheelchair racing promo
|
|
705
|
+
|
|
706
|
+
```tsx
|
|
707
|
+
import { render, Render, Clip, Image, Animate, Title, Subtitle, Music } from "@vargai/react";
|
|
708
|
+
|
|
709
|
+
const luigi = <Image prompt="luigi in racing wheelchair, determined, mario kart style" />;
|
|
710
|
+
|
|
711
|
+
await render(
|
|
712
|
+
<Render width={1080} height={1920}>
|
|
713
|
+
<Music prompt="epic racing music, fast drums, intense" loop />
|
|
714
|
+
|
|
715
|
+
<Clip duration={3}>
|
|
716
|
+
{luigi}
|
|
717
|
+
<Title start={1}>This Summer</Title>
|
|
718
|
+
</Clip>
|
|
719
|
+
|
|
720
|
+
<Clip duration={5} transition={{ name: "fade", duration: 0.5 }}>
|
|
721
|
+
<Animate
|
|
722
|
+
image={luigi}
|
|
723
|
+
motion="wheelchair wheels spinning fast, wind effects, speed lines"
|
|
724
|
+
/>
|
|
725
|
+
<Title position="bottom">NO LIMITS</Title>
|
|
726
|
+
</Clip>
|
|
727
|
+
|
|
728
|
+
<Clip duration={3} transition={{ name: "fade", duration: 0.5 }}>
|
|
729
|
+
{luigi}
|
|
730
|
+
<Title>LUIGI KART: WHEELCHAIR EDITION</Title>
|
|
731
|
+
<Subtitle>He can't walk but he can win</Subtitle>
|
|
732
|
+
</Clip>
|
|
733
|
+
</Render>,
|
|
734
|
+
{ output: "output/luigi-promo.mp4" }
|
|
735
|
+
);
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
## full example: fat tiger's fitness journey
|
|
739
|
+
|
|
740
|
+
```tsx
|
|
741
|
+
import { render, Render, Clip, Image, Animate, Split, Title } from "@vargai/react";
|
|
742
|
+
|
|
743
|
+
const tiger = "fat tiger, orange stripes, cute, pixar style";
|
|
744
|
+
|
|
745
|
+
const before = <Image prompt={`${tiger}, extremely chubby, on couch, pizza boxes`} aspectRatio="3:4" />;
|
|
746
|
+
const after = <Image prompt={`${tiger}, slightly less chubby, still on couch, salad nearby unopened`} aspectRatio="3:4" />;
|
|
747
|
+
|
|
748
|
+
await render(
|
|
749
|
+
<Render width={1280} height={720}>
|
|
750
|
+
<Clip duration={5}>
|
|
751
|
+
<Split direction="horizontal">
|
|
752
|
+
<Animate image={before} motion="breathing heavily, belly jiggles" />
|
|
753
|
+
<Animate image={after} motion="breathing slightly less heavily, one ear twitches" />
|
|
754
|
+
</Split>
|
|
755
|
+
<Title position="bottom" color="#ffffff">
|
|
756
|
+
DAY 1 DAY 365
|
|
757
|
+
</Title>
|
|
758
|
+
</Clip>
|
|
759
|
+
</Render>,
|
|
760
|
+
{ output: "output/tiger-transformation.mp4" }
|
|
761
|
+
);
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
## models
|
|
765
|
+
|
|
766
|
+
specify which ai model to use:
|
|
767
|
+
|
|
768
|
+
```tsx
|
|
769
|
+
import { Image, Animate, Speech, TalkingHead } from "@vargai/react";
|
|
770
|
+
import { fal, openai, replicate, elevenlabs, higgsfield } from "@vargai/sdk";
|
|
771
|
+
|
|
772
|
+
// image models
|
|
773
|
+
<Image prompt="sunset" model={fal.imageModel("flux-schnell")} />
|
|
774
|
+
<Image prompt="sunset" model={fal.imageModel("flux-pro")} />
|
|
775
|
+
<Image prompt="sunset" model={openai.imageModel("dall-e-3")} />
|
|
776
|
+
<Image prompt="sunset" model={replicate.imageModel("sdxl")} />
|
|
777
|
+
|
|
778
|
+
// video models
|
|
779
|
+
<Animate model={fal.videoModel("kling-v2.5")} motion="slow zoom" />
|
|
780
|
+
<Animate model={fal.videoModel("wan-2.5")} motion="camera pan" />
|
|
781
|
+
<Animate model={higgsfield.videoModel("soul")} motion="walking" />
|
|
782
|
+
|
|
783
|
+
// speech models
|
|
784
|
+
<Speech model={elevenlabs.speechModel("turbo")} voice="rachel">
|
|
785
|
+
hello world
|
|
786
|
+
</Speech>
|
|
787
|
+
|
|
788
|
+
// talking head with lipsync
|
|
789
|
+
<TalkingHead
|
|
790
|
+
model={fal.videoModel("wan-2.5")}
|
|
791
|
+
lipsyncModel={fal.videoModel("sync-v2")}
|
|
792
|
+
voice="adam"
|
|
793
|
+
>
|
|
794
|
+
this syncs lips to speech
|
|
795
|
+
</TalkingHead>
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### higgsfield characters
|
|
799
|
+
|
|
800
|
+
for consistent character animation:
|
|
801
|
+
|
|
802
|
+
```tsx
|
|
803
|
+
<Render>
|
|
804
|
+
<Clip duration={5}>
|
|
805
|
+
<Animate
|
|
806
|
+
model={higgsfield.videoModel("soul")}
|
|
807
|
+
character="white-teeth-guy"
|
|
808
|
+
motion="walking forward with perfect posture, teeth gleaming"
|
|
809
|
+
/>
|
|
810
|
+
</Clip>
|
|
811
|
+
|
|
812
|
+
<Clip duration={5}>
|
|
813
|
+
<Animate
|
|
814
|
+
model={higgsfield.videoModel("soul")}
|
|
815
|
+
character="white-teeth-guy"
|
|
816
|
+
motion="pointing at camera, smile intensifies"
|
|
817
|
+
/>
|
|
818
|
+
</Clip>
|
|
819
|
+
</Render>
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
same `character` id = consistent appearance across clips.
|
|
823
|
+
|
|
824
|
+
## why varg-react?
|
|
825
|
+
|
|
826
|
+
| imperative (current) | declarative (varg-react) |
|
|
827
|
+
|---------------------|-------------------------|
|
|
828
|
+
| manual cache keys | automatic from props |
|
|
829
|
+
| step-by-step generation | parallel where possible |
|
|
830
|
+
| explicit file handling | automatic temp files |
|
|
831
|
+
| editly config objects | jsx composition |
|
|
832
|
+
| ~50 lines for talking head | ~10 lines |
|
|
833
|
+
|
|
834
|
+
same power, less code, automatic caching.
|