@mrgoonie/multix 0.0.2 → 0.0.5

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.
Files changed (4) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +136 -2
  3. package/dist/cli.js +3000 -580
  4. package/package.json +4 -2
package/dist/cli.js CHANGED
@@ -155,10 +155,6 @@ var init_http_client = __esm({
155
155
  import { createRequire } from "module";
156
156
  import { Command } from "commander";
157
157
 
158
- // src/providers/gemini/commands/analyze.ts
159
- init_errors();
160
- import fs5 from "fs";
161
-
162
158
  // src/core/logger.ts
163
159
  var ANSI = {
164
160
  reset: "\x1B[0m",
@@ -204,10 +200,6 @@ ${line}
204
200
  }
205
201
  var defaultLogger = createLogger({ verbose: false });
206
202
 
207
- // src/providers/gemini/client.ts
208
- import fs3 from "fs";
209
- import path4 from "path";
210
-
211
203
  // src/core/env-loader.ts
212
204
  import fs from "fs";
213
205
  import os from "os";
@@ -236,234 +228,1130 @@ function redact(key) {
236
228
  return `${key.slice(0, 6)}...${key.slice(-4)}`;
237
229
  }
238
230
 
239
- // src/providers/gemini/client.ts
231
+ // src/providers/byteplus/client.ts
240
232
  init_errors();
241
233
  init_http_client();
242
234
 
243
- // src/providers/gemini/task-resolver.ts
235
+ // src/providers/byteplus/models.ts
236
+ var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
237
+ var DEFAULT_BYTEPLUS_IMAGE_MODEL = "seedream-4-0-250828";
238
+ var DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-2.0";
239
+ var BYTEPLUS_DEFAULTS = {
240
+ imageModel: process.env.BYTEPLUS_IMAGE_MODEL ?? DEFAULT_BYTEPLUS_IMAGE_MODEL,
241
+ videoModel: process.env.BYTEPLUS_VIDEO_MODEL ?? DEFAULT_BYTEPLUS_VIDEO_MODEL,
242
+ videoResolution: "1080p",
243
+ videoDuration: 8,
244
+ videoAspectRatio: "16:9",
245
+ imageSize: "2K"
246
+ };
247
+ function bytePlusVideoParamsMode() {
248
+ const m = process.env.BYTEPLUS_VIDEO_PARAMS_MODE;
249
+ return m === "structured" ? "structured" : "flags";
250
+ }
251
+
252
+ // src/providers/byteplus/client.ts
253
+ function requireBytePlusKey() {
254
+ const key = resolveKey("BYTEPLUS_API_KEY") ?? resolveKey("ARK_API_KEY");
255
+ if (!key) {
256
+ throw new ConfigError(
257
+ "BytePlus API key not set. Set BYTEPLUS_API_KEY or ARK_API_KEY. Get one at https://console.byteplus.com/auth/api-keys"
258
+ );
259
+ }
260
+ return key;
261
+ }
262
+ function bytePlusBaseUrl() {
263
+ return resolveKey("BYTEPLUS_BASE_URL") ?? DEFAULT_BYTEPLUS_BASE_URL;
264
+ }
265
+ var BytePlusClient = class {
266
+ constructor(apiKey, baseUrl = bytePlusBaseUrl()) {
267
+ this.apiKey = apiKey;
268
+ this.baseUrl = baseUrl;
269
+ }
270
+ apiKey;
271
+ baseUrl;
272
+ async request(path35, opts = {}) {
273
+ const base = this.baseUrl.replace(/\/$/, "");
274
+ const url = new URL(base + path35);
275
+ if (opts.query) {
276
+ for (const [k, v] of Object.entries(opts.query)) {
277
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
278
+ }
279
+ }
280
+ opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
281
+ return httpJson({
282
+ url: url.toString(),
283
+ method: opts.method ?? "GET",
284
+ headers: {
285
+ accept: "application/json",
286
+ authorization: `Bearer ${this.apiKey}`
287
+ },
288
+ body: opts.body,
289
+ timeoutMs: opts.timeoutMs ?? 12e4
290
+ });
291
+ }
292
+ get(path35, query, logger) {
293
+ return this.request(path35, { method: "GET", query, logger });
294
+ }
295
+ post(path35, body, logger) {
296
+ return this.request(path35, { method: "POST", body, logger });
297
+ }
298
+ delete(path35, logger) {
299
+ return this.request(path35, { method: "DELETE", logger });
300
+ }
301
+ };
302
+ function createBytePlusClient() {
303
+ return new BytePlusClient(requireBytePlusKey(), bytePlusBaseUrl());
304
+ }
305
+
306
+ // src/providers/byteplus/generators/image.ts
307
+ init_http_client();
308
+ import fs5 from "fs";
309
+ import path5 from "path";
310
+
311
+ // src/core/image-input.ts
312
+ import fs3 from "fs/promises";
244
313
  import path3 from "path";
245
- var MIME_MAP = {
246
- // Audio
247
- ".mp3": "audio/mp3",
248
- ".wav": "audio/wav",
249
- ".aac": "audio/aac",
250
- ".flac": "audio/flac",
251
- ".ogg": "audio/ogg",
252
- ".aiff": "audio/aiff",
253
- ".m4a": "audio/mp4",
254
- // Image
314
+ var IMAGE_MIME_BY_EXT = {
255
315
  ".jpg": "image/jpeg",
256
316
  ".jpeg": "image/jpeg",
257
317
  ".png": "image/png",
258
318
  ".webp": "image/webp",
259
- ".heic": "image/heic",
260
- ".heif": "image/heif",
261
- ".gif": "image/gif",
262
- ".bmp": "image/bmp",
263
- // Video
319
+ ".gif": "image/gif"
320
+ };
321
+ var VIDEO_MIME_BY_EXT = {
264
322
  ".mp4": "video/mp4",
265
- ".mpeg": "video/mpeg",
266
- ".mov": "video/quicktime",
267
- ".avi": "video/x-msvideo",
268
- ".flv": "video/x-flv",
269
- ".mpg": "video/mpeg",
270
323
  ".webm": "video/webm",
271
- ".wmv": "video/x-ms-wmv",
272
- ".3gpp": "video/3gpp",
273
- // Document
274
- ".pdf": "application/pdf",
275
- ".txt": "text/plain",
276
- ".html": "text/html",
277
- ".htm": "text/html",
278
- ".md": "text/markdown",
279
- ".csv": "text/csv",
280
- ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
281
- ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
282
- ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
324
+ ".mov": "video/quicktime",
325
+ ".m4v": "video/x-m4v"
283
326
  };
284
- function getMimeType(filePath) {
285
- const ext = path3.extname(filePath).toLowerCase();
286
- return MIME_MAP[ext] ?? "application/octet-stream";
287
- }
288
- function requiresProcessingWait(mimeType) {
289
- return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
327
+ var AUDIO_MIME_BY_EXT = {
328
+ ".mp3": "audio/mpeg",
329
+ ".wav": "audio/wav",
330
+ ".m4a": "audio/mp4",
331
+ ".flac": "audio/flac",
332
+ ".ogg": "audio/ogg"
333
+ };
334
+ var DEFAULT_SOFT_LIMIT_BYTES = 10 * 1024 * 1024;
335
+ var DEFAULT_HARD_LIMIT_BYTES = 25 * 1024 * 1024;
336
+ function isHttpUrl(input) {
337
+ return /^https?:\/\//i.test(input);
290
338
  }
291
-
292
- // src/providers/gemini/client.ts
293
- var BASE = "https://generativelanguage.googleapis.com";
294
- var FILES_BASE = `${BASE}/upload/v1beta/files`;
295
- var GENERATE_BASE = `${BASE}/v1beta/models`;
296
- function requireApiKey() {
297
- const key = resolveKey("GEMINI_API_KEY");
298
- if (!key)
299
- throw new ConfigError(
300
- "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
339
+ async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
340
+ const ext = path3.extname(filePath).toLowerCase();
341
+ const mime = mimeTable[ext];
342
+ if (!mime) {
343
+ throw new Error(
344
+ `Unsupported ${kindLabel} extension '${ext}'. Allowed: ${Object.keys(mimeTable).join(", ")}`
301
345
  );
302
- return key;
303
- }
304
- function authHeaders(apiKey) {
305
- return { "x-goog-api-key": apiKey };
306
- }
307
- async function uploadFile(filePath, opts = {}) {
308
- const apiKey = requireApiKey();
309
- const { timeoutMs = 3e5, logger } = opts;
310
- const mimeType = getMimeType(filePath);
311
- const fileBytes = fs3.readFileSync(filePath);
312
- const displayName = path4.basename(filePath);
313
- const boundary = `multix_${Date.now()}`;
314
- const metaJson = JSON.stringify({ file: { displayName, mimeType } });
315
- const body = Buffer.concat([
316
- Buffer.from(
317
- `--${boundary}\r
318
- Content-Type: application/json; charset=UTF-8\r
319
- \r
320
- ${metaJson}\r
321
- --${boundary}\r
322
- Content-Type: ${mimeType}\r
323
- \r
324
- `
325
- ),
326
- fileBytes,
327
- Buffer.from(`\r
328
- --${boundary}--`)
329
- ]);
330
- logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
331
- const resp = await globalThis.fetch(FILES_BASE, {
332
- method: "POST",
333
- headers: {
334
- "x-goog-api-key": apiKey,
335
- "Content-Type": `multipart/related; boundary=${boundary}`,
336
- "Content-Length": String(body.length),
337
- "X-Goog-Upload-Protocol": "multipart"
338
- },
339
- body,
340
- signal: AbortSignal.timeout(timeoutMs)
341
- });
342
- if (!resp.ok) {
343
- const txt = await resp.text();
344
- throw new ProviderError(
345
- `File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
346
- "gemini"
346
+ }
347
+ const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
348
+ const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
349
+ const buf = await fs3.readFile(filePath);
350
+ if (buf.byteLength > hard) {
351
+ throw new Error(
352
+ `${kindLabel} file ${filePath} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds ${(hard / 1024 / 1024).toFixed(0)}MB limit. Upload to a URL instead.`
347
353
  );
348
354
  }
349
- const data = await resp.json();
350
- let fileRef = data.file;
351
- if (requiresProcessingWait(mimeType)) {
352
- const deadline = Date.now() + timeoutMs;
353
- while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
354
- await sleep(2e3);
355
- fileRef = await httpJson({
356
- url: `${BASE}/v1beta/${fileRef.name}`,
357
- headers: authHeaders(apiKey)
358
- }).then((r) => r.file);
359
- logger?.debug(`Processing... state=${fileRef.state}`);
360
- }
361
- if (fileRef.state === "FAILED")
362
- throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
363
- if (fileRef.state === "PROCESSING")
364
- throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
355
+ if (buf.byteLength > soft) {
356
+ opts.logger?.warn(
357
+ `${kindLabel} payload ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB is large; consider URL input.`
358
+ );
365
359
  }
366
- logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
367
- return fileRef;
360
+ const dataUrl = `data:${mime};base64,${buf.toString("base64")}`;
361
+ return { dataUrl, mime, bytes: buf.byteLength };
368
362
  }
369
- async function generateContent(req) {
370
- const apiKey = requireApiKey();
371
- return httpJson({
372
- url: `${GENERATE_BASE}/${req.model}:generateContent`,
373
- method: "POST",
374
- headers: authHeaders(apiKey),
375
- body: {
376
- contents: req.contents,
377
- ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
378
- }
379
- });
363
+ async function resolveImageInput(input, opts = {}) {
364
+ const o = normalizeOpts(opts);
365
+ if (isHttpUrl(input)) return { kind: "url", url: input };
366
+ const { dataUrl, mime, bytes } = await readAsDataUrl(input, IMAGE_MIME_BY_EXT, "image", o);
367
+ return { kind: "data", dataUrl, mime, bytes };
380
368
  }
381
- function extractText(resp) {
382
- const parts = resp.candidates?.[0]?.content?.parts ?? [];
383
- return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
369
+ async function resolveVideoInput(input, opts = {}) {
370
+ const o = normalizeOpts(opts);
371
+ if (isHttpUrl(input)) return { kind: "url", url: input };
372
+ const { dataUrl, mime, bytes } = await readAsDataUrl(input, VIDEO_MIME_BY_EXT, "video", o);
373
+ return { kind: "data", dataUrl, mime, bytes };
384
374
  }
385
- function extractImages(resp) {
386
- const parts = resp.candidates?.[0]?.content?.parts ?? [];
387
- return parts.filter(
388
- (p) => p.inlineData !== void 0
389
- ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
375
+ async function resolveAudioInput(input, opts = {}) {
376
+ const o = normalizeOpts(opts);
377
+ if (isHttpUrl(input)) return { kind: "url", url: input };
378
+ const { dataUrl, mime, bytes } = await readAsDataUrl(input, AUDIO_MIME_BY_EXT, "audio", o);
379
+ return { kind: "data", dataUrl, mime, bytes };
390
380
  }
391
- function inlinePart(filePath) {
392
- const data = fs3.readFileSync(filePath).toString("base64");
393
- const mimeType = getMimeType(filePath);
394
- return { inlineData: { mimeType, data } };
381
+ function refUrl(r) {
382
+ return r.kind === "url" ? r.url : r.dataUrl;
395
383
  }
396
- function shouldUseFileApi(filePath) {
397
- const stat = fs3.statSync(filePath);
398
- return stat.size > 20 * 1024 * 1024;
384
+ function normalizeOpts(opts) {
385
+ if (!opts) return {};
386
+ if (typeof opts.info === "function" && typeof opts.warn === "function") {
387
+ return { logger: opts };
388
+ }
389
+ return opts;
399
390
  }
400
- function sleep(ms) {
401
- return new Promise((resolve) => setTimeout(resolve, ms));
391
+
392
+ // src/core/output-dir.ts
393
+ import fs4 from "fs";
394
+ import path4 from "path";
395
+ var cachedOutputDir;
396
+ function getOutputDir() {
397
+ if (cachedOutputDir) return cachedOutputDir;
398
+ const dir = process.env.MULTIX_OUTPUT_DIR ? path4.resolve(process.env.MULTIX_OUTPUT_DIR) : path4.resolve(process.cwd(), "multix-output");
399
+ fs4.mkdirSync(dir, { recursive: true });
400
+ cachedOutputDir = dir;
401
+ return dir;
402
402
  }
403
403
 
404
- // src/providers/gemini/models.ts
405
- var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
406
- var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
407
- var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
408
- var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
409
- var IMAGEN_MODELS = /* @__PURE__ */ new Set([
410
- "imagen-4.0-generate-001",
411
- "imagen-4.0-ultra-generate-001",
412
- "imagen-4.0-fast-generate-001"
413
- ]);
414
- var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
415
- "gemini-3.1-flash-image-preview",
416
- "gemini-3-pro-image-preview",
417
- "gemini-2.5-flash-image",
418
- "gemini-2.5-flash-image-preview",
419
- ...IMAGEN_MODELS
420
- ]);
421
- var ASPECT_RATIOS = [
422
- "1:1",
423
- "2:3",
424
- "3:2",
425
- "3:4",
426
- "4:3",
427
- "4:5",
428
- "5:4",
429
- "9:16",
430
- "16:9",
431
- "21:9"
432
- ];
433
- var IMAGE_SIZES = ["1K", "2K", "4K"];
434
- function getDefaultModel(task) {
435
- if (task === "generate") {
436
- return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
404
+ // src/providers/byteplus/generators/image.ts
405
+ var ASPECT_TO_SIZE = {
406
+ "1:1": "2048x2048",
407
+ "16:9": "2560x1440",
408
+ "9:16": "1440x2560",
409
+ "4:3": "2048x1536",
410
+ "3:4": "1536x2048",
411
+ "21:9": "2560x1080",
412
+ "3:2": "2048x1365",
413
+ "2:3": "1365x2048"
414
+ };
415
+ function resolveSize(opts) {
416
+ if (opts.size && opts.aspectRatio) {
417
+ opts.logger?.warn("Both --size and --aspect-ratio given; using --size.");
418
+ return opts.size;
437
419
  }
438
- if (task === "generate-video") {
439
- return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
420
+ if (opts.size) return opts.size;
421
+ if (opts.aspectRatio) {
422
+ const mapped = ASPECT_TO_SIZE[opts.aspectRatio];
423
+ if (!mapped) {
424
+ throw new Error(
425
+ `Unsupported --aspect-ratio '${opts.aspectRatio}'. Supported: ${Object.keys(ASPECT_TO_SIZE).join(", ")}`
426
+ );
427
+ }
428
+ return mapped;
440
429
  }
441
- return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
430
+ return BYTEPLUS_DEFAULTS.imageSize;
442
431
  }
443
-
444
- // src/providers/gemini/result-formatter.ts
445
- import fs4 from "fs";
446
- import path5 from "path";
447
- function formatResults(results, format, outputPath) {
448
- const content = renderFormat(results, format);
449
- if (outputPath) {
450
- fs4.mkdirSync(path5.dirname(path5.resolve(outputPath)), { recursive: true });
451
- fs4.writeFileSync(outputPath, content, "utf8");
452
- } else {
453
- process.stdout.write(content);
432
+ async function generateSeedream(client, opts) {
433
+ const logger = opts.logger;
434
+ const model = opts.model ?? BYTEPLUS_DEFAULTS.imageModel;
435
+ const resolvedInputs = [];
436
+ for (const input of opts.inputImages ?? []) {
437
+ const r = await resolveImageInput(input, logger);
438
+ resolvedInputs.push(refUrl(r));
454
439
  }
455
- }
456
- function renderFormat(results, format) {
457
- switch (format) {
458
- case "json":
459
- return `${JSON.stringify(results, null, 2)}
460
- `;
461
- case "csv":
462
- return renderCsv(results);
463
- case "markdown":
464
- return renderMarkdown(results);
465
- default:
466
- return renderText(results);
440
+ const body = {
441
+ model,
442
+ prompt: opts.prompt,
443
+ response_format: "url",
444
+ n: opts.numImages ?? 1,
445
+ watermark: opts.watermark ?? true,
446
+ size: resolveSize(opts)
447
+ };
448
+ if (resolvedInputs.length > 0) body.image = resolvedInputs;
449
+ if (opts.seed !== void 0) body.seed = opts.seed;
450
+ logger?.debug(
451
+ `Seedream body: ${JSON.stringify({ ...body, image: body.image ? `[${body.image.length}]` : void 0 })}`
452
+ );
453
+ const res = await client.post("/images/generations", body, logger);
454
+ const items = res.data ?? [];
455
+ const ts = Date.now();
456
+ const outDir = getOutputDir();
457
+ const saved = [];
458
+ for (let i = 0; i < items.length; i++) {
459
+ const item = items[i];
460
+ if (!item) continue;
461
+ const url = item.url;
462
+ if (!url) {
463
+ logger?.warn(`Image ${i + 1}: no URL in response (b64_json fallback not yet supported).`);
464
+ continue;
465
+ }
466
+ const ext = (() => {
467
+ try {
468
+ const e = path5.extname(new URL(url).pathname);
469
+ return e || ".jpg";
470
+ } catch {
471
+ return ".jpg";
472
+ }
473
+ })();
474
+ const filename = opts.output && items.length === 1 ? path5.resolve(opts.output) : path5.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
475
+ fs5.mkdirSync(path5.dirname(filename), { recursive: true });
476
+ await downloadFile(url, filename);
477
+ saved.push({ path: filename, url, seed: item.seed });
478
+ logger?.success(`Saved ${filename}`);
479
+ }
480
+ return saved;
481
+ }
482
+
483
+ // src/providers/byteplus/commands/generate.ts
484
+ function registerBytePlusGenerateCommand(parent) {
485
+ parent.command("generate").alias("gen").description("Generate images with BytePlus Seedream 4.0 (text-to-image, multi-image input)").requiredOption("--prompt <text>", "Text prompt").option("-m, --model <id>", "Model id (default seedream-4-0-250828)").option("--size <size>", "Image size (e.g. 1024x1024, 1K, 2K, 4K)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9, 9:16); maps to size").option("-n, --num-images <n>", "Number of images", "1").option("--seed <n>", "Fixed seed").option("--no-watermark", "Disable watermark").option(
486
+ "--input-image <path>",
487
+ "Reference image (URL or local path); repeatable",
488
+ collect,
489
+ []
490
+ ).option("--output <path>", "Save single image to this path").option("-v, --verbose", "Verbose logging").action(
491
+ async (opts) => {
492
+ const logger = createLogger({ verbose: opts.verbose ?? false });
493
+ const client = createBytePlusClient();
494
+ const saved = await generateSeedream(client, {
495
+ prompt: opts.prompt,
496
+ model: opts.model,
497
+ size: opts.size,
498
+ aspectRatio: opts.aspectRatio,
499
+ numImages: Number.parseInt(opts.numImages, 10) || 1,
500
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
501
+ watermark: opts.watermark,
502
+ inputImages: opts.inputImage,
503
+ output: opts.output,
504
+ logger
505
+ });
506
+ console.log(`
507
+ Generated ${saved.length} image(s):`);
508
+ for (const f of saved) console.log(` ${f.path}`);
509
+ }
510
+ );
511
+ }
512
+ function collect(value, prev) {
513
+ return [...prev, value];
514
+ }
515
+
516
+ // src/providers/byteplus/commands/image-to-image.ts
517
+ function registerBytePlusImageToImageCommand(parent) {
518
+ parent.command("image-to-image").alias("i2i").description(
519
+ "Edit/transform image(s) with BytePlus Seedream/Seededit using one or more reference images"
520
+ ).requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
521
+ "--ref <path>",
522
+ "Reference image (URL or local path); repeatable for multi-ref",
523
+ collect2,
524
+ []
525
+ ).option("-m, --model <id>", "Model id (default seedream-4-0-250828)").option("--size <size>", "Image size (e.g. 1024x1024, 1K, 2K, 4K)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9, 9:16); maps to size").option("-n, --num-images <n>", "Number of images", "1").option("--seed <n>", "Fixed seed").option("--no-watermark", "Disable watermark").option("--output <path>", "Save single image to this path").option("-v, --verbose", "Verbose logging").action(
526
+ async (opts) => {
527
+ if (!opts.ref || opts.ref.length === 0) {
528
+ throw new Error("At least one --ref is required for image-to-image.");
529
+ }
530
+ const logger = createLogger({ verbose: opts.verbose ?? false });
531
+ const client = createBytePlusClient();
532
+ const saved = await generateSeedream(client, {
533
+ prompt: opts.prompt,
534
+ model: opts.model,
535
+ size: opts.size,
536
+ aspectRatio: opts.aspectRatio,
537
+ numImages: Number.parseInt(opts.numImages, 10) || 1,
538
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
539
+ watermark: opts.watermark,
540
+ inputImages: opts.ref,
541
+ output: opts.output,
542
+ logger
543
+ });
544
+ console.log(`
545
+ Generated ${saved.length} image(s):`);
546
+ for (const f of saved) console.log(` ${f.path}`);
547
+ }
548
+ );
549
+ }
550
+ function collect2(value, prev) {
551
+ return [...prev, value];
552
+ }
553
+
554
+ // src/providers/byteplus/commands/image-to-video.ts
555
+ import path7 from "path";
556
+
557
+ // src/core/video-thumb.ts
558
+ init_http_client();
559
+ import fs6 from "fs";
560
+ import path6 from "path";
561
+ var THUMB_KEYS = /* @__PURE__ */ new Set([
562
+ "cover_image_url",
563
+ "coverImageUrl",
564
+ "cover_url",
565
+ "coverUrl",
566
+ "thumbnail_url",
567
+ "thumbnailUrl",
568
+ "thumb_url",
569
+ "thumbUrl",
570
+ "preview_url",
571
+ "previewUrl",
572
+ "first_frame_url",
573
+ "firstFrameUrl",
574
+ "poster_url",
575
+ "posterUrl",
576
+ "image_url",
577
+ "imageUrl"
578
+ ]);
579
+ var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
580
+ function isImageUrl(value) {
581
+ if (typeof value !== "string") return false;
582
+ if (!/^https?:\/\//i.test(value)) return false;
583
+ return IMAGE_EXT_RE.test(value);
584
+ }
585
+ function detectThumbUrl(obj) {
586
+ if (obj === null || obj === void 0) return null;
587
+ if (Array.isArray(obj)) {
588
+ for (const item of obj) {
589
+ const found = detectThumbUrl(item);
590
+ if (found) return found;
591
+ }
592
+ return null;
593
+ }
594
+ if (typeof obj !== "object") return null;
595
+ const record = obj;
596
+ for (const [key, value] of Object.entries(record)) {
597
+ if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
598
+ }
599
+ for (const value of Object.values(record)) {
600
+ if (value && typeof value === "object") {
601
+ const found = detectThumbUrl(value);
602
+ if (found) return found;
603
+ }
604
+ }
605
+ return null;
606
+ }
607
+ async function downloadThumbBeside(thumbUrl, videoPath, logger) {
608
+ const match = thumbUrl.match(IMAGE_EXT_RE);
609
+ const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
610
+ const dir = path6.dirname(videoPath);
611
+ const base = path6.basename(videoPath, path6.extname(videoPath));
612
+ const dest = path6.join(dir, `${base}_thumb.${ext}`);
613
+ try {
614
+ await downloadFile(thumbUrl, dest);
615
+ logger?.success(`Saved thumb ${dest}`);
616
+ return dest;
617
+ } catch (e) {
618
+ logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
619
+ return null;
620
+ }
621
+ }
622
+ async function maybeDownloadThumb(source, videoPath, opts = {}) {
623
+ if (opts.skip) return null;
624
+ const url = detectThumbUrl(source);
625
+ if (!url) {
626
+ opts.logger?.debug("No thumbnail URL detected in provider response");
627
+ return null;
628
+ }
629
+ const saved = await downloadThumbBeside(url, videoPath, opts.logger);
630
+ if (saved && opts.copyTo) {
631
+ const copyExt = path6.extname(saved);
632
+ const copyBase = path6.basename(opts.copyTo, path6.extname(opts.copyTo));
633
+ const copyDir = path6.dirname(path6.resolve(opts.copyTo));
634
+ const copyDest = path6.join(copyDir, `${copyBase}_thumb${copyExt}`);
635
+ fs6.mkdirSync(copyDir, { recursive: true });
636
+ fs6.copyFileSync(saved, copyDest);
637
+ opts.logger?.success(`Copied thumb to ${copyDest}`);
638
+ }
639
+ return saved;
640
+ }
641
+
642
+ // src/providers/leonardo/poll.ts
643
+ var PollTimeoutError = class extends Error {
644
+ constructor() {
645
+ super("Polling timed out before the operation completed.");
646
+ this.name = "PollTimeoutError";
647
+ }
648
+ };
649
+ var PollFailedError = class extends Error {
650
+ constructor(value) {
651
+ super("Operation failed.");
652
+ this.value = value;
653
+ this.name = "PollFailedError";
654
+ }
655
+ value;
656
+ };
657
+ async function poll(opts) {
658
+ const interval = opts.intervalMs ?? 4e3;
659
+ const max = opts.maxAttempts ?? 120;
660
+ for (let attempt = 1; attempt <= max; attempt++) {
661
+ if (opts.signal?.aborted) throw new Error("Aborted");
662
+ const value = await opts.fetch();
663
+ opts.onTick?.(attempt, value);
664
+ if (opts.failed?.(value)) throw new PollFailedError(value);
665
+ if (opts.done(value)) return value;
666
+ await new Promise((resolve, reject) => {
667
+ const t = setTimeout(resolve, interval);
668
+ opts.signal?.addEventListener("abort", () => {
669
+ clearTimeout(t);
670
+ reject(new Error("Aborted"));
671
+ });
672
+ });
673
+ }
674
+ throw new PollTimeoutError();
675
+ }
676
+
677
+ // src/providers/byteplus/generators/video.ts
678
+ init_http_client();
679
+ function buildFlagSuffix(opts) {
680
+ const parts = [];
681
+ if (opts.resolution) parts.push(`--rs ${opts.resolution}`);
682
+ if (opts.duration !== void 0) parts.push(`--dur ${opts.duration}`);
683
+ if (opts.aspectRatio) parts.push(`--rt ${opts.aspectRatio}`);
684
+ if (opts.cameraFixed !== void 0) parts.push(`--cf ${opts.cameraFixed}`);
685
+ if (opts.seed !== void 0) parts.push(`--seed ${opts.seed}`);
686
+ if (opts.audio !== void 0) parts.push(`--wm ${opts.audio}`);
687
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
688
+ }
689
+ function buildVideoTaskBody(opts) {
690
+ const mode = opts.paramsMode ?? bytePlusVideoParamsMode();
691
+ const text = mode === "flags" ? `${opts.prompt}${buildFlagSuffix(opts)}${opts.negativePrompt ? ` negative: "${opts.negativePrompt}"` : ""}` : opts.prompt;
692
+ const content = [{ type: "text", text }];
693
+ for (const img of opts.imageInputs ?? []) {
694
+ content.push({ type: "image_url", image_url: { url: refUrl(img) } });
695
+ }
696
+ for (const ref of opts.references ?? []) {
697
+ if (ref.kind === "image") {
698
+ content.push({
699
+ type: "image_url",
700
+ image_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
701
+ });
702
+ } else if (ref.kind === "video") {
703
+ content.push({
704
+ type: "video_url",
705
+ video_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
706
+ });
707
+ } else {
708
+ content.push({
709
+ type: "audio_url",
710
+ audio_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
711
+ });
712
+ }
713
+ }
714
+ const body = { model: opts.model, content };
715
+ if (mode === "structured") {
716
+ body.parameters = {
717
+ ...opts.resolution ? { resolution: opts.resolution } : {},
718
+ ...opts.duration !== void 0 ? { duration: opts.duration } : {},
719
+ ...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
720
+ ...opts.audio !== void 0 ? { audio: opts.audio } : {},
721
+ ...opts.seed !== void 0 ? { seed: opts.seed } : {},
722
+ ...opts.negativePrompt ? { negative_prompt: opts.negativePrompt } : {},
723
+ ...opts.cameraFixed !== void 0 ? { camera_fixed: opts.cameraFixed } : {}
724
+ };
725
+ }
726
+ return body;
727
+ }
728
+ async function submitVideoTask(client, body, logger) {
729
+ return client.post("/contents/generations/tasks", body, logger);
730
+ }
731
+ async function getVideoTaskStatus(client, id, logger) {
732
+ return client.get(`/contents/generations/tasks/${id}`, void 0, logger);
733
+ }
734
+ async function waitForVideoTask(client, id, opts = {}) {
735
+ const intervalMs = opts.intervalMs ?? 4e3;
736
+ const maxAttempts = opts.maxAttempts ?? (opts.waitTimeoutMs ? Math.max(1, Math.ceil(opts.waitTimeoutMs / intervalMs)) : 120);
737
+ return poll({
738
+ fetch: () => getVideoTaskStatus(client, id, opts.logger),
739
+ done: (v) => v.status === "succeeded",
740
+ failed: (v) => v.status === "failed" || v.status === "cancelled",
741
+ intervalMs,
742
+ maxAttempts,
743
+ onTick: (attempt, v) => opts.logger?.debug(`attempt ${attempt} \u2014 status: ${v.status}`)
744
+ });
745
+ }
746
+ async function downloadVideo(url, outputPath, logger) {
747
+ await downloadFile(url, outputPath);
748
+ logger?.success(`Saved ${outputPath}`);
749
+ }
750
+
751
+ // src/providers/byteplus/commands/image-to-video.ts
752
+ function registerBytePlusImageToVideoCommand(parent) {
753
+ parent.command("image-to-video <image>").alias("i2v").description("Generate a video from an image (URL or local) via BytePlus Seedance 2.0").requiredOption("--prompt <text>", "Motion prompt").option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds", String(BYTEPLUS_DEFAULTS.videoDuration)).option("--aspect-ratio <r>", "Aspect ratio", BYTEPLUS_DEFAULTS.videoAspectRatio).option("--audio", "Enable audio").option("--no-audio", "Disable audio").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--camera-fixed", "Lock camera").option("--last-frame <path>", "Optional last-frame image (URL or local)").option("--async", "Submit task and exit").option("--wait-timeout <ms>", "Poll timeout in ms", "600000").option("--output <path>", "Output MP4 path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
754
+ async (image, opts) => {
755
+ const logger = createLogger({ verbose: opts.verbose ?? false });
756
+ const client = createBytePlusClient();
757
+ const first = await resolveImageInput(image, logger);
758
+ const refs = [];
759
+ if (opts.lastFrame) {
760
+ const last = await resolveImageInput(opts.lastFrame, logger);
761
+ refs.push({
762
+ kind: "image",
763
+ url: last.kind === "url" ? last.url : last.dataUrl,
764
+ mime: last.kind === "data" ? last.mime : void 0,
765
+ role: "last_frame"
766
+ });
767
+ }
768
+ const body = buildVideoTaskBody({
769
+ model: opts.model,
770
+ prompt: opts.prompt,
771
+ resolution: opts.resolution,
772
+ duration: Number.parseInt(opts.duration, 10),
773
+ aspectRatio: opts.aspectRatio,
774
+ audio: opts.audio,
775
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
776
+ negativePrompt: opts.negative,
777
+ cameraFixed: opts.cameraFixed,
778
+ imageInputs: [first],
779
+ references: refs
780
+ });
781
+ logger.info(`Submitting i2v task (${opts.model})`);
782
+ const submitted = await submitVideoTask(client, body, logger);
783
+ const taskId = submitted.id;
784
+ if (opts.async) {
785
+ console.log(taskId);
786
+ console.error(`Poll with: multix byteplus status ${taskId}`);
787
+ return;
788
+ }
789
+ try {
790
+ const final = await waitForVideoTask(client, taskId, {
791
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
792
+ logger
793
+ });
794
+ const url = final.content?.video_url;
795
+ if (!url) {
796
+ logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
797
+ process.exit(1);
798
+ }
799
+ const outPath = opts.output ?? path7.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
800
+ await downloadVideo(url, outPath, logger);
801
+ await maybeDownloadThumb(final, outPath, {
802
+ skip: opts.thumb === false,
803
+ copyTo: opts.output,
804
+ logger
805
+ });
806
+ console.log(`
807
+ Task: ${taskId}`);
808
+ console.log(`Saved: ${outPath}`);
809
+ } catch (e) {
810
+ if (e instanceof PollTimeoutError) {
811
+ logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
812
+ process.exit(1);
813
+ }
814
+ if (e instanceof PollFailedError) {
815
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
816
+ process.exit(1);
817
+ }
818
+ throw e;
819
+ }
820
+ }
821
+ );
822
+ }
823
+
824
+ // src/providers/byteplus/commands/reference-to-video.ts
825
+ import path8 from "path";
826
+
827
+ // src/providers/byteplus/reference-input.ts
828
+ var MAX_TOTAL_REFS = 12;
829
+ var MAX_IMAGE_REFS = 9;
830
+ var MAX_VIDEO_REFS = 3;
831
+ var MAX_AUDIO_REFS = 3;
832
+ function parseRefSpec(input) {
833
+ const PLACEHOLDER = "\0";
834
+ const escaped = input.replace(/\\:/g, PLACEHOLDER);
835
+ const lastIdx = escaped.lastIndexOf(":");
836
+ if (lastIdx === -1) {
837
+ return { path: input.replace(/\\:/g, ":") };
838
+ }
839
+ const left = escaped.slice(0, lastIdx);
840
+ const right = escaped.slice(lastIdx + 1);
841
+ if (/^[a-zA-Z]$/.test(left) && /[\\/]/.test(right)) {
842
+ return { path: input.replace(/\\:/g, ":") };
843
+ }
844
+ if (right.length === 0) {
845
+ return { path: left.replace(new RegExp(PLACEHOLDER, "g"), ":") };
846
+ }
847
+ return {
848
+ path: left.replace(new RegExp(PLACEHOLDER, "g"), ":"),
849
+ role: right.replace(new RegExp(PLACEHOLDER, "g"), ":") || void 0
850
+ };
851
+ }
852
+ function toRef(kind, resolved, role) {
853
+ if (resolved.kind === "url") return { kind, url: resolved.url, role };
854
+ return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
855
+ }
856
+ async function resolveRefSpec(kind, spec, logger) {
857
+ const { path: path35, role } = parseRefSpec(spec);
858
+ if (kind === "image") return toRef(kind, await resolveImageInput(path35, logger), role);
859
+ if (kind === "video") return toRef(kind, await resolveVideoInput(path35, logger), role);
860
+ return toRef(kind, await resolveAudioInput(path35, logger), role);
861
+ }
862
+ async function resolveRefSpecs(specs, logger) {
863
+ const out = [];
864
+ for (const { kind, spec } of specs) {
865
+ out.push(await resolveRefSpec(kind, spec, logger));
866
+ }
867
+ return out;
868
+ }
869
+ function validateRefCounts(refs) {
870
+ if (refs.length > MAX_TOTAL_REFS) {
871
+ throw new Error(`Too many references: ${refs.length} > ${MAX_TOTAL_REFS}`);
872
+ }
873
+ const images = refs.filter((r) => r.kind === "image").length;
874
+ const videos = refs.filter((r) => r.kind === "video").length;
875
+ const audios = refs.filter((r) => r.kind === "audio").length;
876
+ if (images > MAX_IMAGE_REFS)
877
+ throw new Error(`Too many image refs: ${images} > ${MAX_IMAGE_REFS}`);
878
+ if (videos > MAX_VIDEO_REFS)
879
+ throw new Error(`Too many video refs: ${videos} > ${MAX_VIDEO_REFS}`);
880
+ if (audios > MAX_AUDIO_REFS)
881
+ throw new Error(`Too many audio refs: ${audios} > ${MAX_AUDIO_REFS}`);
882
+ }
883
+ var REF_LIMITS = {
884
+ total: MAX_TOTAL_REFS,
885
+ image: MAX_IMAGE_REFS,
886
+ video: MAX_VIDEO_REFS,
887
+ audio: MAX_AUDIO_REFS
888
+ };
889
+
890
+ // src/providers/byteplus/commands/reference-to-video.ts
891
+ function collect3(value, prev) {
892
+ return [...prev, value];
893
+ }
894
+ function registerBytePlusReferenceToVideoCommand(parent) {
895
+ parent.command("reference-to-video").alias("r2v").description(
896
+ `Generate video from multimodal references (\u2264${REF_LIMITS.image} images, \u2264${REF_LIMITS.video} videos, \u2264${REF_LIMITS.audio} audio; total \u2264${REF_LIMITS.total}). Use path:role syntax (escape literal colons with \\:).`
897
+ ).requiredOption("--prompt <text>", "Text prompt").option(
898
+ "--ref-image <spec>",
899
+ "Image ref (path|url[:role]); repeatable",
900
+ collect3,
901
+ []
902
+ ).option(
903
+ "--ref-video <spec>",
904
+ "Video ref (path|url[:role]); repeatable",
905
+ collect3,
906
+ []
907
+ ).option(
908
+ "--ref-audio <spec>",
909
+ "Audio ref (path|url[:role]); repeatable",
910
+ collect3,
911
+ []
912
+ ).option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds", String(BYTEPLUS_DEFAULTS.videoDuration)).option("--aspect-ratio <r>", "Aspect ratio", BYTEPLUS_DEFAULTS.videoAspectRatio).option("--audio", "Enable audio").option("--no-audio", "Disable audio").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--camera-fixed", "Lock camera").option("--async", "Submit task and exit").option("--wait-timeout <ms>", "Poll timeout in ms", "600000").option("--output <path>", "Output MP4 path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
913
+ async (opts) => {
914
+ const logger = createLogger({ verbose: opts.verbose ?? false });
915
+ const specs = [
916
+ ...opts.refImage.map((s) => ({ kind: "image", spec: s })),
917
+ ...opts.refVideo.map((s) => ({ kind: "video", spec: s })),
918
+ ...opts.refAudio.map((s) => ({ kind: "audio", spec: s }))
919
+ ];
920
+ if (specs.length === 0) {
921
+ logger.error("At least one --ref-image / --ref-video / --ref-audio is required");
922
+ process.exit(2);
923
+ }
924
+ const refs = await resolveRefSpecs(specs, logger);
925
+ validateRefCounts(refs);
926
+ const client = createBytePlusClient();
927
+ const body = buildVideoTaskBody({
928
+ model: opts.model,
929
+ prompt: opts.prompt,
930
+ resolution: opts.resolution,
931
+ duration: Number.parseInt(opts.duration, 10),
932
+ aspectRatio: opts.aspectRatio,
933
+ audio: opts.audio,
934
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
935
+ negativePrompt: opts.negative,
936
+ cameraFixed: opts.cameraFixed,
937
+ references: refs
938
+ });
939
+ logger.info(`Submitting r2v task (${opts.model}, ${refs.length} refs)`);
940
+ const submitted = await submitVideoTask(client, body, logger);
941
+ const taskId = submitted.id;
942
+ if (opts.async) {
943
+ console.log(taskId);
944
+ console.error(`Poll with: multix byteplus status ${taskId}`);
945
+ return;
946
+ }
947
+ try {
948
+ const final = await waitForVideoTask(client, taskId, {
949
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
950
+ logger
951
+ });
952
+ const url = final.content?.video_url;
953
+ if (!url) {
954
+ logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
955
+ process.exit(1);
956
+ }
957
+ const outPath = opts.output ?? path8.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
958
+ await downloadVideo(url, outPath, logger);
959
+ await maybeDownloadThumb(final, outPath, {
960
+ skip: opts.thumb === false,
961
+ copyTo: opts.output,
962
+ logger
963
+ });
964
+ console.log(`
965
+ Task: ${taskId}`);
966
+ console.log(`Saved: ${outPath}`);
967
+ } catch (e) {
968
+ if (e instanceof PollTimeoutError) {
969
+ logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
970
+ process.exit(1);
971
+ }
972
+ if (e instanceof PollFailedError) {
973
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
974
+ process.exit(1);
975
+ }
976
+ throw e;
977
+ }
978
+ }
979
+ );
980
+ }
981
+
982
+ // src/providers/byteplus/commands/status.ts
983
+ import path9 from "path";
984
+ function registerBytePlusStatusCommand(parent) {
985
+ parent.command("status <taskId>").description("Get a Seedance task's status; --wait + --download to fetch the result MP4").option("--wait", "Poll until terminal status").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download MP4 on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
986
+ async (taskId, opts) => {
987
+ const logger = createLogger({ verbose: opts.verbose ?? false });
988
+ const client = createBytePlusClient();
989
+ const shouldWait = !!(opts.wait || opts.download);
990
+ try {
991
+ const status = shouldWait ? await waitForVideoTask(client, taskId, {
992
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
993
+ logger
994
+ }) : await getVideoTaskStatus(client, taskId, logger);
995
+ console.log(JSON.stringify(status, null, 2));
996
+ if (opts.download && status.status === "succeeded") {
997
+ const url = status.content?.video_url;
998
+ if (!url) {
999
+ logger.error("Task succeeded but no video_url present");
1000
+ process.exit(1);
1001
+ }
1002
+ const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
1003
+ await downloadVideo(url, outPath, logger);
1004
+ await maybeDownloadThumb(status, outPath, {
1005
+ skip: opts.thumb === false,
1006
+ copyTo: opts.output,
1007
+ logger
1008
+ });
1009
+ console.log(`Saved: ${outPath}`);
1010
+ }
1011
+ } catch (e) {
1012
+ if (e instanceof PollTimeoutError) {
1013
+ logger.error(`Timed out waiting for task ${taskId}`);
1014
+ process.exit(1);
1015
+ }
1016
+ if (e instanceof PollFailedError) {
1017
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
1018
+ process.exit(1);
1019
+ }
1020
+ throw e;
1021
+ }
1022
+ }
1023
+ );
1024
+ }
1025
+
1026
+ // src/providers/byteplus/commands/video.ts
1027
+ import path10 from "path";
1028
+ function registerBytePlusVideoCommand(parent) {
1029
+ parent.command("video").description("Generate a video from a text prompt via BytePlus Seedance 2.0").requiredOption("--prompt <text>", "Text prompt").option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds (4-15)", String(BYTEPLUS_DEFAULTS.videoDuration)).option(
1030
+ "--aspect-ratio <r>",
1031
+ "Aspect ratio (16:9, 9:16, 1:1, ...)",
1032
+ BYTEPLUS_DEFAULTS.videoAspectRatio
1033
+ ).option("--audio", "Enable audio track").option("--no-audio", "Disable audio track").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--camera-fixed", "Lock camera (no motion)").option("--async", "Submit task and exit; print taskId only").option("--wait-timeout <ms>", "Poll timeout in ms", "600000").option("--output <path>", "Output MP4 path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
1034
+ async (opts) => {
1035
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1036
+ const client = createBytePlusClient();
1037
+ const body = buildVideoTaskBody({
1038
+ model: opts.model,
1039
+ prompt: opts.prompt,
1040
+ resolution: opts.resolution,
1041
+ duration: Number.parseInt(opts.duration, 10),
1042
+ aspectRatio: opts.aspectRatio,
1043
+ audio: opts.audio,
1044
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
1045
+ negativePrompt: opts.negative,
1046
+ cameraFixed: opts.cameraFixed
1047
+ });
1048
+ logger.info(
1049
+ `Submitting Seedance task (${opts.model}, ${opts.resolution}, ${opts.duration}s)`
1050
+ );
1051
+ const submitted = await submitVideoTask(client, body, logger);
1052
+ const taskId = submitted.id;
1053
+ logger.debug(`taskId: ${taskId}`);
1054
+ if (opts.async) {
1055
+ console.log(taskId);
1056
+ console.error(`Poll with: multix byteplus status ${taskId}`);
1057
+ return;
1058
+ }
1059
+ try {
1060
+ const final = await waitForVideoTask(client, taskId, {
1061
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
1062
+ logger
1063
+ });
1064
+ const url = final.content?.video_url;
1065
+ if (!url) {
1066
+ logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
1067
+ process.exit(1);
1068
+ }
1069
+ const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1070
+ await downloadVideo(url, outPath, logger);
1071
+ await maybeDownloadThumb(final, outPath, {
1072
+ skip: opts.thumb === false,
1073
+ copyTo: opts.output,
1074
+ logger
1075
+ });
1076
+ console.log(`
1077
+ Task: ${taskId}`);
1078
+ console.log(`Saved: ${outPath}`);
1079
+ } catch (e) {
1080
+ if (e instanceof PollTimeoutError) {
1081
+ logger.error(
1082
+ `Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
1083
+ );
1084
+ process.exit(1);
1085
+ }
1086
+ if (e instanceof PollFailedError) {
1087
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
1088
+ process.exit(1);
1089
+ }
1090
+ throw e;
1091
+ }
1092
+ }
1093
+ );
1094
+ }
1095
+
1096
+ // src/providers/byteplus/commands/index.ts
1097
+ function registerBytePlusCommands(program2) {
1098
+ const byteplus = program2.command("byteplus").description("BytePlus ModelArk: Seedream image generation, Seedance video generation");
1099
+ registerBytePlusGenerateCommand(byteplus);
1100
+ registerBytePlusImageToImageCommand(byteplus);
1101
+ registerBytePlusVideoCommand(byteplus);
1102
+ registerBytePlusImageToVideoCommand(byteplus);
1103
+ registerBytePlusReferenceToVideoCommand(byteplus);
1104
+ registerBytePlusStatusCommand(byteplus);
1105
+ }
1106
+
1107
+ // src/providers/gemini/commands/analyze.ts
1108
+ init_errors();
1109
+ import fs9 from "fs";
1110
+
1111
+ // src/providers/gemini/client.ts
1112
+ import fs7 from "fs";
1113
+ import path12 from "path";
1114
+ init_errors();
1115
+ init_http_client();
1116
+
1117
+ // src/providers/gemini/task-resolver.ts
1118
+ import path11 from "path";
1119
+ var MIME_MAP = {
1120
+ // Audio
1121
+ ".mp3": "audio/mp3",
1122
+ ".wav": "audio/wav",
1123
+ ".aac": "audio/aac",
1124
+ ".flac": "audio/flac",
1125
+ ".ogg": "audio/ogg",
1126
+ ".aiff": "audio/aiff",
1127
+ ".m4a": "audio/mp4",
1128
+ // Image
1129
+ ".jpg": "image/jpeg",
1130
+ ".jpeg": "image/jpeg",
1131
+ ".png": "image/png",
1132
+ ".webp": "image/webp",
1133
+ ".heic": "image/heic",
1134
+ ".heif": "image/heif",
1135
+ ".gif": "image/gif",
1136
+ ".bmp": "image/bmp",
1137
+ // Video
1138
+ ".mp4": "video/mp4",
1139
+ ".mpeg": "video/mpeg",
1140
+ ".mov": "video/quicktime",
1141
+ ".avi": "video/x-msvideo",
1142
+ ".flv": "video/x-flv",
1143
+ ".mpg": "video/mpeg",
1144
+ ".webm": "video/webm",
1145
+ ".wmv": "video/x-ms-wmv",
1146
+ ".3gpp": "video/3gpp",
1147
+ // Document
1148
+ ".pdf": "application/pdf",
1149
+ ".txt": "text/plain",
1150
+ ".html": "text/html",
1151
+ ".htm": "text/html",
1152
+ ".md": "text/markdown",
1153
+ ".csv": "text/csv",
1154
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1155
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1156
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
1157
+ };
1158
+ function getMimeType(filePath) {
1159
+ const ext = path11.extname(filePath).toLowerCase();
1160
+ return MIME_MAP[ext] ?? "application/octet-stream";
1161
+ }
1162
+ function requiresProcessingWait(mimeType) {
1163
+ return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
1164
+ }
1165
+
1166
+ // src/providers/gemini/client.ts
1167
+ var BASE = "https://generativelanguage.googleapis.com";
1168
+ var FILES_BASE = `${BASE}/upload/v1beta/files`;
1169
+ var GENERATE_BASE = `${BASE}/v1beta/models`;
1170
+ function requireApiKey() {
1171
+ const key = resolveKey("GEMINI_API_KEY");
1172
+ if (!key)
1173
+ throw new ConfigError(
1174
+ "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
1175
+ );
1176
+ return key;
1177
+ }
1178
+ function authHeaders(apiKey) {
1179
+ return { "x-goog-api-key": apiKey };
1180
+ }
1181
+ async function uploadFile(filePath, opts = {}) {
1182
+ const apiKey = requireApiKey();
1183
+ const { timeoutMs = 3e5, logger } = opts;
1184
+ const mimeType = getMimeType(filePath);
1185
+ const fileBytes = fs7.readFileSync(filePath);
1186
+ const displayName = path12.basename(filePath);
1187
+ const boundary = `multix_${Date.now()}`;
1188
+ const metaJson = JSON.stringify({ file: { displayName, mimeType } });
1189
+ const body = Buffer.concat([
1190
+ Buffer.from(
1191
+ `--${boundary}\r
1192
+ Content-Type: application/json; charset=UTF-8\r
1193
+ \r
1194
+ ${metaJson}\r
1195
+ --${boundary}\r
1196
+ Content-Type: ${mimeType}\r
1197
+ \r
1198
+ `
1199
+ ),
1200
+ fileBytes,
1201
+ Buffer.from(`\r
1202
+ --${boundary}--`)
1203
+ ]);
1204
+ logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
1205
+ const resp = await globalThis.fetch(FILES_BASE, {
1206
+ method: "POST",
1207
+ headers: {
1208
+ "x-goog-api-key": apiKey,
1209
+ "Content-Type": `multipart/related; boundary=${boundary}`,
1210
+ "Content-Length": String(body.length),
1211
+ "X-Goog-Upload-Protocol": "multipart"
1212
+ },
1213
+ body,
1214
+ signal: AbortSignal.timeout(timeoutMs)
1215
+ });
1216
+ if (!resp.ok) {
1217
+ const txt = await resp.text();
1218
+ throw new ProviderError(
1219
+ `File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
1220
+ "gemini"
1221
+ );
1222
+ }
1223
+ const data = await resp.json();
1224
+ let fileRef = data.file;
1225
+ if (requiresProcessingWait(mimeType)) {
1226
+ const deadline = Date.now() + timeoutMs;
1227
+ while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
1228
+ await sleep(2e3);
1229
+ fileRef = await httpJson({
1230
+ url: `${BASE}/v1beta/${fileRef.name}`,
1231
+ headers: authHeaders(apiKey)
1232
+ }).then((r) => r.file);
1233
+ logger?.debug(`Processing... state=${fileRef.state}`);
1234
+ }
1235
+ if (fileRef.state === "FAILED")
1236
+ throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
1237
+ if (fileRef.state === "PROCESSING")
1238
+ throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
1239
+ }
1240
+ logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
1241
+ return fileRef;
1242
+ }
1243
+ async function generateContent(req) {
1244
+ const apiKey = requireApiKey();
1245
+ return httpJson({
1246
+ url: `${GENERATE_BASE}/${req.model}:generateContent`,
1247
+ method: "POST",
1248
+ headers: authHeaders(apiKey),
1249
+ body: {
1250
+ contents: req.contents,
1251
+ ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
1252
+ }
1253
+ });
1254
+ }
1255
+ function extractText(resp) {
1256
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
1257
+ return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
1258
+ }
1259
+ function extractAudio(resp) {
1260
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
1261
+ for (const p of parts) {
1262
+ if (p.inlineData?.mimeType.startsWith("audio/")) {
1263
+ return { mimeType: p.inlineData.mimeType, data: p.inlineData.data };
1264
+ }
1265
+ }
1266
+ return null;
1267
+ }
1268
+ function extractImages(resp) {
1269
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
1270
+ return parts.filter(
1271
+ (p) => p.inlineData !== void 0
1272
+ ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
1273
+ }
1274
+ function inlinePart(filePath) {
1275
+ const data = fs7.readFileSync(filePath).toString("base64");
1276
+ const mimeType = getMimeType(filePath);
1277
+ return { inlineData: { mimeType, data } };
1278
+ }
1279
+ function shouldUseFileApi(filePath) {
1280
+ const stat = fs7.statSync(filePath);
1281
+ return stat.size > 20 * 1024 * 1024;
1282
+ }
1283
+ function sleep(ms) {
1284
+ return new Promise((resolve) => setTimeout(resolve, ms));
1285
+ }
1286
+
1287
+ // src/providers/gemini/models.ts
1288
+ var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
1289
+ var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
1290
+ var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
1291
+ var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
1292
+ var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
1293
+ var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
1294
+ var IMAGEN_MODELS = /* @__PURE__ */ new Set([
1295
+ "imagen-4.0-generate-001",
1296
+ "imagen-4.0-ultra-generate-001",
1297
+ "imagen-4.0-fast-generate-001"
1298
+ ]);
1299
+ var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
1300
+ "gemini-3.1-flash-image-preview",
1301
+ "gemini-3-pro-image-preview",
1302
+ "gemini-2.5-flash-image",
1303
+ "gemini-2.5-flash-image-preview",
1304
+ ...IMAGEN_MODELS
1305
+ ]);
1306
+ var ASPECT_RATIOS = [
1307
+ "1:1",
1308
+ "2:3",
1309
+ "3:2",
1310
+ "3:4",
1311
+ "4:3",
1312
+ "4:5",
1313
+ "5:4",
1314
+ "9:16",
1315
+ "16:9",
1316
+ "21:9"
1317
+ ];
1318
+ var IMAGE_SIZES = ["1K", "2K", "4K"];
1319
+ function getDefaultModel(task) {
1320
+ if (task === "generate") {
1321
+ return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
1322
+ }
1323
+ if (task === "generate-video") {
1324
+ return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
1325
+ }
1326
+ if (task === "generate-speech") {
1327
+ return process.env.GEMINI_TTS_MODEL ?? process.env.TTS_MODEL ?? TTS_MODEL_DEFAULT;
1328
+ }
1329
+ return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
1330
+ }
1331
+
1332
+ // src/providers/gemini/result-formatter.ts
1333
+ import fs8 from "fs";
1334
+ import path13 from "path";
1335
+ function formatResults(results, format, outputPath) {
1336
+ const content = renderFormat(results, format);
1337
+ if (outputPath) {
1338
+ fs8.mkdirSync(path13.dirname(path13.resolve(outputPath)), { recursive: true });
1339
+ fs8.writeFileSync(outputPath, content, "utf8");
1340
+ } else {
1341
+ process.stdout.write(content);
1342
+ }
1343
+ }
1344
+ function renderFormat(results, format) {
1345
+ switch (format) {
1346
+ case "json":
1347
+ return `${JSON.stringify(results, null, 2)}
1348
+ `;
1349
+ case "csv":
1350
+ return renderCsv(results);
1351
+ case "markdown":
1352
+ return renderMarkdown(results);
1353
+ default:
1354
+ return renderText(results);
467
1355
  }
468
1356
  }
469
1357
  function renderText(results) {
@@ -481,46 +1369,727 @@ Error: ${r.error}
481
1369
  `;
482
1370
  }).join("\n");
483
1371
  }
484
- function renderCsv(results) {
485
- const header = "file,status,response,error\n";
486
- const rows = results.map((r) => {
487
- const csvCell = (v) => `"${(v ?? "").replace(/"/g, '""')}"`;
488
- return [csvCell(r.file), csvCell(r.status), csvCell(r.response), csvCell(r.error)].join(",");
489
- });
490
- return `${header + rows.join("\n")}
491
- `;
1372
+ function renderCsv(results) {
1373
+ const header = "file,status,response,error\n";
1374
+ const rows = results.map((r) => {
1375
+ const csvCell = (v) => `"${(v ?? "").replace(/"/g, '""')}"`;
1376
+ return [csvCell(r.file), csvCell(r.status), csvCell(r.response), csvCell(r.error)].join(",");
1377
+ });
1378
+ return `${header + rows.join("\n")}
1379
+ `;
1380
+ }
1381
+ function renderMarkdown(results) {
1382
+ const lines = ["# Results\n"];
1383
+ for (const r of results) {
1384
+ lines.push(`## ${r.file}
1385
+ `);
1386
+ lines.push(`**Status**: ${r.status}
1387
+ `);
1388
+ if (r.status === "success" && r.response) lines.push(`**Response**:
1389
+
1390
+ ${r.response}
1391
+ `);
1392
+ if (r.status === "error") lines.push(`**Error**: ${r.error}
1393
+ `);
1394
+ lines.push("---\n");
1395
+ }
1396
+ return lines.join("\n");
1397
+ }
1398
+
1399
+ // src/providers/gemini/commands/analyze.ts
1400
+ function registerAnalyzeCommand(parent) {
1401
+ parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
1402
+ async (opts) => {
1403
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1404
+ const model = opts.model ?? getDefaultModel("analyze");
1405
+ for (const f of opts.files) {
1406
+ if (!fs9.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
1407
+ }
1408
+ const results = [];
1409
+ for (let i = 0; i < opts.files.length; i++) {
1410
+ const filePath = opts.files[i];
1411
+ logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
1412
+ try {
1413
+ const useApi = shouldUseFileApi(filePath);
1414
+ let contentParts;
1415
+ if (useApi) {
1416
+ logger.debug("File >20MB \u2014 using Files API");
1417
+ const ref = await uploadFile(filePath, { logger });
1418
+ contentParts = [
1419
+ { text: opts.prompt },
1420
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
1421
+ ];
1422
+ } else {
1423
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
1424
+ }
1425
+ const resp = await generateContent({
1426
+ model,
1427
+ contents: [{ parts: contentParts }]
1428
+ });
1429
+ results.push({
1430
+ file: filePath,
1431
+ status: "success",
1432
+ response: extractText(resp)
1433
+ });
1434
+ } catch (e) {
1435
+ const msg = e instanceof Error ? e.message : String(e);
1436
+ logger.error(`Failed: ${filePath} \u2014 ${msg}`);
1437
+ results.push({ file: filePath, status: "error", error: msg });
1438
+ }
1439
+ }
1440
+ formatResults(results, opts.format, opts.output);
1441
+ logger.debug(
1442
+ `Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
1443
+ );
1444
+ const failed = results.filter((r) => r.status === "error").length;
1445
+ if (failed > 0) process.exit(1);
1446
+ }
1447
+ );
1448
+ }
1449
+
1450
+ // src/providers/gemini/commands/extract.ts
1451
+ init_errors();
1452
+ import fs10 from "fs";
1453
+ function registerExtractCommand(parent) {
1454
+ parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths").requiredOption("--prompt <text>", "Extraction prompt (required)").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
1455
+ async (opts) => {
1456
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1457
+ const model = opts.model ?? getDefaultModel("extract");
1458
+ for (const f of opts.files) {
1459
+ if (!fs10.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
1460
+ }
1461
+ const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
1462
+ const results = [];
1463
+ for (let i = 0; i < opts.files.length; i++) {
1464
+ const filePath = opts.files[i];
1465
+ logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
1466
+ try {
1467
+ const useApi = shouldUseFileApi(filePath);
1468
+ let contentParts;
1469
+ if (useApi) {
1470
+ const ref = await uploadFile(filePath, { logger });
1471
+ contentParts = [
1472
+ { text: opts.prompt },
1473
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
1474
+ ];
1475
+ } else {
1476
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
1477
+ }
1478
+ const resp = await generateContent({
1479
+ model,
1480
+ contents: [{ parts: contentParts }],
1481
+ generationConfig
1482
+ });
1483
+ results.push({ file: filePath, status: "success", response: extractText(resp) });
1484
+ } catch (e) {
1485
+ const msg = e instanceof Error ? e.message : String(e);
1486
+ logger.error(`Failed: ${filePath} \u2014 ${msg}`);
1487
+ results.push({ file: filePath, status: "error", error: msg });
1488
+ }
1489
+ }
1490
+ formatResults(results, opts.format, opts.output);
1491
+ const failed = results.filter((r) => r.status === "error").length;
1492
+ if (failed > 0) process.exit(1);
1493
+ }
1494
+ );
1495
+ }
1496
+
1497
+ // src/providers/gemini/commands/generate-speech.ts
1498
+ init_errors();
1499
+
1500
+ // src/providers/gemini/generators/speech.ts
1501
+ import fs11 from "fs";
1502
+ import path14 from "path";
1503
+
1504
+ // src/providers/gemini/voices.ts
1505
+ var GEMINI_TTS_VOICES = [
1506
+ "Zephyr",
1507
+ "Puck",
1508
+ "Charon",
1509
+ "Kore",
1510
+ "Fenrir",
1511
+ "Leda",
1512
+ "Orus",
1513
+ "Aoede",
1514
+ "Callirrhoe",
1515
+ "Autonoe",
1516
+ "Enceladus",
1517
+ "Iapetus",
1518
+ "Umbriel",
1519
+ "Algieba",
1520
+ "Despina",
1521
+ "Erinome",
1522
+ "Algenib",
1523
+ "Rasalgethi",
1524
+ "Laomedeia",
1525
+ "Achernar",
1526
+ "Alnilam",
1527
+ "Schedar",
1528
+ "Gacrux",
1529
+ "Pulcherrima",
1530
+ "Achird",
1531
+ "Zubenelgenubi",
1532
+ "Vindemiatrix",
1533
+ "Sadachbia",
1534
+ "Sadaltager",
1535
+ "Sulafat"
1536
+ ];
1537
+ var GEMINI_TTS_MODELS = /* @__PURE__ */ new Set([
1538
+ "gemini-3.1-flash-tts-preview",
1539
+ "gemini-2.5-flash-preview-tts",
1540
+ "gemini-2.5-pro-preview-tts"
1541
+ ]);
1542
+ var TTS_VOICE_DEFAULT = "Kore";
1543
+ var TTS_PCM_SAMPLE_RATE = 24e3;
1544
+ var TTS_PCM_CHANNELS = 1;
1545
+ var TTS_PCM_BITS_PER_SAMPLE = 16;
1546
+ var TTS_OUTPUT_FORMATS = ["wav", "pcm"];
1547
+ function isValidGeminiVoice(name) {
1548
+ return GEMINI_TTS_VOICES.includes(name);
1549
+ }
1550
+
1551
+ // src/providers/gemini/generators/speech.ts
1552
+ async function generateGeminiSpeech(opts) {
1553
+ const { text, model, voice, speakers, outputFormat, output, logger } = opts;
1554
+ const speechConfig = speakers && speakers.length > 0 ? {
1555
+ multiSpeakerVoiceConfig: {
1556
+ speakerVoiceConfigs: speakers.map((s) => ({
1557
+ speaker: s.speaker,
1558
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: s.voice } }
1559
+ }))
1560
+ }
1561
+ } : {
1562
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } }
1563
+ };
1564
+ const generationConfig = {
1565
+ responseModalities: ["AUDIO"],
1566
+ speechConfig
1567
+ };
1568
+ logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
1569
+ let resp;
1570
+ try {
1571
+ resp = await generateContent({
1572
+ model,
1573
+ contents: [{ parts: [{ text }] }],
1574
+ generationConfig
1575
+ });
1576
+ } catch (e) {
1577
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1578
+ }
1579
+ const audio = extractAudio(resp);
1580
+ if (!audio) return { status: "error", error: "No audio in response" };
1581
+ const pcmBytes = Buffer.from(audio.data, "base64");
1582
+ const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
1583
+ const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
1584
+ const outDir = getOutputDir();
1585
+ const dest = path14.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
1586
+ fs11.writeFileSync(dest, outBytes);
1587
+ logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
1588
+ if (output) {
1589
+ fs11.mkdirSync(path14.dirname(path14.resolve(output)), { recursive: true });
1590
+ fs11.copyFileSync(dest, output);
1591
+ logger?.success(`Copied to: ${output}`);
1592
+ }
1593
+ return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
1594
+ }
1595
+ function parseSampleRate(mimeType) {
1596
+ const m = mimeType.match(/rate=(\d+)/i);
1597
+ return m ? Number.parseInt(m[1], 10) : null;
1598
+ }
1599
+ function wrapPcmInWav(pcm, sampleRate, channels, bitsPerSample) {
1600
+ const byteRate = sampleRate * channels * bitsPerSample / 8;
1601
+ const blockAlign = channels * bitsPerSample / 8;
1602
+ const dataSize = pcm.length;
1603
+ const header = Buffer.alloc(44);
1604
+ header.write("RIFF", 0, "ascii");
1605
+ header.writeUInt32LE(36 + dataSize, 4);
1606
+ header.write("WAVE", 8, "ascii");
1607
+ header.write("fmt ", 12, "ascii");
1608
+ header.writeUInt32LE(16, 16);
1609
+ header.writeUInt16LE(1, 20);
1610
+ header.writeUInt16LE(channels, 22);
1611
+ header.writeUInt32LE(sampleRate, 24);
1612
+ header.writeUInt32LE(byteRate, 28);
1613
+ header.writeUInt16LE(blockAlign, 32);
1614
+ header.writeUInt16LE(bitsPerSample, 34);
1615
+ header.write("data", 36, "ascii");
1616
+ header.writeUInt32LE(dataSize, 40);
1617
+ return Buffer.concat([header, pcm]);
1618
+ }
1619
+
1620
+ // src/providers/gemini/commands/generate-speech.ts
1621
+ function registerGeminiGenerateSpeechCommand(parent) {
1622
+ parent.command("generate-speech").description("Generate speech (TTS) with Gemini Flash TTS").option("--text <str>", "Text to speak (use --text or --prompt)").option("--prompt <str>", "Alias for --text").option("--model <id>", `TTS model (${[...GEMINI_TTS_MODELS].join("|")})`).option("--voice <name>", "Prebuilt voice for single-speaker mode", TTS_VOICE_DEFAULT).option(
1623
+ "--speaker <name:voice>",
1624
+ "Speaker mapping (repeatable, max 2 \u2014 switches to multi-speaker mode)",
1625
+ collectSpeakers,
1626
+ []
1627
+ ).option("--output-format <fmt>", `Container (${TTS_OUTPUT_FORMATS.join("|")})`, "wav").option("--output <path>", "Copy output audio to this path").option("-v, --verbose", "Verbose logging").action(
1628
+ async (opts) => {
1629
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1630
+ const text = opts.text ?? opts.prompt;
1631
+ if (!text) throw new ValidationError("Either --text or --prompt is required");
1632
+ const model = opts.model ?? getDefaultModel("generate-speech");
1633
+ if (!GEMINI_TTS_MODELS.has(model)) {
1634
+ throw new ValidationError(`--model must be one of: ${[...GEMINI_TTS_MODELS].join(", ")}`);
1635
+ }
1636
+ if (!TTS_OUTPUT_FORMATS.includes(opts.outputFormat)) {
1637
+ throw new ValidationError(
1638
+ `--output-format must be one of: ${TTS_OUTPUT_FORMATS.join(", ")}`
1639
+ );
1640
+ }
1641
+ const speakers = opts.speaker;
1642
+ if (speakers.length > 2) {
1643
+ throw new ValidationError("Gemini TTS supports max 2 speakers in multi-speaker mode");
1644
+ }
1645
+ if (speakers.length === 0 && !isValidGeminiVoice(opts.voice)) {
1646
+ throw new ValidationError(
1647
+ `Invalid --voice "${opts.voice}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
1648
+ );
1649
+ }
1650
+ for (const sv of speakers) {
1651
+ if (!isValidGeminiVoice(sv.voice)) {
1652
+ throw new ValidationError(
1653
+ `Invalid voice "${sv.voice}" for speaker "${sv.speaker}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
1654
+ );
1655
+ }
1656
+ }
1657
+ const result = await generateGeminiSpeech({
1658
+ text,
1659
+ model,
1660
+ voice: speakers.length === 0 ? opts.voice : void 0,
1661
+ speakers: speakers.length > 0 ? speakers : void 0,
1662
+ outputFormat: opts.outputFormat,
1663
+ output: opts.output,
1664
+ logger
1665
+ });
1666
+ if (result.status === "error") {
1667
+ logger.error(result.error ?? "Unknown error");
1668
+ process.exit(1);
1669
+ }
1670
+ console.log(`
1671
+ Generated audio: ${result.generatedAudio}`);
1672
+ }
1673
+ );
1674
+ }
1675
+ function collectSpeakers(value, prev) {
1676
+ const idx = value.indexOf(":");
1677
+ if (idx <= 0 || idx === value.length - 1) {
1678
+ throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
1679
+ }
1680
+ const speaker = value.slice(0, idx).trim();
1681
+ const voice = value.slice(idx + 1).trim();
1682
+ if (!speaker || !voice) {
1683
+ throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
1684
+ }
1685
+ return [...prev, { speaker, voice }];
1686
+ }
1687
+
1688
+ // src/providers/gemini/commands/generate-video.ts
1689
+ import fs12 from "fs";
1690
+ import path15 from "path";
1691
+ init_errors();
1692
+ init_http_client();
1693
+ var BASE2 = "https://generativelanguage.googleapis.com";
1694
+ function registerGenerateVideoCommand(parent) {
1695
+ parent.command("generate-video").description("Generate video with Veo via Gemini API [EXPERIMENTAL]").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", "Veo model to use").option("--resolution <res>", "Video resolution: 720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option(
1696
+ "--reference-images <paths...>",
1697
+ "Reference images (first=opening frame, second=closing)"
1698
+ ).option("--upload-timeout <ms>", "Upload timeout in ms", "120000").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
1699
+ async (opts) => {
1700
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1701
+ const apiKey = resolveKey("GEMINI_API_KEY");
1702
+ if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
1703
+ const model = opts.model ?? getDefaultModel("generate-video");
1704
+ if (!model.startsWith("veo-")) {
1705
+ throw new ValidationError(
1706
+ `Video generation requires a Veo model (got '${model}'). Use: veo-3.1-generate-preview, veo-3.0-generate-001, etc.`
1707
+ );
1708
+ }
1709
+ if (!["720p", "1080p"].includes(opts.resolution)) {
1710
+ throw new ValidationError("--resolution must be 720p or 1080p");
1711
+ }
1712
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
1713
+ throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
1714
+ }
1715
+ logger.debug(`Submitting video generation with ${model}...`);
1716
+ logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
1717
+ const config = {
1718
+ aspectRatio: opts.aspectRatio,
1719
+ resolution: opts.resolution
1720
+ };
1721
+ const contentParts = [{ text: opts.prompt }];
1722
+ if (opts.referenceImages?.length) {
1723
+ for (const imgPath of opts.referenceImages.slice(0, 2)) {
1724
+ if (!fs12.existsSync(imgPath))
1725
+ throw new ValidationError(`Reference image not found: ${imgPath}`);
1726
+ const data = fs12.readFileSync(imgPath).toString("base64");
1727
+ const ext = path15.extname(imgPath).toLowerCase();
1728
+ const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
1729
+ contentParts.push({ inlineData: { mimeType, data } });
1730
+ }
1731
+ }
1732
+ const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
1733
+ let operation;
1734
+ try {
1735
+ operation = await httpJson({
1736
+ url: `${BASE2}/v1beta/models/${model}:generateVideos`,
1737
+ method: "POST",
1738
+ headers,
1739
+ body: {
1740
+ contents: [{ parts: contentParts }],
1741
+ generationConfig: config
1742
+ }
1743
+ });
1744
+ } catch (e) {
1745
+ const msg = e instanceof Error ? e.message : String(e);
1746
+ throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
1747
+ }
1748
+ const operationName = operation.name;
1749
+ logger.debug(`Operation: ${operationName}`);
1750
+ const deadline = Date.now() + 10 * 60 * 1e3;
1751
+ let done = false;
1752
+ let finalOp = operation;
1753
+ while (!done && Date.now() < deadline) {
1754
+ await sleep2(1e4);
1755
+ finalOp = await httpJson({
1756
+ url: `${BASE2}/v1beta/${operationName}`,
1757
+ headers
1758
+ });
1759
+ done = finalOp.done === true;
1760
+ if (!done)
1761
+ logger.debug(
1762
+ `Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
1763
+ );
1764
+ }
1765
+ if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
1766
+ if (finalOp.error)
1767
+ throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
1768
+ const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
1769
+ if (!videoUri) {
1770
+ throw new ProviderError(
1771
+ `Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
1772
+ "gemini"
1773
+ );
1774
+ }
1775
+ const outDir = getOutputDir();
1776
+ const dest = path15.join(outDir, `veo_generated_${Date.now()}.mp4`);
1777
+ const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
1778
+ await downloadFile2(videoUri, dest);
1779
+ logger.success(`Saved video: ${dest}`);
1780
+ if (opts.output) {
1781
+ fs12.mkdirSync(path15.dirname(path15.resolve(opts.output)), { recursive: true });
1782
+ fs12.copyFileSync(dest, opts.output);
1783
+ logger.success(`Copied to: ${opts.output}`);
1784
+ }
1785
+ await maybeDownloadThumb(finalOp.response, dest, {
1786
+ skip: opts.thumb === false,
1787
+ copyTo: opts.output,
1788
+ logger
1789
+ });
1790
+ console.log(`
1791
+ Generated video: ${dest}`);
1792
+ }
1793
+ );
1794
+ }
1795
+ function sleep2(ms) {
1796
+ return new Promise((r) => setTimeout(r, ms));
1797
+ }
1798
+
1799
+ // src/providers/gemini/commands/generate.ts
1800
+ init_errors();
1801
+ import fs13 from "fs";
1802
+ import path16 from "path";
1803
+ function registerGenerateCommand(parent) {
1804
+ parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").option("-v, --verbose", "Verbose logging").action(
1805
+ async (opts) => {
1806
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1807
+ const model = opts.model ?? getDefaultModel("generate");
1808
+ const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 4);
1809
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
1810
+ throw new ValidationError(
1811
+ `Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
1812
+ );
1813
+ }
1814
+ if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
1815
+ throw new ValidationError(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
1816
+ }
1817
+ logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
1818
+ logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
1819
+ const imageConfig = { aspectRatio: opts.aspectRatio };
1820
+ if (opts.size) imageConfig.imageSize = opts.size;
1821
+ const generationConfig = {
1822
+ responseModalities: ["IMAGE"],
1823
+ imageConfig
1824
+ };
1825
+ const savedFiles = [];
1826
+ for (let i = 0; i < numImages; i++) {
1827
+ try {
1828
+ const resp = await generateContent({
1829
+ model,
1830
+ contents: [{ parts: [{ text: opts.prompt }] }],
1831
+ generationConfig
1832
+ });
1833
+ const images = extractImages(resp);
1834
+ if (images.length === 0) {
1835
+ logger.warn(
1836
+ `Request ${i + 1}: no images in response \u2014 model may not support image generation`
1837
+ );
1838
+ continue;
1839
+ }
1840
+ for (const img of images) {
1841
+ const outDir = getOutputDir();
1842
+ const ext = img.mimeType.includes("png") ? "png" : "jpg";
1843
+ const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
1844
+ const dest = path16.join(outDir, fname);
1845
+ fs13.writeFileSync(dest, Buffer.from(img.data, "base64"));
1846
+ savedFiles.push(dest);
1847
+ logger.success(`Saved: ${dest}`);
1848
+ }
1849
+ } catch (e) {
1850
+ const msg = e instanceof Error ? e.message : String(e);
1851
+ logger.error(`Generation failed: ${msg}`);
1852
+ process.exit(1);
1853
+ }
1854
+ }
1855
+ if (savedFiles.length === 0) {
1856
+ logger.error("No images were generated");
1857
+ process.exit(1);
1858
+ }
1859
+ if (opts.output && savedFiles[0]) {
1860
+ fs13.mkdirSync(path16.dirname(path16.resolve(opts.output)), { recursive: true });
1861
+ fs13.copyFileSync(savedFiles[0], opts.output);
1862
+ logger.success(`Copied to: ${opts.output}`);
1863
+ }
1864
+ console.log(`
1865
+ Generated ${savedFiles.length} image(s):`);
1866
+ for (const f of savedFiles) console.log(` ${f}`);
1867
+ }
1868
+ );
1869
+ }
1870
+
1871
+ // src/providers/gemini/commands/image-to-image.ts
1872
+ import fs14 from "fs";
1873
+ import path17 from "path";
1874
+ var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
1875
+ var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
1876
+ function registerGeminiImageToImageCommand(parent) {
1877
+ parent.command("image-to-image").alias("i2i").description(
1878
+ "Edit/compose images with Gemini 2.5 Flash Image (Nano Banana) using one or more reference images"
1879
+ ).requiredOption("--prompt <text>", "Text prompt describing the edit/transformation").requiredOption(
1880
+ "--ref <path>",
1881
+ "Reference image (URL or local path); repeatable for multi-ref",
1882
+ collect4,
1883
+ []
1884
+ ).option("-m, --model <id>", `Gemini image model (default ${IMAGE_MODEL_FALLBACK})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
1885
+ async (opts) => {
1886
+ if (!opts.ref || opts.ref.length === 0) {
1887
+ throw new Error("At least one --ref is required for image-to-image.");
1888
+ }
1889
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1890
+ const model = opts.model ?? IMAGE_MODEL_FALLBACK;
1891
+ const parts = [];
1892
+ for (const r of opts.ref) {
1893
+ const resolved = await resolveImageInput(r, {
1894
+ logger,
1895
+ softLimitBytes: SOFT_LIMIT_BYTES,
1896
+ hardLimitBytes: HARD_LIMIT_BYTES
1897
+ });
1898
+ if (resolved.kind === "url") {
1899
+ const buf = await fetchUrlAsBuffer(resolved.url);
1900
+ const mime = sniffImageMime(buf, resolved.url);
1901
+ if (buf.byteLength > HARD_LIMIT_BYTES) {
1902
+ throw new Error(
1903
+ `Reference ${resolved.url} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds Gemini ${HARD_LIMIT_BYTES / 1024 / 1024}MB inline cap.`
1904
+ );
1905
+ }
1906
+ parts.push({ inlineData: { mimeType: mime, data: buf.toString("base64") } });
1907
+ } else {
1908
+ const data = resolved.dataUrl.split(",", 2)[1] ?? "";
1909
+ parts.push({ inlineData: { mimeType: resolved.mime, data } });
1910
+ }
1911
+ }
1912
+ parts.push({ text: opts.prompt });
1913
+ logger.debug(`Calling ${model} with ${opts.ref.length} reference image(s)`);
1914
+ const req = {
1915
+ model,
1916
+ contents: [{ parts }],
1917
+ generationConfig: { responseModalities: ["IMAGE"] }
1918
+ };
1919
+ const resp = await generateContent(req);
1920
+ const images = extractImages(resp);
1921
+ if (images.length === 0) {
1922
+ logger.error(
1923
+ "No images in response \u2014 model may have refused or doesn't support image output."
1924
+ );
1925
+ process.exit(1);
1926
+ }
1927
+ const outDir = getOutputDir();
1928
+ const ts = Date.now();
1929
+ const saved = [];
1930
+ for (let i = 0; i < images.length; i++) {
1931
+ const img = images[i];
1932
+ if (!img) continue;
1933
+ const ext = img.mimeType.includes("png") ? "png" : "jpg";
1934
+ const dest = path17.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
1935
+ fs14.writeFileSync(dest, Buffer.from(img.data, "base64"));
1936
+ saved.push(dest);
1937
+ logger.success(`Saved: ${dest}`);
1938
+ }
1939
+ if (opts.output && saved[0]) {
1940
+ fs14.mkdirSync(path17.dirname(path17.resolve(opts.output)), { recursive: true });
1941
+ fs14.copyFileSync(saved[0], opts.output);
1942
+ logger.success(`Copied to: ${opts.output}`);
1943
+ }
1944
+ console.log(`
1945
+ Generated ${saved.length} image(s):`);
1946
+ for (const f of saved) console.log(` ${f}`);
1947
+ }
1948
+ );
1949
+ }
1950
+ function collect4(value, prev) {
1951
+ return [...prev, value];
1952
+ }
1953
+ async function fetchUrlAsBuffer(url) {
1954
+ const res = await fetch(url);
1955
+ if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
1956
+ const arr = await res.arrayBuffer();
1957
+ return Buffer.from(arr);
1958
+ }
1959
+ function sniffImageMime(buf, url) {
1960
+ if (buf.length >= 4) {
1961
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
1962
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71)
1963
+ return "image/png";
1964
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
1965
+ if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
1966
+ return "image/webp";
1967
+ }
1968
+ const ext = path17.extname(new URL(url).pathname).toLowerCase();
1969
+ if (ext === ".png") return "image/png";
1970
+ if (ext === ".webp") return "image/webp";
1971
+ if (ext === ".gif") return "image/gif";
1972
+ return "image/jpeg";
492
1973
  }
493
- function renderMarkdown(results) {
494
- const lines = ["# Results\n"];
495
- for (const r of results) {
496
- lines.push(`## ${r.file}
497
- `);
498
- lines.push(`**Status**: ${r.status}
499
- `);
500
- if (r.status === "success" && r.response) lines.push(`**Response**:
501
1974
 
502
- ${r.response}
503
- `);
504
- if (r.status === "error") lines.push(`**Error**: ${r.error}
505
- `);
506
- lines.push("---\n");
1975
+ // src/providers/gemini/commands/image-to-video.ts
1976
+ import fs15 from "fs";
1977
+ import path18 from "path";
1978
+ init_errors();
1979
+ init_http_client();
1980
+ var BASE3 = "https://generativelanguage.googleapis.com";
1981
+ function imagePart(imgPath) {
1982
+ if (!fs15.existsSync(imgPath)) {
1983
+ throw new ValidationError(`Image not found: ${imgPath}`);
507
1984
  }
508
- return lines.join("\n");
1985
+ const data = fs15.readFileSync(imgPath).toString("base64");
1986
+ const ext = path18.extname(imgPath).toLowerCase();
1987
+ const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
1988
+ return { inlineData: { mimeType, data } };
1989
+ }
1990
+ function sleep3(ms) {
1991
+ return new Promise((r) => setTimeout(r, ms));
1992
+ }
1993
+ function registerGeminiImageToVideoCommand(parent) {
1994
+ parent.command("image-to-video <imagePath>").alias("i2v").description("Generate video from a starting image with Veo (image-to-video) [EXPERIMENTAL]").requiredOption("--prompt <text>", "Motion/scene prompt").option("--last-frame <path>", "Optional closing frame image (Veo 3.1)").option("--model <id>", "Veo model id").option("--resolution <res>", "720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
1995
+ async (imagePath, opts) => {
1996
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1997
+ const apiKey = resolveKey("GEMINI_API_KEY");
1998
+ if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
1999
+ const model = opts.model ?? getDefaultModel("generate-video");
2000
+ if (!model.startsWith("veo-")) {
2001
+ throw new ValidationError(
2002
+ `Image-to-video requires a Veo model (got '${model}'). Try veo-3.1-generate-preview.`
2003
+ );
2004
+ }
2005
+ if (!["720p", "1080p"].includes(opts.resolution)) {
2006
+ throw new ValidationError("--resolution must be 720p or 1080p");
2007
+ }
2008
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
2009
+ throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
2010
+ }
2011
+ const contentParts = [
2012
+ { text: opts.prompt },
2013
+ imagePart(imagePath)
2014
+ ];
2015
+ if (opts.lastFrame) contentParts.push(imagePart(opts.lastFrame));
2016
+ const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
2017
+ logger.info(
2018
+ "Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
2019
+ );
2020
+ let operation;
2021
+ try {
2022
+ operation = await httpJson({
2023
+ url: `${BASE3}/v1beta/models/${model}:generateVideos`,
2024
+ method: "POST",
2025
+ headers,
2026
+ body: {
2027
+ contents: [{ parts: contentParts }],
2028
+ generationConfig: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
2029
+ }
2030
+ });
2031
+ } catch (e) {
2032
+ throw new ProviderError(
2033
+ `Submit failed: ${e instanceof Error ? e.message : String(e)}`,
2034
+ "gemini"
2035
+ );
2036
+ }
2037
+ const operationName = operation.name;
2038
+ logger.debug(`Operation: ${operationName}`);
2039
+ const deadline = Date.now() + 10 * 60 * 1e3;
2040
+ let finalOp = operation;
2041
+ while (!finalOp.done && Date.now() < deadline) {
2042
+ await sleep3(1e4);
2043
+ finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
2044
+ if (!finalOp.done) {
2045
+ logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
2046
+ }
2047
+ }
2048
+ if (!finalOp.done) throw new ProviderError("Veo timed out after 10 minutes", "gemini");
2049
+ if (finalOp.error) {
2050
+ throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
2051
+ }
2052
+ const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
2053
+ if (!videoUri) {
2054
+ throw new ProviderError(
2055
+ `Unexpected Veo response: ${JSON.stringify(finalOp.response)}`,
2056
+ "gemini"
2057
+ );
2058
+ }
2059
+ const dest = path18.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
2060
+ await downloadFile(videoUri, dest);
2061
+ logger.success(`Saved video: ${dest}`);
2062
+ if (opts.output) {
2063
+ fs15.mkdirSync(path18.dirname(path18.resolve(opts.output)), { recursive: true });
2064
+ fs15.copyFileSync(dest, opts.output);
2065
+ logger.success(`Copied to: ${opts.output}`);
2066
+ }
2067
+ await maybeDownloadThumb(finalOp.response, dest, {
2068
+ skip: opts.thumb === false,
2069
+ copyTo: opts.output,
2070
+ logger
2071
+ });
2072
+ console.log(`
2073
+ Generated video: ${dest}`);
2074
+ }
2075
+ );
509
2076
  }
510
2077
 
511
- // src/providers/gemini/commands/analyze.ts
512
- function registerAnalyzeCommand(parent) {
513
- parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
2078
+ // src/providers/gemini/commands/transcribe.ts
2079
+ init_errors();
2080
+ import fs16 from "fs";
2081
+ function registerTranscribeCommand(parent) {
2082
+ parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
514
2083
  async (opts) => {
515
2084
  const logger = createLogger({ verbose: opts.verbose ?? false });
516
- const model = opts.model ?? getDefaultModel("analyze");
2085
+ const model = opts.model ?? getDefaultModel("transcribe");
517
2086
  for (const f of opts.files) {
518
- if (!fs5.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2087
+ if (!fs16.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
519
2088
  }
520
2089
  const results = [];
521
2090
  for (let i = 0; i < opts.files.length; i++) {
522
2091
  const filePath = opts.files[i];
523
- logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
2092
+ logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
524
2093
  try {
525
2094
  const useApi = shouldUseFileApi(filePath);
526
2095
  let contentParts;
@@ -550,312 +2119,710 @@ function registerAnalyzeCommand(parent) {
550
2119
  }
551
2120
  }
552
2121
  formatResults(results, opts.format, opts.output);
553
- logger.debug(
554
- `Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
555
- );
556
2122
  const failed = results.filter((r) => r.status === "error").length;
557
2123
  if (failed > 0) process.exit(1);
558
2124
  }
559
2125
  );
560
2126
  }
561
2127
 
562
- // src/providers/gemini/commands/extract.ts
2128
+ // src/providers/gemini/commands/index.ts
2129
+ function registerGeminiCommands(program2) {
2130
+ const gemini = program2.command("gemini").description(
2131
+ "Gemini AI operations: analyze, transcribe, extract, generate images/video/speech"
2132
+ );
2133
+ registerAnalyzeCommand(gemini);
2134
+ registerTranscribeCommand(gemini);
2135
+ registerExtractCommand(gemini);
2136
+ registerGenerateCommand(gemini);
2137
+ registerGeminiImageToImageCommand(gemini);
2138
+ registerGenerateVideoCommand(gemini);
2139
+ registerGeminiImageToVideoCommand(gemini);
2140
+ registerGeminiGenerateSpeechCommand(gemini);
2141
+ }
2142
+
2143
+ // src/providers/leonardo/commands/generate.ts
2144
+ init_http_client();
2145
+ import fs17 from "fs";
2146
+ import path19 from "path";
2147
+
2148
+ // src/providers/leonardo/client.ts
563
2149
  init_errors();
564
- import fs6 from "fs";
565
- function registerExtractCommand(parent) {
566
- parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths").requiredOption("--prompt <text>", "Extraction prompt (required)").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
567
- async (opts) => {
568
- const logger = createLogger({ verbose: opts.verbose ?? false });
569
- const model = opts.model ?? getDefaultModel("extract");
570
- for (const f of opts.files) {
571
- if (!fs6.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2150
+ init_http_client();
2151
+
2152
+ // src/providers/leonardo/models.ts
2153
+ var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
2154
+ var DEFAULT_LEONARDO_MODEL_ID = "7b592283-e8a7-4c5a-9ba6-d18c31f258b9";
2155
+ var LEONARDO_DEFAULTS = {
2156
+ modelId: process.env.LEONARDO_DEFAULT_MODEL ?? DEFAULT_LEONARDO_MODEL_ID,
2157
+ videoModel: process.env.LEONARDO_VIDEO_MODEL ?? "MOTION2",
2158
+ width: 1024,
2159
+ height: 1024,
2160
+ numImages: 1
2161
+ };
2162
+ var LEONARDO_VIDEO_MODELS = [
2163
+ { id: "MOTION2", name: "Motion 2.0", modes: ["t2v", "i2v"] },
2164
+ { id: "MOTION2FAST", name: "Motion 2.0 Fast", modes: ["t2v", "i2v"] },
2165
+ { id: "VEO3", name: "Veo 3.0", modes: ["t2v", "i2v"] },
2166
+ { id: "VEO3FAST", name: "Veo 3.0 Fast", modes: ["t2v"] },
2167
+ { id: "VEO3_1", name: "Veo 3.1", modes: ["t2v", "i2v"] },
2168
+ { id: "KLING2_1", name: "Kling 2.1 Pro", modes: ["t2v", "i2v"] },
2169
+ { id: "Kling2_5", name: "Kling 2.5 Turbo", modes: ["t2v", "i2v"] },
2170
+ { id: "kling-2.6", name: "Kling 2.6", modes: ["t2v", "i2v"] },
2171
+ { id: "kling-3.0", name: "Kling 3.0", modes: ["t2v"] },
2172
+ { id: "kling-video-o-1", name: "Kling O1", modes: ["t2v", "i2v"] },
2173
+ { id: "kling-video-o-3", name: "Kling O3", modes: ["t2v", "i2v"] },
2174
+ { id: "hailuo-2_3", name: "Hailuo 2.3", modes: ["t2v", "i2v"] },
2175
+ { id: "ltxv-2.0-pro", name: "LTX 2.0 Pro", modes: ["t2v", "i2v"] },
2176
+ { id: "ltxv-2.3-pro", name: "LTX 2.3 Pro", modes: ["t2v", "i2v"] },
2177
+ { id: "seedance-2.0", name: "Seedance 2.0", modes: ["t2v", "i2v"] },
2178
+ { id: "seedance-2.0-fast", name: "Seedance 2.0 Fast", modes: ["t2v", "i2v"] }
2179
+ ];
2180
+ var LEONARDO_VIDEO_RESOLUTIONS = [
2181
+ "RESOLUTION_480",
2182
+ "RESOLUTION_720",
2183
+ "RESOLUTION_1080"
2184
+ ];
2185
+ var LEONARDO_V2_VIDEO_MODELS = /* @__PURE__ */ new Set([
2186
+ "kling-2.6",
2187
+ "kling-3.0",
2188
+ "kling-video-o-1",
2189
+ "kling-video-o-3",
2190
+ "hailuo-2_3",
2191
+ "ltxv-2.0-pro",
2192
+ "ltxv-2.3-pro",
2193
+ "seedance-2.0",
2194
+ "seedance-2.0-fast"
2195
+ ]);
2196
+ function isGptImageModel(modelId) {
2197
+ return typeof modelId === "string" && modelId.startsWith("gpt-image-");
2198
+ }
2199
+
2200
+ // src/providers/leonardo/client.ts
2201
+ function requireLeonardoKey() {
2202
+ const key = resolveKey("LEONARDO_API_KEY");
2203
+ if (!key) {
2204
+ throw new ConfigError(
2205
+ "LEONARDO_API_KEY is not set. Get one at https://app.leonardo.ai/settings/api-keys"
2206
+ );
2207
+ }
2208
+ return key;
2209
+ }
2210
+ function leonardoBaseUrl() {
2211
+ return resolveKey("LEONARDO_BASE_URL") ?? DEFAULT_LEONARDO_BASE_URL;
2212
+ }
2213
+ var LeonardoClient = class {
2214
+ constructor(apiKey, baseUrl = leonardoBaseUrl()) {
2215
+ this.apiKey = apiKey;
2216
+ this.baseUrl = baseUrl;
2217
+ }
2218
+ apiKey;
2219
+ baseUrl;
2220
+ async request(path35, opts = {}) {
2221
+ const base = this.baseUrl.replace(/\/$/, "");
2222
+ const v2Override = path35.startsWith("v2:");
2223
+ const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
2224
+ const finalPath = v2Override ? path35.slice(3) : path35;
2225
+ const url = new URL(finalBase + finalPath);
2226
+ if (opts.query) {
2227
+ for (const [k, v] of Object.entries(opts.query)) {
2228
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
572
2229
  }
573
- const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
574
- const results = [];
575
- for (let i = 0; i < opts.files.length; i++) {
576
- const filePath = opts.files[i];
577
- logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
578
- try {
579
- const useApi = shouldUseFileApi(filePath);
580
- let contentParts;
581
- if (useApi) {
582
- const ref = await uploadFile(filePath, { logger });
583
- contentParts = [
584
- { text: opts.prompt },
585
- { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
586
- ];
587
- } else {
588
- contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
2230
+ }
2231
+ opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
2232
+ return httpJson({
2233
+ url: url.toString(),
2234
+ method: opts.method ?? "GET",
2235
+ headers: {
2236
+ accept: "application/json",
2237
+ authorization: `Bearer ${this.apiKey}`
2238
+ },
2239
+ body: opts.body,
2240
+ timeoutMs: opts.timeoutMs ?? 12e4
2241
+ });
2242
+ }
2243
+ get(path35, query, logger) {
2244
+ return this.request(path35, { method: "GET", query, logger });
2245
+ }
2246
+ post(path35, body, logger) {
2247
+ return this.request(path35, { method: "POST", body, logger });
2248
+ }
2249
+ delete(path35, logger) {
2250
+ return this.request(path35, { method: "DELETE", logger });
2251
+ }
2252
+ };
2253
+ function createLeonardoClient() {
2254
+ return new LeonardoClient(requireLeonardoKey(), leonardoBaseUrl());
2255
+ }
2256
+
2257
+ // src/providers/leonardo/commands/generate.ts
2258
+ function extFromUrl(url, fallback = ".png") {
2259
+ try {
2260
+ const u = new URL(url);
2261
+ const e = path19.extname(u.pathname);
2262
+ return e.length > 0 ? e : fallback;
2263
+ } catch {
2264
+ return fallback;
2265
+ }
2266
+ }
2267
+ function autoFilename(generationId, index, ext) {
2268
+ const e = ext.startsWith(".") ? ext : `.${ext}`;
2269
+ return `leonardo-${generationId.slice(0, 8)}-${String(index).padStart(2, "0")}${e}`;
2270
+ }
2271
+ function registerLeonardoGenerateCommand(parent) {
2272
+ parent.command("generate <prompt>").alias("gen").description("Generate images from a text prompt via Leonardo").option("-m, --model <id>", "Model UUID or GPT-image id").option("-w, --width <n>", "Image width").option("-h, --height <n>", "Image height").option("-n, --num <n>", "Number of images").option("--alchemy", "Enable Alchemy").option("--ultra", "Enable Ultra mode").option("--contrast <n>", "Contrast").option("--style <uuid>", "Style UUID").option("--seed <n>", "Seed").option("--negative <text>", "Negative prompt").option("--enhance", "Enhance prompt").option("--quality <level>", "GPT Image quality: LOW|MEDIUM|HIGH", "HIGH").option("--output <path>", "Copy first image to this path (single-image only)").option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
2273
+ async (prompt, opts) => {
2274
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2275
+ const client = createLeonardoClient();
2276
+ const input = {
2277
+ prompt,
2278
+ modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
2279
+ width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
2280
+ height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
2281
+ num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
2282
+ alchemy: opts.alchemy,
2283
+ ultra: opts.ultra,
2284
+ contrast: opts.contrast ? Number.parseFloat(opts.contrast) : void 0,
2285
+ styleUUID: opts.style,
2286
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
2287
+ negative_prompt: opts.negative,
2288
+ enhancePrompt: opts.enhance
2289
+ };
2290
+ logger.info(
2291
+ `Submitting generation (${input.width}x${input.height}, n=${input.num_images})`
2292
+ );
2293
+ let generationId;
2294
+ if (isGptImageModel(input.modelId)) {
2295
+ const v2Body = {
2296
+ public: false,
2297
+ model: input.modelId,
2298
+ parameters: {
2299
+ prompt: input.prompt,
2300
+ quantity: input.num_images ?? 1,
2301
+ width: input.width,
2302
+ height: input.height,
2303
+ quality: opts.quality,
2304
+ prompt_enhance: input.enhancePrompt ? "ON" : "OFF",
2305
+ ...input.seed !== void 0 ? { seed: input.seed } : {}
589
2306
  }
590
- const resp = await generateContent({
591
- model,
592
- contents: [{ parts: contentParts }],
593
- generationConfig
594
- });
595
- results.push({ file: filePath, status: "success", response: extractText(resp) });
2307
+ };
2308
+ const v2Res = await client.post(
2309
+ "v2:/generations",
2310
+ v2Body,
2311
+ logger
2312
+ );
2313
+ generationId = v2Res.generate.generationId;
2314
+ } else {
2315
+ const createRes = await client.post(
2316
+ "/generations",
2317
+ input,
2318
+ logger
2319
+ );
2320
+ generationId = createRes.sdGenerationJob.generationId;
2321
+ }
2322
+ logger.debug(`generationId: ${generationId}`);
2323
+ logger.info("Polling for completion...");
2324
+ const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
2325
+ const intervalMs = 4e3;
2326
+ const final = await poll({
2327
+ fetch: () => client.get(`/generations/${generationId}`),
2328
+ done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
2329
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2330
+ intervalMs,
2331
+ maxAttempts: Math.ceil(waitTimeout / intervalMs),
2332
+ onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
2333
+ });
2334
+ const images = final.generations_by_pk?.generated_images ?? [];
2335
+ if (opts.download === false) {
2336
+ for (const img of images) console.log(img.url);
2337
+ return;
2338
+ }
2339
+ const outDir = getOutputDir();
2340
+ const saved = [];
2341
+ for (let i = 0; i < images.length; i++) {
2342
+ const img = images[i];
2343
+ if (!img) continue;
2344
+ const ext = extFromUrl(img.url);
2345
+ const filename = autoFilename(generationId, i + 1, ext);
2346
+ const filepath = path19.join(outDir, filename);
2347
+ try {
2348
+ await downloadFile(img.url, filepath);
2349
+ saved.push(filepath);
2350
+ logger.success(`Saved ${filepath}`);
596
2351
  } catch (e) {
597
- const msg = e instanceof Error ? e.message : String(e);
598
- logger.error(`Failed: ${filePath} \u2014 ${msg}`);
599
- results.push({ file: filePath, status: "error", error: msg });
2352
+ logger.warn(`Download failed for ${img.id}: ${e instanceof Error ? e.message : e}`);
600
2353
  }
601
2354
  }
602
- formatResults(results, opts.format, opts.output);
603
- const failed = results.filter((r) => r.status === "error").length;
604
- if (failed > 0) process.exit(1);
2355
+ if (opts.output && saved[0]) {
2356
+ const dest = path19.resolve(opts.output);
2357
+ fs17.mkdirSync(path19.dirname(dest), { recursive: true });
2358
+ fs17.copyFileSync(saved[0], dest);
2359
+ logger.success(`Copied to ${dest}`);
2360
+ }
2361
+ console.log(`
2362
+ Generated ${saved.length} image(s):`);
2363
+ for (const f of saved) console.log(` ${f}`);
605
2364
  }
606
2365
  );
607
2366
  }
608
2367
 
609
- // src/providers/gemini/commands/generate-video.ts
610
- import fs8 from "fs";
611
- import path7 from "path";
612
- init_errors();
2368
+ // src/providers/leonardo/commands/image-to-image.ts
613
2369
  init_http_client();
614
-
615
- // src/core/output-dir.ts
616
- import fs7 from "fs";
617
- import path6 from "path";
618
- var cachedOutputDir;
619
- function getOutputDir() {
620
- if (cachedOutputDir) return cachedOutputDir;
621
- const dir = process.env.MULTIX_OUTPUT_DIR ? path6.resolve(process.env.MULTIX_OUTPUT_DIR) : path6.resolve(process.cwd(), "multix-output");
622
- fs7.mkdirSync(dir, { recursive: true });
623
- cachedOutputDir = dir;
624
- return dir;
2370
+ import fs18 from "fs";
2371
+ import path20 from "path";
2372
+ function extFromUrl2(url, fallback = ".png") {
2373
+ try {
2374
+ return path20.extname(new URL(url).pathname) || fallback;
2375
+ } catch {
2376
+ return fallback;
2377
+ }
625
2378
  }
626
-
627
- // src/providers/gemini/commands/generate-video.ts
628
- var BASE2 = "https://generativelanguage.googleapis.com";
629
- function registerGenerateVideoCommand(parent) {
630
- parent.command("generate-video").description("Generate video with Veo via Gemini API [EXPERIMENTAL]").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", "Veo model to use").option("--resolution <res>", "Video resolution: 720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option(
631
- "--reference-images <paths...>",
632
- "Reference images (first=opening frame, second=closing)"
633
- ).option("--upload-timeout <ms>", "Upload timeout in ms", "120000").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").action(
2379
+ function registerLeonardoImageToImageCommand(parent) {
2380
+ parent.command("image-to-image").alias("i2i").description("Generate an image conditioned on an existing Leonardo image id (init image)").requiredOption("--prompt <text>", "Text prompt describing the desired output").requiredOption("--ref <imageId>", "Existing Leonardo image id to use as init image").option("--image-type <type>", "GENERATED | UPLOADED", "GENERATED").option("--init-strength <n>", "Init image strength (0.0-0.9)", "0.5").option("-m, --model <id>", "Model UUID").option("-w, --width <n>", "Image width").option("-h, --height <n>", "Image height").option("-n, --num <n>", "Number of images").option("--seed <n>", "Seed").option("--negative <text>", "Negative prompt").option("--output <path>", "Copy first image to this path (single-image only)").option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
634
2381
  async (opts) => {
635
2382
  const logger = createLogger({ verbose: opts.verbose ?? false });
636
- const apiKey = resolveKey("GEMINI_API_KEY");
637
- if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
638
- const model = opts.model ?? getDefaultModel("generate-video");
639
- if (!model.startsWith("veo-")) {
640
- throw new ValidationError(
641
- `Video generation requires a Veo model (got '${model}'). Use: veo-3.1-generate-preview, veo-3.0-generate-001, etc.`
642
- );
643
- }
644
- if (!["720p", "1080p"].includes(opts.resolution)) {
645
- throw new ValidationError("--resolution must be 720p or 1080p");
2383
+ const imageType = opts.imageType.toUpperCase();
2384
+ if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
2385
+ logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
2386
+ process.exit(2);
646
2387
  }
647
- if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
648
- throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
2388
+ const initStrength = Number.parseFloat(opts.initStrength);
2389
+ if (Number.isNaN(initStrength) || initStrength < 0 || initStrength > 0.9) {
2390
+ logger.error("--init-strength must be a number in [0.0, 0.9]");
2391
+ process.exit(2);
649
2392
  }
650
- logger.debug(`Submitting video generation with ${model}...`);
651
- logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
652
- const config = {
653
- aspectRatio: opts.aspectRatio,
654
- resolution: opts.resolution
2393
+ const client = createLeonardoClient();
2394
+ const body = {
2395
+ prompt: opts.prompt,
2396
+ modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
2397
+ width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
2398
+ height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
2399
+ num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
2400
+ init_strength: initStrength
655
2401
  };
656
- const contentParts = [{ text: opts.prompt }];
657
- if (opts.referenceImages?.length) {
658
- for (const imgPath of opts.referenceImages.slice(0, 2)) {
659
- if (!fs8.existsSync(imgPath))
660
- throw new ValidationError(`Reference image not found: ${imgPath}`);
661
- const data = fs8.readFileSync(imgPath).toString("base64");
662
- const ext = path7.extname(imgPath).toLowerCase();
663
- const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
664
- contentParts.push({ inlineData: { mimeType, data } });
665
- }
666
- }
667
- const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
668
- let operation;
669
- try {
670
- operation = await httpJson({
671
- url: `${BASE2}/v1beta/models/${model}:generateVideos`,
672
- method: "POST",
673
- headers,
674
- body: {
675
- contents: [{ parts: contentParts }],
676
- generationConfig: config
677
- }
678
- });
679
- } catch (e) {
680
- const msg = e instanceof Error ? e.message : String(e);
681
- throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
682
- }
683
- const operationName = operation.name;
684
- logger.debug(`Operation: ${operationName}`);
685
- const deadline = Date.now() + 10 * 60 * 1e3;
686
- let done = false;
687
- let finalOp = operation;
688
- while (!done && Date.now() < deadline) {
689
- await sleep2(1e4);
690
- finalOp = await httpJson({
691
- url: `${BASE2}/v1beta/${operationName}`,
692
- headers
693
- });
694
- done = finalOp.done === true;
695
- if (!done)
696
- logger.debug(
697
- `Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
698
- );
2402
+ if (imageType === "GENERATED") {
2403
+ body.init_generation_image_id = opts.ref;
2404
+ } else {
2405
+ body.init_image_id = opts.ref;
699
2406
  }
700
- if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
701
- if (finalOp.error)
702
- throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
703
- const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
704
- if (!videoUri) {
705
- throw new ProviderError(
706
- `Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
707
- "gemini"
708
- );
2407
+ if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
2408
+ if (opts.negative) body.negative_prompt = opts.negative;
2409
+ logger.info(`Submitting image-to-image (init=${opts.ref}, type=${imageType})`);
2410
+ const createRes = await client.post("/generations", body, logger);
2411
+ const generationId = createRes.sdGenerationJob.generationId;
2412
+ logger.debug(`generationId: ${generationId}`);
2413
+ logger.info("Polling for completion...");
2414
+ const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
2415
+ const intervalMs = 4e3;
2416
+ const final = await poll({
2417
+ fetch: () => client.get(`/generations/${generationId}`),
2418
+ done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
2419
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2420
+ intervalMs,
2421
+ maxAttempts: Math.ceil(waitTimeout / intervalMs),
2422
+ onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
2423
+ });
2424
+ const images = final.generations_by_pk?.generated_images ?? [];
2425
+ if (opts.download === false) {
2426
+ for (const img of images) console.log(img.url);
2427
+ return;
709
2428
  }
710
2429
  const outDir = getOutputDir();
711
- const dest = path7.join(outDir, `veo_generated_${Date.now()}.mp4`);
712
- const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
713
- await downloadFile2(videoUri, dest);
714
- logger.success(`Saved video: ${dest}`);
715
- if (opts.output) {
716
- fs8.mkdirSync(path7.dirname(path7.resolve(opts.output)), { recursive: true });
717
- fs8.copyFileSync(dest, opts.output);
718
- logger.success(`Copied to: ${opts.output}`);
2430
+ const saved = [];
2431
+ for (let i = 0; i < images.length; i++) {
2432
+ const img = images[i];
2433
+ if (!img) continue;
2434
+ const ext = extFromUrl2(img.url);
2435
+ const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
2436
+ const filepath = path20.join(outDir, filename);
2437
+ try {
2438
+ await downloadFile(img.url, filepath);
2439
+ saved.push(filepath);
2440
+ logger.success(`Saved ${filepath}`);
2441
+ } catch (e) {
2442
+ logger.warn(`Download failed: ${e instanceof Error ? e.message : e}`);
2443
+ }
2444
+ }
2445
+ if (opts.output && saved[0]) {
2446
+ const dest = path20.resolve(opts.output);
2447
+ fs18.mkdirSync(path20.dirname(dest), { recursive: true });
2448
+ fs18.copyFileSync(saved[0], dest);
2449
+ logger.success(`Copied to ${dest}`);
719
2450
  }
720
2451
  console.log(`
721
- Generated video: ${dest}`);
2452
+ Generated ${saved.length} image(s):`);
2453
+ for (const f of saved) console.log(` ${f}`);
722
2454
  }
723
2455
  );
724
2456
  }
725
- function sleep2(ms) {
726
- return new Promise((r) => setTimeout(r, ms));
727
- }
728
2457
 
729
- // src/providers/gemini/commands/generate.ts
730
- init_errors();
731
- import fs9 from "fs";
732
- import path8 from "path";
733
- function registerGenerateCommand(parent) {
734
- parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").option("-v, --verbose", "Verbose logging").action(
735
- async (opts) => {
2458
+ // src/providers/leonardo/commands/video.ts
2459
+ init_http_client();
2460
+ import path21 from "path";
2461
+ function v2DimsFor(resolution) {
2462
+ if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
2463
+ if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
2464
+ return { width: 1280, height: 720 };
2465
+ }
2466
+ function registerLeonardoVideoCommand(parent) {
2467
+ parent.command("video <prompt>").description("Generate a video from a text prompt via Leonardo (text-to-video)").option("--model <model>", "Video model (e.g. MOTION2, VEO3, kling-2.6)").option(
2468
+ "--resolution <r>",
2469
+ "Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
2470
+ "RESOLUTION_720"
2471
+ ).option("--enhance", "Enable prompt enhance").option("--frame-interpolation", "Enable frame interpolation (v1 only)").option("--wait", "Poll until COMPLETE/FAILED instead of returning the job id").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download the generated video on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
2472
+ async (prompt, opts) => {
736
2473
  const logger = createLogger({ verbose: opts.verbose ?? false });
737
- const model = opts.model ?? getDefaultModel("generate");
738
- const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 4);
739
- if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
740
- throw new ValidationError(
741
- `Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
2474
+ const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
2475
+ const client = createLeonardoClient();
2476
+ logger.info(`Submitting text-to-video (${model})`);
2477
+ let jobId;
2478
+ if (LEONARDO_V2_VIDEO_MODELS.has(model)) {
2479
+ const { width, height } = v2DimsFor(opts.resolution);
2480
+ const v2Res = await client.post(
2481
+ "v2:/generations",
2482
+ {
2483
+ public: false,
2484
+ model,
2485
+ parameters: {
2486
+ prompt,
2487
+ duration: 8,
2488
+ mode: opts.resolution,
2489
+ prompt_enhance: opts.enhance ? "ON" : "OFF",
2490
+ width,
2491
+ height
2492
+ }
2493
+ },
2494
+ logger
2495
+ );
2496
+ jobId = v2Res.generate.generationId;
2497
+ } else {
2498
+ const res = await client.post(
2499
+ "/generations-text-to-video",
2500
+ {
2501
+ prompt,
2502
+ model,
2503
+ resolution: opts.resolution,
2504
+ promptEnhance: !!opts.enhance,
2505
+ frameInterpolation: !!opts.frameInterpolation
2506
+ },
2507
+ logger
742
2508
  );
2509
+ jobId = res.motionVideoGenerationJob?.generationId;
743
2510
  }
744
- if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
745
- throw new ValidationError(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
2511
+ if (!jobId) {
2512
+ logger.error("No generationId returned by Leonardo");
2513
+ process.exit(1);
746
2514
  }
747
- logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
748
- logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
749
- const imageConfig = { aspectRatio: opts.aspectRatio };
750
- if (opts.size) imageConfig.imageSize = opts.size;
751
- const generationConfig = {
752
- responseModalities: ["IMAGE"],
753
- imageConfig
2515
+ const shouldWait = !!(opts.wait || opts.download);
2516
+ if (!shouldWait) {
2517
+ console.log(jobId);
2518
+ console.error(`Poll with: multix leonardo status ${jobId}`);
2519
+ return;
2520
+ }
2521
+ await pollAndDownload(client, jobId, {
2522
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
2523
+ download: !!opts.download,
2524
+ output: opts.output,
2525
+ thumb: opts.thumb !== false,
2526
+ logger,
2527
+ basename: "leonardo-video"
2528
+ });
2529
+ }
2530
+ );
2531
+ }
2532
+ async function pollAndDownload(client, jobId, opts) {
2533
+ const { logger } = opts;
2534
+ let res;
2535
+ try {
2536
+ res = await poll({
2537
+ fetch: () => client.get(`/generations/${jobId}`, void 0, logger),
2538
+ done: (v) => v.generations_by_pk?.status === "COMPLETE",
2539
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2540
+ intervalMs: 5e3,
2541
+ maxAttempts: Math.max(1, Math.ceil(opts.waitTimeoutMs / 5e3)),
2542
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
2543
+ });
2544
+ } catch (e) {
2545
+ if (e instanceof PollTimeoutError) {
2546
+ logger.error(`Timed out \u2014 multix leonardo status ${jobId}`);
2547
+ process.exit(1);
2548
+ }
2549
+ if (e instanceof PollFailedError) {
2550
+ logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
2551
+ process.exit(1);
2552
+ }
2553
+ throw e;
2554
+ }
2555
+ const gen = res.generations_by_pk;
2556
+ if (!gen) {
2557
+ logger.error("No generation in response");
2558
+ process.exit(1);
2559
+ }
2560
+ const videoUrl = gen.generated_images?.find(
2561
+ (img) => typeof img.url === "string" && /\.(mp4|webm|mov)(\?|$)/i.test(img.url)
2562
+ )?.url ?? gen.generated_images?.[0]?.motionMP4URL;
2563
+ if (!opts.download) {
2564
+ if (typeof videoUrl === "string") console.log(videoUrl);
2565
+ return;
2566
+ }
2567
+ if (typeof videoUrl !== "string") {
2568
+ logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
2569
+ process.exit(1);
2570
+ }
2571
+ const outPath = opts.output ?? path21.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
2572
+ await downloadFile(videoUrl, outPath);
2573
+ logger.success(`Saved ${outPath}`);
2574
+ await maybeDownloadThumb(gen, outPath, {
2575
+ skip: !opts.thumb,
2576
+ copyTo: opts.output,
2577
+ logger
2578
+ });
2579
+ console.log(`
2580
+ Job: ${jobId}`);
2581
+ console.log(`Saved: ${outPath}`);
2582
+ }
2583
+
2584
+ // src/providers/leonardo/commands/image-to-video.ts
2585
+ function registerLeonardoImageToVideoCommand(parent) {
2586
+ parent.command("image-to-video <imageId>").alias("i2v").description("Generate a video from an existing Leonardo image (image-to-video)").requiredOption("--prompt <text>", "Motion / scene prompt for the video").option("--image-type <type>", "GENERATED | UPLOADED", "GENERATED").option("--model <model>", "Video model (e.g. MOTION2, VEO3, KLING2_5)").option(
2587
+ "--resolution <r>",
2588
+ "Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
2589
+ "RESOLUTION_720"
2590
+ ).option("--duration <n>", "Duration seconds (model-dependent: 4|5|6|8|10)").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--enhance", "Enable prompt enhance").option("--frame-interpolation", "Enable frame interpolation").option("--wait", "Poll until COMPLETE/FAILED instead of returning the job id").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download the generated video on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
2591
+ async (imageId, opts) => {
2592
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2593
+ const imageType = opts.imageType.toUpperCase();
2594
+ if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
2595
+ logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
2596
+ process.exit(2);
2597
+ }
2598
+ const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
2599
+ const client = createLeonardoClient();
2600
+ logger.info(`Submitting image-to-video (${model}, image=${imageId}, ${imageType})`);
2601
+ const body = {
2602
+ prompt: opts.prompt,
2603
+ imageId,
2604
+ imageType,
2605
+ model,
2606
+ resolution: opts.resolution,
2607
+ promptEnhance: !!opts.enhance,
2608
+ frameInterpolation: !!opts.frameInterpolation
754
2609
  };
755
- const savedFiles = [];
756
- for (let i = 0; i < numImages; i++) {
757
- try {
758
- const resp = await generateContent({
759
- model,
760
- contents: [{ parts: [{ text: opts.prompt }] }],
761
- generationConfig
762
- });
763
- const images = extractImages(resp);
764
- if (images.length === 0) {
765
- logger.warn(
766
- `Request ${i + 1}: no images in response \u2014 model may not support image generation`
767
- );
768
- continue;
769
- }
770
- for (const img of images) {
771
- const outDir = getOutputDir();
772
- const ext = img.mimeType.includes("png") ? "png" : "jpg";
773
- const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
774
- const dest = path8.join(outDir, fname);
775
- fs9.writeFileSync(dest, Buffer.from(img.data, "base64"));
776
- savedFiles.push(dest);
777
- logger.success(`Saved: ${dest}`);
778
- }
779
- } catch (e) {
780
- const msg = e instanceof Error ? e.message : String(e);
781
- logger.error(`Generation failed: ${msg}`);
2610
+ if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
2611
+ if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
2612
+ if (opts.negative) body.negativePrompt = opts.negative;
2613
+ const res = await client.post(
2614
+ "/generations-image-to-video",
2615
+ body,
2616
+ logger
2617
+ );
2618
+ const jobId = res.motionVideoGenerationJob?.generationId;
2619
+ if (!jobId) {
2620
+ logger.error(`No generationId returned by Leonardo: ${JSON.stringify(res)}`);
2621
+ process.exit(1);
2622
+ }
2623
+ const shouldWait = !!(opts.wait || opts.download);
2624
+ if (!shouldWait) {
2625
+ console.log(jobId);
2626
+ console.error(`Poll with: multix leonardo status ${jobId}`);
2627
+ return;
2628
+ }
2629
+ await pollAndDownload(client, jobId, {
2630
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
2631
+ download: !!opts.download,
2632
+ output: opts.output,
2633
+ thumb: opts.thumb !== false,
2634
+ logger,
2635
+ basename: "leonardo-i2v"
2636
+ });
2637
+ }
2638
+ );
2639
+ }
2640
+
2641
+ // src/providers/leonardo/commands/me.ts
2642
+ function registerLeonardoMeCommand(parent) {
2643
+ parent.command("me").description("Show authenticated Leonardo user info and remaining API credits").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2644
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2645
+ const client = createLeonardoClient();
2646
+ const info = await client.get("/me", void 0, logger);
2647
+ const u = info.user_details?.[0];
2648
+ if (!u) {
2649
+ logger.warn("No user info returned.");
2650
+ return;
2651
+ }
2652
+ logger.success(`User: ${u.user.username} (${u.user.id})`);
2653
+ console.log(`Tokens: ${u.subscriptionTokens} | API credit: ${u.apiCredit ?? "n/a"}`);
2654
+ });
2655
+ }
2656
+
2657
+ // src/providers/leonardo/commands/models.ts
2658
+ function registerLeonardoModelsCommand(parent) {
2659
+ parent.command("models").description("List Leonardo platform image models").option("--limit <n>", "Limit number of results").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2660
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2661
+ const client = createLeonardoClient();
2662
+ const res = await client.get(
2663
+ "/platformModels",
2664
+ void 0,
2665
+ logger
2666
+ );
2667
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
2668
+ const models = limit ? res.custom_models.slice(0, limit) : res.custom_models;
2669
+ for (const m of models) {
2670
+ console.log(`${m.id} ${m.name}`);
2671
+ }
2672
+ logger.debug(`${models.length} model(s)`);
2673
+ });
2674
+ }
2675
+
2676
+ // src/providers/leonardo/commands/status.ts
2677
+ init_http_client();
2678
+ import path22 from "path";
2679
+ var VIDEO_KEYS = ["motionMP4URL", "url"];
2680
+ var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
2681
+ var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
2682
+ function collectAssetUrls(gen) {
2683
+ const videos = [];
2684
+ const images = [];
2685
+ for (const img of gen.generated_images ?? []) {
2686
+ if (!img.url) continue;
2687
+ if (VIDEO_EXT_RE.test(img.url)) videos.push(img.url);
2688
+ else if (IMAGE_EXT_RE2.test(img.url)) images.push(img.url);
2689
+ else images.push(img.url);
2690
+ for (const k of VIDEO_KEYS) {
2691
+ const extra = img[k];
2692
+ if (typeof extra === "string" && VIDEO_EXT_RE.test(extra)) videos.push(extra);
2693
+ }
2694
+ }
2695
+ return { videos, images };
2696
+ }
2697
+ function registerLeonardoStatusCommand(parent) {
2698
+ parent.command("status <generationId>").description("Get a Leonardo generation's status and asset URLs").option("--wait", "Poll until COMPLETE or FAILED").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download assets on success (implies --wait)").option("--output <path>", "Output directory or single-file path (with --download)").option("--no-thumb", "Skip downloading thumbnails if the API returns them").option("-v, --verbose", "Verbose logging").action(
2699
+ async (generationId, opts) => {
2700
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2701
+ const client = createLeonardoClient();
2702
+ const shouldWait = !!(opts.wait || opts.download);
2703
+ const fetchOnce = () => client.get(`/generations/${generationId}`, void 0, logger);
2704
+ let res;
2705
+ try {
2706
+ res = shouldWait ? await poll({
2707
+ fetch: fetchOnce,
2708
+ done: (v) => v.generations_by_pk?.status === "COMPLETE",
2709
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2710
+ intervalMs: 5e3,
2711
+ maxAttempts: Math.max(
2712
+ 1,
2713
+ Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
2714
+ ),
2715
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
2716
+ }) : await fetchOnce();
2717
+ } catch (e) {
2718
+ if (e instanceof PollTimeoutError) {
2719
+ logger.error(`Timed out waiting for generation ${generationId}`);
2720
+ process.exit(1);
2721
+ }
2722
+ if (e instanceof PollFailedError) {
2723
+ logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
782
2724
  process.exit(1);
783
2725
  }
2726
+ throw e;
784
2727
  }
785
- if (savedFiles.length === 0) {
786
- logger.error("No images were generated");
787
- process.exit(1);
2728
+ const gen = res.generations_by_pk;
2729
+ if (!gen) {
2730
+ logger.warn("No generation found.");
2731
+ return;
788
2732
  }
789
- if (opts.output && savedFiles[0]) {
790
- fs9.mkdirSync(path8.dirname(path8.resolve(opts.output)), { recursive: true });
791
- fs9.copyFileSync(savedFiles[0], opts.output);
792
- logger.success(`Copied to: ${opts.output}`);
2733
+ console.error(`status: ${gen.status}`);
2734
+ const { videos, images } = collectAssetUrls(gen);
2735
+ for (const u of [...videos, ...images]) console.log(u);
2736
+ if (!opts.download || gen.status !== "COMPLETE") {
2737
+ if (opts.download) logger.warn(`Cannot download \u2014 status is '${gen.status}'`);
2738
+ return;
2739
+ }
2740
+ const outDir = getOutputDir();
2741
+ let idx = 0;
2742
+ for (const url of videos) {
2743
+ const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path22.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
2744
+ await downloadFile(url, dest);
2745
+ logger.success(`Saved ${dest}`);
2746
+ await maybeDownloadThumb(gen, dest, {
2747
+ skip: opts.thumb === false,
2748
+ copyTo: opts.output,
2749
+ logger
2750
+ });
2751
+ idx++;
2752
+ }
2753
+ for (const url of images) {
2754
+ const m = url.match(IMAGE_EXT_RE2);
2755
+ const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
2756
+ const dest = path22.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
2757
+ await downloadFile(url, dest);
2758
+ logger.success(`Saved ${dest}`);
2759
+ idx++;
793
2760
  }
794
- console.log(`
795
- Generated ${savedFiles.length} image(s):`);
796
- for (const f of savedFiles) console.log(` ${f}`);
797
2761
  }
798
2762
  );
799
2763
  }
800
2764
 
801
- // src/providers/gemini/commands/transcribe.ts
802
- init_errors();
803
- import fs10 from "fs";
804
- function registerTranscribeCommand(parent) {
805
- parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
806
- async (opts) => {
2765
+ // src/providers/leonardo/commands/upscale.ts
2766
+ function registerLeonardoUpscaleCommands(parent) {
2767
+ parent.command("upscale <generatedImageId>").description("Upscale a generated image via Leonardo Universal Upscaler").option("--style <style>", "Upscaler style (GENERAL, CINEMATIC, ...)", "GENERAL").option("--strength <n>", "Creativity strength (0-1)", "0.35").option("--multiplier <n>", "Upscale multiplier (1-2)", "1.5").option("-v, --verbose", "Verbose logging").action(
2768
+ async (generatedImageId, opts) => {
807
2769
  const logger = createLogger({ verbose: opts.verbose ?? false });
808
- const model = opts.model ?? getDefaultModel("transcribe");
809
- for (const f of opts.files) {
810
- if (!fs10.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
811
- }
812
- const results = [];
813
- for (let i = 0; i < opts.files.length; i++) {
814
- const filePath = opts.files[i];
815
- logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
816
- try {
817
- const useApi = shouldUseFileApi(filePath);
818
- let contentParts;
819
- if (useApi) {
820
- logger.debug("File >20MB \u2014 using Files API");
821
- const ref = await uploadFile(filePath, { logger });
822
- contentParts = [
823
- { text: opts.prompt },
824
- { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
825
- ];
826
- } else {
827
- contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
828
- }
829
- const resp = await generateContent({
830
- model,
831
- contents: [{ parts: contentParts }]
832
- });
833
- results.push({
834
- file: filePath,
835
- status: "success",
836
- response: extractText(resp)
837
- });
838
- } catch (e) {
839
- const msg = e instanceof Error ? e.message : String(e);
840
- logger.error(`Failed: ${filePath} \u2014 ${msg}`);
841
- results.push({ file: filePath, status: "error", error: msg });
842
- }
843
- }
844
- formatResults(results, opts.format, opts.output);
845
- const failed = results.filter((r) => r.status === "error").length;
846
- if (failed > 0) process.exit(1);
2770
+ const client = createLeonardoClient();
2771
+ logger.info(`Submitting upscale for image ${generatedImageId}`);
2772
+ const res = await client.post(
2773
+ "/variations/universal-upscaler",
2774
+ {
2775
+ generatedImageId,
2776
+ upscalerStyle: opts.style,
2777
+ creativityStrength: Number.parseFloat(opts.strength),
2778
+ upscaleMultiplier: Number.parseFloat(opts.multiplier)
2779
+ },
2780
+ logger
2781
+ );
2782
+ const variationId = res.universalUpscaler.id;
2783
+ logger.success(`variationId: ${variationId}`);
2784
+ console.log(variationId);
2785
+ console.error(`Use: multix leonardo variation ${variationId}`);
847
2786
  }
848
2787
  );
2788
+ parent.command("variation <variationId>").description("Fetch a Leonardo variation/upscale result by ID").option("-v, --verbose", "Verbose logging").action(async (variationId, opts) => {
2789
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2790
+ const client = createLeonardoClient();
2791
+ const res = await client.get(
2792
+ `/variations/${variationId}`,
2793
+ void 0,
2794
+ logger
2795
+ );
2796
+ for (const v of res.generated_image_variation_generic ?? []) {
2797
+ console.log(`${v.status} ${v.url ?? ""}`);
2798
+ }
2799
+ });
849
2800
  }
850
2801
 
851
- // src/providers/gemini/commands/index.ts
852
- function registerGeminiCommands(program2) {
853
- const gemini = program2.command("gemini").description("Gemini AI operations: analyze, transcribe, extract, generate images/video");
854
- registerAnalyzeCommand(gemini);
855
- registerTranscribeCommand(gemini);
856
- registerExtractCommand(gemini);
857
- registerGenerateCommand(gemini);
858
- registerGenerateVideoCommand(gemini);
2802
+ // src/providers/leonardo/commands/video-models.ts
2803
+ function registerLeonardoVideoModelsCommand(parent) {
2804
+ parent.command("video-models").description("List supported Leonardo video models (static enum from Leonardo docs)").action(() => {
2805
+ for (const m of LEONARDO_VIDEO_MODELS) {
2806
+ console.log(`${m.id} ${m.name} [${m.modes.join(",")}]`);
2807
+ }
2808
+ console.error(`
2809
+ Resolutions: ${LEONARDO_VIDEO_RESOLUTIONS.join(", ")}`);
2810
+ console.error("Source: https://docs.leonardo.ai/llms.txt");
2811
+ });
2812
+ }
2813
+
2814
+ // src/providers/leonardo/commands/index.ts
2815
+ function registerLeonardoCommands(program2) {
2816
+ const leonardo = program2.command("leonardo").description("Leonardo.Ai: image/video generation, upscaling, account info");
2817
+ registerLeonardoGenerateCommand(leonardo);
2818
+ registerLeonardoImageToImageCommand(leonardo);
2819
+ registerLeonardoVideoCommand(leonardo);
2820
+ registerLeonardoImageToVideoCommand(leonardo);
2821
+ registerLeonardoVideoModelsCommand(leonardo);
2822
+ registerLeonardoUpscaleCommands(leonardo);
2823
+ registerLeonardoStatusCommand(leonardo);
2824
+ registerLeonardoModelsCommand(leonardo);
2825
+ registerLeonardoMeCommand(leonardo);
859
2826
  }
860
2827
 
861
2828
  // src/providers/minimax/commands/generate-music.ts
@@ -918,7 +2885,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
918
2885
  if (status === "Failed" || status === "Error") {
919
2886
  throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
920
2887
  }
921
- await sleep3(intervalMs);
2888
+ await sleep4(intervalMs);
922
2889
  elapsed += intervalMs;
923
2890
  }
924
2891
  throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
@@ -937,14 +2904,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
937
2904
  await downloadFile(downloadUrl, dest);
938
2905
  return dest;
939
2906
  }
940
- function sleep3(ms) {
2907
+ function sleep4(ms) {
941
2908
  return new Promise((r) => setTimeout(r, ms));
942
2909
  }
943
2910
 
944
2911
  // src/providers/minimax/generators/music.ts
945
2912
  init_http_client();
946
- import fs11 from "fs";
947
- import path9 from "path";
2913
+ import fs19 from "fs";
2914
+ import path23 from "path";
948
2915
  async function generateMinimaxMusic(opts) {
949
2916
  const {
950
2917
  apiKey,
@@ -989,14 +2956,14 @@ async function generateMinimaxMusic(opts) {
989
2956
  }
990
2957
  const durationMs = resp.extra_info?.music_duration ?? 0;
991
2958
  const outDir = getOutputDir();
992
- const dest = path9.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
2959
+ const dest = path23.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
993
2960
  try {
994
2961
  if (audioData.startsWith("http")) {
995
2962
  const bytes = await fetchBytes(audioData);
996
- fs11.writeFileSync(dest, bytes);
2963
+ fs19.writeFileSync(dest, bytes);
997
2964
  } else {
998
2965
  const audioBytes = Buffer.from(audioData, "hex");
999
- fs11.writeFileSync(dest, audioBytes);
2966
+ fs19.writeFileSync(dest, audioBytes);
1000
2967
  }
1001
2968
  } catch (e) {
1002
2969
  return {
@@ -1007,8 +2974,8 @@ async function generateMinimaxMusic(opts) {
1007
2974
  const durSec = durationMs / 1e3;
1008
2975
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
1009
2976
  if (output) {
1010
- fs11.mkdirSync(path9.dirname(path9.resolve(output)), { recursive: true });
1011
- fs11.copyFileSync(dest, output);
2977
+ fs19.mkdirSync(path23.dirname(path23.resolve(output)), { recursive: true });
2978
+ fs19.copyFileSync(dest, output);
1012
2979
  }
1013
2980
  return { status: "success", generatedAudio: dest, durationMs, model };
1014
2981
  }
@@ -1082,8 +3049,8 @@ Generated music: ${result.generatedAudio}`);
1082
3049
  init_errors();
1083
3050
 
1084
3051
  // src/providers/minimax/generators/speech.ts
1085
- import fs12 from "fs";
1086
- import path10 from "path";
3052
+ import fs20 from "fs";
3053
+ import path24 from "path";
1087
3054
  async function generateMinimaxSpeech(opts) {
1088
3055
  const {
1089
3056
  apiKey,
@@ -1130,12 +3097,12 @@ async function generateMinimaxSpeech(opts) {
1130
3097
  const audioBytes = Buffer.from(audioHex, "hex");
1131
3098
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
1132
3099
  const outDir = getOutputDir();
1133
- const dest = path10.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
1134
- fs12.writeFileSync(dest, audioBytes);
3100
+ const dest = path24.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
3101
+ fs20.writeFileSync(dest, audioBytes);
1135
3102
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
1136
3103
  if (output) {
1137
- fs12.mkdirSync(path10.dirname(path10.resolve(output)), { recursive: true });
1138
- fs12.copyFileSync(dest, output);
3104
+ fs20.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
3105
+ fs20.copyFileSync(dest, output);
1139
3106
  }
1140
3107
  return { status: "success", generatedAudio: dest, model };
1141
3108
  }
@@ -1182,8 +3149,8 @@ Generated audio: ${result.generatedAudio}`);
1182
3149
  init_errors();
1183
3150
 
1184
3151
  // src/providers/minimax/generators/video.ts
1185
- import fs13 from "fs";
1186
- import path11 from "path";
3152
+ import fs21 from "fs";
3153
+ import path25 from "path";
1187
3154
  async function generateMinimaxVideo(opts) {
1188
3155
  const {
1189
3156
  apiKey,
@@ -1193,6 +3160,7 @@ async function generateMinimaxVideo(opts) {
1193
3160
  resolution = "1080P",
1194
3161
  firstFrame,
1195
3162
  output,
3163
+ thumb,
1196
3164
  logger
1197
3165
  } = opts;
1198
3166
  const payload = { prompt, model, duration, resolution };
@@ -1228,19 +3196,24 @@ async function generateMinimaxVideo(opts) {
1228
3196
  return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
1229
3197
  }
1230
3198
  const outDir = getOutputDir();
1231
- const dest = path11.join(outDir, `minimax_video_${Date.now()}.mp4`);
3199
+ const dest = path25.join(outDir, `minimax_video_${Date.now()}.mp4`);
1232
3200
  try {
1233
3201
  await downloadMinimaxFile(fileId, apiKey, dest, logger);
1234
3202
  } catch (e) {
1235
3203
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
1236
3204
  }
1237
3205
  const elapsed = (Date.now() - start) / 1e3;
1238
- const stat = fs13.statSync(dest);
3206
+ const stat = fs21.statSync(dest);
1239
3207
  const fileSizeMb = stat.size / (1024 * 1024);
1240
3208
  if (output) {
1241
- fs13.mkdirSync(path11.dirname(path11.resolve(output)), { recursive: true });
1242
- fs13.copyFileSync(dest, output);
3209
+ fs21.mkdirSync(path25.dirname(path25.resolve(output)), { recursive: true });
3210
+ fs21.copyFileSync(dest, output);
1243
3211
  }
3212
+ await maybeDownloadThumb(pollResult, dest, {
3213
+ skip: thumb === false,
3214
+ copyTo: output,
3215
+ logger
3216
+ });
1244
3217
  logger?.debug(`Generated in ${elapsed.toFixed(1)}s, size: ${fileSizeMb.toFixed(2)} MB`);
1245
3218
  return {
1246
3219
  status: "success",
@@ -1253,7 +3226,7 @@ async function generateMinimaxVideo(opts) {
1253
3226
 
1254
3227
  // src/providers/minimax/commands/generate-video.ts
1255
3228
  function registerMinimaxGenerateVideoCommand(parent) {
1256
- parent.command("generate-video").description("Generate video with MiniMax Hailuo models (async)").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", `Model (${[...MINIMAX_VIDEO_MODELS].join("|")})`).option("--duration <n>", `Duration in seconds (${VIDEO_DURATIONS.join("|")})`, "6").option("--resolution <res>", `Resolution (${VIDEO_RESOLUTIONS.join("|")})`, "1080P").option("--first-frame <url>", "First frame image URL").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").action(
3229
+ parent.command("generate-video").description("Generate video with MiniMax Hailuo models (async)").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", `Model (${[...MINIMAX_VIDEO_MODELS].join("|")})`).option("--duration <n>", `Duration in seconds (${VIDEO_DURATIONS.join("|")})`, "6").option("--resolution <res>", `Resolution (${VIDEO_RESOLUTIONS.join("|")})`, "1080P").option("--first-frame <url>", "First frame image URL").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
1257
3230
  async (opts) => {
1258
3231
  const logger = createLogger({ verbose: opts.verbose ?? false });
1259
3232
  const apiKey = requireMinimaxKey();
@@ -1278,6 +3251,7 @@ function registerMinimaxGenerateVideoCommand(parent) {
1278
3251
  resolution,
1279
3252
  firstFrame: opts.firstFrame,
1280
3253
  output: opts.output,
3254
+ thumb: opts.thumb,
1281
3255
  logger
1282
3256
  });
1283
3257
  if (result.status === "error") {
@@ -1296,8 +3270,8 @@ Generated video: ${result.generatedVideo}`);
1296
3270
  // src/providers/minimax/generators/image.ts
1297
3271
  init_errors();
1298
3272
  init_http_client();
1299
- import fs14 from "fs";
1300
- import path12 from "path";
3273
+ import fs22 from "fs";
3274
+ import path26 from "path";
1301
3275
  async function generateMinimaxImage(opts) {
1302
3276
  const {
1303
3277
  apiKey,
@@ -1335,8 +3309,8 @@ async function generateMinimaxImage(opts) {
1335
3309
  try {
1336
3310
  const bytes = await fetchBytes(url);
1337
3311
  const fname = `minimax_image_${Date.now()}_${i}.png`;
1338
- const dest = path12.join(outDir, fname);
1339
- fs14.writeFileSync(dest, bytes);
3312
+ const dest = path26.join(outDir, fname);
3313
+ fs22.writeFileSync(dest, bytes);
1340
3314
  savedFiles.push(dest);
1341
3315
  logger?.success(`Saved: ${dest}`);
1342
3316
  } catch (e) {
@@ -1347,8 +3321,8 @@ async function generateMinimaxImage(opts) {
1347
3321
  }
1348
3322
  }
1349
3323
  if (output && savedFiles[0]) {
1350
- fs14.mkdirSync(path12.dirname(path12.resolve(output)), { recursive: true });
1351
- fs14.copyFileSync(savedFiles[0], output);
3324
+ fs22.mkdirSync(path26.dirname(path26.resolve(output)), { recursive: true });
3325
+ fs22.copyFileSync(savedFiles[0], output);
1352
3326
  }
1353
3327
  return { status: "success", generatedImages: savedFiles, model };
1354
3328
  }
@@ -1381,18 +3355,101 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
1381
3355
  );
1382
3356
  }
1383
3357
 
3358
+ // src/providers/minimax/commands/image-to-image.ts
3359
+ init_errors();
3360
+ init_http_client();
3361
+ import fs23 from "fs";
3362
+ import path27 from "path";
3363
+ function registerMinimaxImageToImageCommand(parent) {
3364
+ parent.command("image-to-image").alias("i2i").description(
3365
+ "MiniMax character/subject preservation. CAVEAT: NOT free-form image editing \u2014 the --ref image is used only as a character/face reference for a NEW scene described by --prompt."
3366
+ ).requiredOption("--prompt <text>", "Prompt describing the desired NEW scene/composition").requiredOption(
3367
+ "--ref <path>",
3368
+ "Subject reference image (URL or local path) \u2014 character/face reference only, single image"
3369
+ ).option("--model <id>", "Model id (default image-01)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9)", "1:1").option("--num-images <n>", "Number of images (1-9)", "1").option("--output <path>", "Copy first image to this path").option("-v, --verbose", "Verbose logging").action(
3370
+ async (opts) => {
3371
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3372
+ const apiKey = requireMinimaxKey();
3373
+ const model = opts.model ?? TASK_DEFAULTS.image;
3374
+ const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
3375
+ logger.warn(
3376
+ "MiniMax i2i preserves a CHARACTER reference only. It does not edit the input image. If you need free-form edits, use byteplus, gemini, openrouter, or leonardo image-to-image."
3377
+ );
3378
+ const resolved = await resolveImageInput(opts.ref, { logger });
3379
+ const imageFile = refUrl(resolved);
3380
+ const payload = {
3381
+ model,
3382
+ prompt: opts.prompt,
3383
+ aspect_ratio: opts.aspectRatio,
3384
+ n: numImages,
3385
+ response_format: "url",
3386
+ prompt_optimizer: true,
3387
+ subject_reference: [
3388
+ {
3389
+ type: "character",
3390
+ image_file: imageFile
3391
+ }
3392
+ ]
3393
+ };
3394
+ logger.debug(`MiniMax i2i: model=${model}, ref=${opts.ref}, n=${numImages}`);
3395
+ let resp;
3396
+ try {
3397
+ resp = await apiPost("image_generation", payload, apiKey, {
3398
+ logger
3399
+ });
3400
+ } catch (e) {
3401
+ logger.error(e instanceof Error ? e.message : String(e));
3402
+ process.exit(1);
3403
+ }
3404
+ const imageUrls = resp.data?.image_urls ?? [];
3405
+ if (imageUrls.length === 0) {
3406
+ logger.error("No images in response");
3407
+ process.exit(1);
3408
+ }
3409
+ const outDir = getOutputDir();
3410
+ const ts = Date.now();
3411
+ const saved = [];
3412
+ for (let i = 0; i < imageUrls.length; i++) {
3413
+ const url = imageUrls[i];
3414
+ if (!url) continue;
3415
+ try {
3416
+ const bytes = await fetchBytes(url);
3417
+ const dest = path27.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
3418
+ fs23.writeFileSync(dest, bytes);
3419
+ saved.push(dest);
3420
+ logger.success(`Saved: ${dest}`);
3421
+ } catch (e) {
3422
+ throw new ProviderError(
3423
+ `Failed to download image: ${e instanceof Error ? e.message : String(e)}`,
3424
+ "minimax"
3425
+ );
3426
+ }
3427
+ }
3428
+ if (opts.output && saved[0]) {
3429
+ fs23.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
3430
+ fs23.copyFileSync(saved[0], opts.output);
3431
+ logger.success(`Copied to: ${opts.output}`);
3432
+ }
3433
+ console.log(`
3434
+ Generated ${saved.length} image(s):`);
3435
+ for (const f of saved) console.log(` ${f}`);
3436
+ }
3437
+ );
3438
+ }
3439
+
1384
3440
  // src/providers/minimax/commands/index.ts
1385
3441
  function registerMinimaxCommands(program2) {
1386
3442
  const minimax = program2.command("minimax").description("MiniMax generation: images, video, speech, and music");
1387
3443
  registerMinimaxGenerateCommand(minimax);
3444
+ registerMinimaxImageToImageCommand(minimax);
1388
3445
  registerMinimaxGenerateVideoCommand(minimax);
1389
3446
  registerMinimaxGenerateSpeechCommand(minimax);
1390
3447
  registerMinimaxGenerateMusicCommand(minimax);
1391
3448
  }
1392
3449
 
1393
3450
  // src/providers/openrouter/client.ts
1394
- import fs15 from "fs";
1395
- import path13 from "path";
3451
+ import fs24 from "fs";
3452
+ import path28 from "path";
1396
3453
  init_errors();
1397
3454
  init_http_client();
1398
3455
  var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
@@ -1481,8 +3538,8 @@ async function generateOpenRouterImage(opts) {
1481
3538
  if (!url) continue;
1482
3539
  try {
1483
3540
  const bytes = await fetchBytes(url);
1484
- const dest = path13.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
1485
- fs15.writeFileSync(dest, bytes);
3541
+ const dest = path28.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
3542
+ fs24.writeFileSync(dest, bytes);
1486
3543
  savedFiles.push(dest);
1487
3544
  logger?.success(`Saved: ${dest}`);
1488
3545
  } catch (e) {
@@ -1493,8 +3550,8 @@ async function generateOpenRouterImage(opts) {
1493
3550
  }
1494
3551
  }
1495
3552
  if (output && savedFiles[0]) {
1496
- fs15.mkdirSync(path13.dirname(path13.resolve(output)), { recursive: true });
1497
- fs15.writeFileSync(output, fs15.readFileSync(savedFiles[0]));
3553
+ fs24.mkdirSync(path28.dirname(path28.resolve(output)), { recursive: true });
3554
+ fs24.writeFileSync(output, fs24.readFileSync(savedFiles[0]));
1498
3555
  }
1499
3556
  return { status: "success", generatedImages: savedFiles, model: usedModel };
1500
3557
  }
@@ -1528,10 +3585,328 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
1528
3585
  );
1529
3586
  }
1530
3587
 
3588
+ // src/providers/openrouter/commands/image-to-image.ts
3589
+ import fs25 from "fs";
3590
+ import path29 from "path";
3591
+ init_errors();
3592
+ init_http_client();
3593
+ var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
3594
+ var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
3595
+ var HARD_LIMIT_BYTES2 = 8 * 1024 * 1024;
3596
+ function registerOpenRouterImageToImageCommand(parent) {
3597
+ parent.command("image-to-image").alias("i2i").description("Edit/transform images via OpenRouter (default: google/gemini-2.5-flash-image)").requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
3598
+ "--ref <path>",
3599
+ "Reference image (URL or local path); repeatable (most models accept 1)",
3600
+ collect5,
3601
+ []
3602
+ ).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
3603
+ async (opts) => {
3604
+ if (!opts.ref || opts.ref.length === 0) {
3605
+ throw new Error("At least one --ref is required for image-to-image.");
3606
+ }
3607
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3608
+ const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_MODEL;
3609
+ const resolvedRefs = [];
3610
+ for (const r of opts.ref) {
3611
+ const resolved = await resolveImageInput(r, {
3612
+ logger,
3613
+ softLimitBytes: SOFT_LIMIT_BYTES2,
3614
+ hardLimitBytes: HARD_LIMIT_BYTES2
3615
+ });
3616
+ resolvedRefs.push(refUrl(resolved));
3617
+ }
3618
+ logger.debug(`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}`);
3619
+ const saved = await runImageToImage({
3620
+ prompt: opts.prompt,
3621
+ model,
3622
+ refs: resolvedRefs,
3623
+ output: opts.output,
3624
+ logger
3625
+ });
3626
+ console.log(`
3627
+ Generated ${saved.length} image(s):`);
3628
+ for (const f of saved) console.log(` ${f}`);
3629
+ console.log(`Model used: ${model}`);
3630
+ }
3631
+ );
3632
+ }
3633
+ function collect5(value, prev) {
3634
+ return [...prev, value];
3635
+ }
3636
+ async function runImageToImage(opts) {
3637
+ const apiKey = requireOpenRouterKey();
3638
+ const headers = {
3639
+ Authorization: `Bearer ${apiKey}`,
3640
+ "Content-Type": "application/json"
3641
+ };
3642
+ const referer = resolveKey("OPENROUTER_SITE_URL");
3643
+ const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
3644
+ if (referer) headers["HTTP-Referer"] = referer;
3645
+ if (title) headers["X-Title"] = title;
3646
+ const content = opts.refs.map((url) => ({
3647
+ type: "image_url",
3648
+ image_url: { url }
3649
+ }));
3650
+ content.push({ type: "text", text: opts.prompt });
3651
+ const payload = {
3652
+ model: opts.model,
3653
+ messages: [{ role: "user", content }],
3654
+ modalities: opts.model.includes("gemini") ? ["image", "text"] : ["image"]
3655
+ };
3656
+ const data = await httpJson({
3657
+ url: OPENROUTER_API_URL,
3658
+ method: "POST",
3659
+ headers,
3660
+ body: payload,
3661
+ timeoutMs: 24e4
3662
+ });
3663
+ const messageImages = data.choices?.[0]?.message?.images ?? [];
3664
+ if (messageImages.length === 0) {
3665
+ throw new ProviderError(
3666
+ `No images in response \u2014 model '${opts.model}' may not support image-to-image`,
3667
+ "openrouter"
3668
+ );
3669
+ }
3670
+ const outDir = getOutputDir();
3671
+ const ts = Date.now();
3672
+ const saved = [];
3673
+ for (let i = 0; i < messageImages.length; i++) {
3674
+ const url = messageImages[i]?.image_url?.url;
3675
+ if (!url) continue;
3676
+ const bytes = await fetchBytes(url);
3677
+ const dest = path29.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
3678
+ fs25.writeFileSync(dest, bytes);
3679
+ saved.push(dest);
3680
+ opts.logger.success(`Saved: ${dest}`);
3681
+ }
3682
+ if (opts.output && saved[0]) {
3683
+ fs25.mkdirSync(path29.dirname(path29.resolve(opts.output)), { recursive: true });
3684
+ fs25.copyFileSync(saved[0], opts.output);
3685
+ opts.logger.success(`Copied to: ${opts.output}`);
3686
+ }
3687
+ return saved;
3688
+ }
3689
+
3690
+ // src/providers/openrouter/video-client.ts
3691
+ init_http_client();
3692
+ var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
3693
+ function authHeaders3(apiKey) {
3694
+ const headers = {
3695
+ Authorization: `Bearer ${apiKey}`,
3696
+ "Content-Type": "application/json"
3697
+ };
3698
+ const referer = resolveKey("OPENROUTER_SITE_URL");
3699
+ const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
3700
+ if (referer) headers["HTTP-Referer"] = referer;
3701
+ if (title) headers["X-Title"] = title;
3702
+ return headers;
3703
+ }
3704
+ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
3705
+ return httpJson({
3706
+ url: OPENROUTER_VIDEOS_URL,
3707
+ method: "POST",
3708
+ headers: authHeaders3(apiKey),
3709
+ body,
3710
+ timeoutMs: 12e4
3711
+ });
3712
+ }
3713
+ async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
3714
+ return httpJson({
3715
+ url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
3716
+ headers: authHeaders3(apiKey)
3717
+ });
3718
+ }
3719
+ function videoContentUrl(jobId) {
3720
+ return `${OPENROUTER_VIDEOS_URL}/${jobId}/content`;
3721
+ }
3722
+ async function listVideoModels(apiKey = requireOpenRouterKey()) {
3723
+ return httpJson({
3724
+ url: `${OPENROUTER_VIDEOS_URL}/models`,
3725
+ headers: authHeaders3(apiKey)
3726
+ });
3727
+ }
3728
+
3729
+ // src/providers/openrouter/commands/video-status.ts
3730
+ import fs26 from "fs";
3731
+ import path30 from "path";
3732
+ async function downloadOpenRouterVideo(opts) {
3733
+ const { jobId, logger, source } = opts;
3734
+ const apiKey = requireOpenRouterKey();
3735
+ const url = videoContentUrl(jobId);
3736
+ const dest = path30.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
3737
+ const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
3738
+ if (!r.ok) {
3739
+ logger.error(`Download HTTP ${r.status}`);
3740
+ process.exit(1);
3741
+ }
3742
+ if (!r.body) {
3743
+ logger.error("Empty response body");
3744
+ process.exit(1);
3745
+ }
3746
+ fs26.mkdirSync(path30.dirname(dest), { recursive: true });
3747
+ const ws = fs26.createWriteStream(dest);
3748
+ const { pipeline: pipeline2 } = await import("stream/promises");
3749
+ await pipeline2(r.body, ws);
3750
+ logger.success(`Saved ${dest}`);
3751
+ if (opts.output) {
3752
+ fs26.mkdirSync(path30.dirname(path30.resolve(opts.output)), { recursive: true });
3753
+ fs26.copyFileSync(dest, opts.output);
3754
+ logger.success(`Copied to ${opts.output}`);
3755
+ }
3756
+ await maybeDownloadThumb(source, dest, {
3757
+ skip: opts.thumb === false,
3758
+ copyTo: opts.output,
3759
+ logger
3760
+ });
3761
+ return dest;
3762
+ }
3763
+ function registerOpenRouterVideoStatusCommand(parent) {
3764
+ parent.command("video-status <jobId>").description("Poll an OpenRouter video generation job").option("--wait", "Poll until status is 'completed' or 'failed'").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "If completed, download the video (implies --wait)").option("--output <path>", "Copy downloaded video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
3765
+ async (jobId, opts) => {
3766
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3767
+ const shouldWait = !!(opts.wait || opts.download);
3768
+ let res;
3769
+ try {
3770
+ res = shouldWait ? await poll({
3771
+ fetch: () => pollVideoJob(jobId),
3772
+ done: (v) => v.status === "completed",
3773
+ failed: (v) => v.status === "failed",
3774
+ intervalMs: 5e3,
3775
+ maxAttempts: Math.max(
3776
+ 1,
3777
+ Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
3778
+ ),
3779
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
3780
+ }) : await pollVideoJob(jobId);
3781
+ } catch (e) {
3782
+ if (e instanceof PollTimeoutError) {
3783
+ logger.error(`Timed out waiting for job ${jobId}`);
3784
+ process.exit(1);
3785
+ }
3786
+ if (e instanceof PollFailedError) {
3787
+ logger.error(`Job failed: ${JSON.stringify(e.value)}`);
3788
+ process.exit(1);
3789
+ }
3790
+ throw e;
3791
+ }
3792
+ console.error(`status: ${res.status}`);
3793
+ for (const u of res.unsigned_urls ?? []) console.log(u);
3794
+ if (opts.download && res.status === "completed") {
3795
+ await downloadOpenRouterVideo({
3796
+ jobId,
3797
+ output: opts.output,
3798
+ thumb: opts.thumb,
3799
+ logger,
3800
+ source: res
3801
+ });
3802
+ } else if (opts.download) {
3803
+ logger.warn(`Cannot download \u2014 status is '${res.status}', not 'completed'`);
3804
+ }
3805
+ if (res.usage?.cost !== void 0) {
3806
+ logger.debug(`cost: ${res.usage.cost}${res.usage.is_byok ? " (BYOK)" : ""}`);
3807
+ }
3808
+ }
3809
+ );
3810
+ }
3811
+
3812
+ // src/providers/openrouter/commands/image-to-video.ts
3813
+ function registerOpenRouterImageToVideoCommand(parent) {
3814
+ parent.command("image-to-video").alias("i2v").description("Submit an image-to-video job to OpenRouter (async, returns job id)").requiredOption("--prompt <text>", "Motion/scene prompt").requiredOption("--image-url <url>", "First-frame image URL (https)").option("--last-frame-url <url>", "Optional last-frame image URL").option("--model <id>", "OpenRouter video model id (e.g. google/veo-3.1)").option("--resolution <r>", "Resolution hint (e.g. 720p, 1080p)").option("--aspect-ratio <r>", "Aspect ratio hint (e.g. 16:9, 9:16)").option("--duration <n>", "Duration seconds").option("--seed <n>", "Fixed seed").option("--wait", "Poll until status is 'completed' or 'failed'").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download the generated video on success (implies --wait)").option("--output <path>", "Copy downloaded video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
3815
+ async (opts) => {
3816
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3817
+ const model = opts.model ?? resolveKey("OPENROUTER_VIDEO_MODEL") ?? "google/veo-3.1";
3818
+ const frame_images = [
3819
+ {
3820
+ type: "image_url",
3821
+ image_url: { url: opts.imageUrl },
3822
+ frame_type: "first_frame"
3823
+ }
3824
+ ];
3825
+ if (opts.lastFrameUrl) {
3826
+ frame_images.push({
3827
+ type: "image_url",
3828
+ image_url: { url: opts.lastFrameUrl },
3829
+ frame_type: "last_frame"
3830
+ });
3831
+ }
3832
+ const body = {
3833
+ model,
3834
+ prompt: opts.prompt,
3835
+ frame_images
3836
+ };
3837
+ if (opts.resolution) body.resolution = opts.resolution;
3838
+ if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
3839
+ if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
3840
+ if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
3841
+ logger.info(`Submitting image-to-video job (model=${model})`);
3842
+ const res = await submitVideoJob(body);
3843
+ logger.success(`Job ${res.id} (status: ${res.status})`);
3844
+ const shouldWait = !!(opts.wait || opts.download);
3845
+ if (!shouldWait) {
3846
+ console.log(res.id);
3847
+ console.error(`Poll with: multix openrouter video-status ${res.id}`);
3848
+ return;
3849
+ }
3850
+ let final;
3851
+ try {
3852
+ final = await poll({
3853
+ fetch: () => pollVideoJob(res.id),
3854
+ done: (v) => v.status === "completed",
3855
+ failed: (v) => v.status === "failed",
3856
+ intervalMs: 5e3,
3857
+ maxAttempts: Math.max(
3858
+ 1,
3859
+ Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
3860
+ ),
3861
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
3862
+ });
3863
+ } catch (e) {
3864
+ if (e instanceof PollTimeoutError) {
3865
+ logger.error(`Timed out \u2014 multix openrouter video-status ${res.id}`);
3866
+ process.exit(1);
3867
+ }
3868
+ if (e instanceof PollFailedError) {
3869
+ logger.error(`Job failed: ${JSON.stringify(e.value)}`);
3870
+ process.exit(1);
3871
+ }
3872
+ throw e;
3873
+ }
3874
+ console.error(`status: ${final.status}`);
3875
+ for (const u of final.unsigned_urls ?? []) console.log(u);
3876
+ if (opts.download) {
3877
+ await downloadOpenRouterVideo({
3878
+ jobId: res.id,
3879
+ output: opts.output,
3880
+ thumb: opts.thumb,
3881
+ logger,
3882
+ source: final
3883
+ });
3884
+ }
3885
+ }
3886
+ );
3887
+ }
3888
+
3889
+ // src/providers/openrouter/commands/video-models.ts
3890
+ function registerOpenRouterVideoModelsCommand(parent) {
3891
+ parent.command("video-models").description("List available OpenRouter video models").option("-v, --verbose", "Verbose logging").action(async (opts) => {
3892
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3893
+ const res = await listVideoModels();
3894
+ const list = res.data ?? res.models ?? [];
3895
+ for (const m of list) {
3896
+ console.log(`${m.id} ${m.name ?? ""}`);
3897
+ }
3898
+ logger.debug(`${list.length} model(s)`);
3899
+ });
3900
+ }
3901
+
1531
3902
  // src/providers/openrouter/commands/index.ts
1532
3903
  function registerOpenRouterCommands(program2) {
1533
- const openrouter = program2.command("openrouter").description("OpenRouter image generation via chat completions API");
3904
+ const openrouter = program2.command("openrouter").description("OpenRouter image + video generation");
1534
3905
  registerOpenRouterGenerateCommand(openrouter);
3906
+ registerOpenRouterImageToImageCommand(openrouter);
3907
+ registerOpenRouterImageToVideoCommand(openrouter);
3908
+ registerOpenRouterVideoStatusCommand(openrouter);
3909
+ registerOpenRouterVideoModelsCommand(openrouter);
1535
3910
  }
1536
3911
 
1537
3912
  // src/commands/check-helpers/binary-check.ts
@@ -1585,6 +3960,8 @@ To configure multix, set at least one provider API key:
1585
3960
  Gemini / AI Studio : https://aistudio.google.com/apikey
1586
3961
  OpenRouter : https://openrouter.ai/settings/keys
1587
3962
  MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
3963
+ Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
3964
+ BytePlus : https://console.byteplus.com/auth/api-keys
1588
3965
 
1589
3966
  Option A \u2014 user global config (recommended):
1590
3967
  echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
@@ -1604,6 +3981,48 @@ var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands u
1604
3981
  var MAGICK_HINT = "ImageMagick (magick) not found \u2014 image optimization unavailable.\n Linux: sudo apt-get install imagemagick\n macOS: brew install imagemagick\n Windows: https://imagemagick.org/script/download.php";
1605
3982
 
1606
3983
  // src/commands/check.ts
3984
+ var PROVIDERS = [
3985
+ {
3986
+ name: "Gemini",
3987
+ envPrimary: "GEMINI_API_KEY",
3988
+ optional: false,
3989
+ link: "https://aistudio.google.com/apikey"
3990
+ },
3991
+ {
3992
+ name: "OpenRouter",
3993
+ envPrimary: "OPENROUTER_API_KEY",
3994
+ optional: true,
3995
+ link: "https://openrouter.ai/settings/keys"
3996
+ },
3997
+ {
3998
+ name: "MiniMax",
3999
+ envPrimary: "MINIMAX_API_KEY",
4000
+ optional: true,
4001
+ link: "https://platform.minimax.io/user-center/basic-information/interface-key"
4002
+ },
4003
+ {
4004
+ name: "Leonardo.Ai",
4005
+ envPrimary: "LEONARDO_API_KEY",
4006
+ optional: true,
4007
+ link: "https://app.leonardo.ai/settings/api-keys"
4008
+ },
4009
+ {
4010
+ name: "BytePlus",
4011
+ envPrimary: "BYTEPLUS_API_KEY",
4012
+ envFallback: "ARK_API_KEY",
4013
+ optional: true,
4014
+ link: "https://console.byteplus.com/auth/api-keys"
4015
+ }
4016
+ ];
4017
+ function resolveProviderKey(p) {
4018
+ const primary = resolveKey(p.envPrimary);
4019
+ if (primary) return { key: primary, envUsed: p.envPrimary };
4020
+ if (p.envFallback) {
4021
+ const fb = resolveKey(p.envFallback);
4022
+ if (fb) return { key: fb, envUsed: p.envFallback };
4023
+ }
4024
+ return {};
4025
+ }
1607
4026
  function registerCheckCommand(program2) {
1608
4027
  program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
1609
4028
  const logger = createLogger({ verbose: opts.verbose ?? false });
@@ -1624,29 +4043,28 @@ function registerCheckCommand(program2) {
1624
4043
  if (opts.verbose) console.log(` ${MAGICK_HINT}`);
1625
4044
  }
1626
4045
  logger.header("API Keys");
1627
- const geminiKey = resolveKey("GEMINI_API_KEY");
1628
- const openrouterKey = resolveKey("OPENROUTER_API_KEY");
1629
- const minimaxKey = resolveKey("MINIMAX_API_KEY");
1630
- if (geminiKey) {
1631
- logger.success(`GEMINI_API_KEY found \u2014 ${redact(geminiKey)}`);
1632
- } else {
1633
- logger.warn("GEMINI_API_KEY not set (Gemini analyze/transcribe/doc features unavailable)");
1634
- }
1635
- if (openrouterKey) {
1636
- logger.success(`OPENROUTER_API_KEY found \u2014 ${redact(openrouterKey)}`);
1637
- } else {
1638
- logger.info("OPENROUTER_API_KEY not set (optional)");
1639
- }
1640
- if (minimaxKey) {
1641
- logger.success(`MINIMAX_API_KEY found \u2014 ${redact(minimaxKey)}`);
1642
- } else {
1643
- logger.info("MINIMAX_API_KEY not set (optional)");
4046
+ let anyKey = false;
4047
+ const resolved = /* @__PURE__ */ new Map();
4048
+ for (const p of PROVIDERS) {
4049
+ const r = resolveProviderKey(p);
4050
+ resolved.set(p.name, r);
4051
+ if (r.key) anyKey = true;
4052
+ const envLabel = r.envUsed ?? p.envPrimary;
4053
+ if (r.key) {
4054
+ logger.success(`${envLabel} found \u2014 ${redact(r.key)}`);
4055
+ } else if (p.optional) {
4056
+ const fbNote = p.envFallback ? ` (or ${p.envFallback})` : "";
4057
+ logger.info(`${p.envPrimary}${fbNote} not set (optional)`);
4058
+ } else {
4059
+ logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
4060
+ }
1644
4061
  }
1645
- if (!geminiKey && !openrouterKey && !minimaxKey) {
4062
+ if (!anyKey) {
1646
4063
  logger.header("No Provider Keys Configured");
1647
4064
  console.log(SETUP_HINTS);
1648
4065
  process.exit(1);
1649
4066
  }
4067
+ const geminiKey = resolved.get("Gemini")?.key;
1650
4068
  if (geminiKey) {
1651
4069
  logger.header("Gemini Connectivity");
1652
4070
  logger.info("Pinging Gemini models endpoint...");
@@ -1668,7 +4086,7 @@ function registerCheckCommand(program2) {
1668
4086
  " Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
1669
4087
  );
1670
4088
  } else {
1671
- console.log(" Image generation via OpenRouter/MiniMax available");
4089
+ console.log(" Image generation via OpenRouter/MiniMax/Leonardo/BytePlus available");
1672
4090
  console.log(" Add GEMINI_API_KEY for analysis, transcription, and doc conversion");
1673
4091
  }
1674
4092
  } else {
@@ -1680,8 +4098,8 @@ function registerCheckCommand(program2) {
1680
4098
 
1681
4099
  // src/commands/doc/convert.ts
1682
4100
  init_errors();
1683
- import fs16 from "fs";
1684
- import path14 from "path";
4101
+ import fs27 from "fs";
4102
+ import path31 from "path";
1685
4103
 
1686
4104
  // src/commands/doc/default-prompt.ts
1687
4105
  var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
@@ -1702,18 +4120,18 @@ async function convertDocuments(opts) {
1702
4120
  const model = opts.model ?? DOC_MODEL_DEFAULT;
1703
4121
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
1704
4122
  for (const f of inputs) {
1705
- if (!fs16.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
4123
+ if (!fs27.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
1706
4124
  }
1707
4125
  let outputPath;
1708
4126
  if (output) {
1709
- outputPath = path14.resolve(output);
4127
+ outputPath = path31.resolve(output);
1710
4128
  } else if (autoName && inputs.length === 1 && inputs[0]) {
1711
- const base = path14.basename(inputs[0], path14.extname(inputs[0]));
1712
- outputPath = path14.join(getOutputDir(), `${base}-extraction.md`);
4129
+ const base = path31.basename(inputs[0], path31.extname(inputs[0]));
4130
+ outputPath = path31.join(getOutputDir(), `${base}-extraction.md`);
1713
4131
  } else {
1714
- outputPath = path14.join(getOutputDir(), "document-extraction.md");
4132
+ outputPath = path31.join(getOutputDir(), "document-extraction.md");
1715
4133
  }
1716
- fs16.mkdirSync(path14.dirname(outputPath), { recursive: true });
4134
+ fs27.mkdirSync(path31.dirname(outputPath), { recursive: true });
1717
4135
  const results = [];
1718
4136
  for (let i = 0; i < inputs.length; i++) {
1719
4137
  const filePath = inputs[i];
@@ -1745,7 +4163,7 @@ async function convertDocuments(opts) {
1745
4163
  if (attempt < 2) {
1746
4164
  const wait = 2 ** attempt * 1e3;
1747
4165
  logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
1748
- await sleep4(wait);
4166
+ await sleep5(wait);
1749
4167
  }
1750
4168
  }
1751
4169
  }
@@ -1764,7 +4182,7 @@ async function convertDocuments(opts) {
1764
4182
  `);
1765
4183
  for (const r of results) {
1766
4184
  lines.push(`
1767
- ## ${path14.basename(r.file)}
4185
+ ## ${path31.basename(r.file)}
1768
4186
  `);
1769
4187
  if (r.status === "success" && r.markdown) {
1770
4188
  lines.push(`${r.markdown}
@@ -1777,7 +4195,7 @@ async function convertDocuments(opts) {
1777
4195
  }
1778
4196
  lines.push("\n---\n");
1779
4197
  }
1780
- fs16.writeFileSync(outputPath, lines.join("\n"), "utf8");
4198
+ fs27.writeFileSync(outputPath, lines.join("\n"), "utf8");
1781
4199
  const succeeded = results.filter((r) => r.status === "success").length;
1782
4200
  console.log(`
1783
4201
  ${"=".repeat(50)}`);
@@ -1793,7 +4211,7 @@ ${"=".repeat(50)}`);
1793
4211
  const failed = results.filter((r) => r.status === "error").length;
1794
4212
  if (failed > 0) process.exit(1);
1795
4213
  }
1796
- function sleep4(ms) {
4214
+ function sleep5(ms) {
1797
4215
  return new Promise((r) => setTimeout(r, ms));
1798
4216
  }
1799
4217
 
@@ -1823,12 +4241,12 @@ function registerDocCommands(program2) {
1823
4241
 
1824
4242
  // src/commands/media/index.ts
1825
4243
  init_errors();
1826
- import fs19 from "fs";
1827
- import path17 from "path";
4244
+ import fs30 from "fs";
4245
+ import path34 from "path";
1828
4246
 
1829
4247
  // src/commands/media/batch.ts
1830
- import fs17 from "fs";
1831
- import path15 from "path";
4248
+ import fs28 from "fs";
4249
+ import path32 from "path";
1832
4250
 
1833
4251
  // src/commands/media/detect-type.ts
1834
4252
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
@@ -2024,10 +4442,10 @@ async function batchOptimize(opts) {
2024
4442
  verbose,
2025
4443
  logger
2026
4444
  } = opts;
2027
- fs17.mkdirSync(outputDir, { recursive: true });
2028
- const entries = fs17.readdirSync(inputDir);
4445
+ fs28.mkdirSync(outputDir, { recursive: true });
4446
+ const entries = fs28.readdirSync(inputDir);
2029
4447
  const mediaFiles = entries.filter((f) => {
2030
- const ext = path15.extname(f).toLowerCase();
4448
+ const ext = path32.extname(f).toLowerCase();
2031
4449
  return ALL_EXTS.has(ext);
2032
4450
  });
2033
4451
  if (mediaFiles.length === 0) {
@@ -2038,9 +4456,9 @@ async function batchOptimize(opts) {
2038
4456
  let succeeded = 0;
2039
4457
  const errors = [];
2040
4458
  for (const file of mediaFiles) {
2041
- const inputPath = path15.join(inputDir, file);
2042
- const outputPath = path15.join(outputDir, file);
2043
- const ext = path15.extname(file).toLowerCase();
4459
+ const inputPath = path32.join(inputDir, file);
4460
+ const outputPath = path32.join(outputDir, file);
4461
+ const ext = path32.extname(file).toLowerCase();
2044
4462
  const kind = extToKind(ext);
2045
4463
  logger?.debug(`Processing: ${file}`);
2046
4464
  try {
@@ -2077,8 +4495,8 @@ async function batchOptimize(opts) {
2077
4495
 
2078
4496
  // src/commands/media/split-video.ts
2079
4497
  init_errors();
2080
- import fs18 from "fs";
2081
- import path16 from "path";
4498
+ import fs29 from "fs";
4499
+ import path33 from "path";
2082
4500
  async function splitVideo(opts) {
2083
4501
  await assertFfmpeg();
2084
4502
  const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
@@ -2089,10 +4507,10 @@ async function splitVideo(opts) {
2089
4507
  logger?.info("Video is short enough \u2014 no splitting needed");
2090
4508
  return [input];
2091
4509
  }
2092
- fs18.mkdirSync(outputDir, { recursive: true });
2093
- const ext = path16.extname(input) || ".mp4";
2094
- const baseName = path16.basename(input, ext);
2095
- const outputPattern = path16.join(outputDir, `${baseName}_chunk_%03d${ext}`);
4510
+ fs29.mkdirSync(outputDir, { recursive: true });
4511
+ const ext = path33.extname(input) || ".mp4";
4512
+ const baseName = path33.basename(input, ext);
4513
+ const outputPattern = path33.join(outputDir, `${baseName}_chunk_%03d${ext}`);
2096
4514
  logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
2097
4515
  const args = [
2098
4516
  "-i",
@@ -2111,8 +4529,8 @@ async function splitVideo(opts) {
2111
4529
  await runFfmpeg(args, { verbose, logger });
2112
4530
  const files = [];
2113
4531
  for (let i = 0; i < numChunks + 1; i++) {
2114
- const fname = path16.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
2115
- if (fs18.existsSync(fname)) files.push(fname);
4532
+ const fname = path33.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
4533
+ if (fs29.existsSync(fname)) files.push(fname);
2116
4534
  }
2117
4535
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
2118
4536
  return files;
@@ -2126,9 +4544,9 @@ function registerMediaCommands(program2) {
2126
4544
  media.command("optimize").description("Optimize a single media file (video, audio, or image)").requiredOption("--input <file>", "Input media file").requiredOption("--output <file>", "Output file path").option("--target-size <MB>", "Target file size in MB").option("--quality <n>", "Quality: video CRF (0-51) or image (1-100)", "85").option("--max-width <px>", "Max image/video width in pixels", "1920").option("--bitrate <rate>", "Audio bitrate (e.g. 64k)", "64k").option("--resolution <WxH>", "Force video resolution (e.g. 1920x1080)").option("-v, --verbose", "Verbose logging").action(
2127
4545
  async (opts) => {
2128
4546
  const logger = createLogger({ verbose: opts.verbose ?? false });
2129
- if (!fs19.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
2130
- fs19.mkdirSync(path17.dirname(path17.resolve(opts.output)), { recursive: true });
2131
- const ext = path17.extname(opts.input).toLowerCase();
4547
+ if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
4548
+ fs30.mkdirSync(path34.dirname(path34.resolve(opts.output)), { recursive: true });
4549
+ const ext = path34.extname(opts.input).toLowerCase();
2132
4550
  const kind = extToKind(ext);
2133
4551
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
2134
4552
  const quality = Number.parseInt(opts.quality, 10);
@@ -2168,7 +4586,7 @@ function registerMediaCommands(program2) {
2168
4586
  media.command("split").description("Split a long video into fixed-duration chunks").requiredOption("--input <file>", "Input video file").option("--output-dir <dir>", "Output directory for chunks", "./chunks").option("--chunk-duration <sec>", "Chunk duration in seconds", "3600").option("-v, --verbose", "Verbose logging").action(
2169
4587
  async (opts) => {
2170
4588
  const logger = createLogger({ verbose: opts.verbose ?? false });
2171
- if (!fs19.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
4589
+ if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
2172
4590
  const chunks = await splitVideo({
2173
4591
  input: opts.input,
2174
4592
  outputDir: opts.outputDir,
@@ -2184,7 +4602,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
2184
4602
  media.command("batch").description("Batch optimize all media files in a directory").requiredOption("--input-dir <dir>", "Input directory").requiredOption("--output-dir <dir>", "Output directory").option("--quality <n>", "Quality setting", "85").option("--max-width <px>", "Max image/video width", "1920").option("--bitrate <rate>", "Audio bitrate", "64k").option("-v, --verbose", "Verbose logging").action(
2185
4603
  async (opts) => {
2186
4604
  const logger = createLogger({ verbose: opts.verbose ?? false });
2187
- if (!fs19.existsSync(opts.inputDir))
4605
+ if (!fs30.existsSync(opts.inputDir))
2188
4606
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
2189
4607
  const result = await batchOptimize({
2190
4608
  inputDir: opts.inputDir,
@@ -2211,6 +4629,8 @@ function registerCommands(program2) {
2211
4629
  registerGeminiCommands(program2);
2212
4630
  registerMinimaxCommands(program2);
2213
4631
  registerOpenRouterCommands(program2);
4632
+ registerLeonardoCommands(program2);
4633
+ registerBytePlusCommands(program2);
2214
4634
  registerMediaCommands(program2);
2215
4635
  registerDocCommands(program2);
2216
4636
  }