vargai 0.4.0-alpha43 → 0.4.0-alpha45
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 +2 -2
- package/src/ai-sdk/generate-element.ts +2 -2
- package/src/ai-sdk/generate-video.ts +3 -4
- 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/index.ts +4 -7
- package/src/ai-sdk/providers/editly/layers.ts +0 -1
- package/src/ai-sdk/providers/editly/rendi/index.ts +2 -4
- package/src/ai-sdk/providers/fal.ts +19 -8
- 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 +2 -2
- package/src/react/renderers/captions.ts +13 -1
- package/src/react/renderers/packshot/blinking-button.ts +0 -1
- package/src/react/renderers/packshot.ts +0 -3
- package/src/react/renderers/render.ts +0 -1
- package/src/react/renderers/slider.ts +3 -1
- package/src/react/renderers/split.ts +3 -1
- package/src/react/renderers/swipe.ts +3 -1
- package/src/studio/step-renderer.ts +2 -1
|
@@ -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
|
@@ -159,8 +159,8 @@ export class File {
|
|
|
159
159
|
async base64(): Promise<string> {
|
|
160
160
|
const data = await this.arrayBuffer();
|
|
161
161
|
let binary = "";
|
|
162
|
-
for (
|
|
163
|
-
binary += String.fromCharCode(
|
|
162
|
+
for (const byte of data) {
|
|
163
|
+
binary += String.fromCharCode(byte);
|
|
164
164
|
}
|
|
165
165
|
return btoa(binary);
|
|
166
166
|
}
|
|
@@ -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
|
};
|
|
@@ -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";
|
|
@@ -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
|
|
|
@@ -123,8 +123,7 @@ export class RendiBackend implements FFmpegBackend {
|
|
|
123
123
|
const inputFiles: Record<string, string> = {};
|
|
124
124
|
const pathToPlaceholder = new Map<string, string>();
|
|
125
125
|
|
|
126
|
-
for (
|
|
127
|
-
const input = inputs[i]!;
|
|
126
|
+
for (const [i, input] of inputs.entries()) {
|
|
128
127
|
const path = this.getInputPath(input);
|
|
129
128
|
const url = this.ensureUrl(path);
|
|
130
129
|
const placeholder = `in_${i + 1}`;
|
|
@@ -146,8 +145,7 @@ export class RendiBackend implements FFmpegBackend {
|
|
|
146
145
|
};
|
|
147
146
|
|
|
148
147
|
const inputArgs: string[] = [];
|
|
149
|
-
for (
|
|
150
|
-
const input = inputs[i]!;
|
|
148
|
+
for (const [i, input] of inputs.entries()) {
|
|
151
149
|
if (typeof input !== "string" && "options" in input && input.options) {
|
|
152
150
|
inputArgs.push(...input.options);
|
|
153
151
|
}
|
|
@@ -183,6 +183,8 @@ function detectImageType(bytes: Uint8Array): string | undefined {
|
|
|
183
183
|
return undefined;
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
const uploadCache = fileCache({ dir: ".cache/fal-uploads" });
|
|
187
|
+
|
|
186
188
|
async function fileToUrl(file: ImageModelV3File): Promise<string> {
|
|
187
189
|
if (file.type === "url") return file.url;
|
|
188
190
|
const data = file.data;
|
|
@@ -190,9 +192,15 @@ async function fileToUrl(file: ImageModelV3File): Promise<string> {
|
|
|
190
192
|
typeof data === "string"
|
|
191
193
|
? Uint8Array.from(atob(data), (c) => c.charCodeAt(0))
|
|
192
194
|
: data;
|
|
193
|
-
|
|
195
|
+
|
|
196
|
+
const hash = Bun.hash(bytes).toString(16);
|
|
197
|
+
const cached = (await uploadCache.get(hash)) as string | undefined;
|
|
198
|
+
if (cached) return cached;
|
|
199
|
+
|
|
194
200
|
const mediaType = file.mediaType ?? detectImageType(bytes) ?? "image/png";
|
|
195
|
-
|
|
201
|
+
const url = await fal.storage.upload(new Blob([bytes], { type: mediaType }));
|
|
202
|
+
await uploadCache.set(hash, url, 7 * 24 * 60 * 60 * 1000);
|
|
203
|
+
return url;
|
|
196
204
|
}
|
|
197
205
|
|
|
198
206
|
async function uploadBuffer(buffer: ArrayBuffer): Promise<string> {
|
|
@@ -357,7 +365,7 @@ class FalVideoModel implements VideoModelV3 {
|
|
|
357
365
|
} = options;
|
|
358
366
|
const warnings: SharedV3Warning[] = [];
|
|
359
367
|
|
|
360
|
-
const
|
|
368
|
+
const _hasVideoInput = files?.some((f) =>
|
|
361
369
|
getMediaType(f)?.startsWith("video/"),
|
|
362
370
|
);
|
|
363
371
|
const hasImageInput = files?.some((f) =>
|
|
@@ -483,12 +491,14 @@ class FalVideoModel implements VideoModelV3 {
|
|
|
483
491
|
const imageFiles = files.filter((f) =>
|
|
484
492
|
getMediaType(f)?.startsWith("image/"),
|
|
485
493
|
);
|
|
486
|
-
|
|
494
|
+
const firstImage = imageFiles[0];
|
|
495
|
+
if (firstImage) {
|
|
487
496
|
// First image is start image
|
|
488
|
-
input.image_url = await fileToUrl(
|
|
497
|
+
input.image_url = await fileToUrl(firstImage);
|
|
489
498
|
// Second image (if provided) is end image for Kling v2.6 and LTX-2
|
|
490
|
-
|
|
491
|
-
|
|
499
|
+
const secondImage = imageFiles[1];
|
|
500
|
+
if ((isKlingV26 || isLtx2) && secondImage) {
|
|
501
|
+
input.end_image_url = await fileToUrl(secondImage);
|
|
492
502
|
}
|
|
493
503
|
}
|
|
494
504
|
} else if (!isLtx2) {
|
|
@@ -780,7 +790,8 @@ class FalImageModel implements ImageModelV3 {
|
|
|
780
790
|
|
|
781
791
|
const imageBuffers = await Promise.all(
|
|
782
792
|
images.map(async (img) => {
|
|
783
|
-
|
|
793
|
+
if (!img.url) throw new Error("Image URL is missing");
|
|
794
|
+
const response = await fetch(img.url, { signal: abortSignal });
|
|
784
795
|
return new Uint8Array(await response.arrayBuffer());
|
|
785
796
|
}),
|
|
786
797
|
);
|
|
@@ -385,7 +385,7 @@ export const frameCmd = defineCommand({
|
|
|
385
385
|
process.exit(1);
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
const
|
|
388
|
+
const _renderProps = component.props as RenderProps;
|
|
389
389
|
const frames = extractFrames(component);
|
|
390
390
|
|
|
391
391
|
if (frames.length === 0) {
|
|
@@ -532,7 +532,7 @@ Get your free API key at: ${COLORS.cyan}https://fal.ai/dashboard/keys${COLORS.re
|
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
if (added) {
|
|
535
|
-
writeFileSync(gitignorePath, gitignoreContent.trim()
|
|
535
|
+
writeFileSync(gitignorePath, `${gitignoreContent.trim()}\n`);
|
|
536
536
|
log.success("Updated .gitignore");
|
|
537
537
|
} else {
|
|
538
538
|
log.info(".gitignore already configured");
|
|
@@ -470,12 +470,12 @@ function generateHtml(storyboard: Storyboard, sourceFile: string): string {
|
|
|
470
470
|
el.imageDataUrl ||
|
|
471
471
|
(el.src && !isLocalFilePath(el.src) ? el.src : undefined);
|
|
472
472
|
|
|
473
|
-
if (hasSrcWithPreview) {
|
|
473
|
+
if (hasSrcWithPreview && el.src) {
|
|
474
474
|
const shortPath =
|
|
475
|
-
el.src
|
|
475
|
+
el.src.length > 50 ? `${el.src.slice(0, 50)}...` : el.src;
|
|
476
476
|
const isUrl =
|
|
477
|
-
el.src
|
|
478
|
-
const escapedSrc = escapeAttr(el.src
|
|
477
|
+
el.src.startsWith("http://") || el.src.startsWith("https://");
|
|
478
|
+
const escapedSrc = escapeAttr(el.src);
|
|
479
479
|
const previewImgSrc = previewSrc ? escapeAttr(previewSrc) : undefined;
|
|
480
480
|
return `
|
|
481
481
|
<div class="tree-node" style="--depth: ${depth}">
|
|
@@ -567,15 +567,12 @@ function generateHtml(storyboard: Storyboard, sourceFile: string): string {
|
|
|
567
567
|
child.imageDataUrl ||
|
|
568
568
|
(child.src && !isLocalFilePath(child.src) ? child.src : undefined);
|
|
569
569
|
|
|
570
|
-
if (hasSrcWithPreview) {
|
|
570
|
+
if (hasSrcWithPreview && child.src) {
|
|
571
571
|
const shortPath =
|
|
572
|
-
child.src
|
|
573
|
-
? `${child.src!.slice(0, 60)}...`
|
|
574
|
-
: child.src!;
|
|
572
|
+
child.src.length > 60 ? `${child.src.slice(0, 60)}...` : child.src;
|
|
575
573
|
const isUrl =
|
|
576
|
-
child.src
|
|
577
|
-
|
|
578
|
-
const escapedSrc = escapeAttr(child.src!);
|
|
574
|
+
child.src.startsWith("http://") || child.src.startsWith("https://");
|
|
575
|
+
const escapedSrc = escapeAttr(child.src);
|
|
579
576
|
const previewImgSrc = previewSrc ? escapeAttr(previewSrc) : undefined;
|
|
580
577
|
return `
|
|
581
578
|
<div class="timeline-nested">
|
|
@@ -145,12 +145,13 @@ export const definition: ActionDefinition<typeof schema> = {
|
|
|
145
145
|
};
|
|
146
146
|
|
|
147
147
|
const images = data?.images;
|
|
148
|
-
|
|
148
|
+
const firstImage = images?.[0];
|
|
149
|
+
if (!images || !firstImage) {
|
|
149
150
|
throw new Error("No images in result");
|
|
150
151
|
}
|
|
151
152
|
|
|
152
153
|
return {
|
|
153
|
-
imageUrl:
|
|
154
|
+
imageUrl: firstImage.url,
|
|
154
155
|
images,
|
|
155
156
|
seed: data?.seed,
|
|
156
157
|
prompt: data?.prompt,
|
|
@@ -203,12 +204,13 @@ export async function qwenAngles(
|
|
|
203
204
|
};
|
|
204
205
|
|
|
205
206
|
const images = data?.images;
|
|
206
|
-
|
|
207
|
+
const firstImage = images?.[0];
|
|
208
|
+
if (!images || !firstImage) {
|
|
207
209
|
throw new Error("No images in result");
|
|
208
210
|
}
|
|
209
211
|
|
|
210
212
|
return {
|
|
211
|
-
imageUrl:
|
|
213
|
+
imageUrl: firstImage.url,
|
|
212
214
|
images,
|
|
213
215
|
seed: data?.seed,
|
|
214
216
|
prompt: data?.prompt,
|
|
@@ -109,7 +109,7 @@ describe("render cache behavior", () => {
|
|
|
109
109
|
const counters = { imageCalls: 0, videoCalls: 0 };
|
|
110
110
|
|
|
111
111
|
const model = createVideoModel();
|
|
112
|
-
const
|
|
112
|
+
const _imageModel = createImageModel();
|
|
113
113
|
|
|
114
114
|
const base = Video({
|
|
115
115
|
prompt: "walk forward",
|
|
@@ -146,7 +146,7 @@ describe("render cache behavior", () => {
|
|
|
146
146
|
const cacheDir = makeTempDir();
|
|
147
147
|
const counters = { imageCalls: 0, videoCalls: 0 };
|
|
148
148
|
|
|
149
|
-
const
|
|
149
|
+
const _videoModel = createVideoModel();
|
|
150
150
|
const imageModel = createImageModel();
|
|
151
151
|
|
|
152
152
|
const base = Image({
|
|
@@ -291,7 +291,19 @@ export async function renderCaptions(
|
|
|
291
291
|
}
|
|
292
292
|
|
|
293
293
|
const styleName = props.style ?? "tiktok";
|
|
294
|
-
const
|
|
294
|
+
const defaultStyle: SubtitleStyle = {
|
|
295
|
+
fontName: "Montserrat",
|
|
296
|
+
fontSize: 72,
|
|
297
|
+
primaryColor: "&HFFFFFF",
|
|
298
|
+
outlineColor: "&H000000",
|
|
299
|
+
backColor: "&H00000000",
|
|
300
|
+
bold: true,
|
|
301
|
+
outline: 4,
|
|
302
|
+
shadow: 0,
|
|
303
|
+
marginV: 480,
|
|
304
|
+
alignment: 2,
|
|
305
|
+
};
|
|
306
|
+
const baseStyle = STYLE_PRESETS[styleName] ?? defaultStyle;
|
|
295
307
|
|
|
296
308
|
const alignment = props.position
|
|
297
309
|
? (POSITION_ALIGNMENT[props.position] ?? baseStyle.alignment)
|
|
@@ -34,7 +34,9 @@ export async function renderSlider(
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
if (childPaths.length === 1) {
|
|
37
|
-
|
|
37
|
+
const firstPath = childPaths[0];
|
|
38
|
+
if (!firstPath) throw new Error("No path found");
|
|
39
|
+
return firstPath;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
const transitionName = direction === "horizontal" ? "slideleft" : "slideup";
|
|
@@ -40,7 +40,9 @@ export async function renderSwipe(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (childPaths.length === 1) {
|
|
43
|
-
|
|
43
|
+
const firstPath = childPaths[0];
|
|
44
|
+
if (!firstPath) throw new Error("No path found");
|
|
45
|
+
return firstPath;
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
const transitionName = SWIPE_TRANSITION_MAP[direction];
|
|
@@ -255,7 +255,8 @@ export function getSessionStatus(session: StepSession): {
|
|
|
255
255
|
completedStages: session.results.size,
|
|
256
256
|
currentStageIndex: session.currentStageIndex,
|
|
257
257
|
stages: session.extracted.order.map((id) => {
|
|
258
|
-
const stage = session.extracted.stages.find((s) => s.id === id)
|
|
258
|
+
const stage = session.extracted.stages.find((s) => s.id === id);
|
|
259
|
+
if (!stage) throw new Error(`Stage not found: ${id}`);
|
|
259
260
|
return {
|
|
260
261
|
id: stage.id,
|
|
261
262
|
type: stage.type,
|