varg.ai-sdk 0.1.1 → 0.4.0-alpha.1

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 (246) hide show
  1. package/.claude/settings.local.json +1 -1
  2. package/.env.example +3 -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 +10 -3
  11. package/CONTRIBUTING.md +150 -0
  12. package/LICENSE.md +53 -0
  13. package/README.md +56 -209
  14. package/SKILLS.md +26 -10
  15. package/biome.json +7 -1
  16. package/bun.lock +1286 -0
  17. package/commitlint.config.js +22 -0
  18. package/docs/index.html +1130 -0
  19. package/docs/prompting.md +326 -0
  20. package/docs/react.md +834 -0
  21. package/docs/sdk.md +812 -0
  22. package/ffmpeg/CLAUDE.md +68 -0
  23. package/package.json +43 -10
  24. package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
  25. package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
  26. package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
  27. package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
  28. package/pipeline/cookbooks/text-to-tiktok.md +669 -0
  29. package/pipeline/cookbooks/trendwatching.md +156 -0
  30. package/plan.md +281 -0
  31. package/scripts/.gitkeep +0 -0
  32. package/src/ai-sdk/cache.ts +142 -0
  33. package/src/ai-sdk/examples/cached-generation.ts +53 -0
  34. package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
  35. package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
  36. package/src/ai-sdk/examples/duet-video.ts +56 -0
  37. package/src/ai-sdk/examples/editly-composition.ts +63 -0
  38. package/src/ai-sdk/examples/editly-test.ts +57 -0
  39. package/src/ai-sdk/examples/editly-video-test.ts +52 -0
  40. package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
  41. package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
  42. package/src/ai-sdk/examples/music-generation.ts +19 -0
  43. package/src/ai-sdk/examples/openai-sora.ts +34 -0
  44. package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
  45. package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
  46. package/src/ai-sdk/examples/talking-lion.ts +55 -0
  47. package/src/ai-sdk/examples/video-generation.ts +39 -0
  48. package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
  49. package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
  50. package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
  51. package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
  52. package/src/ai-sdk/file-cache.ts +112 -0
  53. package/src/ai-sdk/file.ts +238 -0
  54. package/src/ai-sdk/generate-element.ts +92 -0
  55. package/src/ai-sdk/generate-music.ts +46 -0
  56. package/src/ai-sdk/generate-video.ts +165 -0
  57. package/src/ai-sdk/index.ts +72 -0
  58. package/src/ai-sdk/music-model.ts +110 -0
  59. package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
  60. package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
  61. package/src/ai-sdk/providers/editly/index.ts +817 -0
  62. package/src/ai-sdk/providers/editly/layers.ts +776 -0
  63. package/src/ai-sdk/providers/editly/plan.md +144 -0
  64. package/src/ai-sdk/providers/editly/types.ts +328 -0
  65. package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
  66. package/src/ai-sdk/providers/fal-provider.ts +512 -0
  67. package/src/ai-sdk/providers/higgsfield.ts +379 -0
  68. package/src/ai-sdk/providers/openai.ts +251 -0
  69. package/src/ai-sdk/providers/replicate.ts +16 -0
  70. package/src/ai-sdk/video-model.ts +185 -0
  71. package/src/cli/commands/find.tsx +137 -0
  72. package/src/cli/commands/help.tsx +85 -0
  73. package/src/cli/commands/index.ts +6 -0
  74. package/src/cli/commands/list.tsx +238 -0
  75. package/src/cli/commands/render.tsx +71 -0
  76. package/src/cli/commands/run.tsx +511 -0
  77. package/src/cli/commands/which.tsx +253 -0
  78. package/src/cli/index.ts +114 -0
  79. package/src/cli/quiet.ts +44 -0
  80. package/src/cli/types.ts +32 -0
  81. package/src/cli/ui/components/Badge.tsx +29 -0
  82. package/src/cli/ui/components/DataTable.tsx +51 -0
  83. package/src/cli/ui/components/Header.tsx +23 -0
  84. package/src/cli/ui/components/HelpBlock.tsx +44 -0
  85. package/src/cli/ui/components/KeyValue.tsx +33 -0
  86. package/src/cli/ui/components/OptionRow.tsx +81 -0
  87. package/src/cli/ui/components/Separator.tsx +23 -0
  88. package/src/cli/ui/components/StatusBox.tsx +108 -0
  89. package/src/cli/ui/components/VargBox.tsx +51 -0
  90. package/src/cli/ui/components/VargProgress.tsx +36 -0
  91. package/src/cli/ui/components/VargSpinner.tsx +34 -0
  92. package/src/cli/ui/components/VargText.tsx +56 -0
  93. package/src/cli/ui/components/index.ts +19 -0
  94. package/src/cli/ui/index.ts +12 -0
  95. package/src/cli/ui/render.ts +35 -0
  96. package/src/cli/ui/theme.ts +63 -0
  97. package/src/cli/utils.ts +78 -0
  98. package/src/core/executor/executor.ts +201 -0
  99. package/src/core/executor/index.ts +13 -0
  100. package/src/core/executor/job.ts +214 -0
  101. package/src/core/executor/pipeline.ts +222 -0
  102. package/src/core/index.ts +11 -0
  103. package/src/core/registry/index.ts +9 -0
  104. package/src/core/registry/loader.ts +149 -0
  105. package/src/core/registry/registry.ts +221 -0
  106. package/src/core/registry/resolver.ts +206 -0
  107. package/src/core/schema/helpers.ts +134 -0
  108. package/src/core/schema/index.ts +8 -0
  109. package/src/core/schema/shared.ts +102 -0
  110. package/src/core/schema/types.ts +279 -0
  111. package/src/core/schema/validator.ts +92 -0
  112. package/src/definitions/actions/captions.ts +261 -0
  113. package/src/definitions/actions/edit.ts +298 -0
  114. package/src/definitions/actions/image.ts +125 -0
  115. package/src/definitions/actions/index.ts +114 -0
  116. package/src/definitions/actions/music.ts +205 -0
  117. package/src/definitions/actions/sync.ts +128 -0
  118. package/{action/transcribe/index.ts → src/definitions/actions/transcribe.ts} +58 -68
  119. package/src/definitions/actions/upload.ts +111 -0
  120. package/src/definitions/actions/video.ts +163 -0
  121. package/src/definitions/actions/voice.ts +119 -0
  122. package/src/definitions/index.ts +23 -0
  123. package/src/definitions/models/elevenlabs.ts +50 -0
  124. package/src/definitions/models/flux.ts +56 -0
  125. package/src/definitions/models/index.ts +36 -0
  126. package/src/definitions/models/kling.ts +56 -0
  127. package/src/definitions/models/llama.ts +54 -0
  128. package/src/definitions/models/nano-banana-pro.ts +102 -0
  129. package/src/definitions/models/sonauto.ts +68 -0
  130. package/src/definitions/models/soul.ts +65 -0
  131. package/src/definitions/models/wan.ts +54 -0
  132. package/src/definitions/models/whisper.ts +44 -0
  133. package/src/definitions/skills/index.ts +12 -0
  134. package/src/definitions/skills/talking-character.ts +87 -0
  135. package/src/definitions/skills/text-to-tiktok.ts +97 -0
  136. package/src/index.ts +118 -0
  137. package/src/providers/apify.ts +269 -0
  138. package/src/providers/base.ts +264 -0
  139. package/src/providers/elevenlabs.ts +217 -0
  140. package/src/providers/fal.ts +392 -0
  141. package/src/providers/ffmpeg.ts +544 -0
  142. package/src/providers/fireworks.ts +193 -0
  143. package/src/providers/groq.ts +149 -0
  144. package/src/providers/higgsfield.ts +145 -0
  145. package/src/providers/index.ts +143 -0
  146. package/src/providers/replicate.ts +147 -0
  147. package/src/providers/storage.ts +206 -0
  148. package/src/react/cli.ts +52 -0
  149. package/src/react/elements.ts +146 -0
  150. package/src/react/examples/branching.tsx +66 -0
  151. package/src/react/examples/captions-demo.tsx +37 -0
  152. package/src/react/examples/character-video.tsx +84 -0
  153. package/src/react/examples/grid.tsx +53 -0
  154. package/src/react/examples/layouts-demo.tsx +57 -0
  155. package/src/react/examples/madi.tsx +60 -0
  156. package/src/react/examples/music-test.tsx +35 -0
  157. package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
  158. package/src/react/examples/orange-portrait.tsx +41 -0
  159. package/src/react/examples/split-element-demo.tsx +60 -0
  160. package/src/react/examples/split-layout-demo.tsx +60 -0
  161. package/src/react/examples/split.tsx +41 -0
  162. package/src/react/examples/video-grid.tsx +46 -0
  163. package/src/react/index.ts +43 -0
  164. package/src/react/layouts/grid.tsx +28 -0
  165. package/src/react/layouts/index.ts +2 -0
  166. package/src/react/layouts/split.tsx +20 -0
  167. package/src/react/react.test.ts +309 -0
  168. package/src/react/render.ts +21 -0
  169. package/src/react/renderers/animate.ts +59 -0
  170. package/src/react/renderers/captions.ts +297 -0
  171. package/src/react/renderers/clip.ts +248 -0
  172. package/src/react/renderers/context.ts +17 -0
  173. package/src/react/renderers/image.ts +109 -0
  174. package/src/react/renderers/index.ts +22 -0
  175. package/src/react/renderers/music.ts +60 -0
  176. package/src/react/renderers/packshot.ts +84 -0
  177. package/src/react/renderers/progress.ts +173 -0
  178. package/src/react/renderers/render.ts +243 -0
  179. package/src/react/renderers/slider.ts +69 -0
  180. package/src/react/renderers/speech.ts +53 -0
  181. package/src/react/renderers/split.ts +91 -0
  182. package/src/react/renderers/subtitle.ts +16 -0
  183. package/src/react/renderers/swipe.ts +75 -0
  184. package/src/react/renderers/title.ts +17 -0
  185. package/src/react/renderers/utils.ts +124 -0
  186. package/src/react/renderers/video.ts +127 -0
  187. package/src/react/runtime/jsx-dev-runtime.ts +43 -0
  188. package/src/react/runtime/jsx-runtime.ts +35 -0
  189. package/src/react/types.ts +232 -0
  190. package/src/studio/index.ts +26 -0
  191. package/src/studio/scanner.ts +102 -0
  192. package/src/studio/server.ts +554 -0
  193. package/src/studio/stages.ts +251 -0
  194. package/src/studio/step-renderer.ts +279 -0
  195. package/src/studio/types.ts +60 -0
  196. package/src/studio/ui/cache.html +303 -0
  197. package/src/studio/ui/index.html +1820 -0
  198. package/src/tests/all.test.ts +509 -0
  199. package/src/tests/index.ts +33 -0
  200. package/src/tests/unit.test.ts +403 -0
  201. package/tsconfig.cli.json +8 -0
  202. package/tsconfig.json +21 -3
  203. package/TEST_RESULTS.md +0 -122
  204. package/action/captions/SKILL.md +0 -170
  205. package/action/captions/index.ts +0 -169
  206. package/action/edit/SKILL.md +0 -235
  207. package/action/edit/index.ts +0 -437
  208. package/action/image/SKILL.md +0 -140
  209. package/action/image/index.ts +0 -105
  210. package/action/sync/SKILL.md +0 -136
  211. package/action/sync/index.ts +0 -145
  212. package/action/transcribe/SKILL.md +0 -179
  213. package/action/video/SKILL.md +0 -116
  214. package/action/video/index.ts +0 -125
  215. package/action/voice/SKILL.md +0 -125
  216. package/action/voice/index.ts +0 -136
  217. package/cli/commands/find.ts +0 -58
  218. package/cli/commands/help.ts +0 -70
  219. package/cli/commands/list.ts +0 -49
  220. package/cli/commands/run.ts +0 -237
  221. package/cli/commands/which.ts +0 -66
  222. package/cli/discover.ts +0 -66
  223. package/cli/index.ts +0 -33
  224. package/cli/runner.ts +0 -65
  225. package/cli/types.ts +0 -49
  226. package/cli/ui.ts +0 -185
  227. package/index.ts +0 -75
  228. package/lib/README.md +0 -144
  229. package/lib/ai-sdk/fal.ts +0 -106
  230. package/lib/ai-sdk/replicate.ts +0 -107
  231. package/lib/elevenlabs.ts +0 -382
  232. package/lib/fal.ts +0 -467
  233. package/lib/ffmpeg.ts +0 -467
  234. package/lib/fireworks.ts +0 -235
  235. package/lib/groq.ts +0 -246
  236. package/lib/higgsfield.ts +0 -176
  237. package/lib/remotion/SKILL.md +0 -823
  238. package/lib/remotion/cli.ts +0 -115
  239. package/lib/remotion/functions.ts +0 -283
  240. package/lib/remotion/index.ts +0 -19
  241. package/lib/remotion/templates.ts +0 -73
  242. package/lib/replicate.ts +0 -304
  243. package/output.txt +0 -1
  244. package/test-import.ts +0 -7
  245. package/test-services.ts +0 -97
  246. package/utilities/s3.ts +0 -147
@@ -0,0 +1,217 @@
1
+ /**
2
+ * ElevenLabs provider for voice generation and text-to-speech
3
+ */
4
+
5
+ import { writeFileSync } from "node:fs";
6
+ import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
7
+ import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types";
8
+ import { BaseProvider } from "./base";
9
+
10
+ export class ElevenLabsProvider extends BaseProvider {
11
+ readonly name = "elevenlabs";
12
+ private _client: ElevenLabsClient | null = null;
13
+
14
+ /**
15
+ * Lazy initialization of the client to avoid errors when API keys aren't set
16
+ */
17
+ private get client(): ElevenLabsClient {
18
+ if (!this._client) {
19
+ const apiKey = this.config.apiKey || process.env.ELEVENLABS_API_KEY;
20
+ if (!apiKey) {
21
+ throw new Error(
22
+ "ElevenLabs API key not found. Set ELEVENLABS_API_KEY environment variable.",
23
+ );
24
+ }
25
+ this._client = new ElevenLabsClient({ apiKey });
26
+ }
27
+ return this._client;
28
+ }
29
+
30
+ async submit(
31
+ _model: string,
32
+ _inputs: Record<string, unknown>,
33
+ _config?: ProviderConfig,
34
+ ): Promise<string> {
35
+ // ElevenLabs is synchronous, so we generate immediately and return a fake ID
36
+ const jobId = `el_${Date.now()}_${Math.random().toString(36).slice(2)}`;
37
+ console.log(`[elevenlabs] starting generation: ${jobId}`);
38
+ return jobId;
39
+ }
40
+
41
+ async getStatus(_jobId: string): Promise<JobStatusUpdate> {
42
+ // ElevenLabs is synchronous
43
+ return { status: "completed" };
44
+ }
45
+
46
+ async getResult(_jobId: string): Promise<unknown> {
47
+ return null;
48
+ }
49
+
50
+ // ============================================================================
51
+ // High-level convenience methods
52
+ // ============================================================================
53
+
54
+ async textToSpeech(options: {
55
+ text: string;
56
+ voiceId?: string;
57
+ modelId?: string;
58
+ outputPath?: string;
59
+ }): Promise<Buffer> {
60
+ const {
61
+ text,
62
+ voiceId = VOICES.RACHEL,
63
+ modelId = "eleven_multilingual_v2",
64
+ outputPath,
65
+ } = options;
66
+
67
+ console.log(`[elevenlabs] generating speech with voice ${voiceId}...`);
68
+
69
+ const audio = await this.client.textToSpeech.convert(voiceId, {
70
+ text,
71
+ modelId,
72
+ outputFormat: "mp3_44100_128",
73
+ });
74
+
75
+ const reader = audio.getReader();
76
+ const chunks: Uint8Array[] = [];
77
+
78
+ while (true) {
79
+ const { done, value } = await reader.read();
80
+ if (done) break;
81
+ chunks.push(value);
82
+ }
83
+
84
+ const buffer = Buffer.concat(chunks);
85
+
86
+ if (outputPath) {
87
+ writeFileSync(outputPath, buffer);
88
+ console.log(`[elevenlabs] saved to ${outputPath}`);
89
+ }
90
+
91
+ console.log(`[elevenlabs] generated ${buffer.length} bytes`);
92
+ return buffer;
93
+ }
94
+
95
+ async listVoices() {
96
+ console.log(`[elevenlabs] fetching voices...`);
97
+ const response = await this.client.voices.getAll();
98
+ console.log(`[elevenlabs] found ${response.voices.length} voices`);
99
+ return response.voices;
100
+ }
101
+
102
+ async getVoice(voiceId: string) {
103
+ console.log(`[elevenlabs] fetching voice ${voiceId}...`);
104
+ const voice = await this.client.voices.get(voiceId);
105
+ console.log(`[elevenlabs] found voice: ${voice.name}`);
106
+ return voice;
107
+ }
108
+
109
+ async generateMusic(options: {
110
+ prompt: string;
111
+ musicLengthMs?: number;
112
+ outputPath?: string;
113
+ }): Promise<Buffer> {
114
+ const { prompt, musicLengthMs, outputPath } = options;
115
+
116
+ console.log(`[elevenlabs] generating music from prompt: "${prompt}"...`);
117
+
118
+ const audio = await this.client.music.compose({
119
+ prompt,
120
+ musicLengthMs,
121
+ modelId: "music_v1",
122
+ });
123
+
124
+ const reader = audio.getReader();
125
+ const chunks: Uint8Array[] = [];
126
+
127
+ while (true) {
128
+ const { done, value } = await reader.read();
129
+ if (done) break;
130
+ chunks.push(value);
131
+ }
132
+
133
+ const buffer = Buffer.concat(chunks);
134
+
135
+ if (outputPath) {
136
+ writeFileSync(outputPath, buffer);
137
+ console.log(`[elevenlabs] saved to ${outputPath}`);
138
+ }
139
+
140
+ console.log(`[elevenlabs] generated ${buffer.length} bytes`);
141
+ return buffer;
142
+ }
143
+
144
+ async generateSoundEffect(options: {
145
+ text: string;
146
+ durationSeconds?: number;
147
+ promptInfluence?: number;
148
+ loop?: boolean;
149
+ outputPath?: string;
150
+ }): Promise<Buffer> {
151
+ const {
152
+ text,
153
+ durationSeconds,
154
+ promptInfluence = 0.3,
155
+ loop = false,
156
+ outputPath,
157
+ } = options;
158
+
159
+ console.log(`[elevenlabs] generating sound effect: "${text}"...`);
160
+
161
+ const audio = await this.client.textToSoundEffects.convert({
162
+ text,
163
+ durationSeconds,
164
+ promptInfluence,
165
+ loop,
166
+ });
167
+
168
+ const reader = audio.getReader();
169
+ const chunks: Uint8Array[] = [];
170
+
171
+ while (true) {
172
+ const { done, value } = await reader.read();
173
+ if (done) break;
174
+ chunks.push(value);
175
+ }
176
+
177
+ const buffer = Buffer.concat(chunks);
178
+
179
+ if (outputPath) {
180
+ writeFileSync(outputPath, buffer);
181
+ console.log(`[elevenlabs] saved to ${outputPath}`);
182
+ }
183
+
184
+ console.log(`[elevenlabs] generated ${buffer.length} bytes`);
185
+ return buffer;
186
+ }
187
+ }
188
+
189
+ // Popular voices
190
+ export const VOICES = {
191
+ RACHEL: "21m00Tcm4TlvDq8ikWAM",
192
+ DOMI: "AZnzlk1XvdvUeBnXmlld",
193
+ BELLA: "EXAVITQu4vr4xnSDxMaL",
194
+ ANTONI: "ErXwobaYiN019PkySvjV",
195
+ ELLI: "MF3mGyEYCl7XYWbV9V6O",
196
+ JOSH: "TxGEqnHWrfWFTfGW9XjX",
197
+ ARNOLD: "VR6AewLTigWG4xSOukaG",
198
+ ADAM: "pNInz6obpgDQGcFmaJgB",
199
+ SAM: "yoZ06aMxZJJ28mfd3POQ",
200
+ };
201
+
202
+ // Export singleton instance (lazy initialization means no error on import)
203
+ export const elevenlabsProvider = new ElevenLabsProvider();
204
+
205
+ // Re-export convenience functions for backward compatibility
206
+ export const textToSpeech = (
207
+ options: Parameters<ElevenLabsProvider["textToSpeech"]>[0],
208
+ ) => elevenlabsProvider.textToSpeech(options);
209
+ export const listVoices = () => elevenlabsProvider.listVoices();
210
+ export const getVoice = (voiceId: string) =>
211
+ elevenlabsProvider.getVoice(voiceId);
212
+ export const generateMusic = (
213
+ options: Parameters<ElevenLabsProvider["generateMusic"]>[0],
214
+ ) => elevenlabsProvider.generateMusic(options);
215
+ export const generateSoundEffect = (
216
+ options: Parameters<ElevenLabsProvider["generateSoundEffect"]>[0],
217
+ ) => elevenlabsProvider.generateSoundEffect(options);
@@ -0,0 +1,392 @@
1
+ /**
2
+ * fal.ai provider for video and image generation
3
+ * Supports Kling, Flux, Wan and other models
4
+ */
5
+
6
+ import { fal } from "@fal-ai/client";
7
+ import type { JobStatusUpdate, ProviderConfig } from "../core/schema/types";
8
+ import { BaseProvider, ensureUrl } from "./base";
9
+
10
+ export class FalProvider extends BaseProvider {
11
+ readonly name = "fal";
12
+
13
+ // Track model per job for status/result calls
14
+ private jobModels = new Map<string, string>();
15
+
16
+ async submit(
17
+ model: string,
18
+ inputs: Record<string, unknown>,
19
+ _config?: ProviderConfig,
20
+ ): Promise<string> {
21
+ // Handle nano-banana-pro routing: use /edit endpoint when image_urls provided
22
+ const resolvedModel = this.resolveModelEndpoint(model, inputs);
23
+
24
+ // Upload local files if needed
25
+ const processedInputs = await this.processInputs(inputs);
26
+
27
+ const result = await fal.queue.submit(resolvedModel, {
28
+ input: processedInputs,
29
+ });
30
+
31
+ // Store model for later status/result calls
32
+ this.jobModels.set(result.request_id, resolvedModel);
33
+
34
+ return result.request_id;
35
+ }
36
+
37
+ /**
38
+ * Resolve model endpoint based on inputs
39
+ * Handles special routing for models like nano-banana-pro (text-to-image vs image-to-image)
40
+ */
41
+ private resolveModelEndpoint(
42
+ model: string,
43
+ inputs: Record<string, unknown>,
44
+ ): string {
45
+ // Nano Banana Pro: use /edit endpoint when image_urls are provided
46
+ if (model === "fal-ai/nano-banana-pro") {
47
+ const imageUrls = inputs.image_urls as string[] | undefined;
48
+ if (imageUrls && imageUrls.length > 0) {
49
+ return "fal-ai/nano-banana-pro/edit";
50
+ }
51
+ }
52
+ return model;
53
+ }
54
+
55
+ async getStatus(jobId: string): Promise<JobStatusUpdate> {
56
+ const model = this.jobModels.get(jobId);
57
+ if (!model) {
58
+ throw new Error(`Unknown job: ${jobId}`);
59
+ }
60
+
61
+ const status = await fal.queue.status(model, { requestId: jobId });
62
+
63
+ const statusMap: Record<string, JobStatusUpdate["status"]> = {
64
+ IN_QUEUE: "queued",
65
+ IN_PROGRESS: "processing",
66
+ COMPLETED: "completed",
67
+ FAILED: "failed",
68
+ };
69
+
70
+ // @ts-expect-error - logs may exist on some status types
71
+ const logs = status.logs?.map((l: { message: string }) => l.message);
72
+
73
+ return {
74
+ status: statusMap[status.status] ?? "processing",
75
+ logs,
76
+ };
77
+ }
78
+
79
+ async getResult(jobId: string): Promise<unknown> {
80
+ const model = this.jobModels.get(jobId);
81
+ if (!model) {
82
+ throw new Error(`Unknown job: ${jobId}`);
83
+ }
84
+
85
+ const result = await fal.queue.result(model, { requestId: jobId });
86
+
87
+ // Clean up job model mapping after getting result
88
+ this.jobModels.delete(jobId);
89
+
90
+ return result.data;
91
+ }
92
+
93
+ override async uploadFile(
94
+ file: File | Blob | ArrayBuffer,
95
+ _filename?: string,
96
+ ): Promise<string> {
97
+ const blob =
98
+ file instanceof ArrayBuffer
99
+ ? new Blob([file])
100
+ : file instanceof Blob
101
+ ? file
102
+ : file;
103
+
104
+ const url = await fal.storage.upload(blob);
105
+ return url;
106
+ }
107
+
108
+ /**
109
+ * Process inputs, uploading local files as needed
110
+ */
111
+ private async processInputs(
112
+ inputs: Record<string, unknown>,
113
+ ): Promise<Record<string, unknown>> {
114
+ const processed: Record<string, unknown> = {};
115
+
116
+ for (const [key, value] of Object.entries(inputs)) {
117
+ if (typeof value === "string" && this.looksLikeLocalPath(value)) {
118
+ processed[key] = await ensureUrl(value, (buffer) =>
119
+ this.uploadFile(buffer),
120
+ );
121
+ } else {
122
+ processed[key] = value;
123
+ }
124
+ }
125
+
126
+ return processed;
127
+ }
128
+
129
+ private looksLikeLocalPath(value: string): boolean {
130
+ return (
131
+ !value.startsWith("http://") &&
132
+ !value.startsWith("https://") &&
133
+ (value.includes("/") || value.includes("\\"))
134
+ );
135
+ }
136
+
137
+ // ============================================================================
138
+ // High-level convenience methods (preserved from original lib/fal.ts)
139
+ // ============================================================================
140
+
141
+ async imageToVideo(args: {
142
+ prompt: string;
143
+ imageUrl: string;
144
+ modelVersion?: string;
145
+ duration?: 5 | 10;
146
+ tailImageUrl?: string;
147
+ }) {
148
+ const modelId = `fal-ai/kling-video/${args.modelVersion || "v2.5-turbo/pro"}/image-to-video`;
149
+
150
+ console.log(`[fal] starting image-to-video: ${modelId}`);
151
+ console.log(`[fal] prompt: ${args.prompt}`);
152
+
153
+ const imageUrl = await ensureUrl(args.imageUrl, (buffer) =>
154
+ this.uploadFile(buffer),
155
+ );
156
+ const tailImageUrl = args.tailImageUrl
157
+ ? await ensureUrl(args.tailImageUrl, (buffer) => this.uploadFile(buffer))
158
+ : undefined;
159
+
160
+ const result = await fal.subscribe(modelId, {
161
+ input: {
162
+ prompt: args.prompt,
163
+ image_url: imageUrl,
164
+ duration: args.duration || 5,
165
+ ...(tailImageUrl && { tail_image_url: tailImageUrl }),
166
+ },
167
+ logs: true,
168
+ onQueueUpdate: (update) => {
169
+ if (update.status === "IN_PROGRESS") {
170
+ console.log(
171
+ `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`,
172
+ );
173
+ }
174
+ },
175
+ });
176
+
177
+ console.log("[fal] completed!");
178
+ return result;
179
+ }
180
+
181
+ async textToVideo(args: {
182
+ prompt: string;
183
+ modelVersion?: string;
184
+ duration?: 5 | 10;
185
+ aspectRatio?: "16:9" | "9:16" | "1:1";
186
+ }) {
187
+ const modelId = `fal-ai/kling-video/${args.modelVersion || "v2.5-turbo/pro"}/text-to-video`;
188
+
189
+ console.log(`[fal] starting text-to-video: ${modelId}`);
190
+ console.log(`[fal] prompt: ${args.prompt}`);
191
+
192
+ const result = await fal.subscribe(modelId, {
193
+ input: {
194
+ prompt: args.prompt,
195
+ duration: args.duration || 5,
196
+ aspect_ratio: args.aspectRatio || "16:9",
197
+ },
198
+ logs: true,
199
+ onQueueUpdate: (update) => {
200
+ if (update.status === "IN_PROGRESS") {
201
+ console.log(
202
+ `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`,
203
+ );
204
+ }
205
+ },
206
+ });
207
+
208
+ console.log("[fal] completed!");
209
+ return result;
210
+ }
211
+
212
+ async generateImage(args: {
213
+ prompt: string;
214
+ model?: string;
215
+ imageSize?: string;
216
+ }) {
217
+ const modelId = args.model || "fal-ai/flux-pro/v1.1";
218
+
219
+ console.log(`[fal] generating image with ${modelId}`);
220
+ console.log(`[fal] prompt: ${args.prompt}`);
221
+
222
+ const result = await fal.subscribe(modelId, {
223
+ input: {
224
+ prompt: args.prompt,
225
+ image_size: args.imageSize || "landscape_4_3",
226
+ },
227
+ logs: true,
228
+ onQueueUpdate: (update) => {
229
+ if (update.status === "IN_PROGRESS") {
230
+ console.log(
231
+ `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`,
232
+ );
233
+ }
234
+ },
235
+ });
236
+
237
+ console.log("[fal] completed!");
238
+ return result;
239
+ }
240
+
241
+ async imageToImage(args: {
242
+ prompt: string;
243
+ imageUrl: string;
244
+ aspectRatio?: string;
245
+ }) {
246
+ const modelId = "fal-ai/nano-banana-pro/edit";
247
+
248
+ console.log(`[fal] starting image-to-image: ${modelId}`);
249
+
250
+ const imageUrl = await ensureUrl(args.imageUrl, (buffer) =>
251
+ this.uploadFile(buffer),
252
+ );
253
+
254
+ const result = await fal.subscribe(modelId, {
255
+ input: {
256
+ prompt: args.prompt,
257
+ image_urls: [imageUrl],
258
+ aspect_ratio: (args.aspectRatio || "1:1") as
259
+ | "16:9"
260
+ | "9:16"
261
+ | "1:1"
262
+ | "21:9"
263
+ | "3:2"
264
+ | "4:3"
265
+ | "5:4"
266
+ | "4:5"
267
+ | "3:4"
268
+ | "2:3",
269
+ resolution: "2K",
270
+ },
271
+ logs: true,
272
+ onQueueUpdate: (update) => {
273
+ if (update.status === "IN_PROGRESS") {
274
+ console.log(
275
+ `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`,
276
+ );
277
+ }
278
+ },
279
+ });
280
+
281
+ console.log("[fal] completed!");
282
+ return result;
283
+ }
284
+
285
+ async wan25(args: {
286
+ prompt: string;
287
+ imageUrl: string;
288
+ audioUrl: string;
289
+ resolution?: "480p" | "720p" | "1080p";
290
+ duration?: "5" | "10";
291
+ negativePrompt?: string;
292
+ }) {
293
+ const modelId = "fal-ai/wan-25-preview/image-to-video";
294
+
295
+ console.log(`[fal] starting wan-25: ${modelId}`);
296
+
297
+ const imageUrl = await ensureUrl(args.imageUrl, (buffer) =>
298
+ this.uploadFile(buffer),
299
+ );
300
+ const audioUrl = await ensureUrl(args.audioUrl, (buffer) =>
301
+ this.uploadFile(buffer),
302
+ );
303
+
304
+ const result = await fal.subscribe(modelId, {
305
+ input: {
306
+ prompt: args.prompt,
307
+ image_url: imageUrl,
308
+ audio_url: audioUrl,
309
+ resolution: args.resolution || "480p",
310
+ duration: args.duration || "5",
311
+ negative_prompt:
312
+ args.negativePrompt ||
313
+ "low resolution, error, worst quality, low quality, defects",
314
+ enable_prompt_expansion: true,
315
+ },
316
+ logs: true,
317
+ onQueueUpdate: (update) => {
318
+ if (update.status === "IN_PROGRESS") {
319
+ console.log(
320
+ `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`,
321
+ );
322
+ }
323
+ },
324
+ });
325
+
326
+ console.log("[fal] completed!");
327
+ return result;
328
+ }
329
+
330
+ async textToMusic(args: {
331
+ prompt?: string;
332
+ tags?: string[];
333
+ lyricsPrompt?: string;
334
+ seed?: number;
335
+ promptStrength?: number;
336
+ balanceStrength?: number;
337
+ numSongs?: 1 | 2;
338
+ outputFormat?: "flac" | "mp3" | "wav" | "ogg" | "m4a";
339
+ outputBitRate?: 128 | 192 | 256 | 320;
340
+ bpm?: number | "auto";
341
+ }) {
342
+ const modelId = "fal-ai/sonauto/bark";
343
+
344
+ console.log(`[fal] starting text-to-music: ${modelId}`);
345
+
346
+ const result = await fal.subscribe(modelId, {
347
+ input: {
348
+ prompt: args.prompt,
349
+ tags: args.tags,
350
+ lyrics_prompt: args.lyricsPrompt,
351
+ seed: args.seed,
352
+ prompt_strength: args.promptStrength,
353
+ balance_strength: args.balanceStrength,
354
+ num_songs: args.numSongs,
355
+ output_format: args.outputFormat,
356
+ output_bit_rate: args.outputBitRate,
357
+ bpm: args.bpm,
358
+ },
359
+ logs: true,
360
+ onQueueUpdate: (update) => {
361
+ if (update.status === "IN_PROGRESS") {
362
+ console.log(
363
+ `[fal] ${update.logs?.map((l) => l.message).join(" ") || "processing..."}`,
364
+ );
365
+ }
366
+ },
367
+ });
368
+
369
+ console.log("[fal] completed!");
370
+ return result;
371
+ }
372
+ }
373
+
374
+ // Export singleton instance
375
+ export const falProvider = new FalProvider();
376
+
377
+ // Re-export convenience functions for backward compatibility
378
+ export const imageToVideo = (
379
+ args: Parameters<FalProvider["imageToVideo"]>[0],
380
+ ) => falProvider.imageToVideo(args);
381
+ export const textToVideo = (args: Parameters<FalProvider["textToVideo"]>[0]) =>
382
+ falProvider.textToVideo(args);
383
+ export const generateImage = (
384
+ args: Parameters<FalProvider["generateImage"]>[0],
385
+ ) => falProvider.generateImage(args);
386
+ export const imageToImage = (
387
+ args: Parameters<FalProvider["imageToImage"]>[0],
388
+ ) => falProvider.imageToImage(args);
389
+ export const wan25 = (args: Parameters<FalProvider["wan25"]>[0]) =>
390
+ falProvider.wan25(args);
391
+ export const textToMusic = (args: Parameters<FalProvider["textToMusic"]>[0]) =>
392
+ falProvider.textToMusic(args);