egaki 0.4.1 → 0.6.0
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/CHANGELOG.md +71 -0
- package/README.md +107 -3
- package/dist/cli.js +540 -130
- package/dist/credentials.d.ts +11 -2
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +68 -5
- package/dist/login.d.ts.map +1 -1
- package/dist/login.js +45 -4
- package/dist/model-catalog.d.ts +96 -3
- package/dist/model-catalog.d.ts.map +1 -1
- package/dist/model-catalog.js +427 -3
- package/dist/models.d.ts +4 -4
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +162 -96
- package/dist/video-model-catalog.d.ts +1 -51
- package/dist/video-model-catalog.d.ts.map +1 -1
- package/dist/video-model-catalog.js +5 -306
- package/dist/xai-auth.d.ts +29 -0
- package/dist/xai-auth.d.ts.map +1 -0
- package/dist/xai-auth.js +325 -0
- package/package.json +5 -4
- package/skills/egaki/SKILL.md +10 -1
- package/src/cli.ts +712 -139
- package/src/credentials.ts +70 -7
- package/src/login.ts +48 -3
- package/src/model-catalog.ts +537 -7
- package/src/models.ts +239 -119
- package/src/video-model-catalog.ts +14 -363
- package/src/xai-auth.ts +396 -0
package/dist/cli.js
CHANGED
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
import { goke } from 'goke';
|
|
11
11
|
import { z } from 'zod';
|
|
12
12
|
import dedent from 'string-dedent';
|
|
13
|
-
import { generateImage as aiGenerateImage, generateText, experimental_generateVideo as aiGenerateVideo, } from 'ai';
|
|
13
|
+
import { APICallError, generateImage as aiGenerateImage, generateText, experimental_generateVideo as aiGenerateVideo, NoImageGeneratedError, NoVideoGeneratedError, RetryError, } from 'ai';
|
|
14
|
+
import { select, isCancel, cancel } from '@clack/prompts';
|
|
14
15
|
import fs from 'node:fs';
|
|
15
16
|
import path from 'node:path';
|
|
16
17
|
import pc from 'picocolors';
|
|
@@ -18,25 +19,18 @@ import { createParser } from 'eventsource-parser';
|
|
|
18
19
|
import pkg from '../package.json' with { type: 'json' };
|
|
19
20
|
import { injectCredentialsToEnv, PROVIDERS, getChatGptAuth, saveChatGptAuth, getKeyStatus, } from './credentials.js';
|
|
20
21
|
import { IMAGE_MODELS, VIDEO_MODELS, DEFAULT_MODEL, DEFAULT_VIDEO_MODEL, getModelConfig, createImageModel, createTextModel, createVideoModel, shouldUseResponsesApi, } from './models.js';
|
|
21
|
-
import { VIDEO_CATALOG } from './
|
|
22
|
+
import { CATALOG, VIDEO_CATALOG, describeProviderValues, } from './model-catalog.js';
|
|
22
23
|
import { loginInteractive, loginNonInteractive, showLoginStatus, removeLogin, readKeyFromStdin, } from './login.js';
|
|
23
24
|
import { subscribeInteractive, subscribeNonInteractive, unsubscribe, showUsage, } from './subscription.js';
|
|
24
25
|
import { getValidChatGptAuth } from './chatgpt-auth.js';
|
|
25
26
|
const cli = goke('egaki');
|
|
26
27
|
process.title = 'egaki';
|
|
27
|
-
// Print clean error output for unhandled rejections (e.g. AI SDK APICallError).
|
|
28
|
-
// The AI SDK errors include request bodies, response headers, and other noisy
|
|
29
|
-
// properties that get dumped by Node's default handler. We strip those and just
|
|
30
|
-
// print the message + stack trace.
|
|
31
28
|
process.on('uncaughtException', (err) => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
console.error(pc.dim(stackOnly.join('\n')));
|
|
38
|
-
}
|
|
39
|
-
}
|
|
29
|
+
printErrorDetails(err);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
});
|
|
32
|
+
process.on('unhandledRejection', (reason) => {
|
|
33
|
+
printErrorDetails(reason);
|
|
40
34
|
process.exit(1);
|
|
41
35
|
});
|
|
42
36
|
// ─── login command ───────────────────────────────────────────────────────────
|
|
@@ -75,8 +69,8 @@ cli
|
|
|
75
69
|
}
|
|
76
70
|
// Non-interactive: --provider + --key or stdin
|
|
77
71
|
if (options.provider) {
|
|
78
|
-
//
|
|
79
|
-
if (options.provider === 'chatgpt') {
|
|
72
|
+
// OAuth providers use browser flow — skip key reading
|
|
73
|
+
if (options.provider === 'chatgpt' || options.provider === 'xai-oauth') {
|
|
80
74
|
await loginNonInteractive({ provider: options.provider, key: '' });
|
|
81
75
|
return;
|
|
82
76
|
}
|
|
@@ -144,7 +138,7 @@ cli
|
|
|
144
138
|
from the model ID: imagen-* uses the image API, everything else uses
|
|
145
139
|
the text API with image output enabled.
|
|
146
140
|
`)
|
|
147
|
-
.option('-m, --model [model]', z.
|
|
141
|
+
.option('-m, --model [model]', z.string().describe('Model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'))
|
|
148
142
|
.option('-o, --output [path]', z
|
|
149
143
|
.string()
|
|
150
144
|
.default('egaki-output.png')
|
|
@@ -163,6 +157,18 @@ cli
|
|
|
163
157
|
.option('--mask [file]', z
|
|
164
158
|
.string()
|
|
165
159
|
.describe('Mask image for inpainting. Accepts a local file path or URL (http/https). White areas in the mask are replaced with generated content. Used together with --input'))
|
|
160
|
+
.option('--quality [level]', z
|
|
161
|
+
.string()
|
|
162
|
+
.describe(`Image quality level. ${describeProviderValues('quality', [CATALOG])}`))
|
|
163
|
+
.option('--resolution [res]', z
|
|
164
|
+
.string()
|
|
165
|
+
.describe(`Output resolution. ${describeProviderValues('resolution', [CATALOG])}`))
|
|
166
|
+
.option('--output-format [format]', z
|
|
167
|
+
.string()
|
|
168
|
+
.describe(`Output image format. ${describeProviderValues('output-format', [CATALOG])}`))
|
|
169
|
+
.option('--negative-prompt [text]', z
|
|
170
|
+
.string()
|
|
171
|
+
.describe('Describe what to avoid in the generated image (Fal models)'))
|
|
166
172
|
.option('--allow-people', 'Allow generating images of people (Imagen blocks people by default)')
|
|
167
173
|
.option('--json', 'Output result metadata as JSON to stdout (model, usage, warnings, file paths)')
|
|
168
174
|
.option('--stdout', 'Write raw image bytes to stdout instead of saving to a file. Useful for piping to other tools')
|
|
@@ -187,10 +193,7 @@ cli
|
|
|
187
193
|
.action(async (prompt, options) => {
|
|
188
194
|
// Inject stored API keys as env vars before calling the AI SDK.
|
|
189
195
|
injectCredentialsToEnv();
|
|
190
|
-
|
|
191
|
-
// options.model / options.output / options.count as `T | undefined` even
|
|
192
|
-
// though the runtime always resolves them. Apply the same defaults here.
|
|
193
|
-
const model = options.model ?? DEFAULT_MODEL;
|
|
196
|
+
const model = options.model ?? await resolveImageModel();
|
|
194
197
|
const outputPath = options.output ?? 'egaki-output.png';
|
|
195
198
|
const count = options.count ?? 1;
|
|
196
199
|
const config = getModelConfig(model);
|
|
@@ -235,6 +238,10 @@ cli
|
|
|
235
238
|
inputImages,
|
|
236
239
|
maskImage,
|
|
237
240
|
allowPeople: options.allowPeople || false,
|
|
241
|
+
quality: options.quality,
|
|
242
|
+
resolution: options.resolution,
|
|
243
|
+
outputFormat: options.outputFormat,
|
|
244
|
+
negativePrompt: options.negativePrompt,
|
|
238
245
|
json: options.json || false,
|
|
239
246
|
stdout: options.stdout || false,
|
|
240
247
|
});
|
|
@@ -261,7 +268,7 @@ cli
|
|
|
261
268
|
Agent note: video generation can be slow. When invoking this command from
|
|
262
269
|
automation, use a command timeout of at least 5 minutes.
|
|
263
270
|
`)
|
|
264
|
-
.option('-m, --model [model]', z.
|
|
271
|
+
.option('-m, --model [model]', z.string().describe('Video model ID for generation. If omitted, shows an interactive picker (or uses default in non-TTY mode)'))
|
|
265
272
|
.option('-o, --output [path]', z
|
|
266
273
|
.string()
|
|
267
274
|
.default('egaki-output.mp4')
|
|
@@ -270,13 +277,22 @@ cli
|
|
|
270
277
|
.option('--aspect-ratio [ratio]', z.string().describe('Video aspect ratio in WIDTH:HEIGHT format (e.g. 16:9, 9:16)'))
|
|
271
278
|
.option('--resolution [resolution]', z
|
|
272
279
|
.string()
|
|
273
|
-
.describe(
|
|
274
|
-
.option('--duration [seconds]', z.number().describe('Video duration in seconds
|
|
280
|
+
.describe(`Video resolution. ${describeProviderValues('resolution', [VIDEO_CATALOG])}. Google: 720p, 1080p, 4k`))
|
|
281
|
+
.option('--duration [seconds]', z.number().describe('Video duration in seconds. Veo: 4-8s. xAI Grok: 1-15s. Kling: 5-10s. Wan: 2-15s'))
|
|
275
282
|
.option('--fps [fps]', z.number().describe('Frames per second for video models that support fps override'))
|
|
276
283
|
.option('--seed [seed]', z.number().describe('Seed for reproducible video generation (model support varies)'))
|
|
277
284
|
.option('-i, --input [file]', z
|
|
278
285
|
.string()
|
|
279
|
-
.describe('
|
|
286
|
+
.describe('Input file for video generation. For i2v: a reference image. For edit-video/extend-video: a source video. Accepts local file paths or URLs'))
|
|
287
|
+
.option('--mode [mode]', z
|
|
288
|
+
.enum(['edit-video', 'extend-video', 'reference-to-video'])
|
|
289
|
+
.describe(`Video operation mode. ${describeProviderValues('mode', [VIDEO_CATALOG])}`))
|
|
290
|
+
.option('--reference-images [path]', z
|
|
291
|
+
.array(z.string())
|
|
292
|
+
.describe('Reference images for R2V generation, repeatable, 1-7 files or URLs (xAI)'))
|
|
293
|
+
.option('--negative-prompt [text]', z
|
|
294
|
+
.string()
|
|
295
|
+
.describe('Describe what to avoid in the generated video (Fal models)'))
|
|
280
296
|
.option('--json', 'Output result metadata as JSON to stdout (model, usage, warnings, file paths)')
|
|
281
297
|
.option('--stdout', 'Write raw video bytes to stdout instead of saving to a file. Useful for piping to other tools')
|
|
282
298
|
.example('# Generate a video')
|
|
@@ -291,8 +307,7 @@ cli
|
|
|
291
307
|
.example('egaki video "waves crashing on cliffs" -n 2 -o waves.mp4')
|
|
292
308
|
.action(async (prompt, options) => {
|
|
293
309
|
injectCredentialsToEnv();
|
|
294
|
-
|
|
295
|
-
const model = options.model ?? DEFAULT_VIDEO_MODEL;
|
|
310
|
+
const model = options.model ?? await resolveVideoModel();
|
|
296
311
|
const outputPath = options.output ?? 'egaki-output.mp4';
|
|
297
312
|
const count = options.count ?? 1;
|
|
298
313
|
const config = getModelConfig(model);
|
|
@@ -305,9 +320,34 @@ cli
|
|
|
305
320
|
console.error(pc.dim(`Prompt: ${prompt}`));
|
|
306
321
|
console.error(pc.dim('Mode: Video API (experimental_generateVideo)'));
|
|
307
322
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
323
|
+
// Determine how --input is used based on --mode:
|
|
324
|
+
// - edit-video / extend-video: input is a source video, passed as videoUrl (needs URL)
|
|
325
|
+
// - everything else (i2v, t2v): input is a reference image, passed as bytes
|
|
326
|
+
const isVideoInputMode = options.mode === 'edit-video' || options.mode === 'extend-video';
|
|
327
|
+
// Validate mode-specific required inputs
|
|
328
|
+
if (isVideoInputMode && !options.input) {
|
|
329
|
+
console.error(pc.red(`--input is required with --mode ${options.mode}. Pass a local video file or URL.`));
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
if (options.mode === 'reference-to-video' && (!options.referenceImages || options.referenceImages.length === 0)) {
|
|
333
|
+
console.error(pc.red('--reference-images is required with --mode reference-to-video. Pass 1-7 image files or URLs.'));
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
let inputImage;
|
|
337
|
+
let videoUrl;
|
|
338
|
+
if (options.input && isVideoInputMode) {
|
|
339
|
+
// Upload local file or use URL directly for video editing/extension
|
|
340
|
+
videoUrl = await resolveToUrl(options.input);
|
|
341
|
+
}
|
|
342
|
+
else if (options.input) {
|
|
343
|
+
// Read as bytes for image-to-video
|
|
344
|
+
inputImage = await readInputSource(options.input);
|
|
345
|
+
}
|
|
346
|
+
// Resolve --reference-images: upload local files to get URLs
|
|
347
|
+
let resolvedReferenceImages = options.referenceImages;
|
|
348
|
+
if (resolvedReferenceImages) {
|
|
349
|
+
resolvedReferenceImages = await Promise.all(resolvedReferenceImages.map((ref) => resolveToUrl(ref)));
|
|
350
|
+
}
|
|
311
351
|
await generateWithVideoModel({
|
|
312
352
|
prompt,
|
|
313
353
|
model,
|
|
@@ -319,6 +359,10 @@ cli
|
|
|
319
359
|
fps: options.fps,
|
|
320
360
|
seed: options.seed,
|
|
321
361
|
inputImage,
|
|
362
|
+
mode: options.mode,
|
|
363
|
+
videoUrl,
|
|
364
|
+
referenceImages: resolvedReferenceImages,
|
|
365
|
+
negativePrompt: options.negativePrompt,
|
|
322
366
|
json: options.json || false,
|
|
323
367
|
stdout: options.stdout || false,
|
|
324
368
|
});
|
|
@@ -336,13 +380,12 @@ cli
|
|
|
336
380
|
.describe('Filter by model type: image (image+text-image), video, or all'))
|
|
337
381
|
.option('--json', 'Output as JSON instead of YAML')
|
|
338
382
|
.action(async (options) => {
|
|
339
|
-
const { CATALOG } = await import('./model-catalog.js');
|
|
340
383
|
const yaml = await import('js-yaml');
|
|
341
384
|
const providerStatuses = Object.fromEntries(Object.keys(PROVIDERS).map((provider) => [provider, getKeyStatus(provider)]));
|
|
342
385
|
let models = options.type === 'video'
|
|
343
|
-
? VIDEO_CATALOG
|
|
386
|
+
? [...VIDEO_CATALOG]
|
|
344
387
|
: options.type === 'image'
|
|
345
|
-
? CATALOG
|
|
388
|
+
? [...CATALOG]
|
|
346
389
|
: [...CATALOG, ...VIDEO_CATALOG];
|
|
347
390
|
if (options.provider) {
|
|
348
391
|
models = models.filter((m) => m.provider === options.provider);
|
|
@@ -389,12 +432,269 @@ cli
|
|
|
389
432
|
console.log(yaml.dump(output, { lineWidth: 120, noRefs: true }));
|
|
390
433
|
}
|
|
391
434
|
});
|
|
435
|
+
/** Upload gateway base URL for temporary file uploads. */
|
|
436
|
+
const UPLOAD_GATEWAY_BASE = 'https://egaki.org';
|
|
437
|
+
/** Max upload size in bytes (must match gateway limit). */
|
|
438
|
+
const MAX_UPLOAD_SIZE = 100 * 1024 * 1024;
|
|
392
439
|
cli.help();
|
|
393
440
|
cli.version(pkg.version);
|
|
394
|
-
cli.parse();
|
|
441
|
+
await cli.parse();
|
|
442
|
+
async function runProviderCall(operation, call) {
|
|
443
|
+
const result = await call().catch((error) => {
|
|
444
|
+
printErrorDetails(error, operation);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
});
|
|
447
|
+
return result;
|
|
448
|
+
}
|
|
449
|
+
function printErrorDetails(error, operation) {
|
|
450
|
+
const prefix = operation ? `${operation} failed` : 'Command failed';
|
|
451
|
+
console.error(pc.red(pc.bold(prefix)));
|
|
452
|
+
if (APICallError.isInstance(error)) {
|
|
453
|
+
printKeyValue('name', error.name);
|
|
454
|
+
printKeyValue('message', error.message);
|
|
455
|
+
printKeyValue('statusCode', error.statusCode);
|
|
456
|
+
printKeyValue('url', error.url);
|
|
457
|
+
printKeyValue('isRetryable', error.isRetryable);
|
|
458
|
+
printJsonBlock('responseBody', parseJsonLike(error.responseBody));
|
|
459
|
+
printJsonBlock('responseHeaders', error.responseHeaders);
|
|
460
|
+
printJsonBlock('requestBodyValues', error.requestBodyValues);
|
|
461
|
+
printJsonBlock('data', error.data);
|
|
462
|
+
printCause(error.cause);
|
|
463
|
+
printStack(error);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
if (NoImageGeneratedError.isInstance(error) || NoVideoGeneratedError.isInstance(error)) {
|
|
467
|
+
printKeyValue('name', error.name);
|
|
468
|
+
printKeyValue('message', error.message);
|
|
469
|
+
printJsonBlock('responses', error.responses);
|
|
470
|
+
printCause(error.cause);
|
|
471
|
+
printStack(error);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
if (RetryError.isInstance(error)) {
|
|
475
|
+
printKeyValue('name', error.name);
|
|
476
|
+
printKeyValue('message', error.message);
|
|
477
|
+
printKeyValue('reason', error.reason);
|
|
478
|
+
printJsonBlock('errors', error.errors.map((e) => serializeUnknown(e)));
|
|
479
|
+
printCause(error.cause);
|
|
480
|
+
printStack(error);
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
if (error instanceof Error) {
|
|
484
|
+
printKeyValue('name', error.name);
|
|
485
|
+
printKeyValue('message', error.message);
|
|
486
|
+
printJsonBlock('details', serializeOwnProperties(error));
|
|
487
|
+
printCause(error.cause);
|
|
488
|
+
printStack(error);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
printJsonBlock('error', serializeUnknown(error));
|
|
492
|
+
}
|
|
493
|
+
function printKeyValue(key, value) {
|
|
494
|
+
if (value === undefined)
|
|
495
|
+
return;
|
|
496
|
+
console.error(`${pc.dim(`${key}:`)} ${String(value)}`);
|
|
497
|
+
}
|
|
498
|
+
function printJsonBlock(label, value) {
|
|
499
|
+
if (value === undefined)
|
|
500
|
+
return;
|
|
501
|
+
const serialized = stableStringify(value);
|
|
502
|
+
if (!serialized || serialized === '{}')
|
|
503
|
+
return;
|
|
504
|
+
console.error(pc.dim(`${label}:`));
|
|
505
|
+
console.error(serialized);
|
|
506
|
+
}
|
|
507
|
+
function printCause(cause) {
|
|
508
|
+
if (cause === undefined)
|
|
509
|
+
return;
|
|
510
|
+
console.error(pc.dim('cause:'));
|
|
511
|
+
console.error(stableStringify(serializeUnknown(cause)));
|
|
512
|
+
}
|
|
513
|
+
function printStack(error) {
|
|
514
|
+
if (!error.stack)
|
|
515
|
+
return;
|
|
516
|
+
console.error(pc.dim('stack:'));
|
|
517
|
+
console.error(pc.dim(error.stack));
|
|
518
|
+
}
|
|
519
|
+
function parseJsonLike(value) {
|
|
520
|
+
if (typeof value !== 'string')
|
|
521
|
+
return value;
|
|
522
|
+
try {
|
|
523
|
+
return JSON.parse(value);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function serializeUnknown(value) {
|
|
530
|
+
if (value instanceof Error) {
|
|
531
|
+
return {
|
|
532
|
+
name: value.name,
|
|
533
|
+
message: value.message,
|
|
534
|
+
...serializeOwnProperties(value),
|
|
535
|
+
...(value.cause === undefined ? {} : { cause: serializeUnknown(value.cause) }),
|
|
536
|
+
...(value.stack ? { stack: value.stack } : {}),
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
return value;
|
|
540
|
+
}
|
|
541
|
+
function serializeOwnProperties(error) {
|
|
542
|
+
return Object.fromEntries(Object.entries(error).map(([key, value]) => [key, serializeUnknown(value)]));
|
|
543
|
+
}
|
|
544
|
+
function stableStringify(value) {
|
|
545
|
+
const seen = new WeakSet();
|
|
546
|
+
return JSON.stringify(value, (_key, item) => {
|
|
547
|
+
if (typeof item === 'bigint')
|
|
548
|
+
return item.toString();
|
|
549
|
+
if (item instanceof Date)
|
|
550
|
+
return item.toISOString();
|
|
551
|
+
if (typeof item !== 'object' || item === null)
|
|
552
|
+
return item;
|
|
553
|
+
if (seen.has(item))
|
|
554
|
+
return '[Circular]';
|
|
555
|
+
seen.add(item);
|
|
556
|
+
return item;
|
|
557
|
+
}, 2);
|
|
558
|
+
}
|
|
559
|
+
// ─── interactive model pickers ──────────────────────────────────────────────
|
|
560
|
+
/** Curated image models shown in the interactive picker, ordered by recommendation. */
|
|
561
|
+
const FEATURED_IMAGE_MODELS = [
|
|
562
|
+
'imagen-4.0-generate-001',
|
|
563
|
+
'imagen-4.0-ultra-generate-001',
|
|
564
|
+
'imagen-4.0-fast-generate-001',
|
|
565
|
+
'gemini-2.5-flash-image',
|
|
566
|
+
'gemini-3.1-flash-image-preview',
|
|
567
|
+
'nano-banana-pro-preview',
|
|
568
|
+
'gpt-image-2',
|
|
569
|
+
'gpt-image-1.5',
|
|
570
|
+
'chatgpt-image-latest',
|
|
571
|
+
'flux-kontext-pro',
|
|
572
|
+
'flux-kontext-max',
|
|
573
|
+
'flux-pro-1.1-ultra',
|
|
574
|
+
'recraft-v4',
|
|
575
|
+
'dall-e-3',
|
|
576
|
+
'grok-imagine-image-pro',
|
|
577
|
+
];
|
|
578
|
+
/**
|
|
579
|
+
* Resolve the image model: show interactive picker in TTY, use default otherwise.
|
|
580
|
+
*/
|
|
581
|
+
async function resolveImageModel() {
|
|
582
|
+
const isTTY = process.stdin.isTTY && process.stderr.isTTY;
|
|
583
|
+
if (!isTTY)
|
|
584
|
+
return DEFAULT_MODEL;
|
|
585
|
+
const options = FEATURED_IMAGE_MODELS
|
|
586
|
+
.map((id) => CATALOG.find((m) => m.id === id))
|
|
587
|
+
.filter(Boolean)
|
|
588
|
+
.map((m) => {
|
|
589
|
+
const cost = m.cost.type === 'per-image'
|
|
590
|
+
? pc.dim(`$${m.cost.perImage}/img`)
|
|
591
|
+
: m.cost.type === 'per-token'
|
|
592
|
+
? pc.dim(`$${m.cost.outputPerM}/M out`)
|
|
593
|
+
: '';
|
|
594
|
+
return {
|
|
595
|
+
value: m.id,
|
|
596
|
+
label: `${m.name} ${cost}`,
|
|
597
|
+
hint: m.id,
|
|
598
|
+
};
|
|
599
|
+
});
|
|
600
|
+
const selected = await select({
|
|
601
|
+
message: 'Select an image model',
|
|
602
|
+
options,
|
|
603
|
+
});
|
|
604
|
+
if (isCancel(selected)) {
|
|
605
|
+
cancel('Cancelled.');
|
|
606
|
+
process.exit(0);
|
|
607
|
+
}
|
|
608
|
+
return selected;
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* Resolve the video model: show interactive picker in TTY, use default otherwise.
|
|
612
|
+
*/
|
|
613
|
+
async function resolveVideoModel() {
|
|
614
|
+
const isTTY = process.stdin.isTTY && process.stderr.isTTY;
|
|
615
|
+
if (!isTTY)
|
|
616
|
+
return DEFAULT_VIDEO_MODEL;
|
|
617
|
+
const options = VIDEO_CATALOG.map((m) => {
|
|
618
|
+
const dur = m.features.durationRangeSec
|
|
619
|
+
? pc.dim(`${m.features.durationRangeSec.min}-${m.features.durationRangeSec.max}s`)
|
|
620
|
+
: '';
|
|
621
|
+
return {
|
|
622
|
+
value: m.id,
|
|
623
|
+
label: `${m.name} ${dur}`,
|
|
624
|
+
hint: m.id,
|
|
625
|
+
};
|
|
626
|
+
});
|
|
627
|
+
const selected = await select({
|
|
628
|
+
message: 'Select a video model',
|
|
629
|
+
options,
|
|
630
|
+
});
|
|
631
|
+
if (isCancel(selected)) {
|
|
632
|
+
cancel('Cancelled.');
|
|
633
|
+
process.exit(0);
|
|
634
|
+
}
|
|
635
|
+
return selected;
|
|
636
|
+
}
|
|
395
637
|
function isUrl(input) {
|
|
396
638
|
return /^https?:\/\//i.test(input);
|
|
397
639
|
}
|
|
640
|
+
function mimeTypeFromExtension(ext) {
|
|
641
|
+
const map = {
|
|
642
|
+
'.mp4': 'video/mp4',
|
|
643
|
+
'.webm': 'video/webm',
|
|
644
|
+
'.mov': 'video/quicktime',
|
|
645
|
+
'.avi': 'video/x-msvideo',
|
|
646
|
+
'.png': 'image/png',
|
|
647
|
+
'.jpg': 'image/jpeg',
|
|
648
|
+
'.jpeg': 'image/jpeg',
|
|
649
|
+
'.webp': 'image/webp',
|
|
650
|
+
'.gif': 'image/gif',
|
|
651
|
+
};
|
|
652
|
+
return map[ext.toLowerCase()] || 'application/octet-stream';
|
|
653
|
+
}
|
|
654
|
+
/**
|
|
655
|
+
* Upload a local file to the egaki gateway and return the public URL.
|
|
656
|
+
* Used when a flag that expects a URL receives a local file path instead.
|
|
657
|
+
*/
|
|
658
|
+
async function uploadFileToGateway(filePath) {
|
|
659
|
+
const resolved = path.resolve(filePath);
|
|
660
|
+
if (!fs.existsSync(resolved)) {
|
|
661
|
+
console.error(pc.red(`File not found: ${resolved}`));
|
|
662
|
+
process.exit(1);
|
|
663
|
+
}
|
|
664
|
+
const stat = fs.statSync(resolved);
|
|
665
|
+
if (stat.size > MAX_UPLOAD_SIZE) {
|
|
666
|
+
console.error(pc.red(`File too large: ${resolved} (${Math.round(stat.size / 1024 / 1024)}MB). Maximum is ${MAX_UPLOAD_SIZE / 1024 / 1024}MB`));
|
|
667
|
+
process.exit(1);
|
|
668
|
+
}
|
|
669
|
+
const ext = path.extname(resolved);
|
|
670
|
+
const contentType = mimeTypeFromExtension(ext);
|
|
671
|
+
const fileBytes = fs.readFileSync(resolved);
|
|
672
|
+
console.error(pc.dim(`Uploading ${filePath}...`));
|
|
673
|
+
const response = await fetch(`${UPLOAD_GATEWAY_BASE}/upload`, {
|
|
674
|
+
method: 'POST',
|
|
675
|
+
headers: { 'Content-Type': contentType },
|
|
676
|
+
body: fileBytes,
|
|
677
|
+
});
|
|
678
|
+
if (!response.ok) {
|
|
679
|
+
const body = await response.text().catch(() => '');
|
|
680
|
+
console.error(pc.red(`Upload failed: ${response.status} ${response.statusText}`));
|
|
681
|
+
if (body)
|
|
682
|
+
console.error(pc.dim(body));
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
const result = await response.json();
|
|
686
|
+
console.error(pc.dim(`Uploaded: ${result.url}`));
|
|
687
|
+
return result.url;
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Resolve a value that should be a URL: if it's already a URL, return as-is.
|
|
691
|
+
* If it's a local file path, upload it to the gateway and return the public URL.
|
|
692
|
+
*/
|
|
693
|
+
async function resolveToUrl(input) {
|
|
694
|
+
if (isUrl(input))
|
|
695
|
+
return input;
|
|
696
|
+
return uploadFileToGateway(input);
|
|
697
|
+
}
|
|
398
698
|
async function readInputSource(input) {
|
|
399
699
|
if (isUrl(input)) {
|
|
400
700
|
console.error(pc.dim(`Fetching ${input}...`));
|
|
@@ -418,72 +718,159 @@ async function readInputImages(inputs) {
|
|
|
418
718
|
}
|
|
419
719
|
return Promise.all(inputs.map((f) => readInputSource(f)));
|
|
420
720
|
}
|
|
721
|
+
function buildImageProviderOptions(provider, opts) {
|
|
722
|
+
switch (provider) {
|
|
723
|
+
case 'google': {
|
|
724
|
+
const googleOpts = {
|
|
725
|
+
...(opts.allowPeople ? { personGeneration: 'allow_all' } : {}),
|
|
726
|
+
...(opts.aspectRatio ? { aspectRatio: opts.aspectRatio } : {}),
|
|
727
|
+
};
|
|
728
|
+
return { google: googleOpts };
|
|
729
|
+
}
|
|
730
|
+
case 'vertex': {
|
|
731
|
+
const vertexOpts = {
|
|
732
|
+
...(opts.allowPeople ? { personGeneration: 'allow_all' } : {}),
|
|
733
|
+
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
734
|
+
};
|
|
735
|
+
return { vertex: vertexOpts };
|
|
736
|
+
}
|
|
737
|
+
case 'xai': {
|
|
738
|
+
const xaiOpts = {
|
|
739
|
+
...(opts.quality ? { quality: opts.quality } : {}),
|
|
740
|
+
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
741
|
+
...(opts.outputFormat ? { output_format: opts.outputFormat } : {}),
|
|
742
|
+
};
|
|
743
|
+
return { xai: xaiOpts };
|
|
744
|
+
}
|
|
745
|
+
case 'openai': {
|
|
746
|
+
const openaiOpts = {
|
|
747
|
+
...(opts.quality ? { quality: opts.quality } : {}),
|
|
748
|
+
...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
|
|
749
|
+
};
|
|
750
|
+
return { openai: openaiOpts };
|
|
751
|
+
}
|
|
752
|
+
case 'fal': {
|
|
753
|
+
const falOpts = {
|
|
754
|
+
...(opts.outputFormat ? { outputFormat: opts.outputFormat } : {}),
|
|
755
|
+
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
756
|
+
};
|
|
757
|
+
return { fal: falOpts };
|
|
758
|
+
}
|
|
759
|
+
default:
|
|
760
|
+
// Providers routed through the gateway (bfl, recraft, etc.) pass options
|
|
761
|
+
// as-is since they don't have local SDK types.
|
|
762
|
+
return {};
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
function buildVideoProviderOptions(provider, opts) {
|
|
766
|
+
switch (provider) {
|
|
767
|
+
case 'xai': {
|
|
768
|
+
// xAI video modes are a discriminated union; build the correct variant.
|
|
769
|
+
const base = {
|
|
770
|
+
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
771
|
+
};
|
|
772
|
+
if (opts.mode === 'edit-video' && opts.videoUrl) {
|
|
773
|
+
const xaiOpts = { ...base, mode: 'edit-video', videoUrl: opts.videoUrl };
|
|
774
|
+
return { xai: xaiOpts };
|
|
775
|
+
}
|
|
776
|
+
if (opts.mode === 'extend-video' && opts.videoUrl) {
|
|
777
|
+
const xaiOpts = { ...base, mode: 'extend-video', videoUrl: opts.videoUrl };
|
|
778
|
+
return { xai: xaiOpts };
|
|
779
|
+
}
|
|
780
|
+
if (opts.mode === 'reference-to-video' && opts.referenceImages) {
|
|
781
|
+
const xaiOpts = { ...base, mode: 'reference-to-video', referenceImageUrls: opts.referenceImages };
|
|
782
|
+
return { xai: xaiOpts };
|
|
783
|
+
}
|
|
784
|
+
// Default: text-to-video or image-to-video (no mode)
|
|
785
|
+
if (Object.keys(base).length === 0)
|
|
786
|
+
return undefined;
|
|
787
|
+
const xaiOpts = { ...base };
|
|
788
|
+
return { xai: xaiOpts };
|
|
789
|
+
}
|
|
790
|
+
case 'fal': {
|
|
791
|
+
const falOpts = {
|
|
792
|
+
...(opts.resolution ? { resolution: opts.resolution } : {}),
|
|
793
|
+
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
794
|
+
};
|
|
795
|
+
if (Object.keys(falOpts).length === 0)
|
|
796
|
+
return undefined;
|
|
797
|
+
return { fal: falOpts };
|
|
798
|
+
}
|
|
799
|
+
case 'google': {
|
|
800
|
+
const googleOpts = {
|
|
801
|
+
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
802
|
+
};
|
|
803
|
+
if (Object.keys(googleOpts).length === 0)
|
|
804
|
+
return undefined;
|
|
805
|
+
return { google: googleOpts };
|
|
806
|
+
}
|
|
807
|
+
case 'vertex': {
|
|
808
|
+
const vertexOpts = {
|
|
809
|
+
...(opts.negativePrompt ? { negativePrompt: opts.negativePrompt } : {}),
|
|
810
|
+
};
|
|
811
|
+
if (Object.keys(vertexOpts).length === 0)
|
|
812
|
+
return undefined;
|
|
813
|
+
return { vertex: vertexOpts };
|
|
814
|
+
}
|
|
815
|
+
default:
|
|
816
|
+
return undefined;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
421
819
|
// Generate using the dedicated generateImage API (Imagen models)
|
|
422
|
-
async function generateWithImageModel({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, allowPeople, json, stdout, }) {
|
|
820
|
+
async function generateWithImageModel({ prompt, model, outputPath, count, aspectRatio, seed, inputImages, maskImage, allowPeople, quality, resolution, outputFormat, negativePrompt, json, stdout, }) {
|
|
423
821
|
// Build the prompt: plain string or multimodal with reference images
|
|
424
822
|
const imagePrompt = inputImages.length > 0
|
|
425
823
|
? { text: prompt, images: inputImages, ...(maskImage ? { mask: maskImage } : {}) }
|
|
426
824
|
: prompt;
|
|
427
825
|
const config = getModelConfig(model);
|
|
428
826
|
const imageModel = await createImageModel(model);
|
|
429
|
-
//
|
|
430
|
-
|
|
827
|
+
// Build type-safe provider options using the SDK types with `satisfies`.
|
|
828
|
+
// Each provider has its own shape; we switch on provider to get compile-time safety.
|
|
829
|
+
const providerOptions = buildImageProviderOptions(config.provider, {
|
|
830
|
+
aspectRatio,
|
|
831
|
+
allowPeople,
|
|
832
|
+
quality,
|
|
833
|
+
resolution,
|
|
834
|
+
outputFormat,
|
|
835
|
+
negativePrompt,
|
|
836
|
+
});
|
|
431
837
|
if (!stdout) {
|
|
432
838
|
console.error(pc.cyan('Generating...'));
|
|
433
839
|
}
|
|
434
|
-
const result = await aiGenerateImage({
|
|
840
|
+
const result = await runProviderCall('Image generation', () => aiGenerateImage({
|
|
435
841
|
model: imageModel,
|
|
436
842
|
prompt: imagePrompt,
|
|
437
843
|
n: count,
|
|
438
844
|
...(aspectRatio ? { aspectRatio } : {}),
|
|
439
845
|
...(seed !== undefined ? { seed } : {}),
|
|
440
|
-
providerOptions
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
});
|
|
846
|
+
providerOptions,
|
|
847
|
+
}));
|
|
848
|
+
const files = result.images.map((img) => ({
|
|
849
|
+
uint8Array: img.uint8Array,
|
|
850
|
+
mediaType: img.mediaType,
|
|
851
|
+
}));
|
|
447
852
|
if (stdout) {
|
|
448
|
-
|
|
449
|
-
const image = result.images[0];
|
|
450
|
-
if (image) {
|
|
451
|
-
process.stdout.write(Buffer.from(image.uint8Array));
|
|
452
|
-
}
|
|
853
|
+
writeFirstToStdout(files);
|
|
453
854
|
return;
|
|
454
855
|
}
|
|
455
|
-
const savedFiles =
|
|
456
|
-
for (let i = 0; i < result.images.length; i++) {
|
|
457
|
-
const image = result.images[i];
|
|
458
|
-
const ext = extensionFromMediaType(image.mediaType);
|
|
459
|
-
const filePath = result.images.length === 1
|
|
460
|
-
? ensureExtension(outputPath, ext)
|
|
461
|
-
: insertIndex(outputPath, i, ext);
|
|
462
|
-
fs.writeFileSync(filePath, image.uint8Array);
|
|
463
|
-
console.error(pc.green(`Saved: ${filePath}`));
|
|
464
|
-
savedFiles.push(filePath);
|
|
465
|
-
}
|
|
856
|
+
const savedFiles = saveGeneratedFiles(files, outputPath);
|
|
466
857
|
const cost = calculateCost(config.cost, result.usage, result.images.length);
|
|
467
|
-
|
|
468
|
-
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
469
|
-
}
|
|
858
|
+
printCost(cost);
|
|
470
859
|
if (json) {
|
|
471
|
-
|
|
860
|
+
console.log(JSON.stringify({
|
|
472
861
|
model,
|
|
473
862
|
files: savedFiles,
|
|
474
863
|
count: result.images.length,
|
|
475
864
|
cost,
|
|
476
865
|
usage: result.usage,
|
|
477
866
|
warnings: result.warnings,
|
|
478
|
-
};
|
|
479
|
-
console.log(JSON.stringify(output, null, 2));
|
|
867
|
+
}, null, 2));
|
|
480
868
|
}
|
|
481
869
|
}
|
|
482
870
|
// Generate using generateText with responseModalities (Gemini multimodal models)
|
|
483
871
|
async function generateWithTextModel({ prompt, model, outputPath, inputImages, imageSize, aspectRatio, json, stdout, }) {
|
|
484
872
|
const textModel = await createTextModel(model);
|
|
485
873
|
const config = getModelConfig(model);
|
|
486
|
-
const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google';
|
|
487
874
|
if (!stdout) {
|
|
488
875
|
console.error(pc.cyan('Generating...'));
|
|
489
876
|
}
|
|
@@ -502,65 +889,49 @@ async function generateWithTextModel({ prompt, model, outputPath, inputImages, i
|
|
|
502
889
|
},
|
|
503
890
|
]
|
|
504
891
|
: undefined;
|
|
505
|
-
const
|
|
892
|
+
const googleOpts = {
|
|
893
|
+
responseModalities: ['TEXT', 'IMAGE'],
|
|
894
|
+
...(imageSize || aspectRatio
|
|
895
|
+
? {
|
|
896
|
+
imageConfig: {
|
|
897
|
+
...(imageSize ? { imageSize } : {}),
|
|
898
|
+
...(aspectRatio ? { aspectRatio: aspectRatio } : {}),
|
|
899
|
+
},
|
|
900
|
+
}
|
|
901
|
+
: {}),
|
|
902
|
+
};
|
|
903
|
+
const providerOptionsKey = config.provider === 'vertex' ? 'vertex' : 'google';
|
|
904
|
+
const result = await runProviderCall('Text/image generation', () => generateText({
|
|
506
905
|
model: textModel,
|
|
507
906
|
...(messages ? { messages } : { prompt }),
|
|
508
907
|
providerOptions: {
|
|
509
|
-
[providerOptionsKey]:
|
|
510
|
-
responseModalities: ['TEXT', 'IMAGE'],
|
|
511
|
-
...(imageSize || aspectRatio
|
|
512
|
-
? {
|
|
513
|
-
imageConfig: {
|
|
514
|
-
...(imageSize ? { imageSize } : {}),
|
|
515
|
-
...(aspectRatio ? { aspectRatio } : {}),
|
|
516
|
-
},
|
|
517
|
-
}
|
|
518
|
-
: {}),
|
|
519
|
-
},
|
|
908
|
+
[providerOptionsKey]: googleOpts,
|
|
520
909
|
},
|
|
521
|
-
});
|
|
522
|
-
const imageFiles = result.files.filter((f) =>
|
|
523
|
-
return f.mediaType.startsWith('image/');
|
|
524
|
-
});
|
|
910
|
+
}));
|
|
911
|
+
const imageFiles = result.files.filter((f) => f.mediaType.startsWith('image/'));
|
|
525
912
|
if (imageFiles.length === 0) {
|
|
526
913
|
console.error(pc.red('No images generated.'));
|
|
527
914
|
process.exit(1);
|
|
528
915
|
}
|
|
529
916
|
if (stdout) {
|
|
530
|
-
|
|
531
|
-
if (file) {
|
|
532
|
-
process.stdout.write(Buffer.from(file.uint8Array));
|
|
533
|
-
}
|
|
917
|
+
writeFirstToStdout(imageFiles);
|
|
534
918
|
return;
|
|
535
919
|
}
|
|
536
|
-
const savedFiles =
|
|
537
|
-
for (let i = 0; i < imageFiles.length; i++) {
|
|
538
|
-
const file = imageFiles[i];
|
|
539
|
-
const ext = extensionFromMediaType(file.mediaType);
|
|
540
|
-
const filePath = imageFiles.length === 1
|
|
541
|
-
? ensureExtension(outputPath, ext)
|
|
542
|
-
: insertIndex(outputPath, i, ext);
|
|
543
|
-
fs.writeFileSync(filePath, file.uint8Array);
|
|
544
|
-
console.error(pc.green(`Saved: ${filePath}`));
|
|
545
|
-
savedFiles.push(filePath);
|
|
546
|
-
}
|
|
920
|
+
const savedFiles = saveGeneratedFiles(imageFiles, outputPath);
|
|
547
921
|
const cost = calculateCost(config.cost, result.usage, imageFiles.length);
|
|
548
|
-
|
|
549
|
-
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
550
|
-
}
|
|
922
|
+
printCost(cost);
|
|
551
923
|
if (result.text && !json) {
|
|
552
924
|
console.error(pc.dim(result.text));
|
|
553
925
|
}
|
|
554
926
|
if (json) {
|
|
555
|
-
|
|
927
|
+
console.log(JSON.stringify({
|
|
556
928
|
model,
|
|
557
929
|
files: savedFiles,
|
|
558
930
|
count: imageFiles.length,
|
|
559
931
|
text: result.text || null,
|
|
560
932
|
cost,
|
|
561
933
|
usage: result.usage,
|
|
562
|
-
};
|
|
563
|
-
console.log(JSON.stringify(output, null, 2));
|
|
934
|
+
}, null, 2));
|
|
564
935
|
}
|
|
565
936
|
}
|
|
566
937
|
// Generate using the OpenAI Responses API with imageGeneration tool.
|
|
@@ -706,12 +1077,28 @@ async function generateWithResponsesApi({ prompt, model, outputPath, count, aspe
|
|
|
706
1077
|
}
|
|
707
1078
|
}
|
|
708
1079
|
// Generate using experimental_generateVideo (video models)
|
|
709
|
-
async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, json, stdout, }) {
|
|
1080
|
+
async function generateWithVideoModel({ prompt, model, outputPath, count, aspectRatio, resolution, duration, fps, seed, inputImage, mode, videoUrl, referenceImages, negativePrompt, json, stdout, }) {
|
|
710
1081
|
const videoModel = await createVideoModel(model);
|
|
1082
|
+
const config = getModelConfig(model);
|
|
1083
|
+
// Validate that the model supports video-url input before building options.
|
|
1084
|
+
// Only models with a 'video-url' providerOption (currently xAI) accept edit/extend mode.
|
|
1085
|
+
if (videoUrl && !config.providerOptions?.some((opt) => opt.flag === 'video-url')) {
|
|
1086
|
+
console.error(pc.red(`Model ${model} does not support video URL input for editing/extension`));
|
|
1087
|
+
process.exit(1);
|
|
1088
|
+
}
|
|
1089
|
+
// Build type-safe provider options for video generation.
|
|
1090
|
+
const providerOptions = buildVideoProviderOptions(config.provider, {
|
|
1091
|
+
resolution,
|
|
1092
|
+
mode,
|
|
1093
|
+
videoUrl,
|
|
1094
|
+
referenceImages,
|
|
1095
|
+
negativePrompt,
|
|
1096
|
+
model,
|
|
1097
|
+
});
|
|
711
1098
|
if (!stdout) {
|
|
712
1099
|
console.error(pc.cyan('Generating...'));
|
|
713
1100
|
}
|
|
714
|
-
const result = await aiGenerateVideo({
|
|
1101
|
+
const result = await runProviderCall('Video generation', () => aiGenerateVideo({
|
|
715
1102
|
model: videoModel,
|
|
716
1103
|
prompt: inputImage
|
|
717
1104
|
? { image: inputImage, text: prompt }
|
|
@@ -722,44 +1109,67 @@ async function generateWithVideoModel({ prompt, model, outputPath, count, aspect
|
|
|
722
1109
|
...(duration != null ? { duration } : {}),
|
|
723
1110
|
...(fps != null ? { fps } : {}),
|
|
724
1111
|
...(seed != null ? { seed } : {}),
|
|
725
|
-
|
|
1112
|
+
...(providerOptions ? { providerOptions } : {}),
|
|
1113
|
+
}));
|
|
1114
|
+
const files = result.videos.map((v) => ({
|
|
1115
|
+
uint8Array: v.uint8Array,
|
|
1116
|
+
mediaType: v.mediaType,
|
|
1117
|
+
}));
|
|
726
1118
|
if (stdout) {
|
|
727
|
-
|
|
728
|
-
if (video) {
|
|
729
|
-
process.stdout.write(Buffer.from(video.uint8Array));
|
|
730
|
-
}
|
|
1119
|
+
writeFirstToStdout(files);
|
|
731
1120
|
return;
|
|
732
1121
|
}
|
|
733
|
-
const savedFiles =
|
|
734
|
-
for (let i = 0; i < result.videos.length; i++) {
|
|
735
|
-
const video = result.videos[i];
|
|
736
|
-
const ext = extensionFromMediaType(video.mediaType);
|
|
737
|
-
const filePath = result.videos.length === 1
|
|
738
|
-
? ensureExtension(outputPath, ext)
|
|
739
|
-
: insertIndex(outputPath, i, ext);
|
|
740
|
-
fs.writeFileSync(filePath, video.uint8Array);
|
|
741
|
-
console.error(pc.green(`Saved: ${filePath}`));
|
|
742
|
-
savedFiles.push(filePath);
|
|
743
|
-
}
|
|
744
|
-
const config = getModelConfig(model);
|
|
1122
|
+
const savedFiles = saveGeneratedFiles(files, outputPath);
|
|
745
1123
|
const cost = calculateCost(config.cost, {
|
|
746
1124
|
videosGenerated: result.videos.length,
|
|
747
1125
|
durationSeconds: duration,
|
|
748
1126
|
resolution,
|
|
749
1127
|
}, result.videos.length);
|
|
750
|
-
|
|
751
|
-
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
752
|
-
}
|
|
1128
|
+
printCost(cost);
|
|
753
1129
|
if (json) {
|
|
754
|
-
|
|
1130
|
+
console.log(JSON.stringify({
|
|
755
1131
|
model,
|
|
756
1132
|
files: savedFiles,
|
|
757
1133
|
count: result.videos.length,
|
|
758
1134
|
cost,
|
|
759
1135
|
warnings: result.warnings,
|
|
760
1136
|
responses: result.responses,
|
|
761
|
-
};
|
|
762
|
-
|
|
1137
|
+
}, null, 2));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
/**
|
|
1141
|
+
* Save generated files to disk. Returns the list of saved file paths.
|
|
1142
|
+
* Handles single vs multi-file naming with index suffixes.
|
|
1143
|
+
*/
|
|
1144
|
+
function saveGeneratedFiles(files, outputPath) {
|
|
1145
|
+
const savedFiles = [];
|
|
1146
|
+
for (let i = 0; i < files.length; i++) {
|
|
1147
|
+
const file = files[i];
|
|
1148
|
+
const ext = extensionFromMediaType(file.mediaType);
|
|
1149
|
+
const filePath = files.length === 1
|
|
1150
|
+
? ensureExtension(outputPath, ext)
|
|
1151
|
+
: insertIndex(outputPath, i, ext);
|
|
1152
|
+
fs.writeFileSync(filePath, file.uint8Array);
|
|
1153
|
+
console.error(pc.green(`Saved: ${filePath}`));
|
|
1154
|
+
savedFiles.push(filePath);
|
|
1155
|
+
}
|
|
1156
|
+
return savedFiles;
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Print estimated cost to stderr if calculable.
|
|
1160
|
+
*/
|
|
1161
|
+
function printCost(cost) {
|
|
1162
|
+
if (cost != null) {
|
|
1163
|
+
console.error(pc.dim(`Cost: ${formatCost(cost)}`));
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Write the first file's raw bytes to stdout (for piping to other tools).
|
|
1168
|
+
*/
|
|
1169
|
+
function writeFirstToStdout(files) {
|
|
1170
|
+
const file = files[0];
|
|
1171
|
+
if (file) {
|
|
1172
|
+
process.stdout.write(Buffer.from(file.uint8Array));
|
|
763
1173
|
}
|
|
764
1174
|
}
|
|
765
1175
|
// ─── cost helpers ────────────────────────────────────────────────────────────
|