vargai 0.4.0-alpha44 → 0.4.0-alpha46
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/launch-videos/06-kawaii-fruits.tsx +3 -3
- package/launch-videos/07-ugc-weight-loss.tsx +2 -2
- package/package.json +1 -1
- package/skills/varg-video-generation/scripts/setup.ts +3 -9
- package/src/ai-sdk/cache.ts +1 -1
- package/src/ai-sdk/examples/google-image.ts +4 -4
- package/src/ai-sdk/examples/higgsfield-image.ts +12 -6
- package/src/ai-sdk/examples/replicate-bg-removal.ts +13 -7
- package/src/ai-sdk/examples/talking-lion.ts +3 -1
- package/src/ai-sdk/examples/video-generation.ts +3 -1
- package/src/ai-sdk/examples/workflow-animated-girl.ts +3 -1
- package/src/ai-sdk/examples/workflow-before-after.ts +6 -2
- package/src/ai-sdk/examples/workflow-character-grid.ts +3 -1
- package/src/ai-sdk/examples/workflow-slideshow.ts +7 -3
- package/src/ai-sdk/file.ts +30 -19
- package/src/ai-sdk/generate-element.ts +2 -2
- package/src/ai-sdk/generate-video.ts +3 -4
- package/src/ai-sdk/index.ts +12 -0
- package/src/ai-sdk/middleware/placeholder.ts +3 -3
- package/src/ai-sdk/middleware/wrap-image-model.ts +1 -5
- package/src/ai-sdk/providers/editly/backends/index.ts +1 -0
- package/src/ai-sdk/providers/editly/backends/local.ts +13 -4
- package/src/ai-sdk/providers/editly/backends/types.ts +14 -4
- package/src/ai-sdk/providers/editly/index.ts +4 -7
- package/src/ai-sdk/providers/editly/layers.ts +0 -1
- package/src/ai-sdk/providers/editly/rendi/editly-with-rendi-backend.test.ts +20 -11
- package/src/ai-sdk/providers/editly/rendi/index.ts +27 -14
- package/src/ai-sdk/providers/editly/rendi/rendi.test.ts +9 -2
- package/src/ai-sdk/providers/fal.ts +9 -6
- package/src/ai-sdk/storage/fal.ts +11 -0
- package/src/ai-sdk/storage/index.ts +3 -0
- package/src/ai-sdk/storage/r2.ts +55 -0
- package/src/ai-sdk/storage/types.ts +3 -0
- package/src/cli/commands/frame.tsx +1 -1
- package/src/cli/commands/init.tsx +1 -1
- package/src/cli/commands/storyboard.tsx +8 -11
- package/src/definitions/actions/qwen-angles.ts +6 -4
- package/src/react/renderers/cache.test.ts +6 -3
- package/src/react/renderers/captions.ts +19 -4
- package/src/react/renderers/clip.ts +34 -29
- package/src/react/renderers/context.ts +4 -3
- package/src/react/renderers/image.ts +13 -17
- package/src/react/renderers/index.ts +0 -1
- package/src/react/renderers/music.ts +26 -13
- package/src/react/renderers/packshot/blinking-button.ts +0 -1
- package/src/react/renderers/packshot.ts +2 -4
- package/src/react/renderers/render.ts +17 -12
- package/src/react/renderers/slider.ts +24 -14
- package/src/react/renderers/speech.ts +4 -13
- package/src/react/renderers/split.ts +11 -5
- package/src/react/renderers/swipe.ts +29 -14
- package/src/react/renderers/video.ts +17 -22
- package/src/react/types.ts +2 -0
- package/src/studio/step-renderer.ts +15 -8
|
@@ -65,12 +65,12 @@ export default (
|
|
|
65
65
|
/>
|
|
66
66
|
|
|
67
67
|
{/* Scene 1-4: Each character waves individually */}
|
|
68
|
-
{
|
|
69
|
-
<Clip key={
|
|
68
|
+
{characterImages.map((charImage, i) => (
|
|
69
|
+
<Clip key={CHARACTERS[i]?.name ?? i} duration={2.5}>
|
|
70
70
|
<Video
|
|
71
71
|
prompt={{
|
|
72
72
|
text: "character waves hello enthusiastically, bounces up and down slightly, eyes squint with joy, tiny feet wiggle",
|
|
73
|
-
images: [
|
|
73
|
+
images: [charImage],
|
|
74
74
|
}}
|
|
75
75
|
model={fal.videoModel("kling-v2.5")}
|
|
76
76
|
duration={5}
|
|
@@ -96,8 +96,8 @@ const voiceover = Speech({
|
|
|
96
96
|
// === MUSIC ===
|
|
97
97
|
const MUSIC_PROMPT =
|
|
98
98
|
"upbeat motivational pop, inspiring transformation music, energetic but not overwhelming, modern fitness vibe";
|
|
99
|
-
const
|
|
100
|
-
const
|
|
99
|
+
const _MUSIC_DURATION = 15;
|
|
100
|
+
const _MUSIC_VOLUME = 0.15; // Low volume so voiceover is clear
|
|
101
101
|
|
|
102
102
|
// === CAPTIONS SETTINGS ===
|
|
103
103
|
const CAPTIONS_STYLE = "tiktok";
|
package/package.json
CHANGED
|
@@ -9,14 +9,8 @@
|
|
|
9
9
|
* bun scripts/setup.ts
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
mkdirSync,
|
|
15
|
-
readFileSync,
|
|
16
|
-
symlinkSync,
|
|
17
|
-
writeFileSync,
|
|
18
|
-
} from "node:fs";
|
|
19
|
-
import { dirname, join } from "node:path";
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { join } from "node:path";
|
|
20
14
|
|
|
21
15
|
const COLORS = {
|
|
22
16
|
reset: "\x1b[0m",
|
|
@@ -228,7 +222,7 @@ Get your free API key at: ${COLORS.cyan}https://fal.ai/dashboard/keys${COLORS.re
|
|
|
228
222
|
}
|
|
229
223
|
|
|
230
224
|
if (added) {
|
|
231
|
-
writeFileSync(gitignorePath, gitignoreContent.trim()
|
|
225
|
+
writeFileSync(gitignorePath, `${gitignoreContent.trim()}\n`);
|
|
232
226
|
log.success("Updated .gitignore");
|
|
233
227
|
} else {
|
|
234
228
|
log.info(".gitignore already configured");
|
package/src/ai-sdk/cache.ts
CHANGED
|
@@ -43,7 +43,7 @@ function parseTTL(ttl: number | string | undefined): number | undefined {
|
|
|
43
43
|
const match = ttl.match(/^(\d+)(s|m|h|d)$/);
|
|
44
44
|
if (!match) return undefined;
|
|
45
45
|
|
|
46
|
-
const value = Number.parseInt(match[1]
|
|
46
|
+
const value = Number.parseInt(match[1] ?? "0", 10);
|
|
47
47
|
const unit = match[2];
|
|
48
48
|
|
|
49
49
|
switch (unit) {
|
|
@@ -22,8 +22,8 @@ async function main() {
|
|
|
22
22
|
await Bun.write("output/google-mountain.png", images[0].uint8Array);
|
|
23
23
|
console.log(" saved to output/google-mountain.png");
|
|
24
24
|
}
|
|
25
|
-
} catch (error
|
|
26
|
-
console.error(" error:", error.message
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(" error:", error instanceof Error ? error.message : error);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
console.log("\n2. image-to-image with nano-banana-pro/edit...");
|
|
@@ -52,8 +52,8 @@ async function main() {
|
|
|
52
52
|
);
|
|
53
53
|
console.log(" saved to output/google-mountain-sunset.png");
|
|
54
54
|
}
|
|
55
|
-
} catch (error
|
|
56
|
-
console.error(" error:", error.message
|
|
55
|
+
} catch (error) {
|
|
56
|
+
console.error(" error:", error instanceof Error ? error.message : error);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
console.log("\ndone!");
|
|
@@ -15,8 +15,10 @@ async function main() {
|
|
|
15
15
|
aspectRatio: "1:1",
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const firstImage = images[0];
|
|
19
|
+
if (!firstImage) throw new Error("No image generated");
|
|
20
|
+
console.log(`image generated: ${firstImage.uint8Array.byteLength} bytes`);
|
|
21
|
+
await Bun.write("output/higgsfield-default.png", firstImage.uint8Array);
|
|
20
22
|
|
|
21
23
|
console.log("\ngenerating with model settings...");
|
|
22
24
|
const { images: styledImages } = await generateImage({
|
|
@@ -28,10 +30,12 @@ async function main() {
|
|
|
28
30
|
aspectRatio: "16:9",
|
|
29
31
|
});
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const firstStyledImage = styledImages[0];
|
|
34
|
+
if (!firstStyledImage) throw new Error("No styled image generated");
|
|
35
|
+
console.log(`styled image: ${firstStyledImage.uint8Array.byteLength} bytes`);
|
|
32
36
|
await Bun.write(
|
|
33
37
|
"output/higgsfield-cinematic.png",
|
|
34
|
-
|
|
38
|
+
firstStyledImage.uint8Array,
|
|
35
39
|
);
|
|
36
40
|
|
|
37
41
|
console.log("\ngenerating with provider defaults...");
|
|
@@ -47,12 +51,14 @@ async function main() {
|
|
|
47
51
|
aspectRatio: "4:3",
|
|
48
52
|
});
|
|
49
53
|
|
|
54
|
+
const firstEnhancedImage = enhancedImages[0];
|
|
55
|
+
if (!firstEnhancedImage) throw new Error("No enhanced image generated");
|
|
50
56
|
console.log(
|
|
51
|
-
`enhanced image: ${
|
|
57
|
+
`enhanced image: ${firstEnhancedImage.uint8Array.byteLength} bytes`,
|
|
52
58
|
);
|
|
53
59
|
await Bun.write(
|
|
54
60
|
"output/higgsfield-enhanced.png",
|
|
55
|
-
|
|
61
|
+
firstEnhancedImage.uint8Array,
|
|
56
62
|
);
|
|
57
63
|
|
|
58
64
|
console.log("\ndone!");
|
|
@@ -14,10 +14,12 @@ async function main() {
|
|
|
14
14
|
n: 1,
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
const firstSourceImage = sourceImages[0];
|
|
18
|
+
if (!firstSourceImage) throw new Error("No source image generated");
|
|
19
|
+
console.log(`source image: ${firstSourceImage.uint8Array.byteLength} bytes`);
|
|
20
|
+
await Bun.write("output/bg-removal-source.png", firstSourceImage.uint8Array);
|
|
19
21
|
|
|
20
|
-
const sourceFile = File.from(
|
|
22
|
+
const sourceFile = File.from(firstSourceImage);
|
|
21
23
|
|
|
22
24
|
console.log("\nremoving background...");
|
|
23
25
|
const { images: processedImages } = await generateImage({
|
|
@@ -27,12 +29,14 @@ async function main() {
|
|
|
27
29
|
},
|
|
28
30
|
});
|
|
29
31
|
|
|
32
|
+
const firstProcessedImage = processedImages[0];
|
|
33
|
+
if (!firstProcessedImage) throw new Error("No processed image generated");
|
|
30
34
|
console.log(
|
|
31
|
-
`processed image: ${
|
|
35
|
+
`processed image: ${firstProcessedImage.uint8Array.byteLength} bytes`,
|
|
32
36
|
);
|
|
33
37
|
await Bun.write(
|
|
34
38
|
"output/bg-removal-result.png",
|
|
35
|
-
|
|
39
|
+
firstProcessedImage.uint8Array,
|
|
36
40
|
);
|
|
37
41
|
|
|
38
42
|
console.log("\nusing alternative model...");
|
|
@@ -43,8 +47,10 @@ async function main() {
|
|
|
43
47
|
},
|
|
44
48
|
});
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
const firstAltImage = altImages[0];
|
|
51
|
+
if (!firstAltImage) throw new Error("No alt image generated");
|
|
52
|
+
console.log(`alt result: ${firstAltImage.uint8Array.byteLength} bytes`);
|
|
53
|
+
await Bun.write("output/bg-removal-alt.png", firstAltImage.uint8Array);
|
|
48
54
|
|
|
49
55
|
console.log("\ndone!");
|
|
50
56
|
}
|
|
@@ -26,7 +26,9 @@ Whether you're building social content or creative apps, Varg has got you covere
|
|
|
26
26
|
}),
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
-
const
|
|
29
|
+
const firstImage = imageResult.images[0];
|
|
30
|
+
if (!firstImage) throw new Error("No image generated");
|
|
31
|
+
const image = File.from(firstImage);
|
|
30
32
|
const audio = File.from(speechResult.audio);
|
|
31
33
|
|
|
32
34
|
console.log(`image: ${(await image.data()).byteLength} bytes`);
|
|
@@ -20,7 +20,9 @@ async function main() {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
console.log("animating image to video...");
|
|
23
|
-
const
|
|
23
|
+
const firstImage = images[0];
|
|
24
|
+
if (!firstImage) throw new Error("No image generated");
|
|
25
|
+
const image = File.from(firstImage);
|
|
24
26
|
const { video: animatedVideo } = await generateVideo({
|
|
25
27
|
model: fal.videoModel("wan-2.5"),
|
|
26
28
|
prompt: {
|
|
@@ -30,7 +30,9 @@ Don't forget to like and subscribe for more content like this!`;
|
|
|
30
30
|
cacheKey: ["animated-girl", "portrait"],
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const firstImage = images[0];
|
|
34
|
+
if (!firstImage) throw new Error("No image generated");
|
|
35
|
+
const imageData = firstImage.uint8Array;
|
|
34
36
|
await Bun.write("output/workflow-girl-image.png", imageData);
|
|
35
37
|
console.log("saved: output/workflow-girl-image.png");
|
|
36
38
|
|
|
@@ -24,7 +24,9 @@ async function main() {
|
|
|
24
24
|
cacheKey: ["before-after", "before-image"],
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
const
|
|
27
|
+
const firstBeforeImage = beforeImages[0];
|
|
28
|
+
if (!firstBeforeImage) throw new Error("No before image generated");
|
|
29
|
+
const beforeImage = firstBeforeImage.uint8Array;
|
|
28
30
|
await Bun.write("output/workflow-before-image.png", beforeImage);
|
|
29
31
|
console.log("saved: output/workflow-before-image.png");
|
|
30
32
|
|
|
@@ -37,7 +39,9 @@ async function main() {
|
|
|
37
39
|
cacheKey: ["before-after", "after-image"],
|
|
38
40
|
});
|
|
39
41
|
|
|
40
|
-
const
|
|
42
|
+
const firstAfterImage = afterImages[0];
|
|
43
|
+
if (!firstAfterImage) throw new Error("No after image generated");
|
|
44
|
+
const afterImage = firstAfterImage.uint8Array;
|
|
41
45
|
await Bun.write("output/workflow-after-image.png", afterImage);
|
|
42
46
|
console.log("saved: output/workflow-after-image.png");
|
|
43
47
|
|
|
@@ -71,7 +71,9 @@ async function main() {
|
|
|
71
71
|
n: 1,
|
|
72
72
|
cacheKey: ["character-grid", name],
|
|
73
73
|
});
|
|
74
|
-
const
|
|
74
|
+
const firstImage = images[0];
|
|
75
|
+
if (!firstImage) throw new Error(`No image generated for ${name}`);
|
|
76
|
+
const data = firstImage.uint8Array;
|
|
75
77
|
await Bun.write(`output/character-${i}-${name.toLowerCase()}.png`, data);
|
|
76
78
|
return {
|
|
77
79
|
name,
|
|
@@ -46,7 +46,9 @@ And ending with this amazing view.`;
|
|
|
46
46
|
n: 1,
|
|
47
47
|
cacheKey: ["slideshow", "scene", i],
|
|
48
48
|
});
|
|
49
|
-
const
|
|
49
|
+
const firstImage = images[0];
|
|
50
|
+
if (!firstImage) throw new Error(`No image generated for scene ${i}`);
|
|
51
|
+
const data = firstImage.uint8Array;
|
|
50
52
|
await Bun.write(`output/workflow-scene-${i}.png`, data);
|
|
51
53
|
return `output/workflow-scene-${i}.png`;
|
|
52
54
|
}),
|
|
@@ -62,7 +64,9 @@ And ending with this amazing view.`;
|
|
|
62
64
|
cacheKey: ["slideshow", "talking-head"],
|
|
63
65
|
});
|
|
64
66
|
|
|
65
|
-
const
|
|
67
|
+
const firstTalkingImage = talkingImages[0];
|
|
68
|
+
if (!firstTalkingImage) throw new Error("No talking head image generated");
|
|
69
|
+
const talkingImage = firstTalkingImage.uint8Array;
|
|
66
70
|
await Bun.write("output/workflow-talking-head.png", talkingImage);
|
|
67
71
|
|
|
68
72
|
console.log("\nstep 3: generating voiceover...");
|
|
@@ -121,7 +125,7 @@ And ending with this amazing view.`;
|
|
|
121
125
|
await Bun.write("output/workflow-slideshow.srt", srtContent);
|
|
122
126
|
|
|
123
127
|
console.log("\nstep 7: creating slideshow with pip...");
|
|
124
|
-
const clips = sceneImages.map((imagePath,
|
|
128
|
+
const clips = sceneImages.map((imagePath, _i) => ({
|
|
125
129
|
duration: 2,
|
|
126
130
|
layers: [
|
|
127
131
|
{ type: "image" as const, path: imagePath },
|
package/src/ai-sdk/file.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ImageModelV3File } from "@ai-sdk/provider";
|
|
2
|
+
import type { StorageProvider } from "./storage/types";
|
|
2
3
|
|
|
3
4
|
export class File {
|
|
4
5
|
private _data: Uint8Array | null = null;
|
|
@@ -8,13 +9,14 @@ export class File {
|
|
|
8
9
|
|
|
9
10
|
private constructor(
|
|
10
11
|
options:
|
|
11
|
-
| { data: Uint8Array; mediaType: string }
|
|
12
|
+
| { data: Uint8Array; mediaType: string; url?: string }
|
|
12
13
|
| { url: string; mediaType?: string }
|
|
13
14
|
| { loader: () => Promise<Uint8Array>; mediaType: string },
|
|
14
15
|
) {
|
|
15
16
|
if ("data" in options) {
|
|
16
17
|
this._data = options.data;
|
|
17
18
|
this._mediaType = options.mediaType;
|
|
19
|
+
this._url = options.url ?? null;
|
|
18
20
|
} else if ("url" in options) {
|
|
19
21
|
this._url = options.url;
|
|
20
22
|
this._mediaType = options.mediaType ?? inferMediaType(options.url);
|
|
@@ -46,10 +48,12 @@ export class File {
|
|
|
46
48
|
static fromGenerated(generated: {
|
|
47
49
|
uint8Array: Uint8Array;
|
|
48
50
|
mediaType: string;
|
|
51
|
+
url?: string;
|
|
49
52
|
}): File {
|
|
50
53
|
return new File({
|
|
51
54
|
data: generated.uint8Array,
|
|
52
55
|
mediaType: generated.mediaType,
|
|
56
|
+
url: generated.url,
|
|
53
57
|
});
|
|
54
58
|
}
|
|
55
59
|
|
|
@@ -119,6 +123,10 @@ export class File {
|
|
|
119
123
|
return this._mediaType.startsWith("video/");
|
|
120
124
|
}
|
|
121
125
|
|
|
126
|
+
get url(): string | null {
|
|
127
|
+
return this._url;
|
|
128
|
+
}
|
|
129
|
+
|
|
122
130
|
async data(): Promise<Uint8Array> {
|
|
123
131
|
if (this._data) return this._data;
|
|
124
132
|
if (this._loader) {
|
|
@@ -142,38 +150,41 @@ export class File {
|
|
|
142
150
|
return new Blob([data], { type: this._mediaType });
|
|
143
151
|
}
|
|
144
152
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
153
|
+
/**
|
|
154
|
+
* Upload file to storage and return the URL. Returns cached URL if already uploaded.
|
|
155
|
+
* @param storage - Storage provider to use for upload
|
|
156
|
+
* @returns URL of the uploaded file
|
|
157
|
+
*/
|
|
158
|
+
async upload(storage: StorageProvider): Promise<string> {
|
|
159
|
+
if (this._url) return this._url;
|
|
160
|
+
const data = await this.data();
|
|
161
|
+
const key = `varg/${Date.now()}-${Math.random().toString(36).slice(2)}${this.extensionFromMediaType()}`;
|
|
162
|
+
this._url = await storage.upload(data, key, this._mediaType);
|
|
163
|
+
return this._url;
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
async base64(): Promise<string> {
|
|
160
167
|
const data = await this.arrayBuffer();
|
|
161
168
|
let binary = "";
|
|
162
|
-
for (
|
|
163
|
-
binary += String.fromCharCode(
|
|
169
|
+
for (const byte of data) {
|
|
170
|
+
binary += String.fromCharCode(byte);
|
|
164
171
|
}
|
|
165
172
|
return btoa(binary);
|
|
166
173
|
}
|
|
167
174
|
|
|
168
175
|
async toInput(): Promise<ImageModelV3File> {
|
|
169
|
-
if (this._url
|
|
176
|
+
if (this._url) {
|
|
170
177
|
return { type: "url", url: this._url };
|
|
171
178
|
}
|
|
172
179
|
const data = await this.arrayBuffer();
|
|
173
180
|
return { type: "file", mediaType: this._mediaType, data };
|
|
174
181
|
}
|
|
175
182
|
|
|
176
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Write file data to a temporary file and return the path.
|
|
185
|
+
* @returns Path to the temporary file
|
|
186
|
+
*/
|
|
187
|
+
async toTempFile(): Promise<string> {
|
|
177
188
|
const data = await this.data();
|
|
178
189
|
const ext = this.extensionFromMediaType();
|
|
179
190
|
const tmpDir = process.env.TMPDIR ?? "/tmp";
|
|
@@ -189,10 +200,10 @@ export class File {
|
|
|
189
200
|
| File,
|
|
190
201
|
): Promise<string> {
|
|
191
202
|
if (file instanceof File) {
|
|
192
|
-
return file.
|
|
203
|
+
return file.toTempFile();
|
|
193
204
|
}
|
|
194
205
|
const f = File.from(file);
|
|
195
|
-
return f.
|
|
206
|
+
return f.toTempFile();
|
|
196
207
|
}
|
|
197
208
|
|
|
198
209
|
private extensionFromMediaType(): string {
|
|
@@ -59,9 +59,8 @@ class DefaultGeneratedVideo implements GeneratedVideo {
|
|
|
59
59
|
|
|
60
60
|
get base64(): string {
|
|
61
61
|
let binary = "";
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
binary += String.fromCharCode(bytes[i]!);
|
|
62
|
+
for (const byte of this._data) {
|
|
63
|
+
binary += String.fromCharCode(byte);
|
|
65
64
|
}
|
|
66
65
|
return btoa(binary);
|
|
67
66
|
}
|
|
@@ -158,7 +157,7 @@ export async function generateVideo(
|
|
|
158
157
|
}
|
|
159
158
|
|
|
160
159
|
return {
|
|
161
|
-
video: videos[0]
|
|
160
|
+
video: videos[0] as GeneratedVideo,
|
|
162
161
|
videos,
|
|
163
162
|
warnings,
|
|
164
163
|
};
|
package/src/ai-sdk/index.ts
CHANGED
|
@@ -54,8 +54,14 @@ export {
|
|
|
54
54
|
type Clip as EditlyClip,
|
|
55
55
|
type EditlyConfig,
|
|
56
56
|
editly,
|
|
57
|
+
type FFmpegBackend,
|
|
57
58
|
type Layer as EditlyLayer,
|
|
59
|
+
localBackend,
|
|
58
60
|
} from "./providers/editly";
|
|
61
|
+
export {
|
|
62
|
+
createRendiBackend,
|
|
63
|
+
type RendiBackendOptions,
|
|
64
|
+
} from "./providers/editly/rendi";
|
|
59
65
|
export {
|
|
60
66
|
createElevenLabs,
|
|
61
67
|
type ElevenLabsProvider,
|
|
@@ -92,6 +98,12 @@ export {
|
|
|
92
98
|
createTogetherProvider,
|
|
93
99
|
together,
|
|
94
100
|
} from "./providers/together";
|
|
101
|
+
export {
|
|
102
|
+
falStorage,
|
|
103
|
+
type R2StorageOptions,
|
|
104
|
+
r2Storage,
|
|
105
|
+
type StorageProvider,
|
|
106
|
+
} from "./storage";
|
|
95
107
|
export type {
|
|
96
108
|
VideoModelV3,
|
|
97
109
|
VideoModelV3CallOptions,
|
|
@@ -35,9 +35,9 @@ function hslToHex(hsl: string): string {
|
|
|
35
35
|
const match = hsl.match(/hsl\((\d+),(\d+)%,(\d+)%\)/);
|
|
36
36
|
if (!match) return "333333";
|
|
37
37
|
|
|
38
|
-
const h = Number.parseInt(match[1]
|
|
39
|
-
const s = Number.parseInt(match[2]
|
|
40
|
-
const l = Number.parseInt(match[3]
|
|
38
|
+
const h = Number.parseInt(match[1] ?? "0", 10) / 360;
|
|
39
|
+
const s = Number.parseInt(match[2] ?? "0", 10) / 100;
|
|
40
|
+
const l = Number.parseInt(match[3] ?? "0", 10) / 100;
|
|
41
41
|
|
|
42
42
|
const hue2rgb = (p: number, q: number, t: number) => {
|
|
43
43
|
if (t < 0) t += 1;
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ImageModelV3,
|
|
3
|
-
ImageModelV3CallOptions,
|
|
4
|
-
ImageModelV3Middleware,
|
|
5
|
-
} from "@ai-sdk/provider";
|
|
1
|
+
import type { ImageModelV3, ImageModelV3Middleware } from "@ai-sdk/provider";
|
|
6
2
|
import { wrapImageModel } from "ai";
|
|
7
3
|
import { generatePlaceholder } from "./placeholder";
|
|
8
4
|
import type { RenderMode } from "./wrap-video-model";
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { $ } from "bun";
|
|
2
|
+
import { File } from "../../../file";
|
|
2
3
|
import type {
|
|
3
4
|
FFmpegBackend,
|
|
4
5
|
FFmpegInput,
|
|
5
6
|
FFmpegRunOptions,
|
|
6
7
|
FFmpegRunResult,
|
|
8
|
+
FilePath,
|
|
7
9
|
VideoInfo,
|
|
8
10
|
} from "./types";
|
|
9
11
|
|
|
@@ -38,16 +40,23 @@ export class LocalBackend implements FFmpegBackend {
|
|
|
38
40
|
};
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
async resolvePath(path: FilePath): Promise<string> {
|
|
44
|
+
if (typeof path === "string") return path;
|
|
45
|
+
return path.url ?? (await path.toTempFile());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async buildInputArgs(inputs: FFmpegInput[]): Promise<string[]> {
|
|
42
49
|
const args: string[] = [];
|
|
43
50
|
for (const input of inputs) {
|
|
44
|
-
if (
|
|
51
|
+
if (input instanceof File) {
|
|
52
|
+
args.push("-i", await this.resolvePath(input));
|
|
53
|
+
} else if (typeof input === "string") {
|
|
45
54
|
args.push("-i", input);
|
|
46
55
|
} else if ("raw" in input) {
|
|
47
56
|
args.push(...input.raw);
|
|
48
57
|
} else {
|
|
49
58
|
if (input.options) args.push(...input.options);
|
|
50
|
-
args.push("-i", input.path);
|
|
59
|
+
args.push("-i", await this.resolvePath(input.path));
|
|
51
60
|
}
|
|
52
61
|
}
|
|
53
62
|
return args;
|
|
@@ -63,7 +72,7 @@ export class LocalBackend implements FFmpegBackend {
|
|
|
63
72
|
verbose,
|
|
64
73
|
} = options;
|
|
65
74
|
|
|
66
|
-
const inputArgs = this.buildInputArgs(inputs);
|
|
75
|
+
const inputArgs = await this.buildInputArgs(inputs);
|
|
67
76
|
|
|
68
77
|
const ffmpegArgs = [
|
|
69
78
|
"-hide_banner",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Allows switching between local ffmpeg and cloud services like Rendi
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import type { File } from "../../../file";
|
|
6
7
|
import type { VideoInfo } from "../types";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,14 +11,16 @@ import type { VideoInfo } from "../types";
|
|
|
10
11
|
*/
|
|
11
12
|
export type { VideoInfo };
|
|
12
13
|
|
|
14
|
+
export type FilePath = File | string;
|
|
15
|
+
|
|
13
16
|
/**
|
|
14
|
-
* Represents an input to ffmpeg - can be a simple path/URL or structured with options
|
|
17
|
+
* Represents an input to ffmpeg - can be a simple path/URL, File, or structured with options
|
|
15
18
|
*/
|
|
16
19
|
export type FFmpegInput =
|
|
17
|
-
|
|
|
20
|
+
| FilePath
|
|
18
21
|
| {
|
|
19
|
-
/** Path or
|
|
20
|
-
path:
|
|
22
|
+
/** Path, URL, or File for the input */
|
|
23
|
+
path: FilePath;
|
|
21
24
|
/** Options to apply BEFORE the -i flag (e.g. -ss 5 for seeking) */
|
|
22
25
|
options?: string[];
|
|
23
26
|
}
|
|
@@ -65,6 +68,13 @@ export interface FFmpegBackend {
|
|
|
65
68
|
*/
|
|
66
69
|
ffprobe(input: string): Promise<VideoInfo>;
|
|
67
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Resolve a FilePath to a string path/URL for ffmpeg
|
|
73
|
+
* Local backend: returns URL if available, otherwise writes to temp
|
|
74
|
+
* Rendi backend: uploads local files, returns URL
|
|
75
|
+
*/
|
|
76
|
+
resolvePath(path: FilePath): Promise<string>;
|
|
77
|
+
|
|
68
78
|
/**
|
|
69
79
|
* Run ffmpeg command
|
|
70
80
|
* @param options - Execution options including args, inputs, and output path
|
|
@@ -161,7 +161,7 @@ function isTextOverlayLayer(layer: Layer): boolean {
|
|
|
161
161
|
|
|
162
162
|
function buildBaseClipFilter(
|
|
163
163
|
clip: ProcessedClip,
|
|
164
|
-
|
|
164
|
+
_clipIndex: number,
|
|
165
165
|
width: number,
|
|
166
166
|
height: number,
|
|
167
167
|
inputOffset: number,
|
|
@@ -416,8 +416,7 @@ function buildAudioFilter(
|
|
|
416
416
|
let inputIdx = videoInputCount;
|
|
417
417
|
|
|
418
418
|
if (videoSourceAudio && videoSourceAudio.length > 0) {
|
|
419
|
-
for (
|
|
420
|
-
const src = videoSourceAudio[i]!;
|
|
419
|
+
for (const [i, src] of videoSourceAudio.entries()) {
|
|
421
420
|
const { inputIndex, startTime, duration, cutFrom, mixVolume } = src;
|
|
422
421
|
|
|
423
422
|
const shouldInclude =
|
|
@@ -460,8 +459,7 @@ function buildAudioFilter(
|
|
|
460
459
|
inputIdx++;
|
|
461
460
|
}
|
|
462
461
|
|
|
463
|
-
for (
|
|
464
|
-
const track = audioTracks[i]!;
|
|
462
|
+
for (const [i, track] of audioTracks.entries()) {
|
|
465
463
|
audioInputs.push(track.path);
|
|
466
464
|
const label = `atrk${i}`;
|
|
467
465
|
|
|
@@ -483,8 +481,7 @@ function buildAudioFilter(
|
|
|
483
481
|
inputIdx++;
|
|
484
482
|
}
|
|
485
483
|
|
|
486
|
-
for (
|
|
487
|
-
const { layer, clipStartTime } = clipAudioLayers[i]!;
|
|
484
|
+
for (const [i, { layer, clipStartTime }] of clipAudioLayers.entries()) {
|
|
488
485
|
audioInputs.push(layer.path);
|
|
489
486
|
const label = `aclip${i}`;
|
|
490
487
|
|