vargai 0.4.0-alpha23 → 0.4.0-alpha25
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/package.json +1 -1
- package/pipeline/cookbooks/round-video-character.md +1 -1
- package/src/ai-sdk/providers/fal.ts +4 -2
- package/src/cli/commands/index.ts +1 -0
- package/src/cli/commands/render.tsx +29 -13
- package/src/cli/commands/storyboard.tsx +878 -0
- package/src/cli/index.ts +4 -0
- package/src/providers/fal.ts +5 -0
- package/src/react/examples/quickstart-test.tsx +27 -23
- package/src/studio/ui/index.html +2 -2
- package/src/tests/all.test.ts +4 -4
- package/src/tests/index.ts +1 -1
package/package.json
CHANGED
|
@@ -321,7 +321,7 @@ see all voices: `bun run lib/elevenlabs.ts voices`
|
|
|
321
321
|
```bash
|
|
322
322
|
# required api keys
|
|
323
323
|
export ELEVENLABS_API_KEY="your_key"
|
|
324
|
-
export
|
|
324
|
+
export FAL_API_KEY="your_key" # for wan-25 and image generation (or set FAL_KEY)
|
|
325
325
|
```
|
|
326
326
|
|
|
327
327
|
## changelog
|
|
@@ -483,8 +483,10 @@ export interface FalProvider extends ProviderV3 {
|
|
|
483
483
|
}
|
|
484
484
|
|
|
485
485
|
export function createFal(settings: FalProviderSettings = {}): FalProvider {
|
|
486
|
-
|
|
487
|
-
|
|
486
|
+
const apiKey =
|
|
487
|
+
settings.apiKey ?? process.env.FAL_API_KEY ?? process.env.FAL_KEY;
|
|
488
|
+
if (apiKey) {
|
|
489
|
+
fal.config({ credentials: apiKey });
|
|
488
490
|
}
|
|
489
491
|
|
|
490
492
|
return {
|
|
@@ -10,5 +10,6 @@ export {
|
|
|
10
10
|
showRenderHelp,
|
|
11
11
|
} from "./render.tsx";
|
|
12
12
|
export { runCmd, showRunHelp, showTargetHelp } from "./run.tsx";
|
|
13
|
+
export { showStoryboardHelp, storyboardCmd } from "./storyboard.tsx";
|
|
13
14
|
export { studioCmd } from "./studio.ts";
|
|
14
15
|
export { showWhichHelp, whichCmd } from "./which.tsx";
|
|
@@ -17,7 +17,8 @@ import { fal, elevenlabs, replicate } from "vargai/ai";
|
|
|
17
17
|
async function detectDefaultModels(): Promise<DefaultModels | undefined> {
|
|
18
18
|
const defaults: DefaultModels = {};
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
const falKey = process.env.FAL_API_KEY ?? process.env.FAL_KEY;
|
|
21
|
+
if (falKey) {
|
|
21
22
|
const { fal } = await import("../../ai-sdk/providers/fal");
|
|
22
23
|
defaults.image = fal.imageModel("flux-schnell");
|
|
23
24
|
defaults.video = fal.videoModel("wan-2.5");
|
|
@@ -36,29 +37,44 @@ async function loadComponent(filePath: string): Promise<VargElement> {
|
|
|
36
37
|
const resolvedPath = resolve(filePath);
|
|
37
38
|
const source = await Bun.file(resolvedPath).text();
|
|
38
39
|
|
|
39
|
-
const hasAnyImport = source.includes(" from ");
|
|
40
40
|
const hasVargaiImport =
|
|
41
41
|
source.includes("from 'vargai") ||
|
|
42
42
|
source.includes('from "vargai') ||
|
|
43
|
-
source.includes("
|
|
44
|
-
source.includes('from "@vargai');
|
|
45
|
-
|
|
46
|
-
const hasJsxPragma =
|
|
47
|
-
source.includes("@jsxImportSource") || source.includes("@jsx ");
|
|
43
|
+
source.includes("@jsxImportSource vargai");
|
|
48
44
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const mod = await import(resolvedPath);
|
|
52
|
-
return mod.default;
|
|
53
|
-
}
|
|
45
|
+
const hasRelativeImport =
|
|
46
|
+
source.includes("from './") || source.includes('from "./');
|
|
54
47
|
|
|
55
|
-
// no imports - inject auto-imports and jsx pragma
|
|
56
48
|
const pkgDir = new URL("../../..", import.meta.url).pathname;
|
|
57
49
|
const tmpDir = `${pkgDir}/.cache/varg-render`;
|
|
50
|
+
|
|
58
51
|
if (!existsSync(tmpDir)) {
|
|
59
52
|
mkdirSync(tmpDir, { recursive: true });
|
|
60
53
|
}
|
|
61
54
|
|
|
55
|
+
if (hasRelativeImport) {
|
|
56
|
+
const mod = await import(resolvedPath);
|
|
57
|
+
return mod.default;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (hasVargaiImport) {
|
|
61
|
+
const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
|
|
62
|
+
await Bun.write(tmpFile, source);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const mod = await import(tmpFile);
|
|
66
|
+
return mod.default;
|
|
67
|
+
} finally {
|
|
68
|
+
(await Bun.file(tmpFile).exists()) && (await Bun.write(tmpFile, ""));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const hasAnyImport = source.includes(" from ");
|
|
73
|
+
if (hasAnyImport) {
|
|
74
|
+
const mod = await import(resolvedPath);
|
|
75
|
+
return mod.default;
|
|
76
|
+
}
|
|
77
|
+
|
|
62
78
|
const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
|
|
63
79
|
await Bun.write(tmpFile, AUTO_IMPORTS + source);
|
|
64
80
|
|
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
/** @jsxImportSource react */
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { basename, dirname, resolve } from "node:path";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
import { Box, Text } from "ink";
|
|
7
|
+
import type {
|
|
8
|
+
CaptionsProps,
|
|
9
|
+
ClipProps,
|
|
10
|
+
ImageProps,
|
|
11
|
+
MusicProps,
|
|
12
|
+
PackshotProps,
|
|
13
|
+
SliderProps,
|
|
14
|
+
SpeechProps,
|
|
15
|
+
SplitProps,
|
|
16
|
+
SwipeProps,
|
|
17
|
+
TalkingHeadProps,
|
|
18
|
+
TitleProps,
|
|
19
|
+
VargElement,
|
|
20
|
+
VargNode,
|
|
21
|
+
VideoProps,
|
|
22
|
+
} from "../../react/types";
|
|
23
|
+
import { Header, HelpBlock, VargBox, VargText } from "../ui/index.ts";
|
|
24
|
+
import { renderStatic } from "../ui/render.ts";
|
|
25
|
+
|
|
26
|
+
const AUTO_IMPORTS = `/** @jsxImportSource vargai */
|
|
27
|
+
import { Captions, Clip, Image, Music, Overlay, Packshot, Render, Slider, Speech, Split, Subtitle, Swipe, TalkingHead, Title, Video, Grid, SplitLayout } from "vargai/react";
|
|
28
|
+
import { fal, elevenlabs, replicate } from "vargai/ai";
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
interface StoryboardClip {
|
|
32
|
+
index: number;
|
|
33
|
+
duration: number | "auto";
|
|
34
|
+
transition?: string;
|
|
35
|
+
elements: StoryboardElement[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface StoryboardElement {
|
|
39
|
+
type: string;
|
|
40
|
+
prompt?: string;
|
|
41
|
+
src?: string;
|
|
42
|
+
text?: string;
|
|
43
|
+
voice?: string;
|
|
44
|
+
model?: string;
|
|
45
|
+
details: Record<string, unknown>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Storyboard {
|
|
49
|
+
width: number;
|
|
50
|
+
height: number;
|
|
51
|
+
fps: number;
|
|
52
|
+
clips: StoryboardClip[];
|
|
53
|
+
globalElements: StoryboardElement[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadComponent(filePath: string): Promise<VargElement> {
|
|
57
|
+
const resolvedPath = resolve(filePath);
|
|
58
|
+
const source = await Bun.file(resolvedPath).text();
|
|
59
|
+
|
|
60
|
+
const hasVargaiImport =
|
|
61
|
+
source.includes("from 'vargai") ||
|
|
62
|
+
source.includes('from "vargai') ||
|
|
63
|
+
source.includes("@jsxImportSource vargai");
|
|
64
|
+
|
|
65
|
+
const hasRelativeImport =
|
|
66
|
+
source.includes("from './") || source.includes('from "./');
|
|
67
|
+
|
|
68
|
+
const pkgDir = new URL("../../..", import.meta.url).pathname;
|
|
69
|
+
const tmpDir = `${pkgDir}/.cache/varg-storyboard`;
|
|
70
|
+
|
|
71
|
+
if (!existsSync(tmpDir)) {
|
|
72
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (hasRelativeImport) {
|
|
76
|
+
const mod = await import(resolvedPath);
|
|
77
|
+
return mod.default;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (hasVargaiImport) {
|
|
81
|
+
const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
|
|
82
|
+
await Bun.write(tmpFile, source);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const mod = await import(tmpFile);
|
|
86
|
+
return mod.default;
|
|
87
|
+
} finally {
|
|
88
|
+
(await Bun.file(tmpFile).exists()) && (await Bun.write(tmpFile, ""));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const hasAnyImport = source.includes(" from ");
|
|
93
|
+
if (hasAnyImport) {
|
|
94
|
+
const mod = await import(resolvedPath);
|
|
95
|
+
return mod.default;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const tmpFile = `${tmpDir}/${Date.now()}.tsx`;
|
|
99
|
+
await Bun.write(tmpFile, AUTO_IMPORTS + source);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const mod = await import(tmpFile);
|
|
103
|
+
return mod.default;
|
|
104
|
+
} finally {
|
|
105
|
+
(await Bun.file(tmpFile).exists()) && (await Bun.write(tmpFile, ""));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getPromptText(prompt: unknown): string | undefined {
|
|
110
|
+
if (typeof prompt === "string") return prompt;
|
|
111
|
+
if (prompt && typeof prompt === "object" && "text" in prompt) {
|
|
112
|
+
return (prompt as { text?: string }).text;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getModelName(model: unknown): string | undefined {
|
|
118
|
+
if (!model) return undefined;
|
|
119
|
+
if (typeof model === "string") return model;
|
|
120
|
+
if (typeof model === "object" && "modelId" in model) {
|
|
121
|
+
return (model as { modelId: string }).modelId;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function extractElementInfo(element: VargElement): StoryboardElement {
|
|
127
|
+
const base: StoryboardElement = {
|
|
128
|
+
type: element.type,
|
|
129
|
+
details: {},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
switch (element.type) {
|
|
133
|
+
case "image": {
|
|
134
|
+
const props = element.props as ImageProps;
|
|
135
|
+
base.prompt = getPromptText(props.prompt);
|
|
136
|
+
base.src = props.src;
|
|
137
|
+
base.model = getModelName(props.model);
|
|
138
|
+
base.details = {
|
|
139
|
+
aspectRatio: props.aspectRatio,
|
|
140
|
+
zoom: props.zoom,
|
|
141
|
+
resize: props.resize,
|
|
142
|
+
removeBackground: props.removeBackground,
|
|
143
|
+
};
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
case "video": {
|
|
148
|
+
const props = element.props as VideoProps;
|
|
149
|
+
base.prompt = getPromptText(props.prompt);
|
|
150
|
+
base.src = props.src;
|
|
151
|
+
base.model = getModelName(props.model);
|
|
152
|
+
base.details = {
|
|
153
|
+
aspectRatio: props.aspectRatio,
|
|
154
|
+
resize: props.resize,
|
|
155
|
+
cutFrom: props.cutFrom,
|
|
156
|
+
cutTo: props.cutTo,
|
|
157
|
+
volume: props.volume,
|
|
158
|
+
};
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
case "speech": {
|
|
163
|
+
const props = element.props as SpeechProps;
|
|
164
|
+
base.text = getTextContent(element.children);
|
|
165
|
+
base.voice = props.voice;
|
|
166
|
+
base.model = getModelName(props.model);
|
|
167
|
+
base.details = {
|
|
168
|
+
volume: props.volume,
|
|
169
|
+
};
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case "music": {
|
|
174
|
+
const props = element.props as MusicProps;
|
|
175
|
+
base.prompt = props.prompt;
|
|
176
|
+
base.src = props.src;
|
|
177
|
+
base.model = getModelName(props.model);
|
|
178
|
+
base.details = {
|
|
179
|
+
volume: props.volume,
|
|
180
|
+
loop: props.loop,
|
|
181
|
+
ducking: props.ducking,
|
|
182
|
+
cutFrom: props.cutFrom,
|
|
183
|
+
cutTo: props.cutTo,
|
|
184
|
+
};
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case "title": {
|
|
189
|
+
const props = element.props as TitleProps;
|
|
190
|
+
base.text = getTextContent(element.children);
|
|
191
|
+
base.details = {
|
|
192
|
+
position: props.position,
|
|
193
|
+
color: props.color,
|
|
194
|
+
start: props.start,
|
|
195
|
+
end: props.end,
|
|
196
|
+
};
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
case "captions": {
|
|
201
|
+
const props = element.props as CaptionsProps;
|
|
202
|
+
base.details = {
|
|
203
|
+
style: props.style,
|
|
204
|
+
color: props.color,
|
|
205
|
+
activeColor: props.activeColor,
|
|
206
|
+
fontSize: props.fontSize,
|
|
207
|
+
};
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
case "talking-head": {
|
|
212
|
+
const props = element.props as TalkingHeadProps;
|
|
213
|
+
base.text = getTextContent(element.children);
|
|
214
|
+
base.voice = props.voice;
|
|
215
|
+
base.model = getModelName(props.model);
|
|
216
|
+
base.details = {
|
|
217
|
+
character: props.character,
|
|
218
|
+
src: props.src,
|
|
219
|
+
position: props.position,
|
|
220
|
+
size: props.size,
|
|
221
|
+
};
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
case "packshot": {
|
|
226
|
+
const props = element.props as PackshotProps;
|
|
227
|
+
base.details = {
|
|
228
|
+
logo: props.logo,
|
|
229
|
+
logoPosition: props.logoPosition,
|
|
230
|
+
cta: props.cta,
|
|
231
|
+
ctaPosition: props.ctaPosition,
|
|
232
|
+
ctaColor: props.ctaColor,
|
|
233
|
+
blinkCta: props.blinkCta,
|
|
234
|
+
duration: props.duration,
|
|
235
|
+
};
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
case "split": {
|
|
240
|
+
const props = element.props as SplitProps;
|
|
241
|
+
base.details = {
|
|
242
|
+
direction: props.direction,
|
|
243
|
+
children: extractChildElements(element.children),
|
|
244
|
+
};
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
case "slider": {
|
|
249
|
+
const props = element.props as SliderProps;
|
|
250
|
+
base.details = {
|
|
251
|
+
direction: props.direction,
|
|
252
|
+
children: extractChildElements(element.children),
|
|
253
|
+
};
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case "swipe": {
|
|
258
|
+
const props = element.props as SwipeProps;
|
|
259
|
+
base.details = {
|
|
260
|
+
direction: props.direction,
|
|
261
|
+
interval: props.interval,
|
|
262
|
+
children: extractChildElements(element.children),
|
|
263
|
+
};
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// clean up undefined values from details
|
|
269
|
+
base.details = Object.fromEntries(
|
|
270
|
+
Object.entries(base.details).filter(([, v]) => v !== undefined),
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
return base;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function getTextContent(children: VargNode[]): string | undefined {
|
|
277
|
+
const texts: string[] = [];
|
|
278
|
+
for (const child of children) {
|
|
279
|
+
if (typeof child === "string") {
|
|
280
|
+
texts.push(child);
|
|
281
|
+
} else if (typeof child === "number") {
|
|
282
|
+
texts.push(String(child));
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return texts.length > 0 ? texts.join("") : undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function extractChildElements(children: VargNode[]): StoryboardElement[] {
|
|
289
|
+
const elements: StoryboardElement[] = [];
|
|
290
|
+
for (const child of children) {
|
|
291
|
+
if (!child || typeof child !== "object" || !("type" in child)) continue;
|
|
292
|
+
elements.push(extractElementInfo(child as VargElement));
|
|
293
|
+
}
|
|
294
|
+
return elements;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseStoryboard(element: VargElement): Storyboard {
|
|
298
|
+
const props = element.props as {
|
|
299
|
+
width?: number;
|
|
300
|
+
height?: number;
|
|
301
|
+
fps?: number;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const storyboard: Storyboard = {
|
|
305
|
+
width: props.width ?? 1920,
|
|
306
|
+
height: props.height ?? 1080,
|
|
307
|
+
fps: props.fps ?? 30,
|
|
308
|
+
clips: [],
|
|
309
|
+
globalElements: [],
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
let clipIndex = 0;
|
|
313
|
+
for (const child of element.children) {
|
|
314
|
+
if (!child || typeof child !== "object" || !("type" in child)) continue;
|
|
315
|
+
|
|
316
|
+
const childElement = child as VargElement;
|
|
317
|
+
|
|
318
|
+
if (childElement.type === "clip") {
|
|
319
|
+
const clipProps = childElement.props as ClipProps;
|
|
320
|
+
const clip: StoryboardClip = {
|
|
321
|
+
index: clipIndex++,
|
|
322
|
+
duration: clipProps.duration ?? "auto",
|
|
323
|
+
transition: clipProps.transition?.name,
|
|
324
|
+
elements: extractChildElements(childElement.children),
|
|
325
|
+
};
|
|
326
|
+
storyboard.clips.push(clip);
|
|
327
|
+
} else {
|
|
328
|
+
// global elements like music, captions at render level
|
|
329
|
+
storyboard.globalElements.push(extractElementInfo(childElement));
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return storyboard;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function generateHtml(storyboard: Storyboard, sourceFile: string): string {
|
|
337
|
+
const escapedSourceFile = sourceFile
|
|
338
|
+
.replace(/</g, "<")
|
|
339
|
+
.replace(/>/g, ">");
|
|
340
|
+
|
|
341
|
+
const renderElement = (el: StoryboardElement, depth = 0): string => {
|
|
342
|
+
const indent = " ".repeat(depth);
|
|
343
|
+
const typeColors: Record<string, string> = {
|
|
344
|
+
image: "#4CAF50",
|
|
345
|
+
video: "#2196F3",
|
|
346
|
+
speech: "#9C27B0",
|
|
347
|
+
music: "#FF9800",
|
|
348
|
+
title: "#E91E63",
|
|
349
|
+
subtitle: "#607D8B",
|
|
350
|
+
captions: "#795548",
|
|
351
|
+
"talking-head": "#00BCD4",
|
|
352
|
+
packshot: "#673AB7",
|
|
353
|
+
split: "#3F51B5",
|
|
354
|
+
slider: "#009688",
|
|
355
|
+
swipe: "#FF5722",
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const color = typeColors[el.type] || "#666";
|
|
359
|
+
|
|
360
|
+
let html = `${indent}<div class="element" style="border-left: 4px solid ${color}">
|
|
361
|
+
${indent} <div class="element-header">
|
|
362
|
+
${indent} <span class="element-type" style="background: ${color}">${el.type}</span>`;
|
|
363
|
+
|
|
364
|
+
if (el.model) {
|
|
365
|
+
html += `
|
|
366
|
+
${indent} <span class="element-model">${el.model}</span>`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
html += `
|
|
370
|
+
${indent} </div>`;
|
|
371
|
+
|
|
372
|
+
if (el.prompt) {
|
|
373
|
+
html += `
|
|
374
|
+
${indent} <div class="element-prompt">
|
|
375
|
+
${indent} <strong>Prompt:</strong> ${el.prompt.replace(/</g, "<").replace(/>/g, ">")}
|
|
376
|
+
${indent} </div>`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (el.text) {
|
|
380
|
+
html += `
|
|
381
|
+
${indent} <div class="element-text">
|
|
382
|
+
${indent} <strong>Text:</strong> "${el.text.replace(/</g, "<").replace(/>/g, ">")}"
|
|
383
|
+
${indent} </div>`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (el.src) {
|
|
387
|
+
html += `
|
|
388
|
+
${indent} <div class="element-src">
|
|
389
|
+
${indent} <strong>Source:</strong> ${el.src}
|
|
390
|
+
${indent} </div>`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (el.voice) {
|
|
394
|
+
html += `
|
|
395
|
+
${indent} <div class="element-voice">
|
|
396
|
+
${indent} <strong>Voice:</strong> ${el.voice}
|
|
397
|
+
${indent} </div>`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const detailsToShow = Object.entries(el.details).filter(
|
|
401
|
+
([key, val]) => val !== undefined && key !== "children",
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
if (detailsToShow.length > 0) {
|
|
405
|
+
html += `
|
|
406
|
+
${indent} <div class="element-details">`;
|
|
407
|
+
for (const [key, val] of detailsToShow) {
|
|
408
|
+
const displayVal =
|
|
409
|
+
typeof val === "object" ? JSON.stringify(val) : String(val);
|
|
410
|
+
html += `
|
|
411
|
+
${indent} <span class="detail"><strong>${key}:</strong> ${displayVal}</span>`;
|
|
412
|
+
}
|
|
413
|
+
html += `
|
|
414
|
+
${indent} </div>`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// render nested children if any
|
|
418
|
+
if (el.details.children && Array.isArray(el.details.children)) {
|
|
419
|
+
html += `
|
|
420
|
+
${indent} <div class="nested-children">`;
|
|
421
|
+
for (const child of el.details.children as StoryboardElement[]) {
|
|
422
|
+
html += renderElement(child, depth + 2);
|
|
423
|
+
}
|
|
424
|
+
html += `
|
|
425
|
+
${indent} </div>`;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
html += `
|
|
429
|
+
${indent}</div>`;
|
|
430
|
+
|
|
431
|
+
return html;
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const clipsHtml = storyboard.clips
|
|
435
|
+
.map((clip) => {
|
|
436
|
+
const elementsHtml = clip.elements
|
|
437
|
+
.map((el) => renderElement(el, 3))
|
|
438
|
+
.join("\n");
|
|
439
|
+
|
|
440
|
+
return `
|
|
441
|
+
<div class="clip">
|
|
442
|
+
<div class="clip-header">
|
|
443
|
+
<span class="clip-number">Clip ${clip.index + 1}</span>
|
|
444
|
+
<span class="clip-duration">${clip.duration === "auto" ? "auto" : `${clip.duration}s`}</span>
|
|
445
|
+
${clip.transition ? `<span class="clip-transition">→ ${clip.transition}</span>` : ""}
|
|
446
|
+
</div>
|
|
447
|
+
<div class="clip-elements">
|
|
448
|
+
${elementsHtml}
|
|
449
|
+
</div>
|
|
450
|
+
</div>`;
|
|
451
|
+
})
|
|
452
|
+
.join("\n");
|
|
453
|
+
|
|
454
|
+
const globalHtml =
|
|
455
|
+
storyboard.globalElements.length > 0
|
|
456
|
+
? `
|
|
457
|
+
<div class="global-section">
|
|
458
|
+
<h2>Global Elements</h2>
|
|
459
|
+
<div class="global-elements">
|
|
460
|
+
${storyboard.globalElements.map((el) => renderElement(el, 2)).join("\n")}
|
|
461
|
+
</div>
|
|
462
|
+
</div>`
|
|
463
|
+
: "";
|
|
464
|
+
|
|
465
|
+
return `<!DOCTYPE html>
|
|
466
|
+
<html lang="en">
|
|
467
|
+
<head>
|
|
468
|
+
<meta charset="UTF-8">
|
|
469
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
470
|
+
<title>Storyboard - ${escapedSourceFile}</title>
|
|
471
|
+
<style>
|
|
472
|
+
* {
|
|
473
|
+
box-sizing: border-box;
|
|
474
|
+
margin: 0;
|
|
475
|
+
padding: 0;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
body {
|
|
479
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
480
|
+
background: #0d0d0d;
|
|
481
|
+
color: #e0e0e0;
|
|
482
|
+
line-height: 1.6;
|
|
483
|
+
padding: 2rem;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
.container {
|
|
487
|
+
max-width: 1200px;
|
|
488
|
+
margin: 0 auto;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
header {
|
|
492
|
+
margin-bottom: 2rem;
|
|
493
|
+
padding-bottom: 1rem;
|
|
494
|
+
border-bottom: 1px solid #333;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
h1 {
|
|
498
|
+
color: #fff;
|
|
499
|
+
font-size: 1.75rem;
|
|
500
|
+
margin-bottom: 0.5rem;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
.meta {
|
|
504
|
+
color: #888;
|
|
505
|
+
font-size: 0.9rem;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.meta span {
|
|
509
|
+
margin-right: 1.5rem;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
.meta strong {
|
|
513
|
+
color: #aaa;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.clips {
|
|
517
|
+
display: flex;
|
|
518
|
+
flex-direction: column;
|
|
519
|
+
gap: 1.5rem;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
.clip {
|
|
523
|
+
background: #1a1a1a;
|
|
524
|
+
border-radius: 8px;
|
|
525
|
+
overflow: hidden;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
.clip-header {
|
|
529
|
+
background: #252525;
|
|
530
|
+
padding: 0.75rem 1rem;
|
|
531
|
+
display: flex;
|
|
532
|
+
align-items: center;
|
|
533
|
+
gap: 1rem;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.clip-number {
|
|
537
|
+
font-weight: 600;
|
|
538
|
+
color: #fff;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
.clip-duration {
|
|
542
|
+
background: #333;
|
|
543
|
+
padding: 0.25rem 0.5rem;
|
|
544
|
+
border-radius: 4px;
|
|
545
|
+
font-size: 0.85rem;
|
|
546
|
+
color: #4CAF50;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
.clip-transition {
|
|
550
|
+
color: #FF9800;
|
|
551
|
+
font-size: 0.85rem;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
.clip-elements {
|
|
555
|
+
padding: 1rem;
|
|
556
|
+
display: flex;
|
|
557
|
+
flex-direction: column;
|
|
558
|
+
gap: 0.75rem;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
.element {
|
|
562
|
+
background: #222;
|
|
563
|
+
border-radius: 6px;
|
|
564
|
+
padding: 0.75rem 1rem;
|
|
565
|
+
padding-left: calc(1rem + 4px);
|
|
566
|
+
margin-left: -4px;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.element-header {
|
|
570
|
+
display: flex;
|
|
571
|
+
align-items: center;
|
|
572
|
+
gap: 0.75rem;
|
|
573
|
+
margin-bottom: 0.5rem;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
.element-type {
|
|
577
|
+
color: #fff;
|
|
578
|
+
padding: 0.2rem 0.5rem;
|
|
579
|
+
border-radius: 4px;
|
|
580
|
+
font-size: 0.75rem;
|
|
581
|
+
font-weight: 600;
|
|
582
|
+
text-transform: uppercase;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.element-model {
|
|
586
|
+
color: #888;
|
|
587
|
+
font-size: 0.85rem;
|
|
588
|
+
font-family: monospace;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
.element-prompt,
|
|
592
|
+
.element-text,
|
|
593
|
+
.element-src,
|
|
594
|
+
.element-voice {
|
|
595
|
+
margin-top: 0.5rem;
|
|
596
|
+
color: #ccc;
|
|
597
|
+
font-size: 0.9rem;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
.element-prompt strong,
|
|
601
|
+
.element-text strong,
|
|
602
|
+
.element-src strong,
|
|
603
|
+
.element-voice strong {
|
|
604
|
+
color: #999;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
.element-details {
|
|
608
|
+
margin-top: 0.5rem;
|
|
609
|
+
display: flex;
|
|
610
|
+
flex-wrap: wrap;
|
|
611
|
+
gap: 0.5rem 1rem;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
.detail {
|
|
615
|
+
font-size: 0.8rem;
|
|
616
|
+
color: #888;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
.detail strong {
|
|
620
|
+
color: #666;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
.nested-children {
|
|
624
|
+
margin-top: 0.75rem;
|
|
625
|
+
padding-left: 1rem;
|
|
626
|
+
border-left: 2px solid #333;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.global-section {
|
|
630
|
+
margin-top: 2rem;
|
|
631
|
+
padding-top: 1.5rem;
|
|
632
|
+
border-top: 1px solid #333;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
.global-section h2 {
|
|
636
|
+
color: #fff;
|
|
637
|
+
font-size: 1.25rem;
|
|
638
|
+
margin-bottom: 1rem;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
.global-elements {
|
|
642
|
+
display: flex;
|
|
643
|
+
flex-direction: column;
|
|
644
|
+
gap: 0.75rem;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
.summary {
|
|
648
|
+
margin-top: 2rem;
|
|
649
|
+
padding: 1rem;
|
|
650
|
+
background: #1a1a1a;
|
|
651
|
+
border-radius: 8px;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
.summary h3 {
|
|
655
|
+
color: #fff;
|
|
656
|
+
font-size: 1rem;
|
|
657
|
+
margin-bottom: 0.75rem;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
.summary-stats {
|
|
661
|
+
display: flex;
|
|
662
|
+
gap: 2rem;
|
|
663
|
+
flex-wrap: wrap;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.stat {
|
|
667
|
+
color: #888;
|
|
668
|
+
font-size: 0.9rem;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.stat strong {
|
|
672
|
+
color: #4CAF50;
|
|
673
|
+
font-size: 1.25rem;
|
|
674
|
+
display: block;
|
|
675
|
+
}
|
|
676
|
+
</style>
|
|
677
|
+
</head>
|
|
678
|
+
<body>
|
|
679
|
+
<div class="container">
|
|
680
|
+
<header>
|
|
681
|
+
<h1>Storyboard</h1>
|
|
682
|
+
<div class="meta">
|
|
683
|
+
<span><strong>Source:</strong> ${escapedSourceFile}</span>
|
|
684
|
+
<span><strong>Resolution:</strong> ${storyboard.width}x${storyboard.height}</span>
|
|
685
|
+
<span><strong>FPS:</strong> ${storyboard.fps}</span>
|
|
686
|
+
</div>
|
|
687
|
+
</header>
|
|
688
|
+
|
|
689
|
+
<div class="clips">
|
|
690
|
+
${clipsHtml}
|
|
691
|
+
</div>
|
|
692
|
+
|
|
693
|
+
${globalHtml}
|
|
694
|
+
|
|
695
|
+
<div class="summary">
|
|
696
|
+
<h3>Summary</h3>
|
|
697
|
+
<div class="summary-stats">
|
|
698
|
+
<div class="stat">
|
|
699
|
+
<strong>${storyboard.clips.length}</strong>
|
|
700
|
+
clips
|
|
701
|
+
</div>
|
|
702
|
+
<div class="stat">
|
|
703
|
+
<strong>${countElements(storyboard, "video")}</strong>
|
|
704
|
+
videos
|
|
705
|
+
</div>
|
|
706
|
+
<div class="stat">
|
|
707
|
+
<strong>${countElements(storyboard, "image")}</strong>
|
|
708
|
+
images
|
|
709
|
+
</div>
|
|
710
|
+
<div class="stat">
|
|
711
|
+
<strong>${countElements(storyboard, "speech")}</strong>
|
|
712
|
+
speech
|
|
713
|
+
</div>
|
|
714
|
+
<div class="stat">
|
|
715
|
+
<strong>${countElements(storyboard, "music")}</strong>
|
|
716
|
+
music
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
</div>
|
|
720
|
+
</div>
|
|
721
|
+
</body>
|
|
722
|
+
</html>`;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function countElements(storyboard: Storyboard, type: string): number {
|
|
726
|
+
let count = 0;
|
|
727
|
+
|
|
728
|
+
const countInElements = (elements: StoryboardElement[]) => {
|
|
729
|
+
for (const el of elements) {
|
|
730
|
+
if (el.type === type) count++;
|
|
731
|
+
if (el.details.children && Array.isArray(el.details.children)) {
|
|
732
|
+
countInElements(el.details.children as StoryboardElement[]);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
for (const clip of storyboard.clips) {
|
|
738
|
+
countInElements(clip.elements);
|
|
739
|
+
}
|
|
740
|
+
countInElements(storyboard.globalElements);
|
|
741
|
+
|
|
742
|
+
return count;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export const storyboardCmd = defineCommand({
|
|
746
|
+
meta: {
|
|
747
|
+
name: "storyboard",
|
|
748
|
+
description: "generate html storyboard from component",
|
|
749
|
+
},
|
|
750
|
+
args: {
|
|
751
|
+
file: {
|
|
752
|
+
type: "positional" as const,
|
|
753
|
+
description: "component file (.tsx)",
|
|
754
|
+
required: true,
|
|
755
|
+
},
|
|
756
|
+
output: {
|
|
757
|
+
type: "string" as const,
|
|
758
|
+
alias: "o",
|
|
759
|
+
description: "output html path",
|
|
760
|
+
},
|
|
761
|
+
quiet: {
|
|
762
|
+
type: "boolean" as const,
|
|
763
|
+
alias: "q",
|
|
764
|
+
description: "minimal output",
|
|
765
|
+
default: false,
|
|
766
|
+
},
|
|
767
|
+
open: {
|
|
768
|
+
type: "boolean" as const,
|
|
769
|
+
description: "open in browser after generation",
|
|
770
|
+
default: false,
|
|
771
|
+
},
|
|
772
|
+
},
|
|
773
|
+
async run({ args }) {
|
|
774
|
+
const file = args.file as string;
|
|
775
|
+
|
|
776
|
+
if (!file) {
|
|
777
|
+
console.error("usage: varg storyboard <component.tsx> [-o output.html]");
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const component = await loadComponent(file);
|
|
782
|
+
|
|
783
|
+
if (!component || component.type !== "render") {
|
|
784
|
+
console.error("error: default export must be a <Render> element");
|
|
785
|
+
process.exit(1);
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const baseName = basename(file).replace(/\.tsx?$/, "");
|
|
789
|
+
const outputPath =
|
|
790
|
+
(args.output as string) ?? `output/${baseName}-storyboard.html`;
|
|
791
|
+
|
|
792
|
+
// ensure output directory exists
|
|
793
|
+
const outputDir = dirname(outputPath);
|
|
794
|
+
if (!existsSync(outputDir)) {
|
|
795
|
+
mkdirSync(outputDir, { recursive: true });
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (!args.quiet) {
|
|
799
|
+
console.log(`parsing ${file}...`);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const storyboard = parseStoryboard(component);
|
|
803
|
+
const html = generateHtml(storyboard, file);
|
|
804
|
+
|
|
805
|
+
await Bun.write(outputPath, html);
|
|
806
|
+
|
|
807
|
+
if (!args.quiet) {
|
|
808
|
+
console.log(`storyboard generated: ${outputPath}`);
|
|
809
|
+
console.log(
|
|
810
|
+
` ${storyboard.clips.length} clips, ${storyboard.width}x${storyboard.height}`,
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (args.open) {
|
|
815
|
+
const { $ } = await import("bun");
|
|
816
|
+
await $`open ${outputPath}`.quiet();
|
|
817
|
+
}
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
function StoryboardHelpView() {
|
|
822
|
+
const examples = [
|
|
823
|
+
{
|
|
824
|
+
command: "varg storyboard video.tsx",
|
|
825
|
+
description: "generate storyboard to output/video-storyboard.html",
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
command: "varg storyboard video.tsx -o storyboard.html",
|
|
829
|
+
description: "custom output path",
|
|
830
|
+
},
|
|
831
|
+
{
|
|
832
|
+
command: "varg storyboard video.tsx --open",
|
|
833
|
+
description: "generate and open in browser",
|
|
834
|
+
},
|
|
835
|
+
];
|
|
836
|
+
|
|
837
|
+
return (
|
|
838
|
+
<VargBox title="varg storyboard">
|
|
839
|
+
<Box marginBottom={1}>
|
|
840
|
+
<Text>
|
|
841
|
+
generate an html storyboard from a varg component. shows all clips,
|
|
842
|
+
prompts, and settings in a visual layout.
|
|
843
|
+
</Text>
|
|
844
|
+
</Box>
|
|
845
|
+
|
|
846
|
+
<Header>USAGE</Header>
|
|
847
|
+
<Box paddingLeft={2} marginBottom={1}>
|
|
848
|
+
<VargText variant="accent">
|
|
849
|
+
varg storyboard {"<file.tsx>"} [options]
|
|
850
|
+
</VargText>
|
|
851
|
+
</Box>
|
|
852
|
+
|
|
853
|
+
<Header>OPTIONS</Header>
|
|
854
|
+
<Box flexDirection="column" paddingLeft={2} marginBottom={1}>
|
|
855
|
+
<Text>
|
|
856
|
+
<VargText variant="accent">-o, --output </VargText>output path
|
|
857
|
+
(default: output/{"<name>"}-storyboard.html)
|
|
858
|
+
</Text>
|
|
859
|
+
<Text>
|
|
860
|
+
<VargText variant="accent">--open </VargText>open in browser after
|
|
861
|
+
generation
|
|
862
|
+
</Text>
|
|
863
|
+
<Text>
|
|
864
|
+
<VargText variant="accent">-q, --quiet </VargText>minimal output
|
|
865
|
+
</Text>
|
|
866
|
+
</Box>
|
|
867
|
+
|
|
868
|
+
<Header>EXAMPLES</Header>
|
|
869
|
+
<Box marginTop={1}>
|
|
870
|
+
<HelpBlock examples={examples} />
|
|
871
|
+
</Box>
|
|
872
|
+
</VargBox>
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export function showStoryboardHelp() {
|
|
877
|
+
renderStatic(<StoryboardHelpView />);
|
|
878
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -27,8 +27,10 @@ import {
|
|
|
27
27
|
showPreviewHelp,
|
|
28
28
|
showRenderHelp,
|
|
29
29
|
showRunHelp,
|
|
30
|
+
showStoryboardHelp,
|
|
30
31
|
showTargetHelp,
|
|
31
32
|
showWhichHelp,
|
|
33
|
+
storyboardCmd,
|
|
32
34
|
studioCmd,
|
|
33
35
|
whichCmd,
|
|
34
36
|
} from "./commands";
|
|
@@ -55,6 +57,7 @@ const subcommandHelp: Record<string, () => void> = {
|
|
|
55
57
|
run: showRunHelp,
|
|
56
58
|
render: showRenderHelp,
|
|
57
59
|
preview: showPreviewHelp,
|
|
60
|
+
storyboard: showStoryboardHelp,
|
|
58
61
|
init: showInitHelp,
|
|
59
62
|
list: showListHelp,
|
|
60
63
|
ls: showListHelp,
|
|
@@ -114,6 +117,7 @@ const main = defineCommand({
|
|
|
114
117
|
init: initCmd,
|
|
115
118
|
render: renderCmd,
|
|
116
119
|
preview: previewCmd,
|
|
120
|
+
storyboard: storyboardCmd,
|
|
117
121
|
studio: studioCmd,
|
|
118
122
|
run: runCmd,
|
|
119
123
|
list: listCmd,
|
package/src/providers/fal.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { fal } from "@fal-ai/client";
|
|
|
7
7
|
import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types";
|
|
8
8
|
import { BaseProvider, ensureUrl } from "./base";
|
|
9
9
|
|
|
10
|
+
const falApiKey = process.env.FAL_API_KEY ?? process.env.FAL_KEY;
|
|
11
|
+
if (falApiKey) {
|
|
12
|
+
fal.config({ credentials: falApiKey });
|
|
13
|
+
}
|
|
14
|
+
|
|
10
15
|
export class FalProvider extends BaseProvider {
|
|
11
16
|
readonly name = "fal";
|
|
12
17
|
|
|
@@ -11,6 +11,29 @@
|
|
|
11
11
|
import { fal } from "../../ai-sdk/providers/fal";
|
|
12
12
|
import { Clip, Image, Render, render, Video } from "..";
|
|
13
13
|
|
|
14
|
+
const quickstartVideo = (
|
|
15
|
+
<Render width={720} height={720}>
|
|
16
|
+
<Clip duration={3}>
|
|
17
|
+
<Video
|
|
18
|
+
prompt={{
|
|
19
|
+
text: "robot waves hello, friendly gesture, slight head tilt",
|
|
20
|
+
images: [
|
|
21
|
+
Image({
|
|
22
|
+
prompt:
|
|
23
|
+
"a friendly robot waving hello, simple cartoon style, blue and white colors, clean background",
|
|
24
|
+
model: fal.imageModel("flux-schnell"),
|
|
25
|
+
aspectRatio: "1:1",
|
|
26
|
+
}),
|
|
27
|
+
],
|
|
28
|
+
}}
|
|
29
|
+
model={fal.videoModel("wan-2.5")}
|
|
30
|
+
/>
|
|
31
|
+
</Clip>
|
|
32
|
+
</Render>
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
export default quickstartVideo;
|
|
36
|
+
|
|
14
37
|
async function main() {
|
|
15
38
|
console.log("=== Varg Video Generation - Setup Verification ===\n");
|
|
16
39
|
|
|
@@ -46,29 +69,8 @@ async function main() {
|
|
|
46
69
|
console.log("Generating a simple 3-second animation...");
|
|
47
70
|
console.log("This may take 30-60 seconds on first run.\n");
|
|
48
71
|
|
|
49
|
-
const video = (
|
|
50
|
-
<Render width={720} height={720}>
|
|
51
|
-
<Clip duration={3}>
|
|
52
|
-
<Video
|
|
53
|
-
prompt={{
|
|
54
|
-
text: "robot waves hello, friendly gesture, slight head tilt",
|
|
55
|
-
images: [
|
|
56
|
-
Image({
|
|
57
|
-
prompt:
|
|
58
|
-
"a friendly robot waving hello, simple cartoon style, blue and white colors, clean background",
|
|
59
|
-
model: fal.imageModel("flux-schnell"),
|
|
60
|
-
aspectRatio: "1:1",
|
|
61
|
-
}),
|
|
62
|
-
],
|
|
63
|
-
}}
|
|
64
|
-
model={fal.videoModel("wan-2.5")}
|
|
65
|
-
/>
|
|
66
|
-
</Clip>
|
|
67
|
-
</Render>
|
|
68
|
-
);
|
|
69
|
-
|
|
70
72
|
try {
|
|
71
|
-
const buffer = await render(
|
|
73
|
+
const buffer = await render(quickstartVideo, {
|
|
72
74
|
output: "output/quickstart-test.mp4",
|
|
73
75
|
cache: ".cache/ai",
|
|
74
76
|
});
|
|
@@ -94,4 +96,6 @@ async function main() {
|
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
98
|
|
|
97
|
-
main
|
|
99
|
+
if (import.meta.main) {
|
|
100
|
+
main();
|
|
101
|
+
}
|
package/src/studio/ui/index.html
CHANGED
|
@@ -763,8 +763,8 @@
|
|
|
763
763
|
<script src="https://unpkg.com/drawflow@0.0.60/dist/drawflow.min.js"></script>
|
|
764
764
|
<script src="https://cdn.jsdelivr.net/npm/monaco-editor@0.45.0/min/vs/loader.js"></script>
|
|
765
765
|
<script>
|
|
766
|
-
const DEFAULT_CODE = `import { fal } from "
|
|
767
|
-
import { Clip, Image, Render, Video } from "
|
|
766
|
+
const DEFAULT_CODE = `import { fal } from "vargai/ai";
|
|
767
|
+
import { Clip, Image, Render, Video } from "vargai/react";
|
|
768
768
|
|
|
769
769
|
export default (
|
|
770
770
|
<Render width={1080} height={1920}>
|
package/src/tests/all.test.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Run with: bun run src/tests/all.test.ts
|
|
6
6
|
*
|
|
7
7
|
* Note: Most tests require API keys to be set in environment variables:
|
|
8
|
-
* - FAL_KEY
|
|
8
|
+
* - FAL_API_KEY (or FAL_KEY)
|
|
9
9
|
* - REPLICATE_API_TOKEN
|
|
10
10
|
* - ELEVENLABS_API_KEY
|
|
11
11
|
* - GROQ_API_KEY
|
|
@@ -318,7 +318,7 @@ await test(
|
|
|
318
318
|
}
|
|
319
319
|
console.log(` Generated: ${result.data.images[0].url}`);
|
|
320
320
|
},
|
|
321
|
-
!hasApiKey("FAL_KEY"),
|
|
321
|
+
!hasApiKey(["FAL_API_KEY", "FAL_KEY"]),
|
|
322
322
|
);
|
|
323
323
|
|
|
324
324
|
await test(
|
|
@@ -334,7 +334,7 @@ await test(
|
|
|
334
334
|
}
|
|
335
335
|
console.log(` Generated: ${result.data.video.url}`);
|
|
336
336
|
},
|
|
337
|
-
!hasApiKey("FAL_KEY"),
|
|
337
|
+
!hasApiKey(["FAL_API_KEY", "FAL_KEY"]),
|
|
338
338
|
);
|
|
339
339
|
|
|
340
340
|
// Replicate tests
|
|
@@ -455,7 +455,7 @@ await test(
|
|
|
455
455
|
}
|
|
456
456
|
console.log(` Output: ${JSON.stringify(result.output).slice(0, 100)}...`);
|
|
457
457
|
},
|
|
458
|
-
!hasApiKey("FAL_KEY"),
|
|
458
|
+
!hasApiKey(["FAL_API_KEY", "FAL_KEY"]),
|
|
459
459
|
);
|
|
460
460
|
|
|
461
461
|
await test(
|
package/src/tests/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ Available test files:
|
|
|
20
20
|
bun run src/tests/all.test.ts
|
|
21
21
|
Comprehensive tests including live API calls.
|
|
22
22
|
Requires API keys set in environment variables:
|
|
23
|
-
- FAL_KEY
|
|
23
|
+
- FAL_API_KEY (or FAL_KEY)
|
|
24
24
|
- REPLICATE_API_TOKEN
|
|
25
25
|
- ELEVENLABS_API_KEY
|
|
26
26
|
- GROQ_API_KEY
|