vargai 0.3.0

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 (154) hide show
  1. package/.claude/settings.local.json +7 -0
  2. package/.env.example +27 -0
  3. package/.github/workflows/ci.yml +23 -0
  4. package/.husky/README.md +102 -0
  5. package/.husky/commit-msg +6 -0
  6. package/.husky/pre-commit +9 -0
  7. package/.husky/pre-push +6 -0
  8. package/.size-limit.json +8 -0
  9. package/.test-hooks.ts +5 -0
  10. package/CLAUDE.md +125 -0
  11. package/CONTRIBUTING.md +150 -0
  12. package/LICENSE.md +53 -0
  13. package/README.md +78 -0
  14. package/SKILLS.md +173 -0
  15. package/STRUCTURE.md +92 -0
  16. package/biome.json +34 -0
  17. package/bun.lock +1254 -0
  18. package/commitlint.config.js +22 -0
  19. package/docs/plan.md +66 -0
  20. package/docs/todo.md +14 -0
  21. package/docs/varg-sdk.md +812 -0
  22. package/ffmpeg/CLAUDE.md +68 -0
  23. package/package.json +69 -0
  24. package/pipeline/cookbooks/SKILL.md +285 -0
  25. package/pipeline/cookbooks/remotion-video.md +585 -0
  26. package/pipeline/cookbooks/round-video-character.md +337 -0
  27. package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
  28. package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
  29. package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
  30. package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
  31. package/pipeline/cookbooks/talking-character.md +59 -0
  32. package/pipeline/cookbooks/text-to-tiktok.md +669 -0
  33. package/pipeline/cookbooks/trendwatching.md +156 -0
  34. package/plan.md +281 -0
  35. package/scripts/.gitkeep +0 -0
  36. package/src/ai-sdk/cache.ts +142 -0
  37. package/src/ai-sdk/examples/cached-generation.ts +53 -0
  38. package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
  39. package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
  40. package/src/ai-sdk/examples/duet-video.ts +56 -0
  41. package/src/ai-sdk/examples/editly-composition.ts +63 -0
  42. package/src/ai-sdk/examples/editly-test.ts +57 -0
  43. package/src/ai-sdk/examples/editly-video-test.ts +52 -0
  44. package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
  45. package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
  46. package/src/ai-sdk/examples/music-generation.ts +19 -0
  47. package/src/ai-sdk/examples/openai-sora.ts +34 -0
  48. package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
  49. package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
  50. package/src/ai-sdk/examples/talking-lion.ts +55 -0
  51. package/src/ai-sdk/examples/video-generation.ts +39 -0
  52. package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
  53. package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
  54. package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
  55. package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
  56. package/src/ai-sdk/file-cache.ts +112 -0
  57. package/src/ai-sdk/file.ts +238 -0
  58. package/src/ai-sdk/generate-element.ts +92 -0
  59. package/src/ai-sdk/generate-music.ts +46 -0
  60. package/src/ai-sdk/generate-video.ts +165 -0
  61. package/src/ai-sdk/index.ts +72 -0
  62. package/src/ai-sdk/music-model.ts +110 -0
  63. package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
  64. package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
  65. package/src/ai-sdk/providers/editly/index.ts +817 -0
  66. package/src/ai-sdk/providers/editly/layers.ts +772 -0
  67. package/src/ai-sdk/providers/editly/plan.md +144 -0
  68. package/src/ai-sdk/providers/editly/types.ts +328 -0
  69. package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
  70. package/src/ai-sdk/providers/fal-provider.ts +512 -0
  71. package/src/ai-sdk/providers/higgsfield.ts +379 -0
  72. package/src/ai-sdk/providers/openai.ts +251 -0
  73. package/src/ai-sdk/providers/replicate.ts +16 -0
  74. package/src/ai-sdk/video-model.ts +185 -0
  75. package/src/cli/commands/find.tsx +137 -0
  76. package/src/cli/commands/help.tsx +85 -0
  77. package/src/cli/commands/index.ts +9 -0
  78. package/src/cli/commands/list.tsx +238 -0
  79. package/src/cli/commands/run.tsx +511 -0
  80. package/src/cli/commands/which.tsx +253 -0
  81. package/src/cli/index.ts +112 -0
  82. package/src/cli/quiet.ts +44 -0
  83. package/src/cli/types.ts +32 -0
  84. package/src/cli/ui/components/Badge.tsx +29 -0
  85. package/src/cli/ui/components/DataTable.tsx +51 -0
  86. package/src/cli/ui/components/Header.tsx +23 -0
  87. package/src/cli/ui/components/HelpBlock.tsx +44 -0
  88. package/src/cli/ui/components/KeyValue.tsx +33 -0
  89. package/src/cli/ui/components/OptionRow.tsx +81 -0
  90. package/src/cli/ui/components/Separator.tsx +23 -0
  91. package/src/cli/ui/components/StatusBox.tsx +108 -0
  92. package/src/cli/ui/components/VargBox.tsx +51 -0
  93. package/src/cli/ui/components/VargProgress.tsx +36 -0
  94. package/src/cli/ui/components/VargSpinner.tsx +34 -0
  95. package/src/cli/ui/components/VargText.tsx +56 -0
  96. package/src/cli/ui/components/index.ts +19 -0
  97. package/src/cli/ui/index.ts +12 -0
  98. package/src/cli/ui/render.ts +35 -0
  99. package/src/cli/ui/theme.ts +63 -0
  100. package/src/cli/utils.ts +78 -0
  101. package/src/core/executor/executor.ts +201 -0
  102. package/src/core/executor/index.ts +13 -0
  103. package/src/core/executor/job.ts +214 -0
  104. package/src/core/executor/pipeline.ts +222 -0
  105. package/src/core/index.ts +11 -0
  106. package/src/core/registry/index.ts +9 -0
  107. package/src/core/registry/loader.ts +149 -0
  108. package/src/core/registry/registry.ts +221 -0
  109. package/src/core/registry/resolver.ts +206 -0
  110. package/src/core/schema/helpers.ts +134 -0
  111. package/src/core/schema/index.ts +8 -0
  112. package/src/core/schema/shared.ts +102 -0
  113. package/src/core/schema/types.ts +279 -0
  114. package/src/core/schema/validator.ts +92 -0
  115. package/src/definitions/actions/captions.ts +261 -0
  116. package/src/definitions/actions/edit.ts +298 -0
  117. package/src/definitions/actions/image.ts +125 -0
  118. package/src/definitions/actions/index.ts +114 -0
  119. package/src/definitions/actions/music.ts +205 -0
  120. package/src/definitions/actions/sync.ts +128 -0
  121. package/src/definitions/actions/transcribe.ts +200 -0
  122. package/src/definitions/actions/upload.ts +111 -0
  123. package/src/definitions/actions/video.ts +163 -0
  124. package/src/definitions/actions/voice.ts +119 -0
  125. package/src/definitions/index.ts +23 -0
  126. package/src/definitions/models/elevenlabs.ts +50 -0
  127. package/src/definitions/models/flux.ts +56 -0
  128. package/src/definitions/models/index.ts +36 -0
  129. package/src/definitions/models/kling.ts +56 -0
  130. package/src/definitions/models/llama.ts +54 -0
  131. package/src/definitions/models/nano-banana-pro.ts +102 -0
  132. package/src/definitions/models/sonauto.ts +68 -0
  133. package/src/definitions/models/soul.ts +65 -0
  134. package/src/definitions/models/wan.ts +54 -0
  135. package/src/definitions/models/whisper.ts +44 -0
  136. package/src/definitions/skills/index.ts +12 -0
  137. package/src/definitions/skills/talking-character.ts +87 -0
  138. package/src/definitions/skills/text-to-tiktok.ts +97 -0
  139. package/src/index.ts +118 -0
  140. package/src/providers/apify.ts +269 -0
  141. package/src/providers/base.ts +264 -0
  142. package/src/providers/elevenlabs.ts +217 -0
  143. package/src/providers/fal.ts +392 -0
  144. package/src/providers/ffmpeg.ts +544 -0
  145. package/src/providers/fireworks.ts +193 -0
  146. package/src/providers/groq.ts +149 -0
  147. package/src/providers/higgsfield.ts +145 -0
  148. package/src/providers/index.ts +143 -0
  149. package/src/providers/replicate.ts +147 -0
  150. package/src/providers/storage.ts +206 -0
  151. package/src/tests/all.test.ts +509 -0
  152. package/src/tests/index.ts +33 -0
  153. package/src/tests/unit.test.ts +403 -0
  154. package/tsconfig.json +45 -0
@@ -0,0 +1,104 @@
1
+ import {
2
+ generateImage as _generateImage,
3
+ experimental_generateSpeech as generateSpeech,
4
+ } from "ai";
5
+ import {
6
+ generateVideo as _generateVideo,
7
+ elevenlabs,
8
+ fal,
9
+ fileCache,
10
+ withCache,
11
+ } from "../index";
12
+
13
+ const storage = fileCache({ dir: ".cache/ai" });
14
+ const generateImage = withCache(_generateImage, { storage });
15
+ const generateVideo = withCache(_generateVideo, { storage });
16
+
17
+ async function main() {
18
+ const script = `Hi everyone! Welcome to my channel. Today I want to share something exciting with you.
19
+ I've been learning about AI video generation, and it's absolutely amazing what we can create now.
20
+ From simple images to fully animated talking characters, the possibilities are endless.
21
+ Don't forget to like and subscribe for more content like this!`;
22
+
23
+ console.log("step 1: generating girl image...");
24
+ const { images } = await generateImage({
25
+ model: fal.imageModel("flux-schnell"),
26
+ prompt:
27
+ "young woman portrait, friendly smile, casual style, looking at camera, natural lighting, social media influencer vibe",
28
+ aspectRatio: "9:16",
29
+ n: 1,
30
+ cacheKey: ["animated-girl", "portrait"],
31
+ });
32
+
33
+ const imageData = images[0]!.uint8Array;
34
+ await Bun.write("output/workflow-girl-image.png", imageData);
35
+ console.log("saved: output/workflow-girl-image.png");
36
+
37
+ console.log("\nstep 2: generating voiceover...");
38
+ const { audio } = await generateSpeech({
39
+ model: elevenlabs.speechModel("turbo"),
40
+ text: script,
41
+ voice: "rachel",
42
+ });
43
+
44
+ const audioData = audio.uint8Array;
45
+ await Bun.write("output/workflow-girl-voice.mp3", audioData);
46
+ console.log("saved: output/workflow-girl-voice.mp3");
47
+
48
+ console.log("\nstep 3: animating image to video...");
49
+ const { video } = await generateVideo({
50
+ model: fal.videoModel("wan-2.5"),
51
+ prompt: {
52
+ text: "woman talking naturally, subtle head movements, blinking, friendly expression, lip sync",
53
+ images: [imageData],
54
+ },
55
+ duration: 5,
56
+ cacheKey: ["animated-girl", "video"],
57
+ });
58
+
59
+ await Bun.write("output/workflow-girl-animated.mp4", video.uint8Array);
60
+ console.log("saved: output/workflow-girl-animated.mp4");
61
+
62
+ console.log("\nstep 4: lip syncing video with audio...");
63
+ const { video: syncedVideo } = await generateVideo({
64
+ model: fal.videoModel("sync-v2"),
65
+ prompt: {
66
+ video: video.uint8Array,
67
+ audio: audioData,
68
+ },
69
+ cacheKey: ["animated-girl", "synced"],
70
+ });
71
+
72
+ await Bun.write("output/workflow-girl-synced.mp4", syncedVideo.uint8Array);
73
+ console.log("saved: output/workflow-girl-synced.mp4");
74
+
75
+ console.log("\nstep 5: generating captions...");
76
+ const transcription = fal.transcriptionModel("whisper");
77
+ const result = await transcription.doGenerate({
78
+ audio: audioData,
79
+ mediaType: "audio/mpeg",
80
+ });
81
+
82
+ const srtContent = result.segments
83
+ .map((seg, i) => {
84
+ const start = formatTime(seg.startSecond);
85
+ const end = formatTime(seg.endSecond);
86
+ return `${i + 1}\n${start} --> ${end}\n${seg.text.trim()}\n`;
87
+ })
88
+ .join("\n");
89
+
90
+ await Bun.write("output/workflow-girl-captions.srt", srtContent);
91
+ console.log("saved: output/workflow-girl-captions.srt");
92
+
93
+ console.log("\ndone! all files in output/workflow-girl-*");
94
+ }
95
+
96
+ function formatTime(seconds: number): string {
97
+ const h = Math.floor(seconds / 3600);
98
+ const m = Math.floor((seconds % 3600) / 60);
99
+ const s = Math.floor(seconds % 60);
100
+ const ms = Math.floor((seconds % 1) * 1000);
101
+ return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")},${ms.toString().padStart(3, "0")}`;
102
+ }
103
+
104
+ main().catch(console.error);
@@ -0,0 +1,114 @@
1
+ import { generateImage as _generateImage } from "ai";
2
+ import {
3
+ generateVideo as _generateVideo,
4
+ editly,
5
+ fal,
6
+ fileCache,
7
+ withCache,
8
+ } from "../index";
9
+
10
+ const storage = fileCache({ dir: ".cache/ai" });
11
+ const generateImage = withCache(_generateImage, { storage });
12
+ const generateVideo = withCache(_generateVideo, { storage });
13
+
14
+ async function main() {
15
+ const personBase =
16
+ "30 year old man, brown hair, casual clothes, gym background";
17
+
18
+ console.log("step 1: generating before image (overweight)...");
19
+ const { images: beforeImages } = await generateImage({
20
+ model: fal.imageModel("flux-schnell"),
21
+ prompt: `${personBase}, overweight, tired expression, slouching posture, before fitness transformation`,
22
+ aspectRatio: "3:4",
23
+ n: 1,
24
+ cacheKey: ["before-after", "before-image"],
25
+ });
26
+
27
+ const beforeImage = beforeImages[0]!.uint8Array;
28
+ await Bun.write("output/workflow-before-image.png", beforeImage);
29
+ console.log("saved: output/workflow-before-image.png");
30
+
31
+ console.log("\nstep 2: generating after image (fit)...");
32
+ const { images: afterImages } = await generateImage({
33
+ model: fal.imageModel("flux-schnell"),
34
+ prompt: `${personBase}, fit and muscular, confident smile, good posture, after fitness transformation, healthy glow`,
35
+ aspectRatio: "3:4",
36
+ n: 1,
37
+ cacheKey: ["before-after", "after-image"],
38
+ });
39
+
40
+ const afterImage = afterImages[0]!.uint8Array;
41
+ await Bun.write("output/workflow-after-image.png", afterImage);
42
+ console.log("saved: output/workflow-after-image.png");
43
+
44
+ console.log("\nstep 3: animating before video...");
45
+ const { video: beforeVideo } = await generateVideo({
46
+ model: fal.videoModel("wan-2.5"),
47
+ prompt: {
48
+ text: "man standing, slight breathing movement, looking at camera, subtle movements",
49
+ images: [beforeImage],
50
+ },
51
+ duration: 5,
52
+ cacheKey: ["before-after", "before-video"],
53
+ });
54
+
55
+ await Bun.write("output/workflow-before-video.mp4", beforeVideo.uint8Array);
56
+ console.log("saved: output/workflow-before-video.mp4");
57
+
58
+ console.log("\nstep 4: animating after video...");
59
+ const { video: afterVideo } = await generateVideo({
60
+ model: fal.videoModel("wan-2.5"),
61
+ prompt: {
62
+ text: "fit man standing confidently, slight breathing movement, looking at camera, flexing slightly",
63
+ images: [afterImage],
64
+ },
65
+ duration: 5,
66
+ cacheKey: ["before-after", "after-video"],
67
+ });
68
+
69
+ await Bun.write("output/workflow-after-video.mp4", afterVideo.uint8Array);
70
+ console.log("saved: output/workflow-after-video.mp4");
71
+
72
+ console.log("\nstep 5: creating split-screen comparison...");
73
+ await editly({
74
+ outPath: "output/workflow-before-after.mp4",
75
+ width: 1280,
76
+ height: 720,
77
+ fps: 30,
78
+ verbose: true,
79
+ clips: [
80
+ {
81
+ duration: 5,
82
+ layers: [
83
+ { type: "fill-color", color: "#000000" },
84
+ {
85
+ type: "video",
86
+ path: "output/workflow-before-video.mp4",
87
+ width: 0.48,
88
+ height: 0.9,
89
+ left: 0.01,
90
+ top: 0.05,
91
+ },
92
+ {
93
+ type: "video",
94
+ path: "output/workflow-after-video.mp4",
95
+ width: 0.48,
96
+ height: 0.9,
97
+ left: 0.51,
98
+ top: 0.05,
99
+ },
100
+ {
101
+ type: "title",
102
+ text: "BEFORE AFTER",
103
+ textColor: "#ffffff",
104
+ position: "bottom",
105
+ },
106
+ ],
107
+ },
108
+ ],
109
+ });
110
+
111
+ console.log("\ndone! output/workflow-before-after.mp4");
112
+ }
113
+
114
+ main().catch(console.error);
@@ -0,0 +1,112 @@
1
+ import { generateImage as _generateImage } from "ai";
2
+ import { editly, fal, fileCache, withCache } from "../index";
3
+
4
+ const storage = fileCache({ dir: ".cache/ai" });
5
+ const generateImage = withCache(_generateImage, { storage });
6
+
7
+ const CHARACTER_PROMPTS = [
8
+ {
9
+ name: "Warrior",
10
+ prompt: "fierce warrior with sword, armor, battle-ready stance",
11
+ },
12
+ {
13
+ name: "Mage",
14
+ prompt: "mystical mage with glowing staff, flowing robes, magical aura",
15
+ },
16
+ {
17
+ name: "Rogue",
18
+ prompt: "stealthy rogue with daggers, hooded cloak, shadows",
19
+ },
20
+ {
21
+ name: "Healer",
22
+ prompt: "gentle healer with staff, white robes, soft light",
23
+ },
24
+ {
25
+ name: "Archer",
26
+ prompt: "skilled archer with bow, leather armor, focused eyes",
27
+ },
28
+ {
29
+ name: "Knight",
30
+ prompt: "noble knight with shield, heavy armor, proud stance",
31
+ },
32
+ {
33
+ name: "Necromancer",
34
+ prompt: "dark necromancer with skull staff, black robes, green magic",
35
+ },
36
+ {
37
+ name: "Paladin",
38
+ prompt: "holy paladin with hammer, golden armor, divine light",
39
+ },
40
+ {
41
+ name: "Bard",
42
+ prompt: "charismatic bard with lute, colorful clothes, smile",
43
+ },
44
+ {
45
+ name: "Druid",
46
+ prompt: "nature druid with wooden staff, leaf clothing, animals",
47
+ },
48
+ {
49
+ name: "Monk",
50
+ prompt: "disciplined monk with wrapped fists, simple robes, calm",
51
+ },
52
+ {
53
+ name: "Assassin",
54
+ prompt: "deadly assassin with hidden blades, dark mask, shadows",
55
+ },
56
+ ];
57
+
58
+ async function main() {
59
+ console.log("generating 12 character portraits...\n");
60
+
61
+ const baseStyle =
62
+ "fantasy character portrait, stylized art, vibrant colors, detailed, facing camera";
63
+
64
+ const images = await Promise.all(
65
+ CHARACTER_PROMPTS.map(async ({ name, prompt }, i) => {
66
+ console.log(`generating ${name}...`);
67
+ const { images } = await generateImage({
68
+ model: fal.imageModel("flux-schnell"),
69
+ prompt: `${prompt}, ${baseStyle}`,
70
+ aspectRatio: "1:1",
71
+ n: 1,
72
+ cacheKey: ["character-grid", name],
73
+ });
74
+ const data = images[0]!.uint8Array;
75
+ await Bun.write(`output/character-${i}-${name.toLowerCase()}.png`, data);
76
+ return {
77
+ name,
78
+ data,
79
+ path: `output/character-${i}-${name.toLowerCase()}.png`,
80
+ };
81
+ }),
82
+ );
83
+
84
+ console.log("\ncreating character grid video...");
85
+
86
+ const clips = images.map(({ name, path }) => ({
87
+ duration: 2,
88
+ layers: [
89
+ { type: "image" as const, path },
90
+ {
91
+ type: "title" as const,
92
+ text: name,
93
+ textColor: "#ffffff",
94
+ position: "bottom" as const,
95
+ },
96
+ ],
97
+ transition: { name: "fade", duration: 0.3 },
98
+ }));
99
+
100
+ await editly({
101
+ outPath: "output/workflow-character-grid.mp4",
102
+ width: 720,
103
+ height: 720,
104
+ fps: 30,
105
+ verbose: true,
106
+ clips,
107
+ });
108
+
109
+ console.log("\ndone! output/workflow-character-grid.mp4");
110
+ }
111
+
112
+ main().catch(console.error);
@@ -0,0 +1,161 @@
1
+ import {
2
+ generateImage as _generateImage,
3
+ experimental_generateSpeech as generateSpeech,
4
+ } from "ai";
5
+ import {
6
+ generateVideo as _generateVideo,
7
+ editly,
8
+ elevenlabs,
9
+ fal,
10
+ fileCache,
11
+ withCache,
12
+ } from "../index";
13
+
14
+ const storage = fileCache({ dir: ".cache/ai" });
15
+ const generateImage = withCache(_generateImage, { storage });
16
+ const generateVideo = withCache(_generateVideo, { storage });
17
+
18
+ const SCENES = [
19
+ "standing in a modern coffee shop, holding a latte",
20
+ "walking in a beautiful park with autumn leaves",
21
+ "sitting at a desk in a bright home office",
22
+ "cooking in a stylish modern kitchen",
23
+ "relaxing on a cozy couch with a book",
24
+ "standing on a rooftop with city skyline behind",
25
+ ];
26
+
27
+ async function main() {
28
+ const characterBase =
29
+ "young professional woman, brown hair, warm smile, casual chic style";
30
+ const narration = `Let me take you through a day in my life.
31
+ Morning starts with my favorite coffee.
32
+ Then a refreshing walk in the park.
33
+ Time to get some work done.
34
+ Cooking something delicious for lunch.
35
+ Afternoon reading session.
36
+ And ending with this amazing view.`;
37
+
38
+ console.log("step 1: generating 6 scene images...");
39
+ const sceneImages = await Promise.all(
40
+ SCENES.map(async (scene, i) => {
41
+ console.log(` generating scene ${i + 1}: ${scene.slice(0, 40)}...`);
42
+ const { images } = await generateImage({
43
+ model: fal.imageModel("flux-schnell"),
44
+ prompt: `${characterBase}, ${scene}, lifestyle photography`,
45
+ aspectRatio: "16:9",
46
+ n: 1,
47
+ cacheKey: ["slideshow", "scene", i],
48
+ });
49
+ const data = images[0]!.uint8Array;
50
+ await Bun.write(`output/workflow-scene-${i}.png`, data);
51
+ return `output/workflow-scene-${i}.png`;
52
+ }),
53
+ );
54
+ console.log("saved: output/workflow-scene-*.png");
55
+
56
+ console.log("\nstep 2: generating talking head image...");
57
+ const { images: talkingImages } = await generateImage({
58
+ model: fal.imageModel("flux-schnell"),
59
+ prompt: `${characterBase}, headshot, facing camera, talking, vlog style, clean background`,
60
+ aspectRatio: "1:1",
61
+ n: 1,
62
+ cacheKey: ["slideshow", "talking-head"],
63
+ });
64
+
65
+ const talkingImage = talkingImages[0]!.uint8Array;
66
+ await Bun.write("output/workflow-talking-head.png", talkingImage);
67
+
68
+ console.log("\nstep 3: generating voiceover...");
69
+ const { audio } = await generateSpeech({
70
+ model: elevenlabs.speechModel("turbo"),
71
+ text: narration,
72
+ voice: "bella",
73
+ });
74
+
75
+ const audioData = audio.uint8Array;
76
+ await Bun.write("output/workflow-slideshow-voice.mp3", audioData);
77
+
78
+ console.log("\nstep 4: animating talking head...");
79
+ const { video: talkingVideo } = await generateVideo({
80
+ model: fal.videoModel("wan-2.5"),
81
+ prompt: {
82
+ text: "woman talking naturally, subtle head movements, friendly expression",
83
+ images: [talkingImage],
84
+ },
85
+ duration: 5,
86
+ cacheKey: ["slideshow", "talking-video"],
87
+ });
88
+
89
+ await Bun.write("output/workflow-talking-head.mp4", talkingVideo.uint8Array);
90
+
91
+ console.log("\nstep 5: lip syncing talking head...");
92
+ const { video: syncedTalkingVideo } = await generateVideo({
93
+ model: fal.videoModel("sync-v2"),
94
+ prompt: {
95
+ video: talkingVideo.uint8Array,
96
+ audio: audioData,
97
+ },
98
+ cacheKey: ["slideshow", "synced-video"],
99
+ });
100
+
101
+ await Bun.write(
102
+ "output/workflow-talking-synced.mp4",
103
+ syncedTalkingVideo.uint8Array,
104
+ );
105
+
106
+ console.log("\nstep 6: generating captions...");
107
+ const transcription = fal.transcriptionModel("whisper");
108
+ const result = await transcription.doGenerate({
109
+ audio: audioData,
110
+ mediaType: "audio/mpeg",
111
+ });
112
+
113
+ const srtContent = result.segments
114
+ .map((seg, i) => {
115
+ const start = formatTime(seg.startSecond);
116
+ const end = formatTime(seg.endSecond);
117
+ return `${i + 1}\n${start} --> ${end}\n${seg.text.trim()}\n`;
118
+ })
119
+ .join("\n");
120
+
121
+ await Bun.write("output/workflow-slideshow.srt", srtContent);
122
+
123
+ console.log("\nstep 7: creating slideshow with pip...");
124
+ const clips = sceneImages.map((imagePath, i) => ({
125
+ duration: 2,
126
+ layers: [
127
+ { type: "image" as const, path: imagePath },
128
+ {
129
+ type: "video" as const,
130
+ path: "output/workflow-talking-synced.mp4",
131
+ width: 0.25,
132
+ height: 0.25,
133
+ left: 0.73,
134
+ top: 0.73,
135
+ },
136
+ ],
137
+ transition: { name: "fade", duration: 0.3 } as const,
138
+ }));
139
+
140
+ await editly({
141
+ outPath: "output/workflow-slideshow.mp4",
142
+ width: 1280,
143
+ height: 720,
144
+ fps: 30,
145
+ verbose: true,
146
+ audioFilePath: "output/workflow-slideshow-voice.mp3",
147
+ clips,
148
+ });
149
+
150
+ console.log("\ndone! output/workflow-slideshow.mp4");
151
+ }
152
+
153
+ function formatTime(seconds: number): string {
154
+ const h = Math.floor(seconds / 3600);
155
+ const m = Math.floor((seconds % 3600) / 60);
156
+ const s = Math.floor(seconds % 60);
157
+ const ms = Math.floor((seconds % 1) * 1000);
158
+ return `${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")},${ms.toString().padStart(3, "0")}`;
159
+ }
160
+
161
+ main().catch(console.error);
@@ -0,0 +1,112 @@
1
+ import type { CacheStorage } from "./cache";
2
+
3
+ interface CacheEntry {
4
+ value: unknown;
5
+ expires: number;
6
+ }
7
+
8
+ function serialize(value: unknown): string {
9
+ return JSON.stringify(value, (_key, val) => {
10
+ if (val instanceof Uint8Array) {
11
+ return {
12
+ __type: "Uint8Array",
13
+ data: Buffer.from(val).toString("base64"),
14
+ };
15
+ }
16
+ return val;
17
+ });
18
+ }
19
+
20
+ function deserialize(json: string): unknown {
21
+ return JSON.parse(json, (_key, val) => {
22
+ if (val && typeof val === "object" && val.__type === "Uint8Array") {
23
+ return new Uint8Array(Buffer.from(val.data, "base64"));
24
+ }
25
+ return val;
26
+ });
27
+ }
28
+
29
+ /**
30
+ * File-based cache storage using Bun's file API.
31
+ * Persists cache entries to disk as JSON files.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * import { generateImage } from "ai";
36
+ * import { withCache, fileCache } from "./index";
37
+ *
38
+ * const storage = fileCache({ dir: ".cache/ai" });
39
+ * const generateImage_ = withCache(generateImage, { ttl: "24h", storage });
40
+ *
41
+ * const { images } = await generateImage_({
42
+ * model: fal.imageModel("flux-schnell"),
43
+ * prompt: "lion roaring",
44
+ * cacheKey: ["lion", take],
45
+ * });
46
+ * ```
47
+ */
48
+ export function fileCache(options: { dir: string }): CacheStorage {
49
+ const { dir } = options;
50
+
51
+ const ensureDir = async () => {
52
+ const file = Bun.file(dir);
53
+ if (!(await file.exists())) {
54
+ await Bun.write(`${dir}/.gitkeep`, "");
55
+ }
56
+ };
57
+
58
+ const keyToPath = (key: string): string => {
59
+ const safe = key.replace(/[^a-zA-Z0-9_-]/g, "_");
60
+ if (safe.length > 200) {
61
+ const hash = Bun.hash(key).toString(16);
62
+ return `${dir}/${safe.slice(0, 100)}_${hash}.json`;
63
+ }
64
+ return `${dir}/${safe}.json`;
65
+ };
66
+
67
+ return {
68
+ async get(key: string): Promise<unknown | undefined> {
69
+ try {
70
+ const path = keyToPath(key);
71
+ const file = Bun.file(path);
72
+
73
+ if (!(await file.exists())) {
74
+ return undefined;
75
+ }
76
+
77
+ const content = await file.text();
78
+ const entry = deserialize(content) as CacheEntry;
79
+
80
+ if (entry.expires && Date.now() > entry.expires) {
81
+ // expired, clean up
82
+ await Bun.write(path, "").catch(() => {});
83
+ return undefined;
84
+ }
85
+
86
+ return entry.value;
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ },
91
+
92
+ async set(key: string, value: unknown, ttl?: number): Promise<void> {
93
+ await ensureDir();
94
+ const path = keyToPath(key);
95
+ const entry: CacheEntry = {
96
+ value,
97
+ expires: ttl ? Date.now() + ttl : 0,
98
+ };
99
+ await Bun.write(path, serialize(entry));
100
+ },
101
+
102
+ async delete(key: string): Promise<void> {
103
+ const path = keyToPath(key);
104
+ const file = Bun.file(path);
105
+ if (await file.exists()) {
106
+ // Bun doesn't have a direct delete, use unlink
107
+ const { unlink } = await import("node:fs/promises");
108
+ await unlink(path).catch(() => {});
109
+ }
110
+ },
111
+ };
112
+ }