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.
Files changed (54) hide show
  1. package/launch-videos/06-kawaii-fruits.tsx +3 -3
  2. package/launch-videos/07-ugc-weight-loss.tsx +2 -2
  3. package/package.json +1 -1
  4. package/skills/varg-video-generation/scripts/setup.ts +3 -9
  5. package/src/ai-sdk/cache.ts +1 -1
  6. package/src/ai-sdk/examples/google-image.ts +4 -4
  7. package/src/ai-sdk/examples/higgsfield-image.ts +12 -6
  8. package/src/ai-sdk/examples/replicate-bg-removal.ts +13 -7
  9. package/src/ai-sdk/examples/talking-lion.ts +3 -1
  10. package/src/ai-sdk/examples/video-generation.ts +3 -1
  11. package/src/ai-sdk/examples/workflow-animated-girl.ts +3 -1
  12. package/src/ai-sdk/examples/workflow-before-after.ts +6 -2
  13. package/src/ai-sdk/examples/workflow-character-grid.ts +3 -1
  14. package/src/ai-sdk/examples/workflow-slideshow.ts +7 -3
  15. package/src/ai-sdk/file.ts +30 -19
  16. package/src/ai-sdk/generate-element.ts +2 -2
  17. package/src/ai-sdk/generate-video.ts +3 -4
  18. package/src/ai-sdk/index.ts +12 -0
  19. package/src/ai-sdk/middleware/placeholder.ts +3 -3
  20. package/src/ai-sdk/middleware/wrap-image-model.ts +1 -5
  21. package/src/ai-sdk/providers/editly/backends/index.ts +1 -0
  22. package/src/ai-sdk/providers/editly/backends/local.ts +13 -4
  23. package/src/ai-sdk/providers/editly/backends/types.ts +14 -4
  24. package/src/ai-sdk/providers/editly/index.ts +4 -7
  25. package/src/ai-sdk/providers/editly/layers.ts +0 -1
  26. package/src/ai-sdk/providers/editly/rendi/editly-with-rendi-backend.test.ts +20 -11
  27. package/src/ai-sdk/providers/editly/rendi/index.ts +27 -14
  28. package/src/ai-sdk/providers/editly/rendi/rendi.test.ts +9 -2
  29. package/src/ai-sdk/providers/fal.ts +9 -6
  30. package/src/ai-sdk/storage/fal.ts +11 -0
  31. package/src/ai-sdk/storage/index.ts +3 -0
  32. package/src/ai-sdk/storage/r2.ts +55 -0
  33. package/src/ai-sdk/storage/types.ts +3 -0
  34. package/src/cli/commands/frame.tsx +1 -1
  35. package/src/cli/commands/init.tsx +1 -1
  36. package/src/cli/commands/storyboard.tsx +8 -11
  37. package/src/definitions/actions/qwen-angles.ts +6 -4
  38. package/src/react/renderers/cache.test.ts +6 -3
  39. package/src/react/renderers/captions.ts +19 -4
  40. package/src/react/renderers/clip.ts +34 -29
  41. package/src/react/renderers/context.ts +4 -3
  42. package/src/react/renderers/image.ts +13 -17
  43. package/src/react/renderers/index.ts +0 -1
  44. package/src/react/renderers/music.ts +26 -13
  45. package/src/react/renderers/packshot/blinking-button.ts +0 -1
  46. package/src/react/renderers/packshot.ts +2 -4
  47. package/src/react/renderers/render.ts +17 -12
  48. package/src/react/renderers/slider.ts +24 -14
  49. package/src/react/renderers/speech.ts +4 -13
  50. package/src/react/renderers/split.ts +11 -5
  51. package/src/react/renderers/swipe.ts +29 -14
  52. package/src/react/renderers/video.ts +17 -22
  53. package/src/react/types.ts +2 -0
  54. 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
- {CHARACTERS.map((char, i) => (
69
- <Clip key={char.name} duration={2.5}>
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: [characterImages[i]!],
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 MUSIC_DURATION = 15;
100
- const MUSIC_VOLUME = 0.15; // Low volume so voiceover is clear
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
@@ -69,7 +69,7 @@
69
69
  "sharp": "^0.34.5",
70
70
  "zod": "^4.2.1"
71
71
  },
72
- "version": "0.4.0-alpha44",
72
+ "version": "0.4.0-alpha46",
73
73
  "exports": {
74
74
  ".": "./src/index.ts",
75
75
  "./ai": "./src/ai-sdk/index.ts",
@@ -9,14 +9,8 @@
9
9
  * bun scripts/setup.ts
10
10
  */
11
11
 
12
- import {
13
- existsSync,
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() + "\n");
225
+ writeFileSync(gitignorePath, `${gitignoreContent.trim()}\n`);
232
226
  log.success("Updated .gitignore");
233
227
  } else {
234
228
  log.info(".gitignore already configured");
@@ -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]!, 10);
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: any) {
26
- console.error(" error:", error.message || error);
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: any) {
56
- console.error(" error:", error.message || error);
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
- console.log(`image generated: ${images[0]!.uint8Array.byteLength} bytes`);
19
- await Bun.write("output/higgsfield-default.png", images[0]!.uint8Array);
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
- console.log(`styled image: ${styledImages[0]!.uint8Array.byteLength} bytes`);
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
- styledImages[0]!.uint8Array,
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: ${enhancedImages[0]!.uint8Array.byteLength} bytes`,
57
+ `enhanced image: ${firstEnhancedImage.uint8Array.byteLength} bytes`,
52
58
  );
53
59
  await Bun.write(
54
60
  "output/higgsfield-enhanced.png",
55
- enhancedImages[0]!.uint8Array,
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
- console.log(`source image: ${sourceImages[0]!.uint8Array.byteLength} bytes`);
18
- await Bun.write("output/bg-removal-source.png", sourceImages[0]!.uint8Array);
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(sourceImages[0]!);
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: ${processedImages[0]!.uint8Array.byteLength} bytes`,
35
+ `processed image: ${firstProcessedImage.uint8Array.byteLength} bytes`,
32
36
  );
33
37
  await Bun.write(
34
38
  "output/bg-removal-result.png",
35
- processedImages[0]!.uint8Array,
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
- console.log(`alt result: ${altImages[0]!.uint8Array.byteLength} bytes`);
47
- await Bun.write("output/bg-removal-alt.png", altImages[0]!.uint8Array);
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 image = File.from(imageResult.images[0]!);
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 image = File.from(images[0]!);
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 imageData = images[0]!.uint8Array;
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 beforeImage = beforeImages[0]!.uint8Array;
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 afterImage = afterImages[0]!.uint8Array;
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 data = images[0]!.uint8Array;
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 data = images[0]!.uint8Array;
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 talkingImage = talkingImages[0]!.uint8Array;
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, i) => ({
128
+ const clips = sceneImages.map((imagePath, _i) => ({
125
129
  duration: 2,
126
130
  layers: [
127
131
  { type: "image" as const, path: imagePath },
@@ -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
- async url(uploader?: (blob: Blob) => Promise<string>): Promise<string> {
146
- if (this._url && !this._data && !this._loader) {
147
- return this._url;
148
- }
149
- const blob = await this.blob();
150
- if (uploader) return uploader(blob);
151
- try {
152
- const { fal } = await import("@fal-ai/client");
153
- return fal.storage.upload(blob);
154
- } catch {
155
- throw new Error("No uploader provided and @fal-ai/client not available.");
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 (let i = 0; i < data.byteLength; i++) {
163
- binary += String.fromCharCode(data[i]!);
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 && !this._data && !this._loader) {
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
- async toTemp(): Promise<string> {
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.toTemp();
203
+ return file.toTempFile();
193
204
  }
194
205
  const f = File.from(file);
195
- return f.toTemp();
206
+ return f.toTempFile();
196
207
  }
197
208
 
198
209
  private extensionFromMediaType(): string {
@@ -24,8 +24,8 @@ export function scene(
24
24
 
25
25
  for (let i = 0; i < strings.length; i++) {
26
26
  text += strings[i];
27
- if (i < elements.length) {
28
- const el = elements[i]!;
27
+ const el = elements[i];
28
+ if (el) {
29
29
  const count = el.images.length;
30
30
 
31
31
  if (count === 1) {
@@ -59,9 +59,8 @@ class DefaultGeneratedVideo implements GeneratedVideo {
59
59
 
60
60
  get base64(): string {
61
61
  let binary = "";
62
- const bytes = this._data;
63
- for (let i = 0; i < bytes.byteLength; i++) {
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
  };
@@ -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]!) / 360;
39
- const s = Number.parseInt(match[2]!) / 100;
40
- const l = Number.parseInt(match[3]!) / 100;
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";
@@ -4,5 +4,6 @@ export type {
4
4
  FFmpegInput,
5
5
  FFmpegRunOptions,
6
6
  FFmpegRunResult,
7
+ FilePath,
7
8
  VideoInfo,
8
9
  } from "./types";
@@ -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
- private buildInputArgs(inputs: FFmpegInput[]): string[] {
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 (typeof input === "string") {
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
- | string
20
+ | FilePath
18
21
  | {
19
- /** Path or URL to the input file */
20
- path: string;
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
- clipIndex: number,
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 (let i = 0; i < videoSourceAudio.length; i++) {
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 (let i = 0; i < audioTracks.length; i++) {
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 (let i = 0; i < clipAudioLayers.length; i++) {
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
 
@@ -37,7 +37,6 @@ function getCropPositionExpr(position: CropPosition | undefined): {
37
37
  return { x: "(iw-ow)/2", y: "ih-oh" };
38
38
  case "bottom-right":
39
39
  return { x: "iw-ow", y: "ih-oh" };
40
- case "center":
41
40
  default:
42
41
  return { x: "(iw-ow)/2", y: "(ih-oh)/2" };
43
42
  }