vargai 0.4.0-alpha34 → 0.4.0-alpha35
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/.env.example +6 -0
- package/package.json +1 -2
- package/src/ai-sdk/cache.ts +0 -2
- package/src/ai-sdk/index.ts +4 -0
- package/src/ai-sdk/providers/CONTRIBUTING.md +457 -0
- package/src/ai-sdk/providers/editly/backends/index.ts +1 -0
- package/src/ai-sdk/providers/editly/backends/local.ts +34 -5
- package/src/ai-sdk/providers/editly/backends/types.ts +25 -5
- package/src/ai-sdk/providers/editly/index.ts +17 -14
- package/src/ai-sdk/providers/editly/rendi/index.ts +56 -47
- package/src/ai-sdk/providers/editly/rendi/rendi.test.ts +1 -12
- package/src/ai-sdk/providers/fal.ts +32 -7
- package/src/ai-sdk/providers/together.ts +191 -0
- package/src/react/renderers/burn-captions.ts +2 -14
- package/src/react/renderers/cache.test.ts +182 -0
- package/src/react/renderers/utils.test.ts +80 -0
- package/src/react/renderers/utils.ts +37 -1
package/.env.example
CHANGED
package/package.json
CHANGED
|
@@ -66,10 +66,9 @@
|
|
|
66
66
|
"remotion": "^4.0.377",
|
|
67
67
|
"replicate": "^1.4.0",
|
|
68
68
|
"sharp": "^0.34.5",
|
|
69
|
-
"vargai": "^0.4.0-alpha11",
|
|
70
69
|
"zod": "^4.2.1"
|
|
71
70
|
},
|
|
72
|
-
"version": "0.4.0-
|
|
71
|
+
"version": "0.4.0-alpha35",
|
|
73
72
|
"exports": {
|
|
74
73
|
".": "./src/index.ts",
|
|
75
74
|
"./ai": "./src/ai-sdk/index.ts",
|
package/src/ai-sdk/cache.ts
CHANGED
|
@@ -115,7 +115,6 @@ export function withCache<T extends object, R>(
|
|
|
115
115
|
const storage = options.storage ?? defaultStorage;
|
|
116
116
|
const ttl = parseTTL(options.ttl ?? DEFAULT_TTL);
|
|
117
117
|
const prefix = fn.name || "anonymous";
|
|
118
|
-
|
|
119
118
|
return async (opts: WithCacheKey<T>): Promise<R> => {
|
|
120
119
|
const { cacheKey, ...rest } = opts;
|
|
121
120
|
|
|
@@ -128,7 +127,6 @@ export function withCache<T extends object, R>(
|
|
|
128
127
|
if (cached !== undefined) {
|
|
129
128
|
return cached as R;
|
|
130
129
|
}
|
|
131
|
-
|
|
132
130
|
const result = await fn(rest as T);
|
|
133
131
|
const flattened = flatten(result);
|
|
134
132
|
await storage.set(key, flattened, ttl);
|
package/src/ai-sdk/index.ts
CHANGED
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
# Adding Models & Providers
|
|
2
|
+
|
|
3
|
+
This guide explains how to add new AI models and providers to the varg SDK.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Providers in varg extend the [Vercel AI SDK](https://sdk.vercel.ai/) with additional model types for video, music, and other media generation. Each provider implements a consistent interface pattern.
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/ai-sdk/providers/
|
|
13
|
+
├── fal.ts # Full provider (video, image, transcription)
|
|
14
|
+
├── elevenlabs.ts # Speech & music provider
|
|
15
|
+
├── openai.ts # Extends @ai-sdk/openai with video
|
|
16
|
+
├── google.ts # Image & video provider
|
|
17
|
+
├── higgsfield.ts # Image-only provider
|
|
18
|
+
├── replicate.ts # Re-exports @ai-sdk/replicate
|
|
19
|
+
└── CONTRIBUTING.md # This file
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Model Types
|
|
23
|
+
|
|
24
|
+
| Type | Interface | Use Case |
|
|
25
|
+
|------|-----------|----------|
|
|
26
|
+
| `VideoModelV3` | `../video-model.ts` | Video generation (t2v, i2v, lipsync) |
|
|
27
|
+
| `ImageModelV3` | `@ai-sdk/provider` | Image generation |
|
|
28
|
+
| `SpeechModelV3` | `@ai-sdk/provider` | Text-to-speech |
|
|
29
|
+
| `MusicModelV3` | `../music-model.ts` | Music generation |
|
|
30
|
+
| `TranscriptionModelV3` | `@ai-sdk/provider` | Speech-to-text |
|
|
31
|
+
| `LanguageModelV3` | `@ai-sdk/provider` | LLM text generation |
|
|
32
|
+
| `EmbeddingModelV3` | `@ai-sdk/provider` | Text embeddings |
|
|
33
|
+
|
|
34
|
+
## Adding a New Model to an Existing Provider
|
|
35
|
+
|
|
36
|
+
### Example: Adding a new video model to fal.ts
|
|
37
|
+
|
|
38
|
+
1. **Add to the model mapping:**
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
const VIDEO_MODELS: Record<string, { t2v: string; i2v: string }> = {
|
|
42
|
+
// existing models...
|
|
43
|
+
"new-model-v1": {
|
|
44
|
+
t2v: "fal-ai/new-model/text-to-video",
|
|
45
|
+
i2v: "fal-ai/new-model/image-to-video",
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
2. **That's it!** The existing `FalVideoModel` class handles the rest.
|
|
51
|
+
|
|
52
|
+
### Example: Adding a model with special handling
|
|
53
|
+
|
|
54
|
+
If the new model needs custom logic, add conditional handling in `doGenerate()`:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
async doGenerate(options: VideoModelV3CallOptions) {
|
|
58
|
+
const isNewModel = this.modelId === "new-model-v1";
|
|
59
|
+
|
|
60
|
+
if (isNewModel) {
|
|
61
|
+
// Custom input handling for this model
|
|
62
|
+
input.special_param = options.providerOptions?.fal?.specialParam;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ... rest of generation logic
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Creating a New Provider
|
|
70
|
+
|
|
71
|
+
### Step 1: Define the Provider Interface
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import {
|
|
75
|
+
type EmbeddingModelV3,
|
|
76
|
+
type ImageModelV3,
|
|
77
|
+
type LanguageModelV3,
|
|
78
|
+
NoSuchModelError,
|
|
79
|
+
type ProviderV3,
|
|
80
|
+
} from "@ai-sdk/provider";
|
|
81
|
+
import type { VideoModelV3 } from "../video-model";
|
|
82
|
+
|
|
83
|
+
export interface MyProviderSettings {
|
|
84
|
+
apiKey?: string;
|
|
85
|
+
baseURL?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface MyProvider extends ProviderV3 {
|
|
89
|
+
// Add methods for each model type you support
|
|
90
|
+
videoModel(modelId: string): VideoModelV3;
|
|
91
|
+
imageModel(modelId: string): ImageModelV3;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Step 2: Implement Model Classes
|
|
96
|
+
|
|
97
|
+
Each model class must implement the corresponding interface:
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
class MyVideoModel implements VideoModelV3 {
|
|
101
|
+
readonly specificationVersion = "v3" as const;
|
|
102
|
+
readonly provider = "myprovider";
|
|
103
|
+
readonly modelId: string;
|
|
104
|
+
readonly maxVideosPerCall = 1;
|
|
105
|
+
|
|
106
|
+
private apiKey: string;
|
|
107
|
+
|
|
108
|
+
constructor(modelId: string, options: { apiKey?: string } = {}) {
|
|
109
|
+
this.modelId = modelId;
|
|
110
|
+
this.apiKey = options.apiKey ?? process.env.MY_PROVIDER_API_KEY ?? "";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async doGenerate(options: VideoModelV3CallOptions) {
|
|
114
|
+
const {
|
|
115
|
+
prompt,
|
|
116
|
+
duration,
|
|
117
|
+
aspectRatio,
|
|
118
|
+
files,
|
|
119
|
+
providerOptions,
|
|
120
|
+
abortSignal,
|
|
121
|
+
} = options;
|
|
122
|
+
|
|
123
|
+
const warnings: SharedV3Warning[] = [];
|
|
124
|
+
|
|
125
|
+
// 1. Build API request
|
|
126
|
+
const input: Record<string, unknown> = {
|
|
127
|
+
prompt,
|
|
128
|
+
duration: duration ?? 5,
|
|
129
|
+
...(providerOptions?.myprovider ?? {}),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// 2. Handle file inputs (for image-to-video, etc.)
|
|
133
|
+
if (files && files.length > 0) {
|
|
134
|
+
const imageFile = files.find(f =>
|
|
135
|
+
f.type === "file"
|
|
136
|
+
? f.mediaType?.startsWith("image/")
|
|
137
|
+
: /\.(jpg|jpeg|png|webp)$/i.test(f.url)
|
|
138
|
+
);
|
|
139
|
+
if (imageFile) {
|
|
140
|
+
input.image_url = await this.uploadFile(imageFile);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 3. Call the API
|
|
145
|
+
const response = await fetch("https://api.myprovider.com/v1/generate", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
149
|
+
"Content-Type": "application/json",
|
|
150
|
+
},
|
|
151
|
+
body: JSON.stringify(input),
|
|
152
|
+
signal: abortSignal,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
if (!response.ok) {
|
|
156
|
+
throw new Error(`API error: ${await response.text()}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const data = await response.json();
|
|
160
|
+
|
|
161
|
+
// 4. Download the result
|
|
162
|
+
const videoResponse = await fetch(data.video_url, { signal: abortSignal });
|
|
163
|
+
const videoBuffer = new Uint8Array(await videoResponse.arrayBuffer());
|
|
164
|
+
|
|
165
|
+
// 5. Return in standard format
|
|
166
|
+
return {
|
|
167
|
+
videos: [videoBuffer],
|
|
168
|
+
warnings,
|
|
169
|
+
response: {
|
|
170
|
+
timestamp: new Date(),
|
|
171
|
+
modelId: this.modelId,
|
|
172
|
+
headers: undefined,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async uploadFile(file: ImageModelV3File): Promise<string> {
|
|
178
|
+
// Implementation depends on provider's upload mechanism
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Step 3: Create the Provider Factory
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
export function createMyProvider(
|
|
187
|
+
settings: MyProviderSettings = {},
|
|
188
|
+
): MyProvider {
|
|
189
|
+
const apiKey = settings.apiKey ?? process.env.MY_PROVIDER_API_KEY;
|
|
190
|
+
|
|
191
|
+
if (!apiKey) {
|
|
192
|
+
throw new Error("MY_PROVIDER_API_KEY not set");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
specificationVersion: "v3",
|
|
197
|
+
|
|
198
|
+
videoModel(modelId: string): VideoModelV3 {
|
|
199
|
+
return new MyVideoModel(modelId, { apiKey });
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
imageModel(modelId: string): ImageModelV3 {
|
|
203
|
+
return new MyImageModel(modelId, { apiKey });
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
// Throw NoSuchModelError for unsupported model types
|
|
207
|
+
languageModel(modelId: string): LanguageModelV3 {
|
|
208
|
+
throw new NoSuchModelError({ modelId, modelType: "languageModel" });
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
embeddingModel(modelId: string): EmbeddingModelV3 {
|
|
212
|
+
throw new NoSuchModelError({ modelId, modelType: "embeddingModel" });
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Step 4: Export a Lazy Singleton
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// Lazy initialization - only creates client when first accessed
|
|
222
|
+
let _myprovider: MyProvider | undefined;
|
|
223
|
+
|
|
224
|
+
export const myprovider = new Proxy({} as MyProvider, {
|
|
225
|
+
get(_, prop) {
|
|
226
|
+
if (!_myprovider) {
|
|
227
|
+
_myprovider = createMyProvider();
|
|
228
|
+
}
|
|
229
|
+
return _myprovider[prop as keyof MyProvider];
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Step 5: Re-export from index
|
|
235
|
+
|
|
236
|
+
Add to `src/ai-sdk/index.ts`:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
export { createMyProvider, myprovider } from "./providers/myprovider";
|
|
240
|
+
export type { MyProvider, MyProviderSettings } from "./providers/myprovider";
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Handling Warnings
|
|
244
|
+
|
|
245
|
+
Use warnings to communicate unsupported features without failing:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
if (options.seed !== undefined) {
|
|
249
|
+
warnings.push({
|
|
250
|
+
type: "unsupported",
|
|
251
|
+
feature: "seed",
|
|
252
|
+
details: "Seed is not supported by this model",
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (options.fps !== undefined) {
|
|
257
|
+
warnings.push({
|
|
258
|
+
type: "unsupported",
|
|
259
|
+
feature: "fps",
|
|
260
|
+
details: "FPS is not configurable, using provider default",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## Provider Options Passthrough
|
|
266
|
+
|
|
267
|
+
Allow provider-specific options via `providerOptions`:
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// User code:
|
|
271
|
+
await generateVideo({
|
|
272
|
+
model: myprovider.videoModel("model-v1"),
|
|
273
|
+
prompt: "a cat",
|
|
274
|
+
providerOptions: {
|
|
275
|
+
myprovider: {
|
|
276
|
+
customParam: "value",
|
|
277
|
+
negativePrompt: "blurry",
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// In your model:
|
|
283
|
+
const customOptions = providerOptions?.myprovider ?? {};
|
|
284
|
+
input.custom_param = customOptions.customParam;
|
|
285
|
+
input.negative_prompt = customOptions.negativePrompt;
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Async Job Polling
|
|
289
|
+
|
|
290
|
+
Many video APIs are async. Here's the standard polling pattern:
|
|
291
|
+
|
|
292
|
+
```typescript
|
|
293
|
+
async doGenerate(options: VideoModelV3CallOptions) {
|
|
294
|
+
// 1. Create job
|
|
295
|
+
const createResponse = await fetch(`${this.baseURL}/jobs`, {
|
|
296
|
+
method: "POST",
|
|
297
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
298
|
+
body: JSON.stringify(input),
|
|
299
|
+
signal: options.abortSignal,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const job = await createResponse.json();
|
|
303
|
+
|
|
304
|
+
// 2. Poll for completion
|
|
305
|
+
let status = job.status;
|
|
306
|
+
while (status === "queued" || status === "processing") {
|
|
307
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
308
|
+
|
|
309
|
+
const statusResponse = await fetch(`${this.baseURL}/jobs/${job.id}`, {
|
|
310
|
+
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
311
|
+
signal: options.abortSignal,
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const statusData = await statusResponse.json();
|
|
315
|
+
status = statusData.status;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (status === "failed") {
|
|
319
|
+
throw new Error(`Generation failed: ${job.error}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 3. Download result
|
|
323
|
+
const videoResponse = await fetch(job.output_url);
|
|
324
|
+
return { videos: [new Uint8Array(await videoResponse.arrayBuffer())] };
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## File Upload Helpers
|
|
329
|
+
|
|
330
|
+
Common pattern for handling file inputs:
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import type { ImageModelV3File } from "@ai-sdk/provider";
|
|
334
|
+
|
|
335
|
+
async function fileToUrl(file: ImageModelV3File): Promise<string> {
|
|
336
|
+
if (file.type === "url") {
|
|
337
|
+
return file.url;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Convert base64/Uint8Array to upload
|
|
341
|
+
const bytes = typeof file.data === "string"
|
|
342
|
+
? Uint8Array.from(atob(file.data), c => c.charCodeAt(0))
|
|
343
|
+
: file.data;
|
|
344
|
+
|
|
345
|
+
const blob = new Blob([bytes], { type: file.mediaType ?? "image/png" });
|
|
346
|
+
|
|
347
|
+
// Upload to provider's storage (or use data URL for small files)
|
|
348
|
+
return await uploadToStorage(blob);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function getMediaType(file: ImageModelV3File): string | undefined {
|
|
352
|
+
if (file.type === "file") return file.mediaType;
|
|
353
|
+
|
|
354
|
+
const ext = file.url.split(".").pop()?.toLowerCase();
|
|
355
|
+
const mimeTypes: Record<string, string> = {
|
|
356
|
+
png: "image/png",
|
|
357
|
+
jpg: "image/jpeg",
|
|
358
|
+
jpeg: "image/jpeg",
|
|
359
|
+
mp3: "audio/mpeg",
|
|
360
|
+
wav: "audio/wav",
|
|
361
|
+
mp4: "video/mp4",
|
|
362
|
+
};
|
|
363
|
+
return mimeTypes[ext ?? ""];
|
|
364
|
+
}
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Extending Existing Providers
|
|
368
|
+
|
|
369
|
+
To add video support to an existing AI SDK provider (like OpenAI):
|
|
370
|
+
|
|
371
|
+
```typescript
|
|
372
|
+
import {
|
|
373
|
+
createOpenAI as createOpenAIBase,
|
|
374
|
+
type OpenAIProvider as OpenAIProviderBase,
|
|
375
|
+
} from "@ai-sdk/openai";
|
|
376
|
+
|
|
377
|
+
// Extend the base provider interface
|
|
378
|
+
export interface OpenAIProvider extends OpenAIProviderBase {
|
|
379
|
+
videoModel(modelId: string): VideoModelV3;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function createOpenAI(settings = {}): OpenAIProvider {
|
|
383
|
+
const base = createOpenAIBase(settings);
|
|
384
|
+
|
|
385
|
+
// Create callable function with all base methods
|
|
386
|
+
const provider = ((modelId: string) => base(modelId)) as OpenAIProvider;
|
|
387
|
+
Object.assign(provider, base);
|
|
388
|
+
|
|
389
|
+
// Add video support
|
|
390
|
+
provider.videoModel = (modelId: string): VideoModelV3 =>
|
|
391
|
+
new OpenAIVideoModel(modelId, settings);
|
|
392
|
+
|
|
393
|
+
return provider;
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
## Re-exporting External Providers
|
|
398
|
+
|
|
399
|
+
For providers that work as-is from `@ai-sdk/*`:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
// replicate.ts - simple re-export
|
|
403
|
+
export {
|
|
404
|
+
createReplicate,
|
|
405
|
+
replicate,
|
|
406
|
+
type ReplicateProvider,
|
|
407
|
+
type ReplicateProviderSettings,
|
|
408
|
+
} from "@ai-sdk/replicate";
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
## Testing Your Provider
|
|
412
|
+
|
|
413
|
+
```typescript
|
|
414
|
+
import { describe, test, expect } from "bun:test";
|
|
415
|
+
import { createMyProvider } from "./myprovider";
|
|
416
|
+
|
|
417
|
+
describe("MyProvider", () => {
|
|
418
|
+
test("creates video model", () => {
|
|
419
|
+
const provider = createMyProvider({ apiKey: "test-key" });
|
|
420
|
+
const model = provider.videoModel("model-v1");
|
|
421
|
+
|
|
422
|
+
expect(model.provider).toBe("myprovider");
|
|
423
|
+
expect(model.modelId).toBe("model-v1");
|
|
424
|
+
expect(model.specificationVersion).toBe("v3");
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("throws on missing api key", () => {
|
|
428
|
+
delete process.env.MY_PROVIDER_API_KEY;
|
|
429
|
+
expect(() => createMyProvider()).toThrow("MY_PROVIDER_API_KEY not set");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Checklist for New Providers
|
|
435
|
+
|
|
436
|
+
- [ ] Implements `ProviderV3` interface
|
|
437
|
+
- [ ] Model classes implement correct `*ModelV3` interfaces
|
|
438
|
+
- [ ] `specificationVersion` is `"v3"`
|
|
439
|
+
- [ ] Factory function `createProvider(settings)`
|
|
440
|
+
- [ ] Lazy singleton export for convenience
|
|
441
|
+
- [ ] API key from settings OR environment variable
|
|
442
|
+
- [ ] `NoSuchModelError` for unsupported model types
|
|
443
|
+
- [ ] Warnings for unsupported features (don't fail silently)
|
|
444
|
+
- [ ] `providerOptions` passthrough for provider-specific params
|
|
445
|
+
- [ ] `abortSignal` support for cancellation
|
|
446
|
+
- [ ] Proper error handling with descriptive messages
|
|
447
|
+
- [ ] Re-exported from `src/ai-sdk/index.ts`
|
|
448
|
+
- [ ] Environment variable documented in README
|
|
449
|
+
|
|
450
|
+
## Questions?
|
|
451
|
+
|
|
452
|
+
Check existing providers for reference implementations:
|
|
453
|
+
- **Full provider**: `fal.ts` (video, image, transcription)
|
|
454
|
+
- **Audio provider**: `elevenlabs.ts` (speech, music)
|
|
455
|
+
- **Extended provider**: `openai.ts` (adds video to base)
|
|
456
|
+
- **Simple provider**: `higgsfield.ts` (image only)
|
|
457
|
+
- **Re-export**: `replicate.ts`
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { $ } from "bun";
|
|
2
2
|
import type {
|
|
3
3
|
FFmpegBackend,
|
|
4
|
+
FFmpegInput,
|
|
4
5
|
FFmpegRunOptions,
|
|
5
6
|
FFmpegRunResult,
|
|
6
7
|
VideoInfo,
|
|
7
8
|
} from "./types";
|
|
8
9
|
|
|
9
|
-
const FFMPEG_COMMON_ARGS = ["-hide_banner", "-loglevel", "error"];
|
|
10
|
-
|
|
11
10
|
export class LocalBackend implements FFmpegBackend {
|
|
12
11
|
readonly name = "local";
|
|
13
12
|
|
|
@@ -39,13 +38,43 @@ export class LocalBackend implements FFmpegBackend {
|
|
|
39
38
|
};
|
|
40
39
|
}
|
|
41
40
|
|
|
41
|
+
private buildInputArgs(inputs: FFmpegInput[]): string[] {
|
|
42
|
+
const args: string[] = [];
|
|
43
|
+
for (const input of inputs) {
|
|
44
|
+
if (typeof input === "string") {
|
|
45
|
+
args.push("-i", input);
|
|
46
|
+
} else if ("raw" in input) {
|
|
47
|
+
args.push(...input.raw.split(" "));
|
|
48
|
+
} else {
|
|
49
|
+
if (input.options) args.push(...input.options);
|
|
50
|
+
args.push("-i", input.path);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return args;
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
async run(options: FFmpegRunOptions): Promise<FFmpegRunResult> {
|
|
43
|
-
const {
|
|
57
|
+
const {
|
|
58
|
+
inputs,
|
|
59
|
+
filterComplex,
|
|
60
|
+
videoFilter,
|
|
61
|
+
outputArgs = [],
|
|
62
|
+
outputPath,
|
|
63
|
+
verbose,
|
|
64
|
+
} = options;
|
|
65
|
+
|
|
66
|
+
const inputArgs = this.buildInputArgs(inputs);
|
|
44
67
|
|
|
45
68
|
const ffmpegArgs = [
|
|
46
|
-
|
|
69
|
+
"-hide_banner",
|
|
70
|
+
"-loglevel",
|
|
47
71
|
verbose ? "info" : "error",
|
|
48
|
-
...
|
|
72
|
+
...inputArgs,
|
|
73
|
+
...(filterComplex ? ["-filter_complex", filterComplex] : []),
|
|
74
|
+
...(videoFilter ? ["-vf", videoFilter] : []),
|
|
75
|
+
...outputArgs,
|
|
76
|
+
"-y",
|
|
77
|
+
outputPath,
|
|
49
78
|
];
|
|
50
79
|
|
|
51
80
|
if (verbose) {
|
|
@@ -11,13 +11,33 @@ import type { VideoInfo } from "../types";
|
|
|
11
11
|
export type { VideoInfo };
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
14
|
+
* Represents an input to ffmpeg - can be a simple path/URL or structured with options
|
|
15
|
+
*/
|
|
16
|
+
export type FFmpegInput =
|
|
17
|
+
| string
|
|
18
|
+
| {
|
|
19
|
+
/** Path or URL to the input file */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Options to apply BEFORE the -i flag (e.g. -ss 5 for seeking) */
|
|
22
|
+
options?: string[];
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
/** Raw ffmpeg args that don't use -i (e.g. "-f lavfi -i color=black") */
|
|
26
|
+
raw: string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* FFmpeg execution options - new interface where backend builds -i flags
|
|
15
31
|
*/
|
|
16
32
|
export interface FFmpegRunOptions {
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
|
|
33
|
+
/** Inputs - backend builds -i flags from these */
|
|
34
|
+
inputs: FFmpegInput[];
|
|
35
|
+
/** Filter complex string (uses input indices like [0:v], [1:a]) */
|
|
36
|
+
filterComplex?: string;
|
|
37
|
+
/** Video filter string for single-input operations */
|
|
38
|
+
videoFilter?: string;
|
|
39
|
+
/** Arguments after inputs but before output (codec, map, etc) */
|
|
40
|
+
outputArgs?: string[];
|
|
21
41
|
/** Output file path */
|
|
22
42
|
outputPath: string;
|
|
23
43
|
/** Enable verbose logging */
|
|
@@ -840,10 +840,9 @@ export async function editly(config: EditlyConfig): Promise<EditlyResult> {
|
|
|
840
840
|
allFilters.push(audioFilter.filter);
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
-
const inputArgs = allInputs.flatMap((input) => ["-i", input]);
|
|
844
843
|
const filterComplex = allFilters.join(";");
|
|
845
844
|
|
|
846
|
-
const
|
|
845
|
+
const codecArgs = customOutputArgs ?? [
|
|
847
846
|
"-c:v",
|
|
848
847
|
"libx264",
|
|
849
848
|
"-preset",
|
|
@@ -860,30 +859,34 @@ export async function editly(config: EditlyConfig): Promise<EditlyResult> {
|
|
|
860
859
|
? ["-map", `[${finalVideoLabel}]`, "-map", `[${audioFilter.outputLabel}]`]
|
|
861
860
|
: ["-map", `[${finalVideoLabel}]`];
|
|
862
861
|
|
|
863
|
-
const
|
|
864
|
-
"-hide_banner",
|
|
865
|
-
"-loglevel",
|
|
866
|
-
verbose ? "info" : "error",
|
|
867
|
-
...inputArgs,
|
|
868
|
-
"-filter_complex",
|
|
869
|
-
filterComplex,
|
|
862
|
+
const outputArgs = [
|
|
870
863
|
...mapArgs,
|
|
871
864
|
"-r",
|
|
872
865
|
String(fps),
|
|
873
|
-
...
|
|
866
|
+
...codecArgs,
|
|
874
867
|
...(config.shortest ? ["-shortest"] : []),
|
|
875
|
-
"-y",
|
|
876
|
-
outPath,
|
|
877
868
|
];
|
|
878
869
|
|
|
879
870
|
if (verbose) {
|
|
880
|
-
|
|
871
|
+
const inputArgs = allInputs.flatMap((input) => ["-i", input]);
|
|
872
|
+
console.log(
|
|
873
|
+
"ffmpeg",
|
|
874
|
+
[
|
|
875
|
+
...inputArgs,
|
|
876
|
+
"-filter_complex",
|
|
877
|
+
filterComplex,
|
|
878
|
+
...outputArgs,
|
|
879
|
+
"-y",
|
|
880
|
+
outPath,
|
|
881
|
+
].join(" "),
|
|
882
|
+
);
|
|
881
883
|
console.log("\nFilter complex:\n", filterComplex.split(";").join(";\n"));
|
|
882
884
|
}
|
|
883
885
|
|
|
884
886
|
const result = await backend.run({
|
|
885
|
-
args: ffmpegArgs,
|
|
886
887
|
inputs: allInputs,
|
|
888
|
+
filterComplex,
|
|
889
|
+
outputArgs,
|
|
887
890
|
outputPath: outPath,
|
|
888
891
|
verbose,
|
|
889
892
|
});
|