vargai 0.4.0-alpha2 → 0.4.0-alpha21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +483 -61
- package/launch-videos/06-kawaii-fruits.tsx +93 -0
- package/launch-videos/07-ugc-weight-loss.tsx +132 -0
- package/launch-videos/08-talking-head-varg.tsx +107 -0
- package/launch-videos/09-girl.tsx +160 -0
- package/launch-videos/README.md +42 -0
- package/package.json +8 -4
- package/skills/varg-video-generation/SKILL.md +213 -0
- package/skills/varg-video-generation/references/templates.md +380 -0
- package/skills/varg-video-generation/scripts/setup.ts +265 -0
- package/src/ai-sdk/cache.ts +1 -1
- package/src/ai-sdk/middleware/wrap-image-model.ts +4 -21
- package/src/ai-sdk/middleware/wrap-music-model.ts +4 -16
- package/src/ai-sdk/middleware/wrap-video-model.ts +5 -17
- package/src/ai-sdk/providers/editly/index.ts +110 -53
- package/src/ai-sdk/providers/editly/types.ts +2 -0
- package/src/ai-sdk/providers/elevenlabs.ts +10 -2
- package/src/ai-sdk/providers/fal.ts +6 -1
- package/src/cli/commands/find.tsx +1 -0
- package/src/cli/commands/hello.ts +85 -0
- package/src/cli/commands/help.tsx +18 -30
- package/src/cli/commands/index.ts +9 -1
- package/src/cli/commands/init.tsx +412 -0
- package/src/cli/commands/list.tsx +1 -0
- package/src/cli/commands/render.tsx +292 -80
- package/src/cli/commands/run.tsx +1 -0
- package/src/cli/commands/studio.ts +47 -0
- package/src/cli/commands/which.tsx +1 -0
- package/src/cli/index.ts +20 -5
- package/src/cli/ui/components/Badge.tsx +1 -0
- package/src/cli/ui/components/DataTable.tsx +1 -0
- package/src/cli/ui/components/Header.tsx +1 -0
- package/src/cli/ui/components/HelpBlock.tsx +1 -0
- package/src/cli/ui/components/KeyValue.tsx +1 -0
- package/src/cli/ui/components/OptionRow.tsx +1 -0
- package/src/cli/ui/components/Separator.tsx +1 -0
- package/src/cli/ui/components/StatusBox.tsx +1 -0
- package/src/cli/ui/components/VargBox.tsx +1 -0
- package/src/cli/ui/components/VargProgress.tsx +1 -0
- package/src/cli/ui/components/VargSpinner.tsx +1 -0
- package/src/cli/ui/components/VargText.tsx +1 -0
- package/src/react/assets.ts +9 -0
- package/src/react/elements.ts +0 -5
- package/src/react/examples/branching.tsx +6 -4
- package/src/react/examples/character-video.tsx +13 -10
- package/src/react/examples/madi.tsx +13 -10
- package/src/react/examples/mcmeows.tsx +40 -0
- package/src/react/examples/music-defaults.tsx +24 -0
- package/src/react/examples/quickstart-test.tsx +97 -0
- package/src/react/index.ts +1 -2
- package/src/react/react.test.ts +10 -10
- package/src/react/renderers/clip.ts +13 -24
- package/src/react/renderers/context.ts +3 -0
- package/src/react/renderers/image.ts +4 -2
- package/src/react/renderers/index.ts +0 -1
- package/src/react/renderers/music.ts +3 -3
- package/src/react/renderers/progress.ts +1 -3
- package/src/react/renderers/render.ts +49 -63
- package/src/react/renderers/speech.ts +2 -2
- package/src/react/renderers/video.ts +46 -9
- package/src/react/types.ts +18 -14
- package/src/studio/stages.ts +4 -24
- package/src/studio/step-renderer.ts +0 -15
- package/test-sync-v2.ts +30 -0
- package/test-sync-v2.tsx +29 -0
- package/tsconfig.json +5 -3
- package/video.tsx +7 -0
- package/src/react/cli.ts +0 -52
- package/src/react/renderers/animate.ts +0 -59
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { elevenlabs } from "../../ai-sdk/providers/elevenlabs";
|
|
2
2
|
import { fal } from "../../ai-sdk/providers/fal";
|
|
3
|
-
import {
|
|
3
|
+
import { Clip, Image, Music, Render, Video } from "..";
|
|
4
4
|
|
|
5
5
|
const MADI_REF =
|
|
6
6
|
"https://s3.varg.ai/fellowers/madi/character_shots/madi_shot_03_closeup.png";
|
|
@@ -43,16 +43,19 @@ export default (
|
|
|
43
43
|
|
|
44
44
|
{SCENES.map((scene) => (
|
|
45
45
|
<Clip key={scene.prompt} duration={2}>
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
46
|
+
<Video
|
|
47
|
+
prompt={{
|
|
48
|
+
text: scene.motion,
|
|
49
|
+
images: [
|
|
50
|
+
Image({
|
|
51
|
+
prompt: { text: scene.prompt, images: [MADI_REF] },
|
|
52
|
+
model: fal.imageModel("nano-banana-pro/edit"),
|
|
53
|
+
aspectRatio: "9:16",
|
|
54
|
+
resize: "cover",
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
}}
|
|
54
58
|
model={fal.videoModel("wan-2.5")}
|
|
55
|
-
duration={5}
|
|
56
59
|
/>
|
|
57
60
|
</Clip>
|
|
58
61
|
))}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { elevenlabs } from "../../ai-sdk/providers/elevenlabs";
|
|
2
|
+
import { fal } from "../../ai-sdk/providers/fal";
|
|
3
|
+
import { Clip, Music, Render, render, Title, Video } from "..";
|
|
4
|
+
|
|
5
|
+
export default (
|
|
6
|
+
<Render>
|
|
7
|
+
<Clip duration={4}>
|
|
8
|
+
<Video prompt="A sophisticated tabby cat wearing a tailored tiny business suit and small round glasses, standing upright at a McDonald's counter. One paw raised pointing assertively at the illuminated menu board above. The cat has an extremely serious, concentrated expression with furrowed brows. Cinematic lighting with warm McDonald's interior ambiance, shallow depth of field focusing on the cat's determined face, photorealistic fur texture with fine detail. Professional business atmosphere meets fast food chaos." />
|
|
9
|
+
</Clip>
|
|
10
|
+
|
|
11
|
+
<Clip duration={4}>
|
|
12
|
+
<Video prompt="Absolute mayhem - four to five cats in a chaotic pile fight. Orange tabby, black and white tuxedo cat, calico with patches, and fluffy gray cat all scrambling, paws flailing, tumbling over each other in exaggerated cartoon-style motion. They're all desperately reaching for a single perfect golden McDonald's french fry sitting on a red plastic tray in the center. Wide-eyed expressions, mouths open mid-meow, dynamic motion blur, comedic timing. Fast food restaurant background slightly blurred. Playful, over-the-top energy." />
|
|
13
|
+
</Clip>
|
|
14
|
+
|
|
15
|
+
<Clip duration={3}>
|
|
16
|
+
<Video prompt="Proud orange tabby cat wearing an official McDonald's crew member visor and name tag, standing upright behind a restaurant register counter. Front paws crossed confidently across chest, chin lifted with a smug, self-satisfied expression. Perfect posture, professional demeanor. Bright McDonald's interior lighting, red and yellow color scheme in background. The cat radiates 'employee of the month' energy. Crisp, clean, professional fast food aesthetic." />
|
|
17
|
+
<Title position="bottom">McMeow's: NOW HIRING</Title>
|
|
18
|
+
</Clip>
|
|
19
|
+
|
|
20
|
+
<Music
|
|
21
|
+
model={elevenlabs.musicModel()}
|
|
22
|
+
prompt="playful upbeat comedy music with quirky pizzicato strings and light percussion, funny corporate training video vibes"
|
|
23
|
+
duration={11}
|
|
24
|
+
/>
|
|
25
|
+
</Render>
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
async function main() {
|
|
29
|
+
const component = await import("./mcmeows.tsx").then((m) => m.default);
|
|
30
|
+
await render(component, {
|
|
31
|
+
output: "output/mcmeows.mp4",
|
|
32
|
+
cache: ".cache/ai",
|
|
33
|
+
verbose: true,
|
|
34
|
+
defaults: {
|
|
35
|
+
video: fal.videoModel("wan-2.5"),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { elevenlabs } from "../../ai-sdk/providers/elevenlabs";
|
|
2
|
+
import { Clip, Image, Music, Render, render } from "..";
|
|
3
|
+
|
|
4
|
+
export default (
|
|
5
|
+
<Render width={1920} height={1080}>
|
|
6
|
+
<Clip duration={5}>
|
|
7
|
+
<Image src="media/cyberpunk-street.png" />
|
|
8
|
+
</Clip>
|
|
9
|
+
<Music prompt="calm ambient electronic music" duration={5} />
|
|
10
|
+
</Render>
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
async function main() {
|
|
14
|
+
const component = await import("./music-defaults.tsx").then((m) => m.default);
|
|
15
|
+
await render(component, {
|
|
16
|
+
output: "output/music-defaults.mp4",
|
|
17
|
+
defaults: {
|
|
18
|
+
music: elevenlabs.musicModel(),
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
console.log("done! check output/music-defaults.mp4");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quickstart Test - Verification script for video generation setup
|
|
3
|
+
*
|
|
4
|
+
* This minimal test confirms your FAL_API_KEY is working correctly.
|
|
5
|
+
* It generates a simple 3-second animated image.
|
|
6
|
+
*
|
|
7
|
+
* Run: bun run src/react/examples/quickstart-test.tsx
|
|
8
|
+
* Output: output/quickstart-test.mp4
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { fal } from "../../ai-sdk/providers/fal";
|
|
12
|
+
import { Clip, Image, Render, render, Video } from "..";
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
console.log("=== Varg Video Generation - Setup Verification ===\n");
|
|
16
|
+
|
|
17
|
+
// Check for FAL_API_KEY
|
|
18
|
+
if (!process.env.FAL_API_KEY) {
|
|
19
|
+
console.error("ERROR: FAL_API_KEY not found in environment");
|
|
20
|
+
console.error("\nTo fix this:");
|
|
21
|
+
console.error("1. Get an API key at: https://fal.ai/dashboard/keys");
|
|
22
|
+
console.error("2. Add to .env file: FAL_API_KEY=fal_xxxxx");
|
|
23
|
+
console.error("3. Run this test again");
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log("FAL_API_KEY found");
|
|
28
|
+
|
|
29
|
+
// Check for optional keys
|
|
30
|
+
const hasElevenLabs = !!process.env.ELEVENLABS_API_KEY;
|
|
31
|
+
const hasReplicate = !!process.env.REPLICATE_API_TOKEN;
|
|
32
|
+
const hasGroq = !!process.env.GROQ_API_KEY;
|
|
33
|
+
|
|
34
|
+
console.log("\nOptional keys:");
|
|
35
|
+
console.log(
|
|
36
|
+
` ELEVENLABS_API_KEY: ${hasElevenLabs ? "found" : "not found (music/voice unavailable)"}`,
|
|
37
|
+
);
|
|
38
|
+
console.log(
|
|
39
|
+
` REPLICATE_API_TOKEN: ${hasReplicate ? "found" : "not found (lipsync unavailable)"}`,
|
|
40
|
+
);
|
|
41
|
+
console.log(
|
|
42
|
+
` GROQ_API_KEY: ${hasGroq ? "found" : "not found (transcription unavailable)"}`,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
console.log("\n--- Running verification test ---\n");
|
|
46
|
+
console.log("Generating a simple 3-second animation...");
|
|
47
|
+
console.log("This may take 30-60 seconds on first run.\n");
|
|
48
|
+
|
|
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
|
+
try {
|
|
71
|
+
const buffer = await render(video, {
|
|
72
|
+
output: "output/quickstart-test.mp4",
|
|
73
|
+
cache: ".cache/ai",
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log("\n=== SUCCESS ===");
|
|
77
|
+
console.log(
|
|
78
|
+
`Output: output/quickstart-test.mp4 (${(buffer.byteLength / 1024 / 1024).toFixed(2)} MB)`,
|
|
79
|
+
);
|
|
80
|
+
console.log("\nYour setup is working! You can now:");
|
|
81
|
+
console.log("1. Try the templates in .claude/skills/video-generation.md");
|
|
82
|
+
console.log(
|
|
83
|
+
"2. Run existing examples: bun run src/react/examples/madi.tsx",
|
|
84
|
+
);
|
|
85
|
+
console.log("3. Create your own videos using the React engine");
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error("\n=== VERIFICATION FAILED ===");
|
|
88
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
89
|
+
console.error("\nCommon fixes:");
|
|
90
|
+
console.error("- Check FAL_API_KEY is correct (no extra spaces)");
|
|
91
|
+
console.error("- Ensure you have credits at fal.ai");
|
|
92
|
+
console.error("- Try running: bun install");
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
main();
|
package/src/react/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { SizeValue } from "../ai-sdk/providers/editly/types";
|
|
2
|
+
export { assets } from "./assets";
|
|
2
3
|
export {
|
|
3
|
-
Animate,
|
|
4
4
|
Captions,
|
|
5
5
|
Clip,
|
|
6
6
|
Image,
|
|
@@ -20,7 +20,6 @@ export {
|
|
|
20
20
|
export { Grid, SplitLayout } from "./layouts";
|
|
21
21
|
export { render, renderStream } from "./render";
|
|
22
22
|
export type {
|
|
23
|
-
AnimateProps,
|
|
24
23
|
CaptionsProps,
|
|
25
24
|
ClipProps,
|
|
26
25
|
ImageProps,
|
package/src/react/react.test.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { existsSync, unlinkSync } from "node:fs";
|
|
3
3
|
import { fal } from "../ai-sdk/providers/fal";
|
|
4
4
|
import {
|
|
5
|
-
Animate,
|
|
6
5
|
Captions,
|
|
7
6
|
Clip,
|
|
8
7
|
Image,
|
|
@@ -80,19 +79,20 @@ describe("varg-react elements", () => {
|
|
|
80
79
|
expect(element.children).toContain("I'M IN DANGER");
|
|
81
80
|
});
|
|
82
81
|
|
|
83
|
-
test("
|
|
82
|
+
test("Video creates correct element with nested image", () => {
|
|
84
83
|
const image = Image({ prompt: "luigi in wheelchair" });
|
|
85
|
-
const element =
|
|
86
|
-
image,
|
|
84
|
+
const element = Video({
|
|
85
|
+
prompt: { text: "wheels spinning fast", images: [image] },
|
|
87
86
|
model: fal.videoModel("wan-2.5"),
|
|
88
|
-
motion: "wheels spinning fast",
|
|
89
|
-
duration: 5,
|
|
90
87
|
});
|
|
91
88
|
|
|
92
|
-
expect(element.type).toBe("
|
|
93
|
-
expect(element.props.
|
|
94
|
-
|
|
95
|
-
|
|
89
|
+
expect(element.type).toBe("video");
|
|
90
|
+
expect((element.props.prompt as { images: unknown[] }).images[0]).toBe(
|
|
91
|
+
image,
|
|
92
|
+
);
|
|
93
|
+
expect((element.props.prompt as { text: string }).text).toBe(
|
|
94
|
+
"wheels spinning fast",
|
|
95
|
+
);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
test("nested composition builds correct tree", () => {
|
|
@@ -8,7 +8,6 @@ import type {
|
|
|
8
8
|
VideoLayer,
|
|
9
9
|
} from "../../ai-sdk/providers/editly/types";
|
|
10
10
|
import type {
|
|
11
|
-
AnimateProps,
|
|
12
11
|
ClipProps,
|
|
13
12
|
ImageProps,
|
|
14
13
|
SpeechProps,
|
|
@@ -16,7 +15,6 @@ import type {
|
|
|
16
15
|
VargNode,
|
|
17
16
|
VideoProps,
|
|
18
17
|
} from "../types";
|
|
19
|
-
import { renderAnimate } from "./animate";
|
|
20
18
|
import type { RenderContext } from "./context";
|
|
21
19
|
import { renderImage } from "./image";
|
|
22
20
|
import { renderPackshot } from "./packshot";
|
|
@@ -32,9 +30,15 @@ type PendingLayer =
|
|
|
32
30
|
| { type: "sync"; layer: Layer }
|
|
33
31
|
| { type: "async"; promise: Promise<Layer> };
|
|
34
32
|
|
|
33
|
+
interface ClipLayerOptions {
|
|
34
|
+
cutFrom?: number;
|
|
35
|
+
cutTo?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
35
38
|
async function renderClipLayers(
|
|
36
39
|
children: VargNode[],
|
|
37
40
|
ctx: RenderContext,
|
|
41
|
+
clipOptions?: ClipLayerOptions,
|
|
38
42
|
): Promise<Layer[]> {
|
|
39
43
|
const pending: PendingLayer[] = [];
|
|
40
44
|
|
|
@@ -86,8 +90,9 @@ async function renderClipLayers(
|
|
|
86
90
|
type: "video",
|
|
87
91
|
path,
|
|
88
92
|
resizeMode: props.resize,
|
|
89
|
-
cutFrom
|
|
90
|
-
|
|
93
|
+
// Video-level cutFrom/cutTo take precedence over clip-level
|
|
94
|
+
cutFrom: props.cutFrom ?? clipOptions?.cutFrom,
|
|
95
|
+
cutTo: props.cutTo ?? clipOptions?.cutTo,
|
|
91
96
|
mixVolume: props.keepAudio ? (props.volume ?? 1) : 0,
|
|
92
97
|
left: props.left,
|
|
93
98
|
top: props.top,
|
|
@@ -99,25 +104,6 @@ async function renderClipLayers(
|
|
|
99
104
|
break;
|
|
100
105
|
}
|
|
101
106
|
|
|
102
|
-
case "animate": {
|
|
103
|
-
const props = element.props as AnimateProps;
|
|
104
|
-
pending.push({
|
|
105
|
-
type: "async",
|
|
106
|
-
promise: renderAnimate(element as VargElement<"animate">, ctx).then(
|
|
107
|
-
(path) =>
|
|
108
|
-
({
|
|
109
|
-
type: "video",
|
|
110
|
-
path,
|
|
111
|
-
left: props.left,
|
|
112
|
-
top: props.top,
|
|
113
|
-
width: props.width,
|
|
114
|
-
height: props.height,
|
|
115
|
-
}) as VideoLayer,
|
|
116
|
-
),
|
|
117
|
-
});
|
|
118
|
-
break;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
107
|
case "title": {
|
|
122
108
|
pending.push({
|
|
123
109
|
type: "sync",
|
|
@@ -220,7 +206,10 @@ export async function renderClip(
|
|
|
220
206
|
ctx: RenderContext,
|
|
221
207
|
): Promise<Clip> {
|
|
222
208
|
const props = element.props as ClipProps;
|
|
223
|
-
const layers = await renderClipLayers(element.children, ctx
|
|
209
|
+
const layers = await renderClipLayers(element.children, ctx, {
|
|
210
|
+
cutFrom: props.cutFrom,
|
|
211
|
+
cutTo: props.cutTo,
|
|
212
|
+
});
|
|
224
213
|
|
|
225
214
|
const isOverlayVideo = (l: Layer) =>
|
|
226
215
|
l.type === "video" &&
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { generateImage } from "ai";
|
|
2
2
|
import type { fileCache } from "../../ai-sdk/file-cache";
|
|
3
3
|
import type { generateVideo } from "../../ai-sdk/generate-video";
|
|
4
|
+
import type { DefaultModels } from "../types";
|
|
4
5
|
import type { ProgressTracker } from "./progress";
|
|
5
6
|
|
|
6
7
|
export interface RenderContext {
|
|
@@ -14,4 +15,6 @@ export interface RenderContext {
|
|
|
14
15
|
progress?: ProgressTracker;
|
|
15
16
|
/** In-memory deduplication for concurrent renders of the same element */
|
|
16
17
|
pending: Map<string, Promise<string>>;
|
|
18
|
+
/** Default models for elements that don't specify one */
|
|
19
|
+
defaults?: DefaultModels;
|
|
17
20
|
}
|
|
@@ -54,9 +54,11 @@ export async function renderImage(
|
|
|
54
54
|
throw new Error("Image element requires either 'prompt' or 'src'");
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const model = props.model;
|
|
57
|
+
const model = props.model ?? ctx.defaults?.image;
|
|
58
58
|
if (!model) {
|
|
59
|
-
throw new Error(
|
|
59
|
+
throw new Error(
|
|
60
|
+
"Image element requires 'model' prop (or set defaults.image in render options)",
|
|
61
|
+
);
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
// Compute cache key for deduplication
|
|
@@ -10,9 +10,9 @@ export async function renderMusic(
|
|
|
10
10
|
const props = element.props as MusicProps;
|
|
11
11
|
|
|
12
12
|
const prompt = props.prompt;
|
|
13
|
-
const model = props.model;
|
|
13
|
+
const model = props.model ?? ctx.defaults?.music;
|
|
14
14
|
if (!prompt || !model) {
|
|
15
|
-
throw new Error("Music
|
|
15
|
+
throw new Error("Music requires prompt and model (or set defaults.music)");
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const cacheKey = JSON.stringify({
|
|
@@ -22,7 +22,7 @@ export async function renderMusic(
|
|
|
22
22
|
duration: props.duration,
|
|
23
23
|
});
|
|
24
24
|
|
|
25
|
-
const modelId = model.modelId;
|
|
25
|
+
const modelId = model.modelId ?? "music";
|
|
26
26
|
const taskId = ctx.progress ? addTask(ctx.progress, "music", modelId) : null;
|
|
27
27
|
|
|
28
28
|
const generateFn = async () => {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export type GenerationType =
|
|
2
2
|
| "image"
|
|
3
3
|
| "video"
|
|
4
|
-
| "animate"
|
|
5
4
|
| "speech"
|
|
6
5
|
| "music"
|
|
7
6
|
| "editly"
|
|
@@ -11,7 +10,6 @@ export type GenerationType =
|
|
|
11
10
|
export const TIME_ESTIMATES: Record<GenerationType, number> = {
|
|
12
11
|
image: 30,
|
|
13
12
|
video: 120,
|
|
14
|
-
animate: 90,
|
|
15
13
|
speech: 5,
|
|
16
14
|
music: 45,
|
|
17
15
|
editly: 15,
|
|
@@ -103,7 +101,7 @@ export function completeTask(tracker: ProgressTracker, id: string): void {
|
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
function getEstimate(task: ProgressTask): number {
|
|
106
|
-
const modelLower = task.model
|
|
104
|
+
const modelLower = task.model?.toLowerCase() ?? "";
|
|
107
105
|
for (const [key, estimate] of Object.entries(MODEL_TIME_ESTIMATES)) {
|
|
108
106
|
if (modelLower.includes(key.toLowerCase())) {
|
|
109
107
|
return estimate;
|
|
@@ -25,7 +25,6 @@ import type {
|
|
|
25
25
|
SpeechProps,
|
|
26
26
|
VargElement,
|
|
27
27
|
} from "../types";
|
|
28
|
-
import { renderAnimate } from "./animate";
|
|
29
28
|
import { renderCaptions } from "./captions";
|
|
30
29
|
import { renderClip } from "./clip";
|
|
31
30
|
import type { RenderContext } from "./context";
|
|
@@ -54,58 +53,59 @@ export async function renderRoot(
|
|
|
54
53
|
const props = element.props as RenderProps;
|
|
55
54
|
const progress = createProgressTracker(options.quiet ?? false);
|
|
56
55
|
|
|
57
|
-
const mode: RenderMode = options.mode ?? "
|
|
56
|
+
const mode: RenderMode = options.mode ?? "strict";
|
|
58
57
|
const placeholderCount = { images: 0, videos: 0, total: 0 };
|
|
59
58
|
|
|
60
|
-
const onFallback = (error: Error, prompt: string) => {
|
|
61
|
-
if (!options.quiet) {
|
|
62
|
-
console.warn(
|
|
63
|
-
`\x1b[33m⚠ provider failed: ${error.message} → placeholder\x1b[0m`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
59
|
const trackPlaceholder = (type: "image" | "video") => {
|
|
69
60
|
placeholderCount[type === "image" ? "images" : "videos"]++;
|
|
70
61
|
placeholderCount.total++;
|
|
71
62
|
};
|
|
72
63
|
|
|
64
|
+
const cachedGenerateImage = options.cache
|
|
65
|
+
? withCache(generateImage, { storage: fileCache({ dir: options.cache }) })
|
|
66
|
+
: generateImage;
|
|
67
|
+
|
|
68
|
+
const cachedGenerateVideo = options.cache
|
|
69
|
+
? withCache(generateVideo, { storage: fileCache({ dir: options.cache }) })
|
|
70
|
+
: generateVideo;
|
|
71
|
+
|
|
73
72
|
const wrapGenerateImage: typeof generateImage = async (opts) => {
|
|
74
73
|
if (
|
|
75
74
|
typeof opts.model === "string" ||
|
|
76
75
|
opts.model.specificationVersion !== "v3"
|
|
77
76
|
) {
|
|
78
|
-
return
|
|
77
|
+
return cachedGenerateImage(opts);
|
|
79
78
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
|
|
80
|
+
if (mode === "preview") {
|
|
81
|
+
trackPlaceholder("image");
|
|
82
|
+
const wrappedModel = wrapImageModel({
|
|
83
|
+
model: opts.model,
|
|
84
|
+
middleware: imagePlaceholderFallbackMiddleware({
|
|
85
|
+
mode: "preview",
|
|
86
|
+
onFallback: () => {},
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
return generateImage({ ...opts, model: wrappedModel });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return cachedGenerateImage(opts);
|
|
93
93
|
};
|
|
94
94
|
|
|
95
95
|
const wrapGenerateVideo: typeof generateVideo = async (opts) => {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
onFallback(
|
|
103
|
-
},
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return
|
|
96
|
+
if (mode === "preview") {
|
|
97
|
+
trackPlaceholder("video");
|
|
98
|
+
const wrappedModel = wrapVideoModel({
|
|
99
|
+
model: opts.model,
|
|
100
|
+
middleware: placeholderFallbackMiddleware({
|
|
101
|
+
mode: "preview",
|
|
102
|
+
onFallback: () => {},
|
|
103
|
+
}),
|
|
104
|
+
});
|
|
105
|
+
return generateVideo({ ...opts, model: wrappedModel });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return cachedGenerateVideo(opts);
|
|
109
109
|
};
|
|
110
110
|
|
|
111
111
|
const ctx: RenderContext = {
|
|
@@ -113,19 +113,12 @@ export async function renderRoot(
|
|
|
113
113
|
height: props.height ?? 1080,
|
|
114
114
|
fps: props.fps ?? 30,
|
|
115
115
|
cache: options.cache ? fileCache({ dir: options.cache }) : undefined,
|
|
116
|
-
generateImage:
|
|
117
|
-
|
|
118
|
-
storage: fileCache({ dir: options.cache }),
|
|
119
|
-
})
|
|
120
|
-
: wrapGenerateImage,
|
|
121
|
-
generateVideo: options.cache
|
|
122
|
-
? withCache(wrapGenerateVideo, {
|
|
123
|
-
storage: fileCache({ dir: options.cache }),
|
|
124
|
-
})
|
|
125
|
-
: wrapGenerateVideo,
|
|
116
|
+
generateImage: wrapGenerateImage,
|
|
117
|
+
generateVideo: wrapGenerateVideo,
|
|
126
118
|
tempFiles: [],
|
|
127
119
|
progress,
|
|
128
120
|
pending: new Map(),
|
|
121
|
+
defaults: options.defaults,
|
|
129
122
|
};
|
|
130
123
|
|
|
131
124
|
const clipElements: VargElement<"clip">[] = [];
|
|
@@ -177,13 +170,10 @@ export async function renderRoot(
|
|
|
177
170
|
const childElement = child as VargElement;
|
|
178
171
|
|
|
179
172
|
let path: string | undefined;
|
|
180
|
-
const isVideo =
|
|
181
|
-
childElement.type === "video" || childElement.type === "animate";
|
|
173
|
+
const isVideo = childElement.type === "video";
|
|
182
174
|
|
|
183
175
|
if (childElement.type === "video") {
|
|
184
176
|
path = await renderVideo(childElement as VargElement<"video">, ctx);
|
|
185
|
-
} else if (childElement.type === "animate") {
|
|
186
|
-
path = await renderAnimate(childElement as VargElement<"animate">, ctx);
|
|
187
177
|
} else if (childElement.type === "image") {
|
|
188
178
|
path = await renderImage(childElement as VargElement<"image">, ctx);
|
|
189
179
|
}
|
|
@@ -255,11 +245,11 @@ export async function renderRoot(
|
|
|
255
245
|
let path: string;
|
|
256
246
|
if (musicProps.src) {
|
|
257
247
|
path = resolvePath(musicProps.src);
|
|
258
|
-
} else if (musicProps.prompt
|
|
248
|
+
} else if (musicProps.prompt) {
|
|
259
249
|
const result = await renderMusic(musicElement, ctx);
|
|
260
250
|
path = result.path;
|
|
261
251
|
} else {
|
|
262
|
-
throw new Error("Music requires either src or prompt
|
|
252
|
+
throw new Error("Music requires either src or prompt");
|
|
263
253
|
}
|
|
264
254
|
|
|
265
255
|
audioTracks.push({
|
|
@@ -287,6 +277,8 @@ export async function renderRoot(
|
|
|
287
277
|
fps: ctx.fps,
|
|
288
278
|
clips,
|
|
289
279
|
audioTracks: audioTracks.length > 0 ? audioTracks : undefined,
|
|
280
|
+
shortest: props.shortest,
|
|
281
|
+
verbose: options.verbose,
|
|
290
282
|
});
|
|
291
283
|
|
|
292
284
|
completeTask(progress, editlyTaskId);
|
|
@@ -302,16 +294,10 @@ export async function renderRoot(
|
|
|
302
294
|
completeTask(progress, captionsTaskId);
|
|
303
295
|
}
|
|
304
296
|
|
|
305
|
-
if (!options.quiet && placeholderCount.total > 0) {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
);
|
|
310
|
-
} else {
|
|
311
|
-
console.warn(
|
|
312
|
-
`\x1b[33m⚠ ${placeholderCount.total} elements used placeholders - run with --strict for production\x1b[0m`,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
297
|
+
if (!options.quiet && mode === "preview" && placeholderCount.total > 0) {
|
|
298
|
+
console.log(
|
|
299
|
+
`\x1b[36mℹ preview mode: ${placeholderCount.total} placeholders used (${placeholderCount.images} images, ${placeholderCount.videos} videos)\x1b[0m`,
|
|
300
|
+
);
|
|
315
301
|
}
|
|
316
302
|
|
|
317
303
|
const result = await Bun.file(finalOutPath).arrayBuffer();
|
|
@@ -21,9 +21,9 @@ export async function renderSpeech(
|
|
|
21
21
|
throw new Error("Speech element requires text content");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
const model = props.model;
|
|
24
|
+
const model = props.model ?? ctx.defaults?.speech;
|
|
25
25
|
if (!model) {
|
|
26
|
-
throw new Error("Speech
|
|
26
|
+
throw new Error("Speech requires 'model' prop (or set defaults.speech)");
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const cacheKey = computeCacheKey(element);
|