vargai 0.4.0-alpha4 → 0.4.0-alpha40
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/README.md +483 -61
- package/assets/fonts/TikTokSans-Bold.ttf +0 -0
- package/examples/grok-imagine-test.tsx +155 -0
- package/launch-videos/06-kawaii-fruits.tsx +93 -0
- package/launch-videos/07-ugc-weight-loss.tsx +132 -0
- package/launch-videos/08-talking-head-varg.tsx +107 -0
- package/launch-videos/09-girl.tsx +160 -0
- package/launch-videos/README.md +42 -0
- package/package.json +10 -4
- package/pipeline/cookbooks/round-video-character.md +1 -1
- package/skills/varg-video-generation/SKILL.md +224 -0
- package/skills/varg-video-generation/references/templates.md +380 -0
- package/skills/varg-video-generation/scripts/setup.ts +265 -0
- package/src/ai-sdk/cache.ts +1 -3
- package/src/ai-sdk/examples/google-image.ts +62 -0
- package/src/ai-sdk/index.ts +10 -0
- package/src/ai-sdk/middleware/wrap-image-model.ts +4 -21
- package/src/ai-sdk/middleware/wrap-music-model.ts +4 -16
- package/src/ai-sdk/middleware/wrap-video-model.ts +5 -17
- package/src/ai-sdk/providers/CONTRIBUTING.md +457 -0
- package/src/ai-sdk/providers/editly/backends/index.ts +8 -0
- package/src/ai-sdk/providers/editly/backends/local.ts +94 -0
- package/src/ai-sdk/providers/editly/backends/types.ts +74 -0
- package/src/ai-sdk/providers/editly/editly.test.ts +49 -1
- package/src/ai-sdk/providers/editly/index.ts +164 -80
- package/src/ai-sdk/providers/editly/layers.ts +58 -6
- package/src/ai-sdk/providers/editly/rendi/editly-with-rendi-backend.test.ts +335 -0
- package/src/ai-sdk/providers/editly/rendi/index.ts +289 -0
- package/src/ai-sdk/providers/editly/rendi/rendi.test.ts +35 -0
- package/src/ai-sdk/providers/editly/types.ts +30 -0
- package/src/ai-sdk/providers/elevenlabs.ts +10 -2
- package/src/ai-sdk/providers/fal.test.ts +214 -0
- package/src/ai-sdk/providers/fal.ts +435 -40
- package/src/ai-sdk/providers/google.ts +423 -0
- package/src/ai-sdk/providers/together.ts +191 -0
- package/src/cli/commands/find.tsx +1 -0
- package/src/cli/commands/frame.tsx +616 -0
- package/src/cli/commands/hello.ts +85 -0
- package/src/cli/commands/help.tsx +18 -30
- package/src/cli/commands/index.ts +11 -2
- package/src/cli/commands/init.tsx +570 -0
- package/src/cli/commands/list.tsx +1 -0
- package/src/cli/commands/render.tsx +322 -76
- package/src/cli/commands/run.tsx +1 -0
- package/src/cli/commands/storyboard.tsx +1714 -0
- package/src/cli/commands/which.tsx +1 -0
- package/src/cli/index.ts +23 -4
- package/src/cli/ui/components/Badge.tsx +1 -0
- package/src/cli/ui/components/DataTable.tsx +1 -0
- package/src/cli/ui/components/Header.tsx +1 -0
- package/src/cli/ui/components/HelpBlock.tsx +1 -0
- package/src/cli/ui/components/KeyValue.tsx +1 -0
- package/src/cli/ui/components/OptionRow.tsx +1 -0
- package/src/cli/ui/components/Separator.tsx +1 -0
- package/src/cli/ui/components/StatusBox.tsx +1 -0
- package/src/cli/ui/components/VargBox.tsx +1 -0
- package/src/cli/ui/components/VargProgress.tsx +1 -0
- package/src/cli/ui/components/VargSpinner.tsx +1 -0
- package/src/cli/ui/components/VargText.tsx +1 -0
- package/src/definitions/actions/grok-edit.ts +133 -0
- package/src/definitions/actions/index.ts +16 -0
- package/src/definitions/actions/qwen-angles.ts +218 -0
- package/src/index.ts +1 -0
- package/src/providers/fal.ts +196 -0
- package/src/react/assets.ts +9 -0
- package/src/react/elements.ts +0 -5
- package/src/react/examples/branching.tsx +6 -4
- package/src/react/examples/character-video.tsx +13 -10
- package/src/react/examples/local-files-test.tsx +19 -0
- package/src/react/examples/ltx2-test.tsx +25 -0
- package/src/react/examples/madi.tsx +13 -10
- package/src/react/examples/mcmeows.tsx +40 -0
- package/src/react/examples/music-defaults.tsx +24 -0
- package/src/react/examples/quickstart-test.tsx +101 -0
- package/src/react/examples/qwen-angles-test.tsx +72 -0
- package/src/react/index.ts +3 -3
- package/src/react/layouts/grid.tsx +1 -1
- package/src/react/layouts/index.ts +2 -1
- package/src/react/layouts/slot.tsx +85 -0
- package/src/react/layouts/split.tsx +18 -0
- package/src/react/react.test.ts +60 -11
- package/src/react/renderers/burn-captions.ts +95 -0
- package/src/react/renderers/cache.test.ts +182 -0
- package/src/react/renderers/captions.ts +25 -6
- package/src/react/renderers/clip.ts +56 -25
- package/src/react/renderers/context.ts +5 -2
- package/src/react/renderers/image.ts +5 -2
- package/src/react/renderers/index.ts +0 -1
- package/src/react/renderers/music.ts +8 -3
- package/src/react/renderers/packshot/blinking-button.ts +413 -0
- package/src/react/renderers/packshot.ts +170 -8
- package/src/react/renderers/progress.ts +4 -3
- package/src/react/renderers/render.ts +127 -71
- package/src/react/renderers/speech.ts +2 -2
- package/src/react/renderers/split.ts +34 -13
- package/src/react/renderers/utils.test.ts +80 -0
- package/src/react/renderers/utils.ts +37 -1
- package/src/react/renderers/video.ts +47 -9
- package/src/react/types.ts +70 -17
- package/src/studio/stages.ts +40 -39
- package/src/studio/step-renderer.ts +14 -24
- package/src/studio/ui/index.html +2 -2
- package/src/tests/all.test.ts +4 -4
- package/src/tests/index.ts +1 -1
- package/test-slot-grid.tsx +19 -0
- package/test-slot-userland.tsx +30 -0
- package/test-sync-v2.ts +30 -0
- package/test-sync-v2.tsx +29 -0
- package/tsconfig.json +1 -1
- package/video.tsx +7 -0
- package/src/ai-sdk/providers/editly/ffmpeg.ts +0 -60
- package/src/react/renderers/animate.ts +0 -59
- /package/src/cli/commands/{studio.tsx → studio.ts} +0 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createGoogleGenerativeAI,
|
|
3
|
+
type GoogleGenerativeAIProviderSettings,
|
|
4
|
+
} from "@ai-sdk/google";
|
|
5
|
+
import {
|
|
6
|
+
type EmbeddingModelV3,
|
|
7
|
+
type ImageModelV3,
|
|
8
|
+
type ImageModelV3CallOptions,
|
|
9
|
+
type LanguageModelV3,
|
|
10
|
+
NoSuchModelError,
|
|
11
|
+
type ProviderV3,
|
|
12
|
+
type SharedV3Warning,
|
|
13
|
+
} from "@ai-sdk/provider";
|
|
14
|
+
import { GoogleGenAI } from "@google/genai";
|
|
15
|
+
import { generateText } from "ai";
|
|
16
|
+
import type { VideoModelV3, VideoModelV3CallOptions } from "../video-model";
|
|
17
|
+
|
|
18
|
+
// re-export base types
|
|
19
|
+
export type { GoogleGenerativeAIProviderSettings };
|
|
20
|
+
|
|
21
|
+
// ============================================================================
|
|
22
|
+
// Image Models
|
|
23
|
+
// ============================================================================
|
|
24
|
+
|
|
25
|
+
const IMAGE_MODELS: Record<string, string> = {
|
|
26
|
+
// gemini 3 models (use imageConfig for aspectRatio)
|
|
27
|
+
"gemini-3-pro-image": "gemini-3-pro-image-preview",
|
|
28
|
+
"nano-banana-pro": "gemini-3-pro-image-preview",
|
|
29
|
+
"nano-banana-pro/edit": "gemini-3-pro-image-preview",
|
|
30
|
+
// gemini 2 models (use responseModalities)
|
|
31
|
+
"gemini-2.0-flash-exp-image-generation":
|
|
32
|
+
"gemini-2.0-flash-exp-image-generation",
|
|
33
|
+
"gemini-2-flash-image": "gemini-2.0-flash-exp-image-generation",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const GEMINI_3_MODELS = new Set(["gemini-3-pro-image-preview"]);
|
|
37
|
+
|
|
38
|
+
const GEMINI_2_MODELS = new Set(["gemini-2.0-flash-exp-image-generation"]);
|
|
39
|
+
|
|
40
|
+
class GoogleImageModel implements ImageModelV3 {
|
|
41
|
+
readonly specificationVersion = "v3" as const;
|
|
42
|
+
readonly provider = "google";
|
|
43
|
+
readonly modelId: string;
|
|
44
|
+
readonly maxImagesPerCall = 1;
|
|
45
|
+
|
|
46
|
+
private apiKey: string;
|
|
47
|
+
|
|
48
|
+
constructor(modelId: string, options: { apiKey?: string } = {}) {
|
|
49
|
+
this.modelId = modelId;
|
|
50
|
+
this.apiKey =
|
|
51
|
+
options.apiKey ?? process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async doGenerate(options: ImageModelV3CallOptions) {
|
|
55
|
+
const { prompt, aspectRatio, files, providerOptions } = options;
|
|
56
|
+
const warnings: SharedV3Warning[] = [];
|
|
57
|
+
|
|
58
|
+
const model = IMAGE_MODELS[this.modelId] ?? this.modelId;
|
|
59
|
+
|
|
60
|
+
const content: Array<
|
|
61
|
+
{ type: "text"; text: string } | { type: "image"; image: Buffer | URL }
|
|
62
|
+
> = [{ type: "text", text: prompt ?? "" }];
|
|
63
|
+
|
|
64
|
+
if (files && files.length > 0) {
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
if (file.type === "file") {
|
|
67
|
+
const data =
|
|
68
|
+
typeof file.data === "string"
|
|
69
|
+
? Buffer.from(file.data, "base64")
|
|
70
|
+
: Buffer.from(file.data);
|
|
71
|
+
content.push({ type: "image", image: data });
|
|
72
|
+
} else {
|
|
73
|
+
content.push({ type: "image", image: new URL(file.url) });
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// create google provider with api key
|
|
79
|
+
const googleProvider = createGoogleGenerativeAI({
|
|
80
|
+
apiKey: this.apiKey,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// google returns generated images in result.files[] via generateText
|
|
84
|
+
const googleOptions = (providerOptions?.google ?? {}) as Record<
|
|
85
|
+
string,
|
|
86
|
+
unknown
|
|
87
|
+
>;
|
|
88
|
+
|
|
89
|
+
// gemini 3 uses imageConfig for aspectRatio, gemini 2 uses responseModalities
|
|
90
|
+
const isGemini3 = GEMINI_3_MODELS.has(model);
|
|
91
|
+
const providerOpts: Record<string, unknown> = { ...googleOptions };
|
|
92
|
+
|
|
93
|
+
if (isGemini3) {
|
|
94
|
+
providerOpts.imageConfig = {
|
|
95
|
+
...(aspectRatio ? { aspectRatio } : {}),
|
|
96
|
+
...((googleOptions.imageConfig as Record<string, unknown>) ?? {}),
|
|
97
|
+
};
|
|
98
|
+
} else {
|
|
99
|
+
providerOpts.responseModalities = ["TEXT", "IMAGE"];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const result = await generateText({
|
|
103
|
+
model: googleProvider(model) as unknown as Parameters<
|
|
104
|
+
typeof generateText
|
|
105
|
+
>[0]["model"],
|
|
106
|
+
messages: [
|
|
107
|
+
{
|
|
108
|
+
role: "user",
|
|
109
|
+
content,
|
|
110
|
+
},
|
|
111
|
+
],
|
|
112
|
+
providerOptions: {
|
|
113
|
+
google: providerOpts as Record<
|
|
114
|
+
string,
|
|
115
|
+
string | string[] | Record<string, string>
|
|
116
|
+
>,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const imageFiles = result.files?.filter((file) =>
|
|
121
|
+
file.mediaType?.startsWith("image/"),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!imageFiles || imageFiles.length === 0) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`No images generated. Model response: ${result.text?.slice(0, 200) || "(no text)"}`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const images = imageFiles.map((file) => {
|
|
131
|
+
const bytes =
|
|
132
|
+
typeof file.base64 === "string"
|
|
133
|
+
? Uint8Array.from(atob(file.base64), (c) => c.charCodeAt(0))
|
|
134
|
+
: new Uint8Array(0);
|
|
135
|
+
return bytes;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
if (options.seed !== undefined) {
|
|
139
|
+
warnings.push({
|
|
140
|
+
type: "unsupported",
|
|
141
|
+
feature: "seed",
|
|
142
|
+
details: "Seed is not supported by Google image generation",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (options.size !== undefined) {
|
|
147
|
+
warnings.push({
|
|
148
|
+
type: "unsupported",
|
|
149
|
+
feature: "size",
|
|
150
|
+
details: "Use aspectRatio instead for Google image generation",
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (aspectRatio && GEMINI_2_MODELS.has(model)) {
|
|
155
|
+
warnings.push({
|
|
156
|
+
type: "unsupported",
|
|
157
|
+
feature: "aspectRatio",
|
|
158
|
+
details: `aspectRatio not supported by ${model}, use gemini-3-pro-image instead`,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
images,
|
|
164
|
+
warnings,
|
|
165
|
+
response: {
|
|
166
|
+
timestamp: new Date(),
|
|
167
|
+
modelId: this.modelId,
|
|
168
|
+
headers: undefined,
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Video Models
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
const VIDEO_MODELS: Record<string, string> = {
|
|
179
|
+
"veo-3.1": "veo-3.1-generate-preview",
|
|
180
|
+
"veo-3": "veo-3.0-generate-preview",
|
|
181
|
+
"veo-3-fast": "veo-3.0-fast-generate-001",
|
|
182
|
+
"veo-2": "veo-2.0-generate-001",
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const DEFAULT_POLL_INTERVAL_MS = 10000;
|
|
186
|
+
const DEFAULT_MAX_POLL_DURATION_MS = 300000; // 5 minutes
|
|
187
|
+
|
|
188
|
+
class GoogleVideoModel implements VideoModelV3 {
|
|
189
|
+
readonly specificationVersion = "v3" as const;
|
|
190
|
+
readonly provider = "google";
|
|
191
|
+
readonly modelId: string;
|
|
192
|
+
readonly maxVideosPerCall = 1;
|
|
193
|
+
|
|
194
|
+
private client: GoogleGenAI;
|
|
195
|
+
private apiKey: string;
|
|
196
|
+
private pollIntervalMs: number;
|
|
197
|
+
private maxPollDurationMs: number;
|
|
198
|
+
private onProgress?: (progress: number, operationName: string) => void;
|
|
199
|
+
|
|
200
|
+
constructor(
|
|
201
|
+
modelId: string,
|
|
202
|
+
options: { apiKey?: string; polling?: GooglePollingConfig } = {},
|
|
203
|
+
) {
|
|
204
|
+
this.modelId = modelId;
|
|
205
|
+
this.apiKey =
|
|
206
|
+
options.apiKey ?? process.env.GOOGLE_GENERATIVE_AI_API_KEY ?? "";
|
|
207
|
+
this.client = new GoogleGenAI({ apiKey: this.apiKey });
|
|
208
|
+
this.pollIntervalMs =
|
|
209
|
+
options.polling?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
210
|
+
this.maxPollDurationMs =
|
|
211
|
+
options.polling?.maxPollDurationMs ?? DEFAULT_MAX_POLL_DURATION_MS;
|
|
212
|
+
this.onProgress = options.polling?.onProgress;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async doGenerate(options: VideoModelV3CallOptions) {
|
|
216
|
+
const {
|
|
217
|
+
prompt,
|
|
218
|
+
duration,
|
|
219
|
+
aspectRatio,
|
|
220
|
+
fps,
|
|
221
|
+
seed,
|
|
222
|
+
files,
|
|
223
|
+
providerOptions,
|
|
224
|
+
abortSignal,
|
|
225
|
+
} = options;
|
|
226
|
+
const warnings: SharedV3Warning[] = [];
|
|
227
|
+
|
|
228
|
+
// resolve model endpoint
|
|
229
|
+
const model = VIDEO_MODELS[this.modelId] ?? this.modelId;
|
|
230
|
+
|
|
231
|
+
// build config
|
|
232
|
+
const config: Record<string, unknown> = {
|
|
233
|
+
numberOfVideos: 1,
|
|
234
|
+
...(providerOptions?.google as Record<string, unknown>),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (aspectRatio) {
|
|
238
|
+
config.aspectRatio = aspectRatio;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (duration) {
|
|
242
|
+
config.durationSeconds = duration;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (fps) {
|
|
246
|
+
config.fps = fps;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (seed !== undefined) {
|
|
250
|
+
config.seed = seed;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// handle image input for image-to-video
|
|
254
|
+
let image: { data: string; mimeType: string } | undefined;
|
|
255
|
+
if (files && files.length > 0) {
|
|
256
|
+
const imageFile = files.find((f) => {
|
|
257
|
+
if (f.type === "file") return f.mediaType?.startsWith("image/");
|
|
258
|
+
return /\.(jpg|jpeg|png|webp)$/i.test(f.url);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (imageFile) {
|
|
262
|
+
if (imageFile.type === "file") {
|
|
263
|
+
const base64 =
|
|
264
|
+
typeof imageFile.data === "string"
|
|
265
|
+
? imageFile.data
|
|
266
|
+
: Buffer.from(imageFile.data).toString("base64");
|
|
267
|
+
image = {
|
|
268
|
+
data: base64,
|
|
269
|
+
mimeType: imageFile.mediaType ?? "image/png",
|
|
270
|
+
};
|
|
271
|
+
} else {
|
|
272
|
+
const response = await fetch(imageFile.url, { signal: abortSignal });
|
|
273
|
+
if (!response.ok) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`Failed to fetch image from ${imageFile.url}: ${response.status} ${response.statusText}`,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
const buffer = await response.arrayBuffer();
|
|
279
|
+
const base64 = Buffer.from(buffer).toString("base64");
|
|
280
|
+
const mimeType = response.headers.get("content-type") ?? "image/png";
|
|
281
|
+
image = { data: base64, mimeType };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
let operation = await this.client.models.generateVideos({
|
|
287
|
+
model,
|
|
288
|
+
prompt,
|
|
289
|
+
image,
|
|
290
|
+
config,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const operationName = operation.name ?? "unknown";
|
|
294
|
+
this.onProgress?.(0, operationName);
|
|
295
|
+
|
|
296
|
+
const startTime = Date.now();
|
|
297
|
+
|
|
298
|
+
while (!operation.done) {
|
|
299
|
+
if (abortSignal?.aborted) {
|
|
300
|
+
throw new Error("Video generation aborted");
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const elapsed = Date.now() - startTime;
|
|
304
|
+
if (elapsed > this.maxPollDurationMs) {
|
|
305
|
+
throw new Error(
|
|
306
|
+
`Video generation timed out after ${Math.round(elapsed / 1000)}s (max: ${Math.round(this.maxPollDurationMs / 1000)}s)`,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
311
|
+
operation = await this.client.operations.getVideosOperation({
|
|
312
|
+
operation,
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
if (operation.metadata?.progress) {
|
|
316
|
+
this.onProgress?.(operation.metadata.progress as number, operationName);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (operation.error) {
|
|
321
|
+
throw new Error(
|
|
322
|
+
`Google video generation failed: ${operation.error.message}`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// download generated videos
|
|
327
|
+
const generatedVideos = operation.response?.generatedVideos;
|
|
328
|
+
if (!generatedVideos || generatedVideos.length === 0) {
|
|
329
|
+
throw new Error("No videos generated by Google model");
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const videos: Uint8Array[] = [];
|
|
333
|
+
for (const video of generatedVideos) {
|
|
334
|
+
const videoUri = video.video?.uri;
|
|
335
|
+
if (!videoUri) continue;
|
|
336
|
+
|
|
337
|
+
const videoUrl = `${videoUri}&key=${this.apiKey}`;
|
|
338
|
+
|
|
339
|
+
const response = await fetch(videoUrl, { signal: abortSignal });
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
throw new Error(`Failed to download video: ${response.statusText}`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const buffer = await response.arrayBuffer();
|
|
345
|
+
videos.push(new Uint8Array(buffer));
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (videos.length === 0) {
|
|
349
|
+
throw new Error("Failed to download any videos from Google");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// warn about unsupported options
|
|
353
|
+
if (options.resolution !== undefined) {
|
|
354
|
+
warnings.push({
|
|
355
|
+
type: "unsupported",
|
|
356
|
+
feature: "resolution",
|
|
357
|
+
details:
|
|
358
|
+
"Use aspectRatio instead. Google Veo determines resolution automatically.",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
videos,
|
|
364
|
+
warnings,
|
|
365
|
+
response: {
|
|
366
|
+
timestamp: new Date(),
|
|
367
|
+
modelId: this.modelId,
|
|
368
|
+
headers: undefined,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ============================================================================
|
|
375
|
+
// Provider
|
|
376
|
+
// ============================================================================
|
|
377
|
+
|
|
378
|
+
export interface GooglePollingConfig {
|
|
379
|
+
pollIntervalMs?: number;
|
|
380
|
+
maxPollDurationMs?: number;
|
|
381
|
+
onProgress?: (progress: number, operationName: string) => void;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export interface GoogleProviderSettings {
|
|
385
|
+
apiKey?: string;
|
|
386
|
+
polling?: GooglePollingConfig;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export interface GoogleProvider extends ProviderV3 {
|
|
390
|
+
imageModel(modelId: string): ImageModelV3;
|
|
391
|
+
videoModel(modelId: string): VideoModelV3;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export function createGoogle(
|
|
395
|
+
settings: GoogleProviderSettings = {},
|
|
396
|
+
): GoogleProvider {
|
|
397
|
+
const apiKey = settings.apiKey ?? process.env.GOOGLE_GENERATIVE_AI_API_KEY;
|
|
398
|
+
const polling = settings.polling;
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
specificationVersion: "v3",
|
|
402
|
+
imageModel(modelId: string): GoogleImageModel {
|
|
403
|
+
return new GoogleImageModel(modelId, { apiKey });
|
|
404
|
+
},
|
|
405
|
+
videoModel(modelId: string): GoogleVideoModel {
|
|
406
|
+
return new GoogleVideoModel(modelId, { apiKey, polling });
|
|
407
|
+
},
|
|
408
|
+
languageModel(modelId: string): LanguageModelV3 {
|
|
409
|
+
throw new NoSuchModelError({
|
|
410
|
+
modelId,
|
|
411
|
+
modelType: "languageModel",
|
|
412
|
+
});
|
|
413
|
+
},
|
|
414
|
+
embeddingModel(modelId: string): EmbeddingModelV3 {
|
|
415
|
+
throw new NoSuchModelError({
|
|
416
|
+
modelId,
|
|
417
|
+
modelType: "embeddingModel",
|
|
418
|
+
});
|
|
419
|
+
},
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export const google = createGoogle();
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ImageModelV3,
|
|
3
|
+
type ImageModelV3CallOptions,
|
|
4
|
+
NoSuchModelError,
|
|
5
|
+
type SharedV3Warning,
|
|
6
|
+
} from "@ai-sdk/provider";
|
|
7
|
+
|
|
8
|
+
const IMAGE_MODELS: Record<string, string> = {
|
|
9
|
+
"flux-schnell": "black-forest-labs/FLUX.1-schnell",
|
|
10
|
+
"flux-dev": "black-forest-labs/FLUX.1-dev",
|
|
11
|
+
"flux-pro": "black-forest-labs/FLUX.1-pro",
|
|
12
|
+
"flux-1.1-pro": "black-forest-labs/FLUX.1.1-pro",
|
|
13
|
+
"flux-kontext-pro": "black-forest-labs/FLUX.1-kontext-pro",
|
|
14
|
+
"flux-kontext-max": "black-forest-labs/FLUX.1-kontext-max",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Aspect ratio to width/height mapping
|
|
18
|
+
const ASPECT_RATIO_DIMENSIONS: Record<
|
|
19
|
+
string,
|
|
20
|
+
{ width: number; height: number }
|
|
21
|
+
> = {
|
|
22
|
+
"1:1": { width: 1024, height: 1024 },
|
|
23
|
+
"4:3": { width: 1024, height: 768 },
|
|
24
|
+
"3:4": { width: 768, height: 1024 },
|
|
25
|
+
"16:9": { width: 1344, height: 768 },
|
|
26
|
+
"9:16": { width: 768, height: 1344 },
|
|
27
|
+
"3:2": { width: 1024, height: 683 },
|
|
28
|
+
"2:3": { width: 683, height: 1024 },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
interface TogetherProviderOptions {
|
|
32
|
+
apiKey?: string;
|
|
33
|
+
baseUrl?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class TogetherImageModel implements ImageModelV3 {
|
|
37
|
+
readonly specificationVersion = "v3" as const;
|
|
38
|
+
readonly provider = "together";
|
|
39
|
+
readonly modelId: string;
|
|
40
|
+
readonly maxImagesPerCall = 4;
|
|
41
|
+
|
|
42
|
+
private apiKey: string;
|
|
43
|
+
private baseUrl: string;
|
|
44
|
+
|
|
45
|
+
constructor(modelId: string, options: TogetherProviderOptions = {}) {
|
|
46
|
+
this.modelId = modelId;
|
|
47
|
+
this.apiKey = options.apiKey ?? process.env.TOGETHER_API_KEY ?? "";
|
|
48
|
+
this.baseUrl = options.baseUrl ?? "https://api.together.xyz/v1";
|
|
49
|
+
|
|
50
|
+
if (!this.apiKey) {
|
|
51
|
+
throw new Error(
|
|
52
|
+
"Together API key is required. Set TOGETHER_API_KEY environment variable or pass apiKey option.",
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async doGenerate(options: ImageModelV3CallOptions) {
|
|
58
|
+
const { prompt, n, size, aspectRatio, seed, providerOptions, abortSignal } =
|
|
59
|
+
options;
|
|
60
|
+
const warnings: SharedV3Warning[] = [];
|
|
61
|
+
|
|
62
|
+
// Resolve model endpoint
|
|
63
|
+
const model = IMAGE_MODELS[this.modelId] ?? this.modelId;
|
|
64
|
+
|
|
65
|
+
// Build request body
|
|
66
|
+
const body: Record<string, unknown> = {
|
|
67
|
+
model,
|
|
68
|
+
prompt,
|
|
69
|
+
n: n ?? 1,
|
|
70
|
+
steps: 4, // flux-schnell optimal steps
|
|
71
|
+
...(providerOptions?.together ?? {}),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// Handle size
|
|
75
|
+
if (size) {
|
|
76
|
+
const [width, height] = size.split("x").map(Number);
|
|
77
|
+
body.width = width;
|
|
78
|
+
body.height = height;
|
|
79
|
+
} else if (aspectRatio) {
|
|
80
|
+
const dims = ASPECT_RATIO_DIMENSIONS[aspectRatio];
|
|
81
|
+
if (dims) {
|
|
82
|
+
body.width = dims.width;
|
|
83
|
+
body.height = dims.height;
|
|
84
|
+
} else {
|
|
85
|
+
warnings.push({
|
|
86
|
+
type: "unsupported",
|
|
87
|
+
feature: "aspectRatio",
|
|
88
|
+
details: `Aspect ratio "${aspectRatio}" not supported, using default 1024x1024`,
|
|
89
|
+
});
|
|
90
|
+
body.width = 1024;
|
|
91
|
+
body.height = 1024;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (seed !== undefined) {
|
|
96
|
+
body.seed = seed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Timing diagnostics
|
|
100
|
+
const t0 = Date.now();
|
|
101
|
+
|
|
102
|
+
const response = await fetch(`${this.baseUrl}/images/generations`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: abortSignal,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
const error = await response.text();
|
|
114
|
+
throw new Error(`Together API error: ${response.status} ${error}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = (await response.json()) as {
|
|
118
|
+
data: Array<{ url?: string; b64_json?: string }>;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const apiTime = Date.now() - t0;
|
|
122
|
+
console.log(
|
|
123
|
+
`[together-timing] ${this.modelId}: API call took ${apiTime}ms`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const images = data.data ?? [];
|
|
127
|
+
if (images.length === 0) {
|
|
128
|
+
throw new Error("No images in Together response");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Download images from URLs
|
|
132
|
+
const t1 = Date.now();
|
|
133
|
+
const imageBuffers = await Promise.all(
|
|
134
|
+
images.map(async (img) => {
|
|
135
|
+
if (img.b64_json) {
|
|
136
|
+
// Base64 response
|
|
137
|
+
return new Uint8Array(Buffer.from(img.b64_json, "base64"));
|
|
138
|
+
}
|
|
139
|
+
if (img.url) {
|
|
140
|
+
// URL response - download
|
|
141
|
+
const imgResponse = await fetch(img.url, { signal: abortSignal });
|
|
142
|
+
return new Uint8Array(await imgResponse.arrayBuffer());
|
|
143
|
+
}
|
|
144
|
+
throw new Error("Image has neither url nor b64_json");
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
const downloadTime = Date.now() - t1;
|
|
148
|
+
console.log(
|
|
149
|
+
`[together-timing] ${this.modelId}: image download took ${downloadTime}ms`,
|
|
150
|
+
);
|
|
151
|
+
console.log(
|
|
152
|
+
`[together-timing] ${this.modelId}: TOTAL ${Date.now() - t0}ms`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
images: imageBuffers,
|
|
157
|
+
warnings,
|
|
158
|
+
response: {
|
|
159
|
+
timestamp: new Date(),
|
|
160
|
+
modelId: this.modelId,
|
|
161
|
+
headers: undefined,
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
interface TogetherProvider {
|
|
168
|
+
imageModel: (modelId: string) => ImageModelV3;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function createTogetherProvider(
|
|
172
|
+
options: TogetherProviderOptions = {},
|
|
173
|
+
): TogetherProvider {
|
|
174
|
+
const imageModel = (modelId: string): ImageModelV3 => {
|
|
175
|
+
if (!IMAGE_MODELS[modelId] && !modelId.includes("/")) {
|
|
176
|
+
throw new NoSuchModelError({
|
|
177
|
+
modelId,
|
|
178
|
+
modelType: "imageModel",
|
|
179
|
+
message: `Unknown image model: ${modelId}. Available: ${Object.keys(IMAGE_MODELS).join(", ")}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
return new TogetherImageModel(modelId, options);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
imageModel,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export const together = createTogetherProvider();
|
|
191
|
+
export { createTogetherProvider };
|