vargai 0.4.0-alpha24 → 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 CHANGED
@@ -65,7 +65,7 @@
65
65
  "vargai": "^0.4.0-alpha11",
66
66
  "zod": "^4.2.1"
67
67
  },
68
- "version": "0.4.0-alpha24",
68
+ "version": "0.4.0-alpha25",
69
69
  "exports": {
70
70
  ".": "./src/index.ts",
71
71
  "./ai": "./src/ai-sdk/index.ts",
@@ -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 FAL_KEY="your_key" # for wan-25 and image generation
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
- if (settings.apiKey) {
487
- fal.config({ credentials: settings.apiKey });
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
- if (process.env.FAL_KEY) {
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");
@@ -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, "&lt;")
339
+ .replace(/>/g, "&gt;");
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, "&lt;").replace(/>/g, "&gt;")}
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, "&lt;").replace(/>/g, "&gt;")}"
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,
@@ -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(video, {
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
+ }
@@ -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 "../fal-provider";
767
- import { Clip, Image, Render, Video } from "../react";
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}>
@@ -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(
@@ -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