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.
Files changed (36) 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 +2 -2
  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/middleware/placeholder.ts +3 -3
  19. package/src/ai-sdk/middleware/wrap-image-model.ts +1 -5
  20. package/src/ai-sdk/providers/editly/index.ts +4 -7
  21. package/src/ai-sdk/providers/editly/layers.ts +0 -1
  22. package/src/ai-sdk/providers/editly/rendi/index.ts +2 -4
  23. package/src/ai-sdk/providers/fal.ts +19 -8
  24. package/src/cli/commands/frame.tsx +1 -1
  25. package/src/cli/commands/init.tsx +1 -1
  26. package/src/cli/commands/storyboard.tsx +8 -11
  27. package/src/definitions/actions/qwen-angles.ts +6 -4
  28. package/src/react/renderers/cache.test.ts +2 -2
  29. package/src/react/renderers/captions.ts +13 -1
  30. package/src/react/renderers/packshot/blinking-button.ts +0 -1
  31. package/src/react/renderers/packshot.ts +0 -3
  32. package/src/react/renderers/render.ts +0 -1
  33. package/src/react/renderers/slider.ts +3 -1
  34. package/src/react/renderers/split.ts +3 -1
  35. package/src/react/renderers/swipe.ts +3 -1
  36. 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
- {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-alpha43",
72
+ "version": "0.4.0-alpha45",
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 },
@@ -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 (let i = 0; i < data.byteLength; i++) {
163
- binary += String.fromCharCode(data[i]!);
162
+ for (const byte of data) {
163
+ binary += String.fromCharCode(byte);
164
164
  }
165
165
  return btoa(binary);
166
166
  }
@@ -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
  };
@@ -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";
@@ -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
  }
@@ -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 (let i = 0; i < inputs.length; i++) {
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 (let i = 0; i < inputs.length; i++) {
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
- // Use mediaType from file if available, otherwise detect from bytes or default to png
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
- return fal.storage.upload(new Blob([bytes], { type: mediaType }));
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 hasVideoInput = files?.some((f) =>
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
- if (imageFiles.length > 0) {
494
+ const firstImage = imageFiles[0];
495
+ if (firstImage) {
487
496
  // First image is start image
488
- input.image_url = await fileToUrl(imageFiles[0]!);
497
+ input.image_url = await fileToUrl(firstImage);
489
498
  // Second image (if provided) is end image for Kling v2.6 and LTX-2
490
- if ((isKlingV26 || isLtx2) && imageFiles.length > 1) {
491
- input.end_image_url = await fileToUrl(imageFiles[1]!);
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
- const response = await fetch(img.url!, { signal: abortSignal });
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 renderProps = component.props as RenderProps;
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() + "\n");
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!.length > 50 ? `${el.src!.slice(0, 50)}...` : el.src!;
475
+ el.src.length > 50 ? `${el.src.slice(0, 50)}...` : el.src;
476
476
  const isUrl =
477
- el.src!.startsWith("http://") || el.src!.startsWith("https://");
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!.length > 60
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!.startsWith("http://") ||
577
- child.src!.startsWith("https://");
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
- if (!images || images.length === 0) {
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: images[0]!.url,
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
- if (!images || images.length === 0) {
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: images[0]!.url,
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 imageModel = createImageModel();
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 videoModel = createVideoModel();
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 baseStyle = STYLE_PRESETS[styleName] ?? STYLE_PRESETS.tiktok!;
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)
@@ -368,7 +368,6 @@ function getButtonYPosition(
368
368
  return Math.floor(videoHeight * 0.15);
369
369
  case "center":
370
370
  return Math.floor((videoHeight - buttonHeight) / 2);
371
- case "bottom":
372
371
  default:
373
372
  return Math.floor(videoHeight * 0.78 - buttonHeight / 2);
374
373
  }
@@ -237,9 +237,6 @@ function mapCtaPosition(
237
237
  case "center-left":
238
238
  case "center-right":
239
239
  return "center";
240
- case "bottom":
241
- case "bottom-left":
242
- case "bottom-right":
243
240
  default:
244
241
  return "bottom";
245
242
  }
@@ -16,7 +16,6 @@ import type {
16
16
  } from "../../ai-sdk/providers/editly/types";
17
17
 
18
18
  import type {
19
- CaptionsProps,
20
19
  ClipProps,
21
20
  MusicProps,
22
21
  OverlayProps,
@@ -34,7 +34,9 @@ export async function renderSlider(
34
34
  }
35
35
 
36
36
  if (childPaths.length === 1) {
37
- return childPaths[0]!;
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";
@@ -52,7 +52,9 @@ export async function renderSplit(
52
52
  }
53
53
 
54
54
  if (cells.length === 1) {
55
- return cells[0]!.path;
55
+ const firstCell = cells[0];
56
+ if (!firstCell) throw new Error("No cell found");
57
+ return firstCell.path;
56
58
  }
57
59
 
58
60
  const numChildren = cells.length;
@@ -40,7 +40,9 @@ export async function renderSwipe(
40
40
  }
41
41
 
42
42
  if (childPaths.length === 1) {
43
- return childPaths[0]!;
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,