varg.ai-sdk 0.1.0 → 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.
- package/.claude/settings.local.json +1 -1
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +23 -0
- package/.husky/README.md +102 -0
- package/.husky/commit-msg +6 -0
- package/.husky/pre-commit +9 -0
- package/.husky/pre-push +6 -0
- package/.size-limit.json +8 -0
- package/.test-hooks.ts +5 -0
- package/CLAUDE.md +10 -3
- package/CONTRIBUTING.md +150 -0
- package/LICENSE.md +53 -0
- package/README.md +56 -209
- package/SKILLS.md +26 -10
- package/biome.json +7 -1
- package/bun.lock +1286 -0
- package/commitlint.config.js +22 -0
- package/docs/index.html +1130 -0
- package/docs/prompting.md +326 -0
- package/docs/react.md +834 -0
- package/docs/sdk.md +812 -0
- package/ffmpeg/CLAUDE.md +68 -0
- package/package.json +48 -8
- package/pipeline/cookbooks/scripts/animate-frames-parallel.ts +84 -0
- package/pipeline/cookbooks/scripts/combine-scenes.sh +53 -0
- package/pipeline/cookbooks/scripts/generate-frames-parallel.ts +99 -0
- package/pipeline/cookbooks/scripts/still-to-video.sh +37 -0
- package/pipeline/cookbooks/text-to-tiktok.md +669 -0
- package/pipeline/cookbooks/trendwatching.md +156 -0
- package/plan.md +281 -0
- package/scripts/.gitkeep +0 -0
- package/src/ai-sdk/cache.ts +142 -0
- package/src/ai-sdk/examples/cached-generation.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-4.ts +53 -0
- package/src/ai-sdk/examples/duet-scene-5-audio.ts +32 -0
- package/src/ai-sdk/examples/duet-video.ts +56 -0
- package/src/ai-sdk/examples/editly-composition.ts +63 -0
- package/src/ai-sdk/examples/editly-test.ts +57 -0
- package/src/ai-sdk/examples/editly-video-test.ts +52 -0
- package/src/ai-sdk/examples/fal-lipsync.ts +43 -0
- package/src/ai-sdk/examples/higgsfield-image.ts +61 -0
- package/src/ai-sdk/examples/music-generation.ts +19 -0
- package/src/ai-sdk/examples/openai-sora.ts +34 -0
- package/src/ai-sdk/examples/replicate-bg-removal.ts +52 -0
- package/src/ai-sdk/examples/simpsons-scene.ts +61 -0
- package/src/ai-sdk/examples/talking-lion.ts +55 -0
- package/src/ai-sdk/examples/video-generation.ts +39 -0
- package/src/ai-sdk/examples/workflow-animated-girl.ts +104 -0
- package/src/ai-sdk/examples/workflow-before-after.ts +114 -0
- package/src/ai-sdk/examples/workflow-character-grid.ts +112 -0
- package/src/ai-sdk/examples/workflow-slideshow.ts +161 -0
- package/src/ai-sdk/file-cache.ts +112 -0
- package/src/ai-sdk/file.ts +238 -0
- package/src/ai-sdk/generate-element.ts +92 -0
- package/src/ai-sdk/generate-music.ts +46 -0
- package/src/ai-sdk/generate-video.ts +165 -0
- package/src/ai-sdk/index.ts +72 -0
- package/src/ai-sdk/music-model.ts +110 -0
- package/src/ai-sdk/providers/editly/editly.test.ts +1108 -0
- package/src/ai-sdk/providers/editly/ffmpeg.ts +60 -0
- package/src/ai-sdk/providers/editly/index.ts +817 -0
- package/src/ai-sdk/providers/editly/layers.ts +776 -0
- package/src/ai-sdk/providers/editly/plan.md +144 -0
- package/src/ai-sdk/providers/editly/types.ts +328 -0
- package/src/ai-sdk/providers/elevenlabs-provider.ts +255 -0
- package/src/ai-sdk/providers/fal-provider.ts +512 -0
- package/src/ai-sdk/providers/higgsfield.ts +379 -0
- package/src/ai-sdk/providers/openai.ts +251 -0
- package/src/ai-sdk/providers/replicate.ts +16 -0
- package/src/ai-sdk/video-model.ts +185 -0
- package/src/cli/commands/find.tsx +137 -0
- package/src/cli/commands/help.tsx +85 -0
- package/src/cli/commands/index.ts +6 -0
- package/src/cli/commands/list.tsx +238 -0
- package/src/cli/commands/render.tsx +71 -0
- package/src/cli/commands/run.tsx +511 -0
- package/src/cli/commands/which.tsx +253 -0
- package/src/cli/index.ts +114 -0
- package/src/cli/quiet.ts +44 -0
- package/src/cli/types.ts +32 -0
- package/src/cli/ui/components/Badge.tsx +29 -0
- package/src/cli/ui/components/DataTable.tsx +51 -0
- package/src/cli/ui/components/Header.tsx +23 -0
- package/src/cli/ui/components/HelpBlock.tsx +44 -0
- package/src/cli/ui/components/KeyValue.tsx +33 -0
- package/src/cli/ui/components/OptionRow.tsx +81 -0
- package/src/cli/ui/components/Separator.tsx +23 -0
- package/src/cli/ui/components/StatusBox.tsx +108 -0
- package/src/cli/ui/components/VargBox.tsx +51 -0
- package/src/cli/ui/components/VargProgress.tsx +36 -0
- package/src/cli/ui/components/VargSpinner.tsx +34 -0
- package/src/cli/ui/components/VargText.tsx +56 -0
- package/src/cli/ui/components/index.ts +19 -0
- package/src/cli/ui/index.ts +12 -0
- package/src/cli/ui/render.ts +35 -0
- package/src/cli/ui/theme.ts +63 -0
- package/src/cli/utils.ts +78 -0
- package/src/core/executor/executor.ts +201 -0
- package/src/core/executor/index.ts +13 -0
- package/src/core/executor/job.ts +214 -0
- package/src/core/executor/pipeline.ts +222 -0
- package/src/core/index.ts +11 -0
- package/src/core/registry/index.ts +9 -0
- package/src/core/registry/loader.ts +149 -0
- package/src/core/registry/registry.ts +221 -0
- package/src/core/registry/resolver.ts +206 -0
- package/src/core/schema/helpers.ts +134 -0
- package/src/core/schema/index.ts +8 -0
- package/src/core/schema/shared.ts +102 -0
- package/src/core/schema/types.ts +279 -0
- package/src/core/schema/validator.ts +92 -0
- package/src/definitions/actions/captions.ts +261 -0
- package/src/definitions/actions/edit.ts +298 -0
- package/src/definitions/actions/image.ts +125 -0
- package/src/definitions/actions/index.ts +114 -0
- package/src/definitions/actions/music.ts +205 -0
- package/src/definitions/actions/sync.ts +128 -0
- package/{action/transcribe/index.ts → src/definitions/actions/transcribe.ts} +63 -90
- package/src/definitions/actions/upload.ts +111 -0
- package/src/definitions/actions/video.ts +163 -0
- package/src/definitions/actions/voice.ts +119 -0
- package/src/definitions/index.ts +23 -0
- package/src/definitions/models/elevenlabs.ts +50 -0
- package/src/definitions/models/flux.ts +56 -0
- package/src/definitions/models/index.ts +36 -0
- package/src/definitions/models/kling.ts +56 -0
- package/src/definitions/models/llama.ts +54 -0
- package/src/definitions/models/nano-banana-pro.ts +102 -0
- package/src/definitions/models/sonauto.ts +68 -0
- package/src/definitions/models/soul.ts +65 -0
- package/src/definitions/models/wan.ts +54 -0
- package/src/definitions/models/whisper.ts +44 -0
- package/src/definitions/skills/index.ts +12 -0
- package/src/definitions/skills/talking-character.ts +87 -0
- package/src/definitions/skills/text-to-tiktok.ts +97 -0
- package/src/index.ts +118 -0
- package/src/providers/apify.ts +269 -0
- package/src/providers/base.ts +264 -0
- package/src/providers/elevenlabs.ts +217 -0
- package/src/providers/fal.ts +392 -0
- package/src/providers/ffmpeg.ts +544 -0
- package/src/providers/fireworks.ts +193 -0
- package/src/providers/groq.ts +149 -0
- package/src/providers/higgsfield.ts +145 -0
- package/src/providers/index.ts +143 -0
- package/src/providers/replicate.ts +147 -0
- package/src/providers/storage.ts +206 -0
- package/src/react/cli.ts +52 -0
- package/src/react/elements.ts +146 -0
- package/src/react/examples/branching.tsx +66 -0
- package/src/react/examples/captions-demo.tsx +37 -0
- package/src/react/examples/character-video.tsx +84 -0
- package/src/react/examples/grid.tsx +53 -0
- package/src/react/examples/layouts-demo.tsx +57 -0
- package/src/react/examples/madi.tsx +60 -0
- package/src/react/examples/music-test.tsx +35 -0
- package/src/react/examples/onlyfans-1m/workflow.tsx +88 -0
- package/src/react/examples/orange-portrait.tsx +41 -0
- package/src/react/examples/split-element-demo.tsx +60 -0
- package/src/react/examples/split-layout-demo.tsx +60 -0
- package/src/react/examples/split.tsx +41 -0
- package/src/react/examples/video-grid.tsx +46 -0
- package/src/react/index.ts +43 -0
- package/src/react/layouts/grid.tsx +28 -0
- package/src/react/layouts/index.ts +2 -0
- package/src/react/layouts/split.tsx +20 -0
- package/src/react/react.test.ts +309 -0
- package/src/react/render.ts +21 -0
- package/src/react/renderers/animate.ts +59 -0
- package/src/react/renderers/captions.ts +297 -0
- package/src/react/renderers/clip.ts +248 -0
- package/src/react/renderers/context.ts +17 -0
- package/src/react/renderers/image.ts +109 -0
- package/src/react/renderers/index.ts +22 -0
- package/src/react/renderers/music.ts +60 -0
- package/src/react/renderers/packshot.ts +84 -0
- package/src/react/renderers/progress.ts +173 -0
- package/src/react/renderers/render.ts +243 -0
- package/src/react/renderers/slider.ts +69 -0
- package/src/react/renderers/speech.ts +53 -0
- package/src/react/renderers/split.ts +91 -0
- package/src/react/renderers/subtitle.ts +16 -0
- package/src/react/renderers/swipe.ts +75 -0
- package/src/react/renderers/title.ts +17 -0
- package/src/react/renderers/utils.ts +124 -0
- package/src/react/renderers/video.ts +127 -0
- package/src/react/runtime/jsx-dev-runtime.ts +43 -0
- package/src/react/runtime/jsx-runtime.ts +35 -0
- package/src/react/types.ts +232 -0
- package/src/studio/index.ts +26 -0
- package/src/studio/scanner.ts +102 -0
- package/src/studio/server.ts +554 -0
- package/src/studio/stages.ts +251 -0
- package/src/studio/step-renderer.ts +279 -0
- package/src/studio/types.ts +60 -0
- package/src/studio/ui/cache.html +303 -0
- package/src/studio/ui/index.html +1820 -0
- package/src/tests/all.test.ts +509 -0
- package/src/tests/index.ts +33 -0
- package/src/tests/unit.test.ts +403 -0
- package/tsconfig.cli.json +8 -0
- package/tsconfig.json +21 -3
- package/TEST_RESULTS.md +0 -122
- package/action/captions/SKILL.md +0 -170
- package/action/captions/index.ts +0 -227
- package/action/edit/SKILL.md +0 -235
- package/action/edit/index.ts +0 -493
- package/action/image/SKILL.md +0 -140
- package/action/image/index.ts +0 -112
- package/action/sync/SKILL.md +0 -136
- package/action/sync/index.ts +0 -187
- package/action/transcribe/SKILL.md +0 -179
- package/action/video/SKILL.md +0 -116
- package/action/video/index.ts +0 -135
- package/action/voice/SKILL.md +0 -125
- package/action/voice/index.ts +0 -201
- package/index.ts +0 -38
- package/lib/README.md +0 -144
- package/lib/ai-sdk/fal.ts +0 -106
- package/lib/ai-sdk/replicate.ts +0 -107
- package/lib/elevenlabs.ts +0 -382
- package/lib/fal.ts +0 -478
- package/lib/ffmpeg.ts +0 -467
- package/lib/fireworks.ts +0 -235
- package/lib/groq.ts +0 -246
- package/lib/higgsfield.ts +0 -176
- package/lib/remotion/SKILL.md +0 -823
- package/lib/remotion/cli.ts +0 -115
- package/lib/remotion/functions.ts +0 -283
- package/lib/remotion/index.ts +0 -19
- package/lib/remotion/templates.ts +0 -73
- package/lib/replicate.ts +0 -304
- package/output.txt +0 -1
- package/test-import.ts +0 -7
- package/test-services.ts +0 -97
- 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);
|