vargai 0.4.0-alpha106 → 0.4.0-alpha108

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/package.json CHANGED
@@ -28,7 +28,6 @@
28
28
  "@commitlint/config-conventional": "^20.0.0",
29
29
  "@size-limit/preset-small-lib": "^11.2.0",
30
30
  "@types/bun": "latest",
31
- "@types/opentype.js": "^1.3.9",
32
31
  "@types/react": "^19.2.7",
33
32
  "husky": "^9.1.7",
34
33
  "lint-staged": "^16.2.7"
@@ -59,11 +58,9 @@
59
58
  "ai": "^6.0.26",
60
59
  "apify-client": "^2.20.0",
61
60
  "citty": "^0.1.6",
62
- "fflate": "^0.8.2",
63
61
  "fluent-ffmpeg": "^2.1.3",
64
62
  "groq-sdk": "^0.36.0",
65
63
  "ink": "^6.5.1",
66
- "opentype.js": "^1.3.4",
67
64
  "p-limit": "^6.2.0",
68
65
  "p-map": "^7.0.4",
69
66
  "react": "^19.2.0",
@@ -107,7 +104,7 @@
107
104
  "license": "Apache-2.0",
108
105
  "author": "varg.ai <hello@varg.ai> (https://varg.ai)",
109
106
  "sideEffects": false,
110
- "version": "0.4.0-alpha106",
107
+ "version": "0.4.0-alpha108",
111
108
  "exports": {
112
109
  ".": "./src/index.ts",
113
110
  "./ai": "./src/ai-sdk/index.ts",
@@ -0,0 +1,197 @@
1
+ ================================================================================
2
+ PRICING FIX LOG - Verified from Official Provider llms.txt & Pricing Pages
3
+ ================================================================================
4
+ Date: 2026-04-22
5
+ Sources: fal.ai/models/*/llms.txt, elevenlabs.io/pricing/api, groq.com/pricing,
6
+ fireworks.ai/pricing, replicate.com/pricing
7
+
8
+ ================================================================================
9
+ SUMMARY: 13 model definitions updated, 4 confirmed correct
10
+ ================================================================================
11
+
12
+ Models confirmed CORRECT (no changes needed):
13
+ - reve ($0.04/image) src/definitions/models/reve.ts
14
+ - phota ($0.09/1K image) src/definitions/models/phota.ts
15
+ - phota/edit ($0.09/1K image) src/definitions/models/phota.ts
16
+ - phota/enhance ($0.13/image) src/definitions/models/phota.ts
17
+
18
+ ================================================================================
19
+ CHANGES APPLIED
20
+ ================================================================================
21
+
22
+ --- FIX #1: nano-banana-pro.ts (CRITICAL - was 3.75x underpriced) ---
23
+ File: src/definitions/models/nano-banana-pro.ts
24
+ Source: https://fal.ai/models/fal-ai/nano-banana-pro/llms.txt
25
+ Old: $0.04 per image flat
26
+ New: $0.15 per image (1K), $0.30 per image (4K)
27
+ Web search adds $0.015 per request
28
+ Formula: resolution === "4K" ? 0.30 : 0.15, multiplied by numImages
29
+
30
+ --- FIX #2: recraft-v4.ts (CRITICAL - was 3.1x underpriced) ---
31
+ File: src/definitions/models/recraft-v4.ts
32
+ Source: https://fal.ai/models/fal-ai/recraft/v4/pro/text-to-image/llms.txt
33
+ Old: $0.08 per image flat
34
+ New: $0.25 per image flat
35
+ Formula: flat 0.25
36
+
37
+ --- FIX #3: kling.ts (was flat, now per-second) ---
38
+ File: src/definitions/models/kling.ts
39
+ Source: https://fal.ai/models/fal-ai/kling-video/o3/pro/text-to-video/llms.txt
40
+ Old: $1.20 flat per generation
41
+ New: $0.112 per second (audio off), $0.14 per second (audio on)
42
+ 5s video = $0.56 (no audio) or $0.70 (audio)
43
+ Duration range: 3-15 seconds
44
+ Formula: 0.112 * duration
45
+ Note: Using base rate $0.112 (no audio). Audio surcharge not auto-detected.
46
+ Kling v2.5 Turbo Pro ($0.07/sec) is a different endpoint used in
47
+ provider code, not captured in this model definition.
48
+
49
+ --- FIX #4: wan.ts (was flat, now per-second with resolution tiers) ---
50
+ File: src/definitions/models/wan.ts
51
+ Source: https://fal.ai/models/fal-ai/wan-25-preview/image-to-video/llms.txt
52
+ Old: $0.60 flat per generation
53
+ New: $0.05/sec (480p), $0.10/sec (720p), $0.15/sec (1080p)
54
+ Duration options: 5s or 10s
55
+ Formula: rateMap[resolution] * duration
56
+ rateMap = { "480p": 0.05, "720p": 0.10, "1080p": 0.15 }
57
+ Default resolution: "480p"
58
+
59
+ --- FIX #5: omnihuman.ts (was flat, now per-second) ---
60
+ File: src/definitions/models/omnihuman.ts
61
+ Source: https://fal.ai/models/fal-ai/bytedance/omnihuman/v1.5/llms.txt
62
+ Old: $0.80 flat per generation
63
+ New: $0.16 per second of output video
64
+ Max audio: 30s (1080p), 60s (720p)
65
+ Formula: 0.16 * duration
66
+ Note: Duration driven by audio length. Output includes billing duration field.
67
+
68
+ --- FIX #6: veed-fabric.ts (was flat, now per-second with resolution tiers) ---
69
+ File: src/definitions/models/veed-fabric.ts
70
+ Source: https://fal.ai/models/veed/fabric-1.0/llms.txt
71
+ Old: $0.80 flat per generation
72
+ New: $0.08/sec (480p), $0.15/sec (720p)
73
+ Formula: (resolution === "720p" ? 0.15 : 0.08) * duration
74
+ Note: Duration driven by audio length. Resolution is required input.
75
+
76
+ --- FIX #7: nano-banana-2.ts (was 2x underpriced, now has resolution tiers) ---
77
+ File: src/definitions/models/nano-banana-2.ts
78
+ Source: https://fal.ai/models/fal-ai/nano-banana-2/llms.txt
79
+ Old: $0.04 per image flat
80
+ New: $0.06 (0.5K), $0.08 (1K), $0.12 (2K), $0.16 (4K) per image
81
+ Web search adds $0.015, high thinking adds $0.002
82
+ Formula: rateMap[resolution] * numImages
83
+ rateMap = { "0.5K": 0.06, "1K": 0.08, "2K": 0.12, "4K": 0.16 }
84
+ Default resolution: "1K"
85
+
86
+ --- FIX #8: elevenlabs.ts (was 1.8x overpriced) ---
87
+ File: src/definitions/models/elevenlabs.ts
88
+ Source: https://elevenlabs.io/pricing/api
89
+ Old: $0.18 per 1,000 characters ($0.00018/char)
90
+ New: $0.10 per 1,000 characters ($0.0001/char) for Multilingual v2/v3
91
+ Flash/Turbo models: $0.05 per 1,000 characters
92
+ Formula: 0.0001 * characters
93
+ Note: Default model is eleven_multilingual_v2, so v2 rate is used.
94
+ Flash/Turbo pricing noted in description for reference.
95
+
96
+ --- FIX #9: ltx-a2v.ts (was flat, now per-megapixel of video data) ---
97
+ File: src/definitions/models/ltx-a2v.ts
98
+ Source: https://fal.ai/models/fal-ai/ltx-2-19b/audio-to-video/llms.txt
99
+ Old: $0.40 flat per generation
100
+ New: $0.0018 per megapixel of generated video data (width x height x frames)
101
+ Example: 121 frames at 1280x720 = ~112 MP = $0.2016
102
+ Formula: Math.ceil((width * height * numFrames) / 1_000_000) * 0.0018
103
+ Default: width=1024, height=768, numFrames=121
104
+
105
+ --- FIX #10: flux.ts (unit changed from per-image to per-megapixel) ---
106
+ File: src/definitions/models/flux.ts
107
+ Source: https://fal.ai/models/fal-ai/flux-pro/v1.1/llms.txt
108
+ Old: $0.05 per image flat
109
+ New: $0.04 per megapixel (rounded up to nearest MP)
110
+ Standard 1MP image (1024x1024) = $0.04
111
+ Formula: 0.04 * Math.ceil((width * height) / 1_000_000) * numImages
112
+ Default: width=1024, height=768 -> 1MP -> $0.04
113
+
114
+ --- FIX #11: whisper.ts / groq (was flat, now per-hour) ---
115
+ File: src/definitions/models/whisper.ts
116
+ Source: https://groq.com/pricing
117
+ Old: $0.03 flat per transcription
118
+ New: $0.111 per hour of audio (minimum 10s billing per request)
119
+ Formula: (Math.max(duration, 10) / 3600) * 0.111
120
+ Default duration: 60 seconds
121
+
122
+ --- FIX #12: whisper.ts / fireworks (was flat, now per-minute) ---
123
+ File: src/definitions/models/whisper.ts
124
+ Source: https://fireworks.ai/pricing
125
+ Old: $0.02 flat per transcription
126
+ New: $0.0015 per audio minute (billed per second)
127
+ Formula: (duration / 60) * 0.0015
128
+ Default duration: 60 seconds
129
+ $0.0015/min = $0.000025/sec = $0.09/hour
130
+
131
+ --- FIX #13: qwen-image-2.ts (minor adjustment) ---
132
+ File: src/definitions/models/qwen-image-2.ts
133
+ Source: https://fal.ai/models/fal-ai/qwen-image-2/text-to-image/llms.txt
134
+ Old: $0.04 per image
135
+ New: $0.035 per image (standard endpoint)
136
+ Pro endpoint: $0.075/image (separate endpoint, not this definition)
137
+ Formula: 0.035 * numImages
138
+
139
+ ================================================================================
140
+ MODELS WITHOUT DEFINITIONS (AI-SDK only, not updated)
141
+ ================================================================================
142
+
143
+ These are registered in src/ai-sdk/providers/ but have no model definition
144
+ files in src/definitions/models/. Verified pricing from llms.txt for reference:
145
+
146
+ Grok Imagine Video (fal):
147
+ - text-to-video: $0.05/sec (480p), $0.07/sec (720p)
148
+ - image-to-video: $0.05/sec (480p) + $0.002/image, $0.07/sec (720p) + $0.002
149
+ - edit-video: $0.06/sec (480p), $0.08/sec (720p) (includes input+output)
150
+
151
+ Sync Lipsync (fal):
152
+ - $0.70 per minute of video processed
153
+
154
+ ElevenLabs Music (elevenlabs):
155
+ - $0.30 per minute of music generated
156
+
157
+ Replicate Flux models:
158
+ - flux-1.1-pro: $0.04/output image
159
+ - flux-dev: $0.025/output image
160
+ - flux-schnell: $0.003/output image ($3/1K images)
161
+
162
+ ================================================================================
163
+ UNVERIFIED MODELS (no public pricing available)
164
+ ================================================================================
165
+
166
+ seedance.ts - PiAPI credit-based pricing, not publicly listed per-second
167
+ heygen.ts - HeyGen API uses pay-as-you-go credits, no public $/sec rate
168
+ soul.ts - Higgsfield has no public pricing page
169
+ sonauto.ts - fal model page returned 404, may be deprecated/moved
170
+ llama.ts - Groq LLM pricing is negligible, current approx is acceptable
171
+
172
+ ================================================================================
173
+ PRICING PARAMS INTERFACE
174
+ ================================================================================
175
+
176
+ The PricingParams interface (src/core/schema/types.ts) already has all fields
177
+ used in the updated calculate functions:
178
+ - duration: number (video/audio seconds)
179
+ - resolution: string (e.g. "480p", "720p", "1080p", "1K", "4K")
180
+ - width: number (image/video width in pixels)
181
+ - height: number (image/video height in pixels)
182
+ - numFrames: number (video frame count)
183
+ - numImages: number (batch image count)
184
+ - characters: number (TTS text length)
185
+
186
+ No changes were needed to the interface.
187
+
188
+ ================================================================================
189
+ TYPE CHECK
190
+ ================================================================================
191
+
192
+ All 13 model definition files pass TypeScript type checking with zero errors.
193
+ Pre-existing errors in ai-sdk provider files (Uint8Array/BlobPart) are unrelated.
194
+
195
+ ================================================================================
196
+ END OF PRICING FIX LOG
197
+ ================================================================================
@@ -47,11 +47,6 @@ export interface FFmpegRunOptions {
47
47
  verbose?: boolean;
48
48
  /** Max execution time in seconds (used by cloud backends like Rendi, ignored by local) */
49
49
  timeoutSeconds?: number;
50
- /** Extra files (e.g. fonts, ASS subtitles) to include alongside inputs.
51
- * When present, cloud backends like Rendi use compressed folder mode
52
- * (input_compressed_folder) to bundle all files together.
53
- * Each entry provides either a `url` to download or raw `data` bytes. */
54
- auxiliaryFiles?: { url?: string; data?: Uint8Array; fileName: string }[];
55
50
  }
56
51
 
57
52
  export type FFmpegOutput =
@@ -1,4 +1,3 @@
1
- import { zipSync } from "fflate";
2
1
  import sharp from "sharp";
3
2
  import { File } from "../../../file";
4
3
  import type { StorageProvider } from "../../../storage/types";
@@ -129,11 +128,6 @@ export class RendiBackend implements FFmpegBackend {
129
128
  }
130
129
 
131
130
  async run(options: FFmpegRunOptions): Promise<FFmpegRunResult> {
132
- // When auxiliary files (e.g. fonts) are present, use compressed folder mode
133
- if (options.auxiliaryFiles && options.auxiliaryFiles.length > 0) {
134
- return this.runWithCompressedFolder(options);
135
- }
136
-
137
131
  let {
138
132
  inputs,
139
133
  filterComplex,
@@ -293,204 +287,6 @@ export class RendiBackend implements FFmpegBackend {
293
287
  throw new Error("Rendi command timed out");
294
288
  }
295
289
 
296
- /**
297
- * Run an FFmpeg command using Rendi's input_compressed_folder mode.
298
- *
299
- * Used when auxiliary files (e.g. fonts for subtitle rendering) need to be
300
- * bundled alongside regular inputs. Creates a ZIP containing all input files
301
- * and auxiliary files, uploads it to storage, and submits to Rendi with
302
- * `input_compressed_folder` instead of `input_files`.
303
- *
304
- * Inside the ZIP, all files are at the root level. The ffmpeg command
305
- * references files by their bare filenames (not placeholders).
306
- */
307
- private async runWithCompressedFolder(
308
- options: FFmpegRunOptions,
309
- ): Promise<FFmpegRunResult> {
310
- const {
311
- inputs,
312
- videoFilter,
313
- filterComplex,
314
- outputArgs = [],
315
- outputPath,
316
- verbose,
317
- auxiliaryFiles = [],
318
- } = options;
319
-
320
- // 1. Resolve all input files to URLs
321
- const inputEntries: { fileName: string; url: string }[] = [];
322
- for (const input of inputs ?? []) {
323
- const path = this.getInputPath(input);
324
- const url = await this.resolvePath(path);
325
- // Extract filename from URL or path
326
- const fileName =
327
- url.split("/").pop()?.split("?")[0] ?? `input_${inputEntries.length}`;
328
- inputEntries.push({ fileName, url });
329
- }
330
-
331
- // 2. Download all files (inputs + auxiliary) into memory
332
- const zipContents: Record<string, Uint8Array> = {};
333
-
334
- const downloadTasks = [
335
- ...inputEntries.map(async (entry) => {
336
- const res = await fetch(entry.url);
337
- if (!res.ok)
338
- throw new Error(
339
- `Failed to download input ${entry.fileName}: ${res.status}`,
340
- );
341
- zipContents[entry.fileName] = new Uint8Array(await res.arrayBuffer());
342
- }),
343
- ...auxiliaryFiles.map(async (file) => {
344
- if (file.data) {
345
- // Inline data — no download needed
346
- zipContents[file.fileName] = file.data;
347
- return;
348
- }
349
- if (!file.url) {
350
- throw new Error(
351
- `Auxiliary file ${file.fileName} has neither url nor data`,
352
- );
353
- }
354
- const res = await fetch(file.url);
355
- if (!res.ok)
356
- throw new Error(
357
- `Failed to download auxiliary file ${file.fileName}: ${res.status}`,
358
- );
359
- zipContents[file.fileName] = new Uint8Array(await res.arrayBuffer());
360
- }),
361
- ];
362
-
363
- await Promise.all(downloadTasks);
364
-
365
- if (verbose) {
366
- const totalSize = Object.values(zipContents).reduce(
367
- (sum, buf) => sum + buf.length,
368
- 0,
369
- );
370
- console.log(
371
- `[rendi] creating ZIP with ${Object.keys(zipContents).length} files (${(totalSize / 1024 / 1024).toFixed(1)} MB)`,
372
- );
373
- }
374
-
375
- // 3. Create ZIP
376
- const zipData = zipSync(zipContents, { level: 1 }); // fast compression
377
-
378
- // 4. Upload ZIP to storage
379
- const zipKey = `internal/rendi-compressed-${Date.now()}.zip`;
380
- const zipUrl = await this.storage.upload(
381
- zipData,
382
- zipKey,
383
- "application/zip",
384
- );
385
-
386
- if (verbose) {
387
- console.log(
388
- `[rendi] uploaded ZIP (${(zipData.length / 1024 / 1024).toFixed(1)} MB) -> ${zipUrl}`,
389
- );
390
- }
391
-
392
- // 5. Build ffmpeg command using bare filenames (not {{in_X}} placeholders)
393
- const inputArgs: string[] = [];
394
- for (const [i, input] of (inputs ?? []).entries()) {
395
- if (typeof input !== "string" && "options" in input && input.options) {
396
- inputArgs.push(...input.options);
397
- }
398
- inputArgs.push("-i", inputEntries[i]!.fileName);
399
- }
400
-
401
- const filterArgs: string[] = [];
402
- if (filterComplex) {
403
- filterArgs.push("-filter_complex", filterComplex);
404
- }
405
- if (videoFilter) {
406
- // For compressed folder mode, the video filter references files by
407
- // their bare filenames (already resolved in the working directory)
408
- filterArgs.push("-vf", videoFilter);
409
- }
410
-
411
- const processedOutputArgs = outputArgs.filter((arg) => arg !== "-y");
412
-
413
- const commandParts = [
414
- ...inputArgs,
415
- ...filterArgs,
416
- ...processedOutputArgs,
417
- "{{out_1}}",
418
- ];
419
- const ffmpegCommand = this.buildCommandString(commandParts);
420
- const outputFilename = outputPath?.split("/").pop() ?? "output.mp4";
421
-
422
- if (verbose) {
423
- console.log("[rendi] input_compressed_folder:", zipUrl);
424
- console.log("[rendi] ffmpeg_command:", ffmpegCommand);
425
- }
426
-
427
- // 6. Submit to Rendi with input_compressed_folder
428
- const submitResponse = await fetch(`${RENDI_API_BASE}/run-ffmpeg-command`, {
429
- method: "POST",
430
- headers: {
431
- "X-API-KEY": this.apiKey,
432
- "Content-Type": "application/json",
433
- },
434
- body: JSON.stringify({
435
- input_compressed_folder: zipUrl,
436
- output_files: { out_1: outputFilename },
437
- ffmpeg_command: ffmpegCommand,
438
- max_command_run_seconds:
439
- options.timeoutSeconds ?? this.maxCommandRunSeconds,
440
- }),
441
- });
442
-
443
- if (!submitResponse.ok) {
444
- const errorText = await submitResponse.text();
445
- throw new Error(
446
- `Rendi submit failed: ${submitResponse.status} - ${errorText}`,
447
- );
448
- }
449
-
450
- const { command_id } =
451
- (await submitResponse.json()) as RendiCommandResponse;
452
-
453
- if (verbose) {
454
- console.log("[rendi] command_id:", command_id);
455
- }
456
-
457
- // 7. Poll for completion (same as standard run)
458
- let attempts = 0;
459
- while (attempts < MAX_POLL_ATTEMPTS) {
460
- const statusResponse = await fetch(
461
- `${RENDI_API_BASE}/commands/${command_id}`,
462
- {
463
- headers: { "X-API-KEY": this.apiKey },
464
- },
465
- );
466
-
467
- if (!statusResponse.ok) {
468
- throw new Error(`Rendi poll failed: ${statusResponse.status}`);
469
- }
470
-
471
- const status = (await statusResponse.json()) as RendiStatusResponse;
472
-
473
- if (status.status === "SUCCESS") {
474
- const outputFile = status.output_files?.out_1;
475
- if (!outputFile?.storage_url) {
476
- throw new Error("Rendi completed but no output URL");
477
- }
478
- return { output: { type: "url", url: outputFile.storage_url } };
479
- }
480
-
481
- if (status.status === "FAILED") {
482
- throw new Error(
483
- `Rendi command failed: ${status.error_message ?? "unknown error"}`,
484
- );
485
- }
486
-
487
- await this.sleep(POLL_INTERVAL_MS);
488
- attempts++;
489
- }
490
-
491
- throw new Error("Rendi command timed out");
492
- }
493
-
494
290
  async resolvePath(input: FilePath): Promise<string> {
495
291
  if (input instanceof File) {
496
292
  return input.upload(this.storage);
@@ -157,7 +157,7 @@ export function Title(props: TitleProps): VargElement<"title"> {
157
157
  return createElement(
158
158
  "title",
159
159
  props as Record<string, unknown>,
160
- props.children,
160
+ props.children ?? props.text,
161
161
  );
162
162
  }
163
163
 
@@ -165,7 +165,7 @@ export function Subtitle(props: SubtitleProps): VargElement<"subtitle"> {
165
165
  return createElement(
166
166
  "subtitle",
167
167
  props as Record<string, unknown>,
168
- props.children,
168
+ props.children ?? props.text,
169
169
  );
170
170
  }
171
171