@mrgoonie/multix 0.0.2 → 0.0.6

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 +29 -0
  2. package/README.md +154 -2
  3. package/dist/cli.js +3161 -602
  4. package/package.json +11 -3
package/dist/cli.js CHANGED
@@ -67,8 +67,8 @@ __export(http_client_exports, {
67
67
  fetchBytes: () => fetchBytes,
68
68
  httpJson: () => httpJson
69
69
  });
70
- import fs2 from "fs";
71
- import path2 from "path";
70
+ import fs4 from "fs";
71
+ import path4 from "path";
72
72
  import { pipeline } from "stream/promises";
73
73
  async function httpJson(opts) {
74
74
  const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
@@ -103,7 +103,7 @@ async function httpJson(opts) {
103
103
  return response.json();
104
104
  }
105
105
  async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
106
- fs2.mkdirSync(path2.dirname(dest), { recursive: true });
106
+ fs4.mkdirSync(path4.dirname(dest), { recursive: true });
107
107
  let response;
108
108
  try {
109
109
  response = await globalThis.fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
@@ -117,7 +117,7 @@ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
117
117
  if (!response.body) {
118
118
  throw new HttpError(0, "Response body is null", url);
119
119
  }
120
- const fileStream = fs2.createWriteStream(dest);
120
+ const fileStream = fs4.createWriteStream(dest);
121
121
  await pipeline(response.body, fileStream);
122
122
  }
123
123
  async function fetchBytes(urlOrDataUri) {
@@ -155,9 +155,89 @@ 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";
158
+ // src/providers/byteplus/commands/generate-3d.ts
159
+ import path5 from "path";
160
+
161
+ // src/core/image-input.ts
162
+ import fs from "fs/promises";
163
+ import path from "path";
164
+ var IMAGE_MIME_BY_EXT = {
165
+ ".jpg": "image/jpeg",
166
+ ".jpeg": "image/jpeg",
167
+ ".png": "image/png",
168
+ ".webp": "image/webp",
169
+ ".gif": "image/gif"
170
+ };
171
+ var VIDEO_MIME_BY_EXT = {
172
+ ".mp4": "video/mp4",
173
+ ".webm": "video/webm",
174
+ ".mov": "video/quicktime",
175
+ ".m4v": "video/x-m4v"
176
+ };
177
+ var AUDIO_MIME_BY_EXT = {
178
+ ".mp3": "audio/mpeg",
179
+ ".wav": "audio/wav",
180
+ ".m4a": "audio/mp4",
181
+ ".flac": "audio/flac",
182
+ ".ogg": "audio/ogg"
183
+ };
184
+ var DEFAULT_SOFT_LIMIT_BYTES = 10 * 1024 * 1024;
185
+ var DEFAULT_HARD_LIMIT_BYTES = 25 * 1024 * 1024;
186
+ function isHttpUrl(input) {
187
+ return /^https?:\/\//i.test(input);
188
+ }
189
+ async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
190
+ const ext = path.extname(filePath).toLowerCase();
191
+ const mime = mimeTable[ext];
192
+ if (!mime) {
193
+ throw new Error(
194
+ `Unsupported ${kindLabel} extension '${ext}'. Allowed: ${Object.keys(mimeTable).join(", ")}`
195
+ );
196
+ }
197
+ const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
198
+ const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
199
+ const buf = await fs.readFile(filePath);
200
+ if (buf.byteLength > hard) {
201
+ throw new Error(
202
+ `${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.`
203
+ );
204
+ }
205
+ if (buf.byteLength > soft) {
206
+ opts.logger?.warn(
207
+ `${kindLabel} payload ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB is large; consider URL input.`
208
+ );
209
+ }
210
+ const dataUrl = `data:${mime};base64,${buf.toString("base64")}`;
211
+ return { dataUrl, mime, bytes: buf.byteLength };
212
+ }
213
+ async function resolveImageInput(input, opts = {}) {
214
+ const o = normalizeOpts(opts);
215
+ if (isHttpUrl(input)) return { kind: "url", url: input };
216
+ const { dataUrl, mime, bytes } = await readAsDataUrl(input, IMAGE_MIME_BY_EXT, "image", o);
217
+ return { kind: "data", dataUrl, mime, bytes };
218
+ }
219
+ async function resolveVideoInput(input, opts = {}) {
220
+ const o = normalizeOpts(opts);
221
+ if (isHttpUrl(input)) return { kind: "url", url: input };
222
+ const { dataUrl, mime, bytes } = await readAsDataUrl(input, VIDEO_MIME_BY_EXT, "video", o);
223
+ return { kind: "data", dataUrl, mime, bytes };
224
+ }
225
+ async function resolveAudioInput(input, opts = {}) {
226
+ const o = normalizeOpts(opts);
227
+ if (isHttpUrl(input)) return { kind: "url", url: input };
228
+ const { dataUrl, mime, bytes } = await readAsDataUrl(input, AUDIO_MIME_BY_EXT, "audio", o);
229
+ return { kind: "data", dataUrl, mime, bytes };
230
+ }
231
+ function refUrl(r) {
232
+ return r.kind === "url" ? r.url : r.dataUrl;
233
+ }
234
+ function normalizeOpts(opts) {
235
+ if (!opts) return {};
236
+ if (typeof opts.info === "function" && typeof opts.warn === "function") {
237
+ return { logger: opts };
238
+ }
239
+ return opts;
240
+ }
161
241
 
162
242
  // src/core/logger.ts
163
243
  var ANSI = {
@@ -204,26 +284,69 @@ ${line}
204
284
  }
205
285
  var defaultLogger = createLogger({ verbose: false });
206
286
 
207
- // src/providers/gemini/client.ts
208
- import fs3 from "fs";
209
- import path4 from "path";
287
+ // src/core/output-dir.ts
288
+ import fs2 from "fs";
289
+ import path2 from "path";
290
+ var cachedOutputDir;
291
+ function getOutputDir() {
292
+ if (cachedOutputDir) return cachedOutputDir;
293
+ const dir = process.env.MULTIX_OUTPUT_DIR ? path2.resolve(process.env.MULTIX_OUTPUT_DIR) : path2.resolve(process.cwd(), "multix-output");
294
+ fs2.mkdirSync(dir, { recursive: true });
295
+ cachedOutputDir = dir;
296
+ return dir;
297
+ }
298
+
299
+ // src/providers/leonardo/poll.ts
300
+ var PollTimeoutError = class extends Error {
301
+ constructor() {
302
+ super("Polling timed out before the operation completed.");
303
+ this.name = "PollTimeoutError";
304
+ }
305
+ };
306
+ var PollFailedError = class extends Error {
307
+ constructor(value) {
308
+ super("Operation failed.");
309
+ this.value = value;
310
+ this.name = "PollFailedError";
311
+ }
312
+ value;
313
+ };
314
+ async function poll(opts) {
315
+ const interval = opts.intervalMs ?? 4e3;
316
+ const max = opts.maxAttempts ?? 120;
317
+ for (let attempt = 1; attempt <= max; attempt++) {
318
+ if (opts.signal?.aborted) throw new Error("Aborted");
319
+ const value = await opts.fetch();
320
+ opts.onTick?.(attempt, value);
321
+ if (opts.failed?.(value)) throw new PollFailedError(value);
322
+ if (opts.done(value)) return value;
323
+ await new Promise((resolve, reject) => {
324
+ const t = setTimeout(resolve, interval);
325
+ opts.signal?.addEventListener("abort", () => {
326
+ clearTimeout(t);
327
+ reject(new Error("Aborted"));
328
+ });
329
+ });
330
+ }
331
+ throw new PollTimeoutError();
332
+ }
210
333
 
211
334
  // src/core/env-loader.ts
212
- import fs from "fs";
335
+ import fs3 from "fs";
213
336
  import os from "os";
214
- import path from "path";
337
+ import path3 from "path";
215
338
  import { config as dotenvConfig } from "dotenv";
216
339
  var loaded = false;
217
340
  function loadEnv() {
218
341
  if (loaded) return;
219
342
  loaded = true;
220
- const cwdEnv = path.resolve(process.cwd(), ".env");
221
- if (fs.existsSync(cwdEnv)) {
343
+ const cwdEnv = path3.resolve(process.cwd(), ".env");
344
+ if (fs3.existsSync(cwdEnv)) {
222
345
  dotenvConfig({ path: cwdEnv, override: false });
223
346
  }
224
347
  if (!process.env.MULTIX_DISABLE_HOME_ENV) {
225
- const homeEnv = path.join(os.homedir(), ".multix", ".env");
226
- if (fs.existsSync(homeEnv)) {
348
+ const homeEnv = path3.join(os.homedir(), ".multix", ".env");
349
+ if (fs3.existsSync(homeEnv)) {
227
350
  dotenvConfig({ path: homeEnv, override: false });
228
351
  }
229
352
  }
@@ -236,291 +359,1876 @@ function redact(key) {
236
359
  return `${key.slice(0, 6)}...${key.slice(-4)}`;
237
360
  }
238
361
 
239
- // src/providers/gemini/client.ts
362
+ // src/providers/byteplus/client.ts
240
363
  init_errors();
241
364
  init_http_client();
242
365
 
243
- // src/providers/gemini/task-resolver.ts
244
- 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
255
- ".jpg": "image/jpeg",
256
- ".jpeg": "image/jpeg",
257
- ".png": "image/png",
258
- ".webp": "image/webp",
259
- ".heic": "image/heic",
260
- ".heif": "image/heif",
261
- ".gif": "image/gif",
262
- ".bmp": "image/bmp",
263
- // Video
264
- ".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
- ".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"
366
+ // src/providers/byteplus/models.ts
367
+ var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
368
+ var DEFAULT_BYTEPLUS_IMAGE_MODEL = "seedream-4-0-250828";
369
+ var DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-2.0";
370
+ var DEFAULT_BYTEPLUS_3D_MODEL = "hyper3d-gen2-260112";
371
+ var BYTEPLUS_3D_MODELS = [
372
+ { id: "hyper3d-gen2-260112", name: "Hyper3D Gen2", family: "hyper3d" },
373
+ { id: "hitem3d-2-0-251223", name: "Hitem3d 2.0", family: "hitem3d" }
374
+ ];
375
+ var BYTEPLUS_DEFAULTS = {
376
+ imageModel: process.env.BYTEPLUS_IMAGE_MODEL ?? DEFAULT_BYTEPLUS_IMAGE_MODEL,
377
+ videoModel: process.env.BYTEPLUS_VIDEO_MODEL ?? DEFAULT_BYTEPLUS_VIDEO_MODEL,
378
+ threeDModel: process.env.BYTEPLUS_3D_MODEL ?? DEFAULT_BYTEPLUS_3D_MODEL,
379
+ videoResolution: "1080p",
380
+ videoDuration: 8,
381
+ videoAspectRatio: "16:9",
382
+ imageSize: "2K"
283
383
  };
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/");
384
+ function bytePlusVideoParamsMode() {
385
+ const m = process.env.BYTEPLUS_VIDEO_PARAMS_MODE;
386
+ return m === "structured" ? "structured" : "flags";
290
387
  }
291
388
 
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)
389
+ // src/providers/byteplus/client.ts
390
+ function requireBytePlusKey() {
391
+ const key = resolveKey("BYTEPLUS_API_KEY") ?? resolveKey("ARK_API_KEY");
392
+ if (!key) {
299
393
  throw new ConfigError(
300
- "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
394
+ "BytePlus API key not set. Set BYTEPLUS_API_KEY or ARK_API_KEY. Get one at https://console.byteplus.com/auth/api-keys"
301
395
  );
396
+ }
302
397
  return key;
303
398
  }
304
- function authHeaders(apiKey) {
305
- return { "x-goog-api-key": apiKey };
399
+ function bytePlusBaseUrl() {
400
+ return resolveKey("BYTEPLUS_BASE_URL") ?? DEFAULT_BYTEPLUS_BASE_URL;
306
401
  }
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"
347
- );
402
+ var BytePlusClient = class {
403
+ constructor(apiKey, baseUrl = bytePlusBaseUrl()) {
404
+ this.apiKey = apiKey;
405
+ this.baseUrl = baseUrl;
348
406
  }
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}`);
407
+ apiKey;
408
+ baseUrl;
409
+ async request(path36, opts = {}) {
410
+ const base = this.baseUrl.replace(/\/$/, "");
411
+ const url = new URL(base + path36);
412
+ if (opts.query) {
413
+ for (const [k, v] of Object.entries(opts.query)) {
414
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
415
+ }
360
416
  }
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");
417
+ opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
418
+ return httpJson({
419
+ url: url.toString(),
420
+ method: opts.method ?? "GET",
421
+ headers: {
422
+ accept: "application/json",
423
+ authorization: `Bearer ${this.apiKey}`
424
+ },
425
+ body: opts.body,
426
+ timeoutMs: opts.timeoutMs ?? 12e4
427
+ });
365
428
  }
366
- logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
367
- return fileRef;
368
- }
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
- });
429
+ get(path36, query, logger) {
430
+ return this.request(path36, { method: "GET", query, logger });
431
+ }
432
+ post(path36, body, logger) {
433
+ return this.request(path36, { method: "POST", body, logger });
434
+ }
435
+ delete(path36, logger) {
436
+ return this.request(path36, { method: "DELETE", logger });
437
+ }
438
+ };
439
+ function createBytePlusClient() {
440
+ return new BytePlusClient(requireBytePlusKey(), bytePlusBaseUrl());
380
441
  }
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("");
442
+
443
+ // src/providers/byteplus/generators/three-d.ts
444
+ init_http_client();
445
+ function buildThreeDTaskBody(opts) {
446
+ const promptText = (opts.prompt ?? "").trim();
447
+ const flagPart = opts.flags?.trim() ?? "";
448
+ const text = [promptText, flagPart].filter((s) => s.length > 0).join(" ");
449
+ const content = [];
450
+ if (text.length > 0) content.push({ type: "text", text });
451
+ for (const img of opts.imageInputs ?? []) {
452
+ content.push({ type: "image_url", image_url: { url: refUrl(img) } });
453
+ }
454
+ if (content.length === 0) {
455
+ throw new Error("3D task requires either a prompt or at least one image input.");
456
+ }
457
+ const body = { model: opts.model, content };
458
+ if (opts.seed !== void 0) body.seed = opts.seed;
459
+ return body;
384
460
  }
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 }));
461
+ async function submitThreeDTask(client, body, logger) {
462
+ return client.post("/contents/generations/tasks", body, logger);
390
463
  }
391
- function inlinePart(filePath) {
392
- const data = fs3.readFileSync(filePath).toString("base64");
393
- const mimeType = getMimeType(filePath);
394
- return { inlineData: { mimeType, data } };
464
+ async function getThreeDTaskStatus(client, id, logger) {
465
+ return client.get(`/contents/generations/tasks/${id}`, void 0, logger);
395
466
  }
396
- function shouldUseFileApi(filePath) {
397
- const stat = fs3.statSync(filePath);
398
- return stat.size > 20 * 1024 * 1024;
467
+ async function waitForThreeDTask(client, id, opts = {}) {
468
+ const intervalMs = opts.intervalMs ?? 4e3;
469
+ const maxAttempts = opts.maxAttempts ?? (opts.waitTimeoutMs ? Math.max(1, Math.ceil(opts.waitTimeoutMs / intervalMs)) : 180);
470
+ return poll({
471
+ fetch: () => getThreeDTaskStatus(client, id, opts.logger),
472
+ done: (v) => v.status === "succeeded",
473
+ failed: (v) => v.status === "failed" || v.status === "cancelled",
474
+ intervalMs,
475
+ maxAttempts,
476
+ onTick: (attempt, v) => opts.logger?.debug(`attempt ${attempt} \u2014 status: ${v.status}`)
477
+ });
399
478
  }
400
- function sleep(ms) {
401
- return new Promise((resolve) => setTimeout(resolve, ms));
479
+ function inferModelFileExt(url) {
480
+ const m = url.match(/\.(glb|gltf|obj|fbx|usdz|stl|ply|zip)(?:\?|#|$)/i);
481
+ return m?.[1] ? m[1].toLowerCase() : "glb";
402
482
  }
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;
437
- }
438
- if (task === "generate-video") {
439
- return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
440
- }
441
- return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
483
+ async function downloadModelFile(url, outputPath, logger) {
484
+ await downloadFile(url, outputPath);
485
+ logger?.success(`Saved ${outputPath}`);
442
486
  }
443
487
 
444
- // src/providers/gemini/result-formatter.ts
445
- import fs4 from "fs";
446
- import path5 from "path";
488
+ // src/providers/byteplus/commands/generate-3d.ts
489
+ function registerBytePlusGenerate3DCommand(parent) {
490
+ parent.command("generate-3d").alias("3d").description(
491
+ `Generate a 3D model via BytePlus (Hyper3D / Hitem3d). Models: ${BYTEPLUS_3D_MODELS.map((m) => m.id).join(", ")}`
492
+ ).option("--prompt <text>", "Text prompt (text-to-3D or to guide image-to-3D)").option(
493
+ "-m, --model <id>",
494
+ `Model id (default ${BYTEPLUS_DEFAULTS.threeDModel})`,
495
+ BYTEPLUS_DEFAULTS.threeDModel
496
+ ).option("--input-image <path|url...>", "1\u20135 reference images for image-to-3D (path or URL)").option(
497
+ "--flags <raw>",
498
+ "Raw flags appended to the prompt (e.g. '--mesh_mode Raw --hd_texture true' for Hyper3D, '--ff 2 --resolution 1536pro' for Hitem3d)"
499
+ ).option("--seed <n>", "Fixed seed").option("--async", "Submit task and exit; print taskId only").option("--wait-timeout <ms>", "Poll timeout in ms", "900000").option("--output <path>", "Output file path (extension auto-detected from URL if omitted)").option("-v, --verbose", "Verbose logging").action(
500
+ async (opts) => {
501
+ const logger = createLogger({ verbose: opts.verbose ?? false });
502
+ const client = createBytePlusClient();
503
+ const images = opts.inputImage ?? [];
504
+ if (images.length > 5) {
505
+ logger.error("BytePlus 3D supports at most 5 reference images.");
506
+ process.exit(1);
507
+ }
508
+ const imageInputs = await Promise.all(images.map((src) => resolveImageInput(src, logger)));
509
+ if (!opts.prompt && imageInputs.length === 0) {
510
+ logger.error("Provide --prompt and/or --input-image to describe the 3D content.");
511
+ process.exit(1);
512
+ }
513
+ const body = buildThreeDTaskBody({
514
+ model: opts.model,
515
+ prompt: opts.prompt,
516
+ flags: opts.flags,
517
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
518
+ imageInputs
519
+ });
520
+ logger.info(
521
+ `Submitting 3D task (${opts.model}${imageInputs.length > 0 ? `, ${imageInputs.length} image ref${imageInputs.length === 1 ? "" : "s"}` : ""})`
522
+ );
523
+ const submitted = await submitThreeDTask(client, body, logger);
524
+ const taskId = submitted.id;
525
+ logger.debug(`taskId: ${taskId}`);
526
+ if (opts.async) {
527
+ console.log(taskId);
528
+ console.error(`Poll with: multix byteplus status ${taskId} --wait --download`);
529
+ return;
530
+ }
531
+ try {
532
+ const final = await waitForThreeDTask(client, taskId, {
533
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 9e5,
534
+ logger
535
+ });
536
+ const url = final.content?.file_url;
537
+ if (!url) {
538
+ logger.error(`Task succeeded but no file_url in response: ${JSON.stringify(final)}`);
539
+ process.exit(1);
540
+ }
541
+ const ext = inferModelFileExt(url);
542
+ const outPath = opts.output ?? path5.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}-${Date.now()}.${ext}`);
543
+ await downloadModelFile(url, outPath, logger);
544
+ console.log(`
545
+ Task: ${taskId}`);
546
+ console.log(`Saved: ${outPath}`);
547
+ } catch (e) {
548
+ if (e instanceof PollTimeoutError) {
549
+ logger.error(
550
+ `Polling timed out \u2014 keep the task and check later: multix byteplus status ${taskId} --wait --download`
551
+ );
552
+ process.exit(1);
553
+ }
554
+ if (e instanceof PollFailedError) {
555
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
556
+ process.exit(1);
557
+ }
558
+ throw e;
559
+ }
560
+ }
561
+ );
562
+ }
563
+
564
+ // src/providers/byteplus/generators/image.ts
565
+ init_http_client();
566
+ import fs5 from "fs";
567
+ import path6 from "path";
568
+ var ASPECT_TO_SIZE = {
569
+ "1:1": "2048x2048",
570
+ "16:9": "2560x1440",
571
+ "9:16": "1440x2560",
572
+ "4:3": "2048x1536",
573
+ "3:4": "1536x2048",
574
+ "21:9": "2560x1080",
575
+ "3:2": "2048x1365",
576
+ "2:3": "1365x2048"
577
+ };
578
+ function resolveSize(opts) {
579
+ if (opts.size && opts.aspectRatio) {
580
+ opts.logger?.warn("Both --size and --aspect-ratio given; using --size.");
581
+ return opts.size;
582
+ }
583
+ if (opts.size) return opts.size;
584
+ if (opts.aspectRatio) {
585
+ const mapped = ASPECT_TO_SIZE[opts.aspectRatio];
586
+ if (!mapped) {
587
+ throw new Error(
588
+ `Unsupported --aspect-ratio '${opts.aspectRatio}'. Supported: ${Object.keys(ASPECT_TO_SIZE).join(", ")}`
589
+ );
590
+ }
591
+ return mapped;
592
+ }
593
+ return BYTEPLUS_DEFAULTS.imageSize;
594
+ }
595
+ async function generateSeedream(client, opts) {
596
+ const logger = opts.logger;
597
+ const model = opts.model ?? BYTEPLUS_DEFAULTS.imageModel;
598
+ const resolvedInputs = [];
599
+ for (const input of opts.inputImages ?? []) {
600
+ const r = await resolveImageInput(input, logger);
601
+ resolvedInputs.push(refUrl(r));
602
+ }
603
+ const body = {
604
+ model,
605
+ prompt: opts.prompt,
606
+ response_format: "url",
607
+ n: opts.numImages ?? 1,
608
+ watermark: opts.watermark ?? true,
609
+ size: resolveSize(opts)
610
+ };
611
+ if (resolvedInputs.length > 0) body.image = resolvedInputs;
612
+ if (opts.seed !== void 0) body.seed = opts.seed;
613
+ logger?.debug(
614
+ `Seedream body: ${JSON.stringify({ ...body, image: body.image ? `[${body.image.length}]` : void 0 })}`
615
+ );
616
+ const res = await client.post("/images/generations", body, logger);
617
+ const items = res.data ?? [];
618
+ const ts = Date.now();
619
+ const outDir = getOutputDir();
620
+ const saved = [];
621
+ for (let i = 0; i < items.length; i++) {
622
+ const item = items[i];
623
+ if (!item) continue;
624
+ const url = item.url;
625
+ if (!url) {
626
+ logger?.warn(`Image ${i + 1}: no URL in response (b64_json fallback not yet supported).`);
627
+ continue;
628
+ }
629
+ const ext = (() => {
630
+ try {
631
+ const e = path6.extname(new URL(url).pathname);
632
+ return e || ".jpg";
633
+ } catch {
634
+ return ".jpg";
635
+ }
636
+ })();
637
+ const filename = opts.output && items.length === 1 ? path6.resolve(opts.output) : path6.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
638
+ fs5.mkdirSync(path6.dirname(filename), { recursive: true });
639
+ await downloadFile(url, filename);
640
+ saved.push({ path: filename, url, seed: item.seed });
641
+ logger?.success(`Saved ${filename}`);
642
+ }
643
+ return saved;
644
+ }
645
+
646
+ // src/providers/byteplus/commands/generate.ts
647
+ function registerBytePlusGenerateCommand(parent) {
648
+ 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(
649
+ "--input-image <path>",
650
+ "Reference image (URL or local path); repeatable",
651
+ collect,
652
+ []
653
+ ).option("--output <path>", "Save single image to this path").option("-v, --verbose", "Verbose logging").action(
654
+ async (opts) => {
655
+ const logger = createLogger({ verbose: opts.verbose ?? false });
656
+ const client = createBytePlusClient();
657
+ const saved = await generateSeedream(client, {
658
+ prompt: opts.prompt,
659
+ model: opts.model,
660
+ size: opts.size,
661
+ aspectRatio: opts.aspectRatio,
662
+ numImages: Number.parseInt(opts.numImages, 10) || 1,
663
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
664
+ watermark: opts.watermark,
665
+ inputImages: opts.inputImage,
666
+ output: opts.output,
667
+ logger
668
+ });
669
+ console.log(`
670
+ Generated ${saved.length} image(s):`);
671
+ for (const f of saved) console.log(` ${f.path}`);
672
+ }
673
+ );
674
+ }
675
+ function collect(value, prev) {
676
+ return [...prev, value];
677
+ }
678
+
679
+ // src/providers/byteplus/commands/image-to-image.ts
680
+ function registerBytePlusImageToImageCommand(parent) {
681
+ parent.command("image-to-image").alias("i2i").description(
682
+ "Edit/transform image(s) with BytePlus Seedream/Seededit using one or more reference images"
683
+ ).requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
684
+ "--ref <path>",
685
+ "Reference image (URL or local path); repeatable for multi-ref",
686
+ collect2,
687
+ []
688
+ ).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(
689
+ async (opts) => {
690
+ if (!opts.ref || opts.ref.length === 0) {
691
+ throw new Error("At least one --ref is required for image-to-image.");
692
+ }
693
+ const logger = createLogger({ verbose: opts.verbose ?? false });
694
+ const client = createBytePlusClient();
695
+ const saved = await generateSeedream(client, {
696
+ prompt: opts.prompt,
697
+ model: opts.model,
698
+ size: opts.size,
699
+ aspectRatio: opts.aspectRatio,
700
+ numImages: Number.parseInt(opts.numImages, 10) || 1,
701
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
702
+ watermark: opts.watermark,
703
+ inputImages: opts.ref,
704
+ output: opts.output,
705
+ logger
706
+ });
707
+ console.log(`
708
+ Generated ${saved.length} image(s):`);
709
+ for (const f of saved) console.log(` ${f.path}`);
710
+ }
711
+ );
712
+ }
713
+ function collect2(value, prev) {
714
+ return [...prev, value];
715
+ }
716
+
717
+ // src/providers/byteplus/commands/image-to-video.ts
718
+ import path8 from "path";
719
+
720
+ // src/core/video-thumb.ts
721
+ init_http_client();
722
+ import fs6 from "fs";
723
+ import path7 from "path";
724
+ var THUMB_KEYS = /* @__PURE__ */ new Set([
725
+ "cover_image_url",
726
+ "coverImageUrl",
727
+ "cover_url",
728
+ "coverUrl",
729
+ "thumbnail_url",
730
+ "thumbnailUrl",
731
+ "thumb_url",
732
+ "thumbUrl",
733
+ "preview_url",
734
+ "previewUrl",
735
+ "first_frame_url",
736
+ "firstFrameUrl",
737
+ "poster_url",
738
+ "posterUrl",
739
+ "image_url",
740
+ "imageUrl"
741
+ ]);
742
+ var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
743
+ function isImageUrl(value) {
744
+ if (typeof value !== "string") return false;
745
+ if (!/^https?:\/\//i.test(value)) return false;
746
+ return IMAGE_EXT_RE.test(value);
747
+ }
748
+ function detectThumbUrl(obj) {
749
+ if (obj === null || obj === void 0) return null;
750
+ if (Array.isArray(obj)) {
751
+ for (const item of obj) {
752
+ const found = detectThumbUrl(item);
753
+ if (found) return found;
754
+ }
755
+ return null;
756
+ }
757
+ if (typeof obj !== "object") return null;
758
+ const record = obj;
759
+ for (const [key, value] of Object.entries(record)) {
760
+ if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
761
+ }
762
+ for (const value of Object.values(record)) {
763
+ if (value && typeof value === "object") {
764
+ const found = detectThumbUrl(value);
765
+ if (found) return found;
766
+ }
767
+ }
768
+ return null;
769
+ }
770
+ async function downloadThumbBeside(thumbUrl, videoPath, logger) {
771
+ const match = thumbUrl.match(IMAGE_EXT_RE);
772
+ const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
773
+ const dir = path7.dirname(videoPath);
774
+ const base = path7.basename(videoPath, path7.extname(videoPath));
775
+ const dest = path7.join(dir, `${base}_thumb.${ext}`);
776
+ try {
777
+ await downloadFile(thumbUrl, dest);
778
+ logger?.success(`Saved thumb ${dest}`);
779
+ return dest;
780
+ } catch (e) {
781
+ logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
782
+ return null;
783
+ }
784
+ }
785
+ async function maybeDownloadThumb(source, videoPath, opts = {}) {
786
+ if (opts.skip) return null;
787
+ const url = detectThumbUrl(source);
788
+ if (!url) {
789
+ opts.logger?.debug("No thumbnail URL detected in provider response");
790
+ return null;
791
+ }
792
+ const saved = await downloadThumbBeside(url, videoPath, opts.logger);
793
+ if (saved && opts.copyTo) {
794
+ const copyExt = path7.extname(saved);
795
+ const copyBase = path7.basename(opts.copyTo, path7.extname(opts.copyTo));
796
+ const copyDir = path7.dirname(path7.resolve(opts.copyTo));
797
+ const copyDest = path7.join(copyDir, `${copyBase}_thumb${copyExt}`);
798
+ fs6.mkdirSync(copyDir, { recursive: true });
799
+ fs6.copyFileSync(saved, copyDest);
800
+ opts.logger?.success(`Copied thumb to ${copyDest}`);
801
+ }
802
+ return saved;
803
+ }
804
+
805
+ // src/providers/byteplus/generators/video.ts
806
+ init_http_client();
807
+ function buildFlagSuffix(opts) {
808
+ const parts = [];
809
+ if (opts.resolution) parts.push(`--rs ${opts.resolution}`);
810
+ if (opts.duration !== void 0) parts.push(`--dur ${opts.duration}`);
811
+ if (opts.aspectRatio) parts.push(`--rt ${opts.aspectRatio}`);
812
+ if (opts.cameraFixed !== void 0) parts.push(`--cf ${opts.cameraFixed}`);
813
+ if (opts.seed !== void 0) parts.push(`--seed ${opts.seed}`);
814
+ if (opts.audio !== void 0) parts.push(`--wm ${opts.audio}`);
815
+ return parts.length > 0 ? ` ${parts.join(" ")}` : "";
816
+ }
817
+ function buildVideoTaskBody(opts) {
818
+ const mode = opts.paramsMode ?? bytePlusVideoParamsMode();
819
+ const text = mode === "flags" ? `${opts.prompt}${buildFlagSuffix(opts)}${opts.negativePrompt ? ` negative: "${opts.negativePrompt}"` : ""}` : opts.prompt;
820
+ const content = [{ type: "text", text }];
821
+ for (const img of opts.imageInputs ?? []) {
822
+ content.push({ type: "image_url", image_url: { url: refUrl(img) } });
823
+ }
824
+ for (const ref of opts.references ?? []) {
825
+ if (ref.kind === "image") {
826
+ content.push({
827
+ type: "image_url",
828
+ image_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
829
+ });
830
+ } else if (ref.kind === "video") {
831
+ content.push({
832
+ type: "video_url",
833
+ video_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
834
+ });
835
+ } else {
836
+ content.push({
837
+ type: "audio_url",
838
+ audio_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
839
+ });
840
+ }
841
+ }
842
+ const body = { model: opts.model, content };
843
+ if (mode === "structured") {
844
+ body.parameters = {
845
+ ...opts.resolution ? { resolution: opts.resolution } : {},
846
+ ...opts.duration !== void 0 ? { duration: opts.duration } : {},
847
+ ...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
848
+ ...opts.audio !== void 0 ? { audio: opts.audio } : {},
849
+ ...opts.seed !== void 0 ? { seed: opts.seed } : {},
850
+ ...opts.negativePrompt ? { negative_prompt: opts.negativePrompt } : {},
851
+ ...opts.cameraFixed !== void 0 ? { camera_fixed: opts.cameraFixed } : {}
852
+ };
853
+ }
854
+ return body;
855
+ }
856
+ async function submitVideoTask(client, body, logger) {
857
+ return client.post("/contents/generations/tasks", body, logger);
858
+ }
859
+ async function getVideoTaskStatus(client, id, logger) {
860
+ return client.get(`/contents/generations/tasks/${id}`, void 0, logger);
861
+ }
862
+ async function waitForVideoTask(client, id, opts = {}) {
863
+ const intervalMs = opts.intervalMs ?? 4e3;
864
+ const maxAttempts = opts.maxAttempts ?? (opts.waitTimeoutMs ? Math.max(1, Math.ceil(opts.waitTimeoutMs / intervalMs)) : 120);
865
+ return poll({
866
+ fetch: () => getVideoTaskStatus(client, id, opts.logger),
867
+ done: (v) => v.status === "succeeded",
868
+ failed: (v) => v.status === "failed" || v.status === "cancelled",
869
+ intervalMs,
870
+ maxAttempts,
871
+ onTick: (attempt, v) => opts.logger?.debug(`attempt ${attempt} \u2014 status: ${v.status}`)
872
+ });
873
+ }
874
+ async function downloadVideo(url, outputPath, logger) {
875
+ await downloadFile(url, outputPath);
876
+ logger?.success(`Saved ${outputPath}`);
877
+ }
878
+
879
+ // src/providers/byteplus/commands/image-to-video.ts
880
+ function registerBytePlusImageToVideoCommand(parent) {
881
+ 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(
882
+ async (image, opts) => {
883
+ const logger = createLogger({ verbose: opts.verbose ?? false });
884
+ const client = createBytePlusClient();
885
+ const first = await resolveImageInput(image, logger);
886
+ const refs = [];
887
+ if (opts.lastFrame) {
888
+ const last = await resolveImageInput(opts.lastFrame, logger);
889
+ refs.push({
890
+ kind: "image",
891
+ url: last.kind === "url" ? last.url : last.dataUrl,
892
+ mime: last.kind === "data" ? last.mime : void 0,
893
+ role: "last_frame"
894
+ });
895
+ }
896
+ const body = buildVideoTaskBody({
897
+ model: opts.model,
898
+ prompt: opts.prompt,
899
+ resolution: opts.resolution,
900
+ duration: Number.parseInt(opts.duration, 10),
901
+ aspectRatio: opts.aspectRatio,
902
+ audio: opts.audio,
903
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
904
+ negativePrompt: opts.negative,
905
+ cameraFixed: opts.cameraFixed,
906
+ imageInputs: [first],
907
+ references: refs
908
+ });
909
+ logger.info(`Submitting i2v task (${opts.model})`);
910
+ const submitted = await submitVideoTask(client, body, logger);
911
+ const taskId = submitted.id;
912
+ if (opts.async) {
913
+ console.log(taskId);
914
+ console.error(`Poll with: multix byteplus status ${taskId}`);
915
+ return;
916
+ }
917
+ try {
918
+ const final = await waitForVideoTask(client, taskId, {
919
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
920
+ logger
921
+ });
922
+ const url = final.content?.video_url;
923
+ if (!url) {
924
+ logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
925
+ process.exit(1);
926
+ }
927
+ const outPath = opts.output ?? path8.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
928
+ await downloadVideo(url, outPath, logger);
929
+ await maybeDownloadThumb(final, outPath, {
930
+ skip: opts.thumb === false,
931
+ copyTo: opts.output,
932
+ logger
933
+ });
934
+ console.log(`
935
+ Task: ${taskId}`);
936
+ console.log(`Saved: ${outPath}`);
937
+ } catch (e) {
938
+ if (e instanceof PollTimeoutError) {
939
+ logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
940
+ process.exit(1);
941
+ }
942
+ if (e instanceof PollFailedError) {
943
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
944
+ process.exit(1);
945
+ }
946
+ throw e;
947
+ }
948
+ }
949
+ );
950
+ }
951
+
952
+ // src/providers/byteplus/commands/reference-to-video.ts
953
+ import path9 from "path";
954
+
955
+ // src/providers/byteplus/reference-input.ts
956
+ var MAX_TOTAL_REFS = 12;
957
+ var MAX_IMAGE_REFS = 9;
958
+ var MAX_VIDEO_REFS = 3;
959
+ var MAX_AUDIO_REFS = 3;
960
+ function parseRefSpec(input) {
961
+ const PLACEHOLDER = "\0";
962
+ const escaped = input.replace(/\\:/g, PLACEHOLDER);
963
+ const lastIdx = escaped.lastIndexOf(":");
964
+ if (lastIdx === -1) {
965
+ return { path: input.replace(/\\:/g, ":") };
966
+ }
967
+ const left = escaped.slice(0, lastIdx);
968
+ const right = escaped.slice(lastIdx + 1);
969
+ if (/^[a-zA-Z]$/.test(left) && /[\\/]/.test(right)) {
970
+ return { path: input.replace(/\\:/g, ":") };
971
+ }
972
+ if (right.length === 0) {
973
+ return { path: left.replace(new RegExp(PLACEHOLDER, "g"), ":") };
974
+ }
975
+ return {
976
+ path: left.replace(new RegExp(PLACEHOLDER, "g"), ":"),
977
+ role: right.replace(new RegExp(PLACEHOLDER, "g"), ":") || void 0
978
+ };
979
+ }
980
+ function toRef(kind, resolved, role) {
981
+ if (resolved.kind === "url") return { kind, url: resolved.url, role };
982
+ return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
983
+ }
984
+ async function resolveRefSpec(kind, spec, logger) {
985
+ const { path: path36, role } = parseRefSpec(spec);
986
+ if (kind === "image") return toRef(kind, await resolveImageInput(path36, logger), role);
987
+ if (kind === "video") return toRef(kind, await resolveVideoInput(path36, logger), role);
988
+ return toRef(kind, await resolveAudioInput(path36, logger), role);
989
+ }
990
+ async function resolveRefSpecs(specs, logger) {
991
+ const out = [];
992
+ for (const { kind, spec } of specs) {
993
+ out.push(await resolveRefSpec(kind, spec, logger));
994
+ }
995
+ return out;
996
+ }
997
+ function validateRefCounts(refs) {
998
+ if (refs.length > MAX_TOTAL_REFS) {
999
+ throw new Error(`Too many references: ${refs.length} > ${MAX_TOTAL_REFS}`);
1000
+ }
1001
+ const images = refs.filter((r) => r.kind === "image").length;
1002
+ const videos = refs.filter((r) => r.kind === "video").length;
1003
+ const audios = refs.filter((r) => r.kind === "audio").length;
1004
+ if (images > MAX_IMAGE_REFS)
1005
+ throw new Error(`Too many image refs: ${images} > ${MAX_IMAGE_REFS}`);
1006
+ if (videos > MAX_VIDEO_REFS)
1007
+ throw new Error(`Too many video refs: ${videos} > ${MAX_VIDEO_REFS}`);
1008
+ if (audios > MAX_AUDIO_REFS)
1009
+ throw new Error(`Too many audio refs: ${audios} > ${MAX_AUDIO_REFS}`);
1010
+ }
1011
+ var REF_LIMITS = {
1012
+ total: MAX_TOTAL_REFS,
1013
+ image: MAX_IMAGE_REFS,
1014
+ video: MAX_VIDEO_REFS,
1015
+ audio: MAX_AUDIO_REFS
1016
+ };
1017
+
1018
+ // src/providers/byteplus/commands/reference-to-video.ts
1019
+ function collect3(value, prev) {
1020
+ return [...prev, value];
1021
+ }
1022
+ function registerBytePlusReferenceToVideoCommand(parent) {
1023
+ parent.command("reference-to-video").alias("r2v").description(
1024
+ `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 \\:).`
1025
+ ).requiredOption("--prompt <text>", "Text prompt").option(
1026
+ "--ref-image <spec>",
1027
+ "Image ref (path|url[:role]); repeatable",
1028
+ collect3,
1029
+ []
1030
+ ).option(
1031
+ "--ref-video <spec>",
1032
+ "Video ref (path|url[:role]); repeatable",
1033
+ collect3,
1034
+ []
1035
+ ).option(
1036
+ "--ref-audio <spec>",
1037
+ "Audio ref (path|url[:role]); repeatable",
1038
+ collect3,
1039
+ []
1040
+ ).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(
1041
+ async (opts) => {
1042
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1043
+ const specs = [
1044
+ ...opts.refImage.map((s) => ({ kind: "image", spec: s })),
1045
+ ...opts.refVideo.map((s) => ({ kind: "video", spec: s })),
1046
+ ...opts.refAudio.map((s) => ({ kind: "audio", spec: s }))
1047
+ ];
1048
+ if (specs.length === 0) {
1049
+ logger.error("At least one --ref-image / --ref-video / --ref-audio is required");
1050
+ process.exit(2);
1051
+ }
1052
+ const refs = await resolveRefSpecs(specs, logger);
1053
+ validateRefCounts(refs);
1054
+ const client = createBytePlusClient();
1055
+ const body = buildVideoTaskBody({
1056
+ model: opts.model,
1057
+ prompt: opts.prompt,
1058
+ resolution: opts.resolution,
1059
+ duration: Number.parseInt(opts.duration, 10),
1060
+ aspectRatio: opts.aspectRatio,
1061
+ audio: opts.audio,
1062
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
1063
+ negativePrompt: opts.negative,
1064
+ cameraFixed: opts.cameraFixed,
1065
+ references: refs
1066
+ });
1067
+ logger.info(`Submitting r2v task (${opts.model}, ${refs.length} refs)`);
1068
+ const submitted = await submitVideoTask(client, body, logger);
1069
+ const taskId = submitted.id;
1070
+ if (opts.async) {
1071
+ console.log(taskId);
1072
+ console.error(`Poll with: multix byteplus status ${taskId}`);
1073
+ return;
1074
+ }
1075
+ try {
1076
+ const final = await waitForVideoTask(client, taskId, {
1077
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
1078
+ logger
1079
+ });
1080
+ const url = final.content?.video_url;
1081
+ if (!url) {
1082
+ logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
1083
+ process.exit(1);
1084
+ }
1085
+ const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1086
+ await downloadVideo(url, outPath, logger);
1087
+ await maybeDownloadThumb(final, outPath, {
1088
+ skip: opts.thumb === false,
1089
+ copyTo: opts.output,
1090
+ logger
1091
+ });
1092
+ console.log(`
1093
+ Task: ${taskId}`);
1094
+ console.log(`Saved: ${outPath}`);
1095
+ } catch (e) {
1096
+ if (e instanceof PollTimeoutError) {
1097
+ logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
1098
+ process.exit(1);
1099
+ }
1100
+ if (e instanceof PollFailedError) {
1101
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
1102
+ process.exit(1);
1103
+ }
1104
+ throw e;
1105
+ }
1106
+ }
1107
+ );
1108
+ }
1109
+
1110
+ // src/providers/byteplus/commands/status.ts
1111
+ import path10 from "path";
1112
+ function registerBytePlusStatusCommand(parent) {
1113
+ 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(
1114
+ async (taskId, opts) => {
1115
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1116
+ const client = createBytePlusClient();
1117
+ const shouldWait = !!(opts.wait || opts.download);
1118
+ try {
1119
+ const status = shouldWait ? await waitForVideoTask(client, taskId, {
1120
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
1121
+ logger
1122
+ }) : await getVideoTaskStatus(client, taskId, logger);
1123
+ console.log(JSON.stringify(status, null, 2));
1124
+ if (opts.download && status.status === "succeeded") {
1125
+ const content = status.content;
1126
+ const videoUrl = content?.video_url;
1127
+ const fileUrl = content?.file_url;
1128
+ if (videoUrl) {
1129
+ const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
1130
+ await downloadVideo(videoUrl, outPath, logger);
1131
+ await maybeDownloadThumb(status, outPath, {
1132
+ skip: opts.thumb === false,
1133
+ copyTo: opts.output,
1134
+ logger
1135
+ });
1136
+ console.log(`Saved: ${outPath}`);
1137
+ } else if (fileUrl) {
1138
+ const ext = inferModelFileExt(fileUrl);
1139
+ const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
1140
+ await downloadModelFile(fileUrl, outPath, logger);
1141
+ console.log(`Saved: ${outPath}`);
1142
+ } else {
1143
+ logger.error("Task succeeded but no video_url or file_url present");
1144
+ process.exit(1);
1145
+ }
1146
+ }
1147
+ } catch (e) {
1148
+ if (e instanceof PollTimeoutError) {
1149
+ logger.error(`Timed out waiting for task ${taskId}`);
1150
+ process.exit(1);
1151
+ }
1152
+ if (e instanceof PollFailedError) {
1153
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
1154
+ process.exit(1);
1155
+ }
1156
+ throw e;
1157
+ }
1158
+ }
1159
+ );
1160
+ }
1161
+
1162
+ // src/providers/byteplus/commands/video.ts
1163
+ import path11 from "path";
1164
+ function registerBytePlusVideoCommand(parent) {
1165
+ 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(
1166
+ "--aspect-ratio <r>",
1167
+ "Aspect ratio (16:9, 9:16, 1:1, ...)",
1168
+ BYTEPLUS_DEFAULTS.videoAspectRatio
1169
+ ).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(
1170
+ async (opts) => {
1171
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1172
+ const client = createBytePlusClient();
1173
+ const body = buildVideoTaskBody({
1174
+ model: opts.model,
1175
+ prompt: opts.prompt,
1176
+ resolution: opts.resolution,
1177
+ duration: Number.parseInt(opts.duration, 10),
1178
+ aspectRatio: opts.aspectRatio,
1179
+ audio: opts.audio,
1180
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
1181
+ negativePrompt: opts.negative,
1182
+ cameraFixed: opts.cameraFixed
1183
+ });
1184
+ logger.info(
1185
+ `Submitting Seedance task (${opts.model}, ${opts.resolution}, ${opts.duration}s)`
1186
+ );
1187
+ const submitted = await submitVideoTask(client, body, logger);
1188
+ const taskId = submitted.id;
1189
+ logger.debug(`taskId: ${taskId}`);
1190
+ if (opts.async) {
1191
+ console.log(taskId);
1192
+ console.error(`Poll with: multix byteplus status ${taskId}`);
1193
+ return;
1194
+ }
1195
+ try {
1196
+ const final = await waitForVideoTask(client, taskId, {
1197
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
1198
+ logger
1199
+ });
1200
+ const url = final.content?.video_url;
1201
+ if (!url) {
1202
+ logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
1203
+ process.exit(1);
1204
+ }
1205
+ const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1206
+ await downloadVideo(url, outPath, logger);
1207
+ await maybeDownloadThumb(final, outPath, {
1208
+ skip: opts.thumb === false,
1209
+ copyTo: opts.output,
1210
+ logger
1211
+ });
1212
+ console.log(`
1213
+ Task: ${taskId}`);
1214
+ console.log(`Saved: ${outPath}`);
1215
+ } catch (e) {
1216
+ if (e instanceof PollTimeoutError) {
1217
+ logger.error(
1218
+ `Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
1219
+ );
1220
+ process.exit(1);
1221
+ }
1222
+ if (e instanceof PollFailedError) {
1223
+ logger.error(`Task failed: ${JSON.stringify(e.value)}`);
1224
+ process.exit(1);
1225
+ }
1226
+ throw e;
1227
+ }
1228
+ }
1229
+ );
1230
+ }
1231
+
1232
+ // src/providers/byteplus/commands/index.ts
1233
+ function registerBytePlusCommands(program2) {
1234
+ const byteplus = program2.command("byteplus").description(
1235
+ "BytePlus ModelArk: Seedream image, Seedance video, Hyper3D / Hitem3d 3D generation"
1236
+ );
1237
+ registerBytePlusGenerateCommand(byteplus);
1238
+ registerBytePlusImageToImageCommand(byteplus);
1239
+ registerBytePlusVideoCommand(byteplus);
1240
+ registerBytePlusImageToVideoCommand(byteplus);
1241
+ registerBytePlusReferenceToVideoCommand(byteplus);
1242
+ registerBytePlusGenerate3DCommand(byteplus);
1243
+ registerBytePlusStatusCommand(byteplus);
1244
+ }
1245
+
1246
+ // src/providers/gemini/commands/analyze.ts
1247
+ init_errors();
1248
+ import fs9 from "fs";
1249
+
1250
+ // src/providers/gemini/client.ts
1251
+ import fs7 from "fs";
1252
+ import path13 from "path";
1253
+ init_errors();
1254
+ init_http_client();
1255
+
1256
+ // src/providers/gemini/task-resolver.ts
1257
+ import path12 from "path";
1258
+ var MIME_MAP = {
1259
+ // Audio
1260
+ ".mp3": "audio/mp3",
1261
+ ".wav": "audio/wav",
1262
+ ".aac": "audio/aac",
1263
+ ".flac": "audio/flac",
1264
+ ".ogg": "audio/ogg",
1265
+ ".aiff": "audio/aiff",
1266
+ ".m4a": "audio/mp4",
1267
+ // Image
1268
+ ".jpg": "image/jpeg",
1269
+ ".jpeg": "image/jpeg",
1270
+ ".png": "image/png",
1271
+ ".webp": "image/webp",
1272
+ ".heic": "image/heic",
1273
+ ".heif": "image/heif",
1274
+ ".gif": "image/gif",
1275
+ ".bmp": "image/bmp",
1276
+ // Video
1277
+ ".mp4": "video/mp4",
1278
+ ".mpeg": "video/mpeg",
1279
+ ".mov": "video/quicktime",
1280
+ ".avi": "video/x-msvideo",
1281
+ ".flv": "video/x-flv",
1282
+ ".mpg": "video/mpeg",
1283
+ ".webm": "video/webm",
1284
+ ".wmv": "video/x-ms-wmv",
1285
+ ".3gpp": "video/3gpp",
1286
+ // Document
1287
+ ".pdf": "application/pdf",
1288
+ ".txt": "text/plain",
1289
+ ".html": "text/html",
1290
+ ".htm": "text/html",
1291
+ ".md": "text/markdown",
1292
+ ".csv": "text/csv",
1293
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1294
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1295
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
1296
+ };
1297
+ function getMimeType(filePath) {
1298
+ const ext = path12.extname(filePath).toLowerCase();
1299
+ return MIME_MAP[ext] ?? "application/octet-stream";
1300
+ }
1301
+ function requiresProcessingWait(mimeType) {
1302
+ return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
1303
+ }
1304
+
1305
+ // src/providers/gemini/client.ts
1306
+ var BASE = "https://generativelanguage.googleapis.com";
1307
+ var FILES_BASE = `${BASE}/upload/v1beta/files`;
1308
+ var GENERATE_BASE = `${BASE}/v1beta/models`;
1309
+ function requireApiKey() {
1310
+ const key = resolveKey("GEMINI_API_KEY");
1311
+ if (!key)
1312
+ throw new ConfigError(
1313
+ "GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
1314
+ );
1315
+ return key;
1316
+ }
1317
+ function authHeaders(apiKey) {
1318
+ return { "x-goog-api-key": apiKey };
1319
+ }
1320
+ async function uploadFile(filePath, opts = {}) {
1321
+ const apiKey = requireApiKey();
1322
+ const { timeoutMs = 3e5, logger } = opts;
1323
+ const mimeType = getMimeType(filePath);
1324
+ const fileBytes = fs7.readFileSync(filePath);
1325
+ const displayName = path13.basename(filePath);
1326
+ const boundary = `multix_${Date.now()}`;
1327
+ const metaJson = JSON.stringify({ file: { displayName, mimeType } });
1328
+ const body = Buffer.concat([
1329
+ Buffer.from(
1330
+ `--${boundary}\r
1331
+ Content-Type: application/json; charset=UTF-8\r
1332
+ \r
1333
+ ${metaJson}\r
1334
+ --${boundary}\r
1335
+ Content-Type: ${mimeType}\r
1336
+ \r
1337
+ `
1338
+ ),
1339
+ fileBytes,
1340
+ Buffer.from(`\r
1341
+ --${boundary}--`)
1342
+ ]);
1343
+ logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
1344
+ const resp = await globalThis.fetch(FILES_BASE, {
1345
+ method: "POST",
1346
+ headers: {
1347
+ "x-goog-api-key": apiKey,
1348
+ "Content-Type": `multipart/related; boundary=${boundary}`,
1349
+ "Content-Length": String(body.length),
1350
+ "X-Goog-Upload-Protocol": "multipart"
1351
+ },
1352
+ body,
1353
+ signal: AbortSignal.timeout(timeoutMs)
1354
+ });
1355
+ if (!resp.ok) {
1356
+ const txt = await resp.text();
1357
+ throw new ProviderError(
1358
+ `File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
1359
+ "gemini"
1360
+ );
1361
+ }
1362
+ const data = await resp.json();
1363
+ let fileRef = data.file;
1364
+ if (requiresProcessingWait(mimeType)) {
1365
+ const deadline = Date.now() + timeoutMs;
1366
+ while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
1367
+ await sleep(2e3);
1368
+ fileRef = await httpJson({
1369
+ url: `${BASE}/v1beta/${fileRef.name}`,
1370
+ headers: authHeaders(apiKey)
1371
+ }).then((r) => r.file);
1372
+ logger?.debug(`Processing... state=${fileRef.state}`);
1373
+ }
1374
+ if (fileRef.state === "FAILED")
1375
+ throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
1376
+ if (fileRef.state === "PROCESSING")
1377
+ throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
1378
+ }
1379
+ logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
1380
+ return fileRef;
1381
+ }
1382
+ async function generateContent(req) {
1383
+ const apiKey = requireApiKey();
1384
+ return httpJson({
1385
+ url: `${GENERATE_BASE}/${req.model}:generateContent`,
1386
+ method: "POST",
1387
+ headers: authHeaders(apiKey),
1388
+ body: {
1389
+ contents: req.contents,
1390
+ ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
1391
+ }
1392
+ });
1393
+ }
1394
+ function extractText(resp) {
1395
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
1396
+ return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
1397
+ }
1398
+ function extractAudio(resp) {
1399
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
1400
+ for (const p of parts) {
1401
+ if (p.inlineData?.mimeType.startsWith("audio/")) {
1402
+ return { mimeType: p.inlineData.mimeType, data: p.inlineData.data };
1403
+ }
1404
+ }
1405
+ return null;
1406
+ }
1407
+ function extractImages(resp) {
1408
+ const parts = resp.candidates?.[0]?.content?.parts ?? [];
1409
+ return parts.filter(
1410
+ (p) => p.inlineData !== void 0
1411
+ ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
1412
+ }
1413
+ function inlinePart(filePath) {
1414
+ const data = fs7.readFileSync(filePath).toString("base64");
1415
+ const mimeType = getMimeType(filePath);
1416
+ return { inlineData: { mimeType, data } };
1417
+ }
1418
+ function shouldUseFileApi(filePath) {
1419
+ const stat = fs7.statSync(filePath);
1420
+ return stat.size > 20 * 1024 * 1024;
1421
+ }
1422
+ function sleep(ms) {
1423
+ return new Promise((resolve) => setTimeout(resolve, ms));
1424
+ }
1425
+
1426
+ // src/providers/gemini/models.ts
1427
+ var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
1428
+ var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
1429
+ var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
1430
+ var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
1431
+ var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
1432
+ var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
1433
+ var IMAGEN_MODELS = /* @__PURE__ */ new Set([
1434
+ "imagen-4.0-generate-001",
1435
+ "imagen-4.0-ultra-generate-001",
1436
+ "imagen-4.0-fast-generate-001"
1437
+ ]);
1438
+ var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
1439
+ "gemini-3.1-flash-image-preview",
1440
+ "gemini-3-pro-image-preview",
1441
+ "gemini-2.5-flash-image",
1442
+ "gemini-2.5-flash-image-preview",
1443
+ ...IMAGEN_MODELS
1444
+ ]);
1445
+ var ASPECT_RATIOS = [
1446
+ "1:1",
1447
+ "2:3",
1448
+ "3:2",
1449
+ "3:4",
1450
+ "4:3",
1451
+ "4:5",
1452
+ "5:4",
1453
+ "9:16",
1454
+ "16:9",
1455
+ "21:9"
1456
+ ];
1457
+ var IMAGE_SIZES = ["1K", "2K", "4K"];
1458
+ function getDefaultModel(task) {
1459
+ if (task === "generate") {
1460
+ return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
1461
+ }
1462
+ if (task === "generate-video") {
1463
+ return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
1464
+ }
1465
+ if (task === "generate-speech") {
1466
+ return process.env.GEMINI_TTS_MODEL ?? process.env.TTS_MODEL ?? TTS_MODEL_DEFAULT;
1467
+ }
1468
+ return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
1469
+ }
1470
+
1471
+ // src/providers/gemini/result-formatter.ts
1472
+ import fs8 from "fs";
1473
+ import path14 from "path";
447
1474
  function formatResults(results, format, outputPath) {
448
1475
  const content = renderFormat(results, format);
449
1476
  if (outputPath) {
450
- fs4.mkdirSync(path5.dirname(path5.resolve(outputPath)), { recursive: true });
451
- fs4.writeFileSync(outputPath, content, "utf8");
1477
+ fs8.mkdirSync(path14.dirname(path14.resolve(outputPath)), { recursive: true });
1478
+ fs8.writeFileSync(outputPath, content, "utf8");
452
1479
  } else {
453
1480
  process.stdout.write(content);
454
1481
  }
455
1482
  }
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);
467
- }
1483
+ function renderFormat(results, format) {
1484
+ switch (format) {
1485
+ case "json":
1486
+ return `${JSON.stringify(results, null, 2)}
1487
+ `;
1488
+ case "csv":
1489
+ return renderCsv(results);
1490
+ case "markdown":
1491
+ return renderMarkdown(results);
1492
+ default:
1493
+ return renderText(results);
1494
+ }
1495
+ }
1496
+ function renderText(results) {
1497
+ return results.map((r) => {
1498
+ const header = `[${r.file}]
1499
+ Status: ${r.status}`;
1500
+ if (r.status === "success" && r.response) return `${header}
1501
+ Result:
1502
+ ${r.response}
1503
+ `;
1504
+ if (r.status === "error") return `${header}
1505
+ Error: ${r.error}
1506
+ `;
1507
+ return `${header}
1508
+ `;
1509
+ }).join("\n");
1510
+ }
1511
+ function renderCsv(results) {
1512
+ const header = "file,status,response,error\n";
1513
+ const rows = results.map((r) => {
1514
+ const csvCell = (v) => `"${(v ?? "").replace(/"/g, '""')}"`;
1515
+ return [csvCell(r.file), csvCell(r.status), csvCell(r.response), csvCell(r.error)].join(",");
1516
+ });
1517
+ return `${header + rows.join("\n")}
1518
+ `;
1519
+ }
1520
+ function renderMarkdown(results) {
1521
+ const lines = ["# Results\n"];
1522
+ for (const r of results) {
1523
+ lines.push(`## ${r.file}
1524
+ `);
1525
+ lines.push(`**Status**: ${r.status}
1526
+ `);
1527
+ if (r.status === "success" && r.response) lines.push(`**Response**:
1528
+
1529
+ ${r.response}
1530
+ `);
1531
+ if (r.status === "error") lines.push(`**Error**: ${r.error}
1532
+ `);
1533
+ lines.push("---\n");
1534
+ }
1535
+ return lines.join("\n");
1536
+ }
1537
+
1538
+ // src/providers/gemini/commands/analyze.ts
1539
+ function registerAnalyzeCommand(parent) {
1540
+ 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(
1541
+ async (opts) => {
1542
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1543
+ const model = opts.model ?? getDefaultModel("analyze");
1544
+ for (const f of opts.files) {
1545
+ if (!fs9.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
1546
+ }
1547
+ const results = [];
1548
+ for (let i = 0; i < opts.files.length; i++) {
1549
+ const filePath = opts.files[i];
1550
+ logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
1551
+ try {
1552
+ const useApi = shouldUseFileApi(filePath);
1553
+ let contentParts;
1554
+ if (useApi) {
1555
+ logger.debug("File >20MB \u2014 using Files API");
1556
+ const ref = await uploadFile(filePath, { logger });
1557
+ contentParts = [
1558
+ { text: opts.prompt },
1559
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
1560
+ ];
1561
+ } else {
1562
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
1563
+ }
1564
+ const resp = await generateContent({
1565
+ model,
1566
+ contents: [{ parts: contentParts }]
1567
+ });
1568
+ results.push({
1569
+ file: filePath,
1570
+ status: "success",
1571
+ response: extractText(resp)
1572
+ });
1573
+ } catch (e) {
1574
+ const msg = e instanceof Error ? e.message : String(e);
1575
+ logger.error(`Failed: ${filePath} \u2014 ${msg}`);
1576
+ results.push({ file: filePath, status: "error", error: msg });
1577
+ }
1578
+ }
1579
+ formatResults(results, opts.format, opts.output);
1580
+ logger.debug(
1581
+ `Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
1582
+ );
1583
+ const failed = results.filter((r) => r.status === "error").length;
1584
+ if (failed > 0) process.exit(1);
1585
+ }
1586
+ );
1587
+ }
1588
+
1589
+ // src/providers/gemini/commands/extract.ts
1590
+ init_errors();
1591
+ import fs10 from "fs";
1592
+ function registerExtractCommand(parent) {
1593
+ 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(
1594
+ async (opts) => {
1595
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1596
+ const model = opts.model ?? getDefaultModel("extract");
1597
+ for (const f of opts.files) {
1598
+ if (!fs10.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
1599
+ }
1600
+ const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
1601
+ const results = [];
1602
+ for (let i = 0; i < opts.files.length; i++) {
1603
+ const filePath = opts.files[i];
1604
+ logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
1605
+ try {
1606
+ const useApi = shouldUseFileApi(filePath);
1607
+ let contentParts;
1608
+ if (useApi) {
1609
+ const ref = await uploadFile(filePath, { logger });
1610
+ contentParts = [
1611
+ { text: opts.prompt },
1612
+ { fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
1613
+ ];
1614
+ } else {
1615
+ contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
1616
+ }
1617
+ const resp = await generateContent({
1618
+ model,
1619
+ contents: [{ parts: contentParts }],
1620
+ generationConfig
1621
+ });
1622
+ results.push({ file: filePath, status: "success", response: extractText(resp) });
1623
+ } catch (e) {
1624
+ const msg = e instanceof Error ? e.message : String(e);
1625
+ logger.error(`Failed: ${filePath} \u2014 ${msg}`);
1626
+ results.push({ file: filePath, status: "error", error: msg });
1627
+ }
1628
+ }
1629
+ formatResults(results, opts.format, opts.output);
1630
+ const failed = results.filter((r) => r.status === "error").length;
1631
+ if (failed > 0) process.exit(1);
1632
+ }
1633
+ );
1634
+ }
1635
+
1636
+ // src/providers/gemini/commands/generate-speech.ts
1637
+ init_errors();
1638
+
1639
+ // src/providers/gemini/generators/speech.ts
1640
+ import fs11 from "fs";
1641
+ import path15 from "path";
1642
+
1643
+ // src/providers/gemini/voices.ts
1644
+ var GEMINI_TTS_VOICES = [
1645
+ "Zephyr",
1646
+ "Puck",
1647
+ "Charon",
1648
+ "Kore",
1649
+ "Fenrir",
1650
+ "Leda",
1651
+ "Orus",
1652
+ "Aoede",
1653
+ "Callirrhoe",
1654
+ "Autonoe",
1655
+ "Enceladus",
1656
+ "Iapetus",
1657
+ "Umbriel",
1658
+ "Algieba",
1659
+ "Despina",
1660
+ "Erinome",
1661
+ "Algenib",
1662
+ "Rasalgethi",
1663
+ "Laomedeia",
1664
+ "Achernar",
1665
+ "Alnilam",
1666
+ "Schedar",
1667
+ "Gacrux",
1668
+ "Pulcherrima",
1669
+ "Achird",
1670
+ "Zubenelgenubi",
1671
+ "Vindemiatrix",
1672
+ "Sadachbia",
1673
+ "Sadaltager",
1674
+ "Sulafat"
1675
+ ];
1676
+ var GEMINI_TTS_MODELS = /* @__PURE__ */ new Set([
1677
+ "gemini-3.1-flash-tts-preview",
1678
+ "gemini-2.5-flash-preview-tts",
1679
+ "gemini-2.5-pro-preview-tts"
1680
+ ]);
1681
+ var TTS_VOICE_DEFAULT = "Kore";
1682
+ var TTS_PCM_SAMPLE_RATE = 24e3;
1683
+ var TTS_PCM_CHANNELS = 1;
1684
+ var TTS_PCM_BITS_PER_SAMPLE = 16;
1685
+ var TTS_OUTPUT_FORMATS = ["wav", "pcm"];
1686
+ function isValidGeminiVoice(name) {
1687
+ return GEMINI_TTS_VOICES.includes(name);
1688
+ }
1689
+
1690
+ // src/providers/gemini/generators/speech.ts
1691
+ async function generateGeminiSpeech(opts) {
1692
+ const { text, model, voice, speakers, outputFormat, output, logger } = opts;
1693
+ const speechConfig = speakers && speakers.length > 0 ? {
1694
+ multiSpeakerVoiceConfig: {
1695
+ speakerVoiceConfigs: speakers.map((s) => ({
1696
+ speaker: s.speaker,
1697
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: s.voice } }
1698
+ }))
1699
+ }
1700
+ } : {
1701
+ voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } }
1702
+ };
1703
+ const generationConfig = {
1704
+ responseModalities: ["AUDIO"],
1705
+ speechConfig
1706
+ };
1707
+ logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
1708
+ let resp;
1709
+ try {
1710
+ resp = await generateContent({
1711
+ model,
1712
+ contents: [{ parts: [{ text }] }],
1713
+ generationConfig
1714
+ });
1715
+ } catch (e) {
1716
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
1717
+ }
1718
+ const audio = extractAudio(resp);
1719
+ if (!audio) return { status: "error", error: "No audio in response" };
1720
+ const pcmBytes = Buffer.from(audio.data, "base64");
1721
+ const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
1722
+ const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
1723
+ const outDir = getOutputDir();
1724
+ const dest = path15.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
1725
+ fs11.writeFileSync(dest, outBytes);
1726
+ logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
1727
+ if (output) {
1728
+ fs11.mkdirSync(path15.dirname(path15.resolve(output)), { recursive: true });
1729
+ fs11.copyFileSync(dest, output);
1730
+ logger?.success(`Copied to: ${output}`);
1731
+ }
1732
+ return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
1733
+ }
1734
+ function parseSampleRate(mimeType) {
1735
+ const m = mimeType.match(/rate=(\d+)/i);
1736
+ return m ? Number.parseInt(m[1], 10) : null;
1737
+ }
1738
+ function wrapPcmInWav(pcm, sampleRate, channels, bitsPerSample) {
1739
+ const byteRate = sampleRate * channels * bitsPerSample / 8;
1740
+ const blockAlign = channels * bitsPerSample / 8;
1741
+ const dataSize = pcm.length;
1742
+ const header = Buffer.alloc(44);
1743
+ header.write("RIFF", 0, "ascii");
1744
+ header.writeUInt32LE(36 + dataSize, 4);
1745
+ header.write("WAVE", 8, "ascii");
1746
+ header.write("fmt ", 12, "ascii");
1747
+ header.writeUInt32LE(16, 16);
1748
+ header.writeUInt16LE(1, 20);
1749
+ header.writeUInt16LE(channels, 22);
1750
+ header.writeUInt32LE(sampleRate, 24);
1751
+ header.writeUInt32LE(byteRate, 28);
1752
+ header.writeUInt16LE(blockAlign, 32);
1753
+ header.writeUInt16LE(bitsPerSample, 34);
1754
+ header.write("data", 36, "ascii");
1755
+ header.writeUInt32LE(dataSize, 40);
1756
+ return Buffer.concat([header, pcm]);
1757
+ }
1758
+
1759
+ // src/providers/gemini/commands/generate-speech.ts
1760
+ function registerGeminiGenerateSpeechCommand(parent) {
1761
+ 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(
1762
+ "--speaker <name:voice>",
1763
+ "Speaker mapping (repeatable, max 2 \u2014 switches to multi-speaker mode)",
1764
+ collectSpeakers,
1765
+ []
1766
+ ).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(
1767
+ async (opts) => {
1768
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1769
+ const text = opts.text ?? opts.prompt;
1770
+ if (!text) throw new ValidationError("Either --text or --prompt is required");
1771
+ const model = opts.model ?? getDefaultModel("generate-speech");
1772
+ if (!GEMINI_TTS_MODELS.has(model)) {
1773
+ throw new ValidationError(`--model must be one of: ${[...GEMINI_TTS_MODELS].join(", ")}`);
1774
+ }
1775
+ if (!TTS_OUTPUT_FORMATS.includes(opts.outputFormat)) {
1776
+ throw new ValidationError(
1777
+ `--output-format must be one of: ${TTS_OUTPUT_FORMATS.join(", ")}`
1778
+ );
1779
+ }
1780
+ const speakers = opts.speaker;
1781
+ if (speakers.length > 2) {
1782
+ throw new ValidationError("Gemini TTS supports max 2 speakers in multi-speaker mode");
1783
+ }
1784
+ if (speakers.length === 0 && !isValidGeminiVoice(opts.voice)) {
1785
+ throw new ValidationError(
1786
+ `Invalid --voice "${opts.voice}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
1787
+ );
1788
+ }
1789
+ for (const sv of speakers) {
1790
+ if (!isValidGeminiVoice(sv.voice)) {
1791
+ throw new ValidationError(
1792
+ `Invalid voice "${sv.voice}" for speaker "${sv.speaker}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
1793
+ );
1794
+ }
1795
+ }
1796
+ const result = await generateGeminiSpeech({
1797
+ text,
1798
+ model,
1799
+ voice: speakers.length === 0 ? opts.voice : void 0,
1800
+ speakers: speakers.length > 0 ? speakers : void 0,
1801
+ outputFormat: opts.outputFormat,
1802
+ output: opts.output,
1803
+ logger
1804
+ });
1805
+ if (result.status === "error") {
1806
+ logger.error(result.error ?? "Unknown error");
1807
+ process.exit(1);
1808
+ }
1809
+ console.log(`
1810
+ Generated audio: ${result.generatedAudio}`);
1811
+ }
1812
+ );
1813
+ }
1814
+ function collectSpeakers(value, prev) {
1815
+ const idx = value.indexOf(":");
1816
+ if (idx <= 0 || idx === value.length - 1) {
1817
+ throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
1818
+ }
1819
+ const speaker = value.slice(0, idx).trim();
1820
+ const voice = value.slice(idx + 1).trim();
1821
+ if (!speaker || !voice) {
1822
+ throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
1823
+ }
1824
+ return [...prev, { speaker, voice }];
1825
+ }
1826
+
1827
+ // src/providers/gemini/commands/generate-video.ts
1828
+ import fs12 from "fs";
1829
+ import path16 from "path";
1830
+ init_errors();
1831
+ init_http_client();
1832
+ var BASE2 = "https://generativelanguage.googleapis.com";
1833
+ function registerGenerateVideoCommand(parent) {
1834
+ 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(
1835
+ "--reference-images <paths...>",
1836
+ "Reference images (first=opening frame, second=closing)"
1837
+ ).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(
1838
+ async (opts) => {
1839
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1840
+ const apiKey = resolveKey("GEMINI_API_KEY");
1841
+ if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
1842
+ const model = opts.model ?? getDefaultModel("generate-video");
1843
+ if (!model.startsWith("veo-")) {
1844
+ throw new ValidationError(
1845
+ `Video generation requires a Veo model (got '${model}'). Use: veo-3.1-generate-preview, veo-3.0-generate-001, etc.`
1846
+ );
1847
+ }
1848
+ if (!["720p", "1080p"].includes(opts.resolution)) {
1849
+ throw new ValidationError("--resolution must be 720p or 1080p");
1850
+ }
1851
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
1852
+ throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
1853
+ }
1854
+ logger.debug(`Submitting video generation with ${model}...`);
1855
+ logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
1856
+ const config = {
1857
+ aspectRatio: opts.aspectRatio,
1858
+ resolution: opts.resolution
1859
+ };
1860
+ const contentParts = [{ text: opts.prompt }];
1861
+ if (opts.referenceImages?.length) {
1862
+ for (const imgPath of opts.referenceImages.slice(0, 2)) {
1863
+ if (!fs12.existsSync(imgPath))
1864
+ throw new ValidationError(`Reference image not found: ${imgPath}`);
1865
+ const data = fs12.readFileSync(imgPath).toString("base64");
1866
+ const ext = path16.extname(imgPath).toLowerCase();
1867
+ const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
1868
+ contentParts.push({ inlineData: { mimeType, data } });
1869
+ }
1870
+ }
1871
+ const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
1872
+ let operation;
1873
+ try {
1874
+ operation = await httpJson({
1875
+ url: `${BASE2}/v1beta/models/${model}:generateVideos`,
1876
+ method: "POST",
1877
+ headers,
1878
+ body: {
1879
+ contents: [{ parts: contentParts }],
1880
+ generationConfig: config
1881
+ }
1882
+ });
1883
+ } catch (e) {
1884
+ const msg = e instanceof Error ? e.message : String(e);
1885
+ throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
1886
+ }
1887
+ const operationName = operation.name;
1888
+ logger.debug(`Operation: ${operationName}`);
1889
+ const deadline = Date.now() + 10 * 60 * 1e3;
1890
+ let done = false;
1891
+ let finalOp = operation;
1892
+ while (!done && Date.now() < deadline) {
1893
+ await sleep2(1e4);
1894
+ finalOp = await httpJson({
1895
+ url: `${BASE2}/v1beta/${operationName}`,
1896
+ headers
1897
+ });
1898
+ done = finalOp.done === true;
1899
+ if (!done)
1900
+ logger.debug(
1901
+ `Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
1902
+ );
1903
+ }
1904
+ if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
1905
+ if (finalOp.error)
1906
+ throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
1907
+ const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
1908
+ if (!videoUri) {
1909
+ throw new ProviderError(
1910
+ `Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
1911
+ "gemini"
1912
+ );
1913
+ }
1914
+ const outDir = getOutputDir();
1915
+ const dest = path16.join(outDir, `veo_generated_${Date.now()}.mp4`);
1916
+ const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
1917
+ await downloadFile2(videoUri, dest);
1918
+ logger.success(`Saved video: ${dest}`);
1919
+ if (opts.output) {
1920
+ fs12.mkdirSync(path16.dirname(path16.resolve(opts.output)), { recursive: true });
1921
+ fs12.copyFileSync(dest, opts.output);
1922
+ logger.success(`Copied to: ${opts.output}`);
1923
+ }
1924
+ await maybeDownloadThumb(finalOp.response, dest, {
1925
+ skip: opts.thumb === false,
1926
+ copyTo: opts.output,
1927
+ logger
1928
+ });
1929
+ console.log(`
1930
+ Generated video: ${dest}`);
1931
+ }
1932
+ );
1933
+ }
1934
+ function sleep2(ms) {
1935
+ return new Promise((r) => setTimeout(r, ms));
1936
+ }
1937
+
1938
+ // src/providers/gemini/commands/generate.ts
1939
+ init_errors();
1940
+ import fs13 from "fs";
1941
+ import path17 from "path";
1942
+ function registerGenerateCommand(parent) {
1943
+ 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(
1944
+ async (opts) => {
1945
+ const logger = createLogger({ verbose: opts.verbose ?? false });
1946
+ const model = opts.model ?? getDefaultModel("generate");
1947
+ const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 4);
1948
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
1949
+ throw new ValidationError(
1950
+ `Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
1951
+ );
1952
+ }
1953
+ if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
1954
+ throw new ValidationError(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
1955
+ }
1956
+ logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
1957
+ logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
1958
+ const imageConfig = { aspectRatio: opts.aspectRatio };
1959
+ if (opts.size) imageConfig.imageSize = opts.size;
1960
+ const generationConfig = {
1961
+ responseModalities: ["IMAGE"],
1962
+ imageConfig
1963
+ };
1964
+ const savedFiles = [];
1965
+ for (let i = 0; i < numImages; i++) {
1966
+ try {
1967
+ const resp = await generateContent({
1968
+ model,
1969
+ contents: [{ parts: [{ text: opts.prompt }] }],
1970
+ generationConfig
1971
+ });
1972
+ const images = extractImages(resp);
1973
+ if (images.length === 0) {
1974
+ logger.warn(
1975
+ `Request ${i + 1}: no images in response \u2014 model may not support image generation`
1976
+ );
1977
+ continue;
1978
+ }
1979
+ for (const img of images) {
1980
+ const outDir = getOutputDir();
1981
+ const ext = img.mimeType.includes("png") ? "png" : "jpg";
1982
+ const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
1983
+ const dest = path17.join(outDir, fname);
1984
+ fs13.writeFileSync(dest, Buffer.from(img.data, "base64"));
1985
+ savedFiles.push(dest);
1986
+ logger.success(`Saved: ${dest}`);
1987
+ }
1988
+ } catch (e) {
1989
+ const msg = e instanceof Error ? e.message : String(e);
1990
+ logger.error(`Generation failed: ${msg}`);
1991
+ process.exit(1);
1992
+ }
1993
+ }
1994
+ if (savedFiles.length === 0) {
1995
+ logger.error("No images were generated");
1996
+ process.exit(1);
1997
+ }
1998
+ if (opts.output && savedFiles[0]) {
1999
+ fs13.mkdirSync(path17.dirname(path17.resolve(opts.output)), { recursive: true });
2000
+ fs13.copyFileSync(savedFiles[0], opts.output);
2001
+ logger.success(`Copied to: ${opts.output}`);
2002
+ }
2003
+ console.log(`
2004
+ Generated ${savedFiles.length} image(s):`);
2005
+ for (const f of savedFiles) console.log(` ${f}`);
2006
+ }
2007
+ );
2008
+ }
2009
+
2010
+ // src/providers/gemini/commands/image-to-image.ts
2011
+ import fs14 from "fs";
2012
+ import path18 from "path";
2013
+ var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
2014
+ var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
2015
+ function registerGeminiImageToImageCommand(parent) {
2016
+ parent.command("image-to-image").alias("i2i").description(
2017
+ "Edit/compose images with Gemini 2.5 Flash Image (Nano Banana) using one or more reference images"
2018
+ ).requiredOption("--prompt <text>", "Text prompt describing the edit/transformation").requiredOption(
2019
+ "--ref <path>",
2020
+ "Reference image (URL or local path); repeatable for multi-ref",
2021
+ collect4,
2022
+ []
2023
+ ).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(
2024
+ async (opts) => {
2025
+ if (!opts.ref || opts.ref.length === 0) {
2026
+ throw new Error("At least one --ref is required for image-to-image.");
2027
+ }
2028
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2029
+ const model = opts.model ?? IMAGE_MODEL_FALLBACK;
2030
+ const parts = [];
2031
+ for (const r of opts.ref) {
2032
+ const resolved = await resolveImageInput(r, {
2033
+ logger,
2034
+ softLimitBytes: SOFT_LIMIT_BYTES,
2035
+ hardLimitBytes: HARD_LIMIT_BYTES
2036
+ });
2037
+ if (resolved.kind === "url") {
2038
+ const buf = await fetchUrlAsBuffer(resolved.url);
2039
+ const mime = sniffImageMime(buf, resolved.url);
2040
+ if (buf.byteLength > HARD_LIMIT_BYTES) {
2041
+ throw new Error(
2042
+ `Reference ${resolved.url} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds Gemini ${HARD_LIMIT_BYTES / 1024 / 1024}MB inline cap.`
2043
+ );
2044
+ }
2045
+ parts.push({ inlineData: { mimeType: mime, data: buf.toString("base64") } });
2046
+ } else {
2047
+ const data = resolved.dataUrl.split(",", 2)[1] ?? "";
2048
+ parts.push({ inlineData: { mimeType: resolved.mime, data } });
2049
+ }
2050
+ }
2051
+ parts.push({ text: opts.prompt });
2052
+ logger.debug(`Calling ${model} with ${opts.ref.length} reference image(s)`);
2053
+ const req = {
2054
+ model,
2055
+ contents: [{ parts }],
2056
+ generationConfig: { responseModalities: ["IMAGE"] }
2057
+ };
2058
+ const resp = await generateContent(req);
2059
+ const images = extractImages(resp);
2060
+ if (images.length === 0) {
2061
+ logger.error(
2062
+ "No images in response \u2014 model may have refused or doesn't support image output."
2063
+ );
2064
+ process.exit(1);
2065
+ }
2066
+ const outDir = getOutputDir();
2067
+ const ts = Date.now();
2068
+ const saved = [];
2069
+ for (let i = 0; i < images.length; i++) {
2070
+ const img = images[i];
2071
+ if (!img) continue;
2072
+ const ext = img.mimeType.includes("png") ? "png" : "jpg";
2073
+ const dest = path18.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
2074
+ fs14.writeFileSync(dest, Buffer.from(img.data, "base64"));
2075
+ saved.push(dest);
2076
+ logger.success(`Saved: ${dest}`);
2077
+ }
2078
+ if (opts.output && saved[0]) {
2079
+ fs14.mkdirSync(path18.dirname(path18.resolve(opts.output)), { recursive: true });
2080
+ fs14.copyFileSync(saved[0], opts.output);
2081
+ logger.success(`Copied to: ${opts.output}`);
2082
+ }
2083
+ console.log(`
2084
+ Generated ${saved.length} image(s):`);
2085
+ for (const f of saved) console.log(` ${f}`);
2086
+ }
2087
+ );
468
2088
  }
469
- function renderText(results) {
470
- return results.map((r) => {
471
- const header = `[${r.file}]
472
- Status: ${r.status}`;
473
- if (r.status === "success" && r.response) return `${header}
474
- Result:
475
- ${r.response}
476
- `;
477
- if (r.status === "error") return `${header}
478
- Error: ${r.error}
479
- `;
480
- return `${header}
481
- `;
482
- }).join("\n");
2089
+ function collect4(value, prev) {
2090
+ return [...prev, value];
483
2091
  }
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
- `;
2092
+ async function fetchUrlAsBuffer(url) {
2093
+ const res = await fetch(url);
2094
+ if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
2095
+ const arr = await res.arrayBuffer();
2096
+ return Buffer.from(arr);
2097
+ }
2098
+ function sniffImageMime(buf, url) {
2099
+ if (buf.length >= 4) {
2100
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
2101
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71)
2102
+ return "image/png";
2103
+ if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
2104
+ if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
2105
+ return "image/webp";
2106
+ }
2107
+ const ext = path18.extname(new URL(url).pathname).toLowerCase();
2108
+ if (ext === ".png") return "image/png";
2109
+ if (ext === ".webp") return "image/webp";
2110
+ if (ext === ".gif") return "image/gif";
2111
+ return "image/jpeg";
492
2112
  }
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
2113
 
502
- ${r.response}
503
- `);
504
- if (r.status === "error") lines.push(`**Error**: ${r.error}
505
- `);
506
- lines.push("---\n");
2114
+ // src/providers/gemini/commands/image-to-video.ts
2115
+ import fs15 from "fs";
2116
+ import path19 from "path";
2117
+ init_errors();
2118
+ init_http_client();
2119
+ var BASE3 = "https://generativelanguage.googleapis.com";
2120
+ function imagePart(imgPath) {
2121
+ if (!fs15.existsSync(imgPath)) {
2122
+ throw new ValidationError(`Image not found: ${imgPath}`);
507
2123
  }
508
- return lines.join("\n");
2124
+ const data = fs15.readFileSync(imgPath).toString("base64");
2125
+ const ext = path19.extname(imgPath).toLowerCase();
2126
+ const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
2127
+ return { inlineData: { mimeType, data } };
2128
+ }
2129
+ function sleep3(ms) {
2130
+ return new Promise((r) => setTimeout(r, ms));
2131
+ }
2132
+ function registerGeminiImageToVideoCommand(parent) {
2133
+ 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(
2134
+ async (imagePath, opts) => {
2135
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2136
+ const apiKey = resolveKey("GEMINI_API_KEY");
2137
+ if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
2138
+ const model = opts.model ?? getDefaultModel("generate-video");
2139
+ if (!model.startsWith("veo-")) {
2140
+ throw new ValidationError(
2141
+ `Image-to-video requires a Veo model (got '${model}'). Try veo-3.1-generate-preview.`
2142
+ );
2143
+ }
2144
+ if (!["720p", "1080p"].includes(opts.resolution)) {
2145
+ throw new ValidationError("--resolution must be 720p or 1080p");
2146
+ }
2147
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
2148
+ throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
2149
+ }
2150
+ const contentParts = [
2151
+ { text: opts.prompt },
2152
+ imagePart(imagePath)
2153
+ ];
2154
+ if (opts.lastFrame) contentParts.push(imagePart(opts.lastFrame));
2155
+ const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
2156
+ logger.info(
2157
+ "Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
2158
+ );
2159
+ let operation;
2160
+ try {
2161
+ operation = await httpJson({
2162
+ url: `${BASE3}/v1beta/models/${model}:generateVideos`,
2163
+ method: "POST",
2164
+ headers,
2165
+ body: {
2166
+ contents: [{ parts: contentParts }],
2167
+ generationConfig: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
2168
+ }
2169
+ });
2170
+ } catch (e) {
2171
+ throw new ProviderError(
2172
+ `Submit failed: ${e instanceof Error ? e.message : String(e)}`,
2173
+ "gemini"
2174
+ );
2175
+ }
2176
+ const operationName = operation.name;
2177
+ logger.debug(`Operation: ${operationName}`);
2178
+ const deadline = Date.now() + 10 * 60 * 1e3;
2179
+ let finalOp = operation;
2180
+ while (!finalOp.done && Date.now() < deadline) {
2181
+ await sleep3(1e4);
2182
+ finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
2183
+ if (!finalOp.done) {
2184
+ logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
2185
+ }
2186
+ }
2187
+ if (!finalOp.done) throw new ProviderError("Veo timed out after 10 minutes", "gemini");
2188
+ if (finalOp.error) {
2189
+ throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
2190
+ }
2191
+ const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
2192
+ if (!videoUri) {
2193
+ throw new ProviderError(
2194
+ `Unexpected Veo response: ${JSON.stringify(finalOp.response)}`,
2195
+ "gemini"
2196
+ );
2197
+ }
2198
+ const dest = path19.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
2199
+ await downloadFile(videoUri, dest);
2200
+ logger.success(`Saved video: ${dest}`);
2201
+ if (opts.output) {
2202
+ fs15.mkdirSync(path19.dirname(path19.resolve(opts.output)), { recursive: true });
2203
+ fs15.copyFileSync(dest, opts.output);
2204
+ logger.success(`Copied to: ${opts.output}`);
2205
+ }
2206
+ await maybeDownloadThumb(finalOp.response, dest, {
2207
+ skip: opts.thumb === false,
2208
+ copyTo: opts.output,
2209
+ logger
2210
+ });
2211
+ console.log(`
2212
+ Generated video: ${dest}`);
2213
+ }
2214
+ );
509
2215
  }
510
2216
 
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(
2217
+ // src/providers/gemini/commands/transcribe.ts
2218
+ init_errors();
2219
+ import fs16 from "fs";
2220
+ function registerTranscribeCommand(parent) {
2221
+ 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
2222
  async (opts) => {
515
2223
  const logger = createLogger({ verbose: opts.verbose ?? false });
516
- const model = opts.model ?? getDefaultModel("analyze");
2224
+ const model = opts.model ?? getDefaultModel("transcribe");
517
2225
  for (const f of opts.files) {
518
- if (!fs5.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2226
+ if (!fs16.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
519
2227
  }
520
2228
  const results = [];
521
2229
  for (let i = 0; i < opts.files.length; i++) {
522
2230
  const filePath = opts.files[i];
523
- logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
2231
+ logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
524
2232
  try {
525
2233
  const useApi = shouldUseFileApi(filePath);
526
2234
  let contentParts;
@@ -550,312 +2258,710 @@ function registerAnalyzeCommand(parent) {
550
2258
  }
551
2259
  }
552
2260
  formatResults(results, opts.format, opts.output);
553
- logger.debug(
554
- `Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
555
- );
556
2261
  const failed = results.filter((r) => r.status === "error").length;
557
2262
  if (failed > 0) process.exit(1);
558
2263
  }
559
2264
  );
560
2265
  }
561
2266
 
562
- // src/providers/gemini/commands/extract.ts
2267
+ // src/providers/gemini/commands/index.ts
2268
+ function registerGeminiCommands(program2) {
2269
+ const gemini = program2.command("gemini").description(
2270
+ "Gemini AI operations: analyze, transcribe, extract, generate images/video/speech"
2271
+ );
2272
+ registerAnalyzeCommand(gemini);
2273
+ registerTranscribeCommand(gemini);
2274
+ registerExtractCommand(gemini);
2275
+ registerGenerateCommand(gemini);
2276
+ registerGeminiImageToImageCommand(gemini);
2277
+ registerGenerateVideoCommand(gemini);
2278
+ registerGeminiImageToVideoCommand(gemini);
2279
+ registerGeminiGenerateSpeechCommand(gemini);
2280
+ }
2281
+
2282
+ // src/providers/leonardo/commands/generate.ts
2283
+ init_http_client();
2284
+ import fs17 from "fs";
2285
+ import path20 from "path";
2286
+
2287
+ // src/providers/leonardo/client.ts
563
2288
  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}`);
2289
+ init_http_client();
2290
+
2291
+ // src/providers/leonardo/models.ts
2292
+ var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
2293
+ var DEFAULT_LEONARDO_MODEL_ID = "7b592283-e8a7-4c5a-9ba6-d18c31f258b9";
2294
+ var LEONARDO_DEFAULTS = {
2295
+ modelId: process.env.LEONARDO_DEFAULT_MODEL ?? DEFAULT_LEONARDO_MODEL_ID,
2296
+ videoModel: process.env.LEONARDO_VIDEO_MODEL ?? "MOTION2",
2297
+ width: 1024,
2298
+ height: 1024,
2299
+ numImages: 1
2300
+ };
2301
+ var LEONARDO_VIDEO_MODELS = [
2302
+ { id: "MOTION2", name: "Motion 2.0", modes: ["t2v", "i2v"] },
2303
+ { id: "MOTION2FAST", name: "Motion 2.0 Fast", modes: ["t2v", "i2v"] },
2304
+ { id: "VEO3", name: "Veo 3.0", modes: ["t2v", "i2v"] },
2305
+ { id: "VEO3FAST", name: "Veo 3.0 Fast", modes: ["t2v"] },
2306
+ { id: "VEO3_1", name: "Veo 3.1", modes: ["t2v", "i2v"] },
2307
+ { id: "KLING2_1", name: "Kling 2.1 Pro", modes: ["t2v", "i2v"] },
2308
+ { id: "Kling2_5", name: "Kling 2.5 Turbo", modes: ["t2v", "i2v"] },
2309
+ { id: "kling-2.6", name: "Kling 2.6", modes: ["t2v", "i2v"] },
2310
+ { id: "kling-3.0", name: "Kling 3.0", modes: ["t2v"] },
2311
+ { id: "kling-video-o-1", name: "Kling O1", modes: ["t2v", "i2v"] },
2312
+ { id: "kling-video-o-3", name: "Kling O3", modes: ["t2v", "i2v"] },
2313
+ { id: "hailuo-2_3", name: "Hailuo 2.3", modes: ["t2v", "i2v"] },
2314
+ { id: "ltxv-2.0-pro", name: "LTX 2.0 Pro", modes: ["t2v", "i2v"] },
2315
+ { id: "ltxv-2.3-pro", name: "LTX 2.3 Pro", modes: ["t2v", "i2v"] },
2316
+ { id: "seedance-2.0", name: "Seedance 2.0", modes: ["t2v", "i2v"] },
2317
+ { id: "seedance-2.0-fast", name: "Seedance 2.0 Fast", modes: ["t2v", "i2v"] }
2318
+ ];
2319
+ var LEONARDO_VIDEO_RESOLUTIONS = [
2320
+ "RESOLUTION_480",
2321
+ "RESOLUTION_720",
2322
+ "RESOLUTION_1080"
2323
+ ];
2324
+ var LEONARDO_V2_VIDEO_MODELS = /* @__PURE__ */ new Set([
2325
+ "kling-2.6",
2326
+ "kling-3.0",
2327
+ "kling-video-o-1",
2328
+ "kling-video-o-3",
2329
+ "hailuo-2_3",
2330
+ "ltxv-2.0-pro",
2331
+ "ltxv-2.3-pro",
2332
+ "seedance-2.0",
2333
+ "seedance-2.0-fast"
2334
+ ]);
2335
+ function isGptImageModel(modelId) {
2336
+ return typeof modelId === "string" && modelId.startsWith("gpt-image-");
2337
+ }
2338
+
2339
+ // src/providers/leonardo/client.ts
2340
+ function requireLeonardoKey() {
2341
+ const key = resolveKey("LEONARDO_API_KEY");
2342
+ if (!key) {
2343
+ throw new ConfigError(
2344
+ "LEONARDO_API_KEY is not set. Get one at https://app.leonardo.ai/settings/api-keys"
2345
+ );
2346
+ }
2347
+ return key;
2348
+ }
2349
+ function leonardoBaseUrl() {
2350
+ return resolveKey("LEONARDO_BASE_URL") ?? DEFAULT_LEONARDO_BASE_URL;
2351
+ }
2352
+ var LeonardoClient = class {
2353
+ constructor(apiKey, baseUrl = leonardoBaseUrl()) {
2354
+ this.apiKey = apiKey;
2355
+ this.baseUrl = baseUrl;
2356
+ }
2357
+ apiKey;
2358
+ baseUrl;
2359
+ async request(path36, opts = {}) {
2360
+ const base = this.baseUrl.replace(/\/$/, "");
2361
+ const v2Override = path36.startsWith("v2:");
2362
+ const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
2363
+ const finalPath = v2Override ? path36.slice(3) : path36;
2364
+ const url = new URL(finalBase + finalPath);
2365
+ if (opts.query) {
2366
+ for (const [k, v] of Object.entries(opts.query)) {
2367
+ if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
572
2368
  }
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)];
2369
+ }
2370
+ opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
2371
+ return httpJson({
2372
+ url: url.toString(),
2373
+ method: opts.method ?? "GET",
2374
+ headers: {
2375
+ accept: "application/json",
2376
+ authorization: `Bearer ${this.apiKey}`
2377
+ },
2378
+ body: opts.body,
2379
+ timeoutMs: opts.timeoutMs ?? 12e4
2380
+ });
2381
+ }
2382
+ get(path36, query, logger) {
2383
+ return this.request(path36, { method: "GET", query, logger });
2384
+ }
2385
+ post(path36, body, logger) {
2386
+ return this.request(path36, { method: "POST", body, logger });
2387
+ }
2388
+ delete(path36, logger) {
2389
+ return this.request(path36, { method: "DELETE", logger });
2390
+ }
2391
+ };
2392
+ function createLeonardoClient() {
2393
+ return new LeonardoClient(requireLeonardoKey(), leonardoBaseUrl());
2394
+ }
2395
+
2396
+ // src/providers/leonardo/commands/generate.ts
2397
+ function extFromUrl(url, fallback = ".png") {
2398
+ try {
2399
+ const u = new URL(url);
2400
+ const e = path20.extname(u.pathname);
2401
+ return e.length > 0 ? e : fallback;
2402
+ } catch {
2403
+ return fallback;
2404
+ }
2405
+ }
2406
+ function autoFilename(generationId, index, ext) {
2407
+ const e = ext.startsWith(".") ? ext : `.${ext}`;
2408
+ return `leonardo-${generationId.slice(0, 8)}-${String(index).padStart(2, "0")}${e}`;
2409
+ }
2410
+ function registerLeonardoGenerateCommand(parent) {
2411
+ 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(
2412
+ async (prompt, opts) => {
2413
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2414
+ const client = createLeonardoClient();
2415
+ const input = {
2416
+ prompt,
2417
+ modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
2418
+ width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
2419
+ height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
2420
+ num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
2421
+ alchemy: opts.alchemy,
2422
+ ultra: opts.ultra,
2423
+ contrast: opts.contrast ? Number.parseFloat(opts.contrast) : void 0,
2424
+ styleUUID: opts.style,
2425
+ seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
2426
+ negative_prompt: opts.negative,
2427
+ enhancePrompt: opts.enhance
2428
+ };
2429
+ logger.info(
2430
+ `Submitting generation (${input.width}x${input.height}, n=${input.num_images})`
2431
+ );
2432
+ let generationId;
2433
+ if (isGptImageModel(input.modelId)) {
2434
+ const v2Body = {
2435
+ public: false,
2436
+ model: input.modelId,
2437
+ parameters: {
2438
+ prompt: input.prompt,
2439
+ quantity: input.num_images ?? 1,
2440
+ width: input.width,
2441
+ height: input.height,
2442
+ quality: opts.quality,
2443
+ prompt_enhance: input.enhancePrompt ? "ON" : "OFF",
2444
+ ...input.seed !== void 0 ? { seed: input.seed } : {}
589
2445
  }
590
- const resp = await generateContent({
591
- model,
592
- contents: [{ parts: contentParts }],
593
- generationConfig
594
- });
595
- results.push({ file: filePath, status: "success", response: extractText(resp) });
2446
+ };
2447
+ const v2Res = await client.post(
2448
+ "v2:/generations",
2449
+ v2Body,
2450
+ logger
2451
+ );
2452
+ generationId = v2Res.generate.generationId;
2453
+ } else {
2454
+ const createRes = await client.post(
2455
+ "/generations",
2456
+ input,
2457
+ logger
2458
+ );
2459
+ generationId = createRes.sdGenerationJob.generationId;
2460
+ }
2461
+ logger.debug(`generationId: ${generationId}`);
2462
+ logger.info("Polling for completion...");
2463
+ const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
2464
+ const intervalMs = 4e3;
2465
+ const final = await poll({
2466
+ fetch: () => client.get(`/generations/${generationId}`),
2467
+ done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
2468
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2469
+ intervalMs,
2470
+ maxAttempts: Math.ceil(waitTimeout / intervalMs),
2471
+ onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
2472
+ });
2473
+ const images = final.generations_by_pk?.generated_images ?? [];
2474
+ if (opts.download === false) {
2475
+ for (const img of images) console.log(img.url);
2476
+ return;
2477
+ }
2478
+ const outDir = getOutputDir();
2479
+ const saved = [];
2480
+ for (let i = 0; i < images.length; i++) {
2481
+ const img = images[i];
2482
+ if (!img) continue;
2483
+ const ext = extFromUrl(img.url);
2484
+ const filename = autoFilename(generationId, i + 1, ext);
2485
+ const filepath = path20.join(outDir, filename);
2486
+ try {
2487
+ await downloadFile(img.url, filepath);
2488
+ saved.push(filepath);
2489
+ logger.success(`Saved ${filepath}`);
596
2490
  } 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 });
2491
+ logger.warn(`Download failed for ${img.id}: ${e instanceof Error ? e.message : e}`);
600
2492
  }
601
2493
  }
602
- formatResults(results, opts.format, opts.output);
603
- const failed = results.filter((r) => r.status === "error").length;
604
- if (failed > 0) process.exit(1);
2494
+ if (opts.output && saved[0]) {
2495
+ const dest = path20.resolve(opts.output);
2496
+ fs17.mkdirSync(path20.dirname(dest), { recursive: true });
2497
+ fs17.copyFileSync(saved[0], dest);
2498
+ logger.success(`Copied to ${dest}`);
2499
+ }
2500
+ console.log(`
2501
+ Generated ${saved.length} image(s):`);
2502
+ for (const f of saved) console.log(` ${f}`);
605
2503
  }
606
2504
  );
607
2505
  }
608
2506
 
609
- // src/providers/gemini/commands/generate-video.ts
610
- import fs8 from "fs";
611
- import path7 from "path";
612
- init_errors();
2507
+ // src/providers/leonardo/commands/image-to-image.ts
613
2508
  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;
2509
+ import fs18 from "fs";
2510
+ import path21 from "path";
2511
+ function extFromUrl2(url, fallback = ".png") {
2512
+ try {
2513
+ return path21.extname(new URL(url).pathname) || fallback;
2514
+ } catch {
2515
+ return fallback;
2516
+ }
625
2517
  }
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(
2518
+ function registerLeonardoImageToImageCommand(parent) {
2519
+ 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
2520
  async (opts) => {
635
2521
  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");
2522
+ const imageType = opts.imageType.toUpperCase();
2523
+ if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
2524
+ logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
2525
+ process.exit(2);
646
2526
  }
647
- if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
648
- throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
2527
+ const initStrength = Number.parseFloat(opts.initStrength);
2528
+ if (Number.isNaN(initStrength) || initStrength < 0 || initStrength > 0.9) {
2529
+ logger.error("--init-strength must be a number in [0.0, 0.9]");
2530
+ process.exit(2);
649
2531
  }
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
2532
+ const client = createLeonardoClient();
2533
+ const body = {
2534
+ prompt: opts.prompt,
2535
+ modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
2536
+ width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
2537
+ height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
2538
+ num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
2539
+ init_strength: initStrength
655
2540
  };
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
- );
2541
+ if (imageType === "GENERATED") {
2542
+ body.init_generation_image_id = opts.ref;
2543
+ } else {
2544
+ body.init_image_id = opts.ref;
699
2545
  }
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
- );
2546
+ if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
2547
+ if (opts.negative) body.negative_prompt = opts.negative;
2548
+ logger.info(`Submitting image-to-image (init=${opts.ref}, type=${imageType})`);
2549
+ const createRes = await client.post("/generations", body, logger);
2550
+ const generationId = createRes.sdGenerationJob.generationId;
2551
+ logger.debug(`generationId: ${generationId}`);
2552
+ logger.info("Polling for completion...");
2553
+ const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
2554
+ const intervalMs = 4e3;
2555
+ const final = await poll({
2556
+ fetch: () => client.get(`/generations/${generationId}`),
2557
+ done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
2558
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2559
+ intervalMs,
2560
+ maxAttempts: Math.ceil(waitTimeout / intervalMs),
2561
+ onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
2562
+ });
2563
+ const images = final.generations_by_pk?.generated_images ?? [];
2564
+ if (opts.download === false) {
2565
+ for (const img of images) console.log(img.url);
2566
+ return;
709
2567
  }
710
2568
  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}`);
2569
+ const saved = [];
2570
+ for (let i = 0; i < images.length; i++) {
2571
+ const img = images[i];
2572
+ if (!img) continue;
2573
+ const ext = extFromUrl2(img.url);
2574
+ const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
2575
+ const filepath = path21.join(outDir, filename);
2576
+ try {
2577
+ await downloadFile(img.url, filepath);
2578
+ saved.push(filepath);
2579
+ logger.success(`Saved ${filepath}`);
2580
+ } catch (e) {
2581
+ logger.warn(`Download failed: ${e instanceof Error ? e.message : e}`);
2582
+ }
2583
+ }
2584
+ if (opts.output && saved[0]) {
2585
+ const dest = path21.resolve(opts.output);
2586
+ fs18.mkdirSync(path21.dirname(dest), { recursive: true });
2587
+ fs18.copyFileSync(saved[0], dest);
2588
+ logger.success(`Copied to ${dest}`);
719
2589
  }
720
2590
  console.log(`
721
- Generated video: ${dest}`);
2591
+ Generated ${saved.length} image(s):`);
2592
+ for (const f of saved) console.log(` ${f}`);
722
2593
  }
723
2594
  );
724
2595
  }
725
- function sleep2(ms) {
726
- return new Promise((r) => setTimeout(r, ms));
727
- }
728
2596
 
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) => {
2597
+ // src/providers/leonardo/commands/video.ts
2598
+ init_http_client();
2599
+ import path22 from "path";
2600
+ function v2DimsFor(resolution) {
2601
+ if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
2602
+ if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
2603
+ return { width: 1280, height: 720 };
2604
+ }
2605
+ function registerLeonardoVideoCommand(parent) {
2606
+ 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(
2607
+ "--resolution <r>",
2608
+ "Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
2609
+ "RESOLUTION_720"
2610
+ ).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(
2611
+ async (prompt, opts) => {
736
2612
  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(", ")}`
2613
+ const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
2614
+ const client = createLeonardoClient();
2615
+ logger.info(`Submitting text-to-video (${model})`);
2616
+ let jobId;
2617
+ if (LEONARDO_V2_VIDEO_MODELS.has(model)) {
2618
+ const { width, height } = v2DimsFor(opts.resolution);
2619
+ const v2Res = await client.post(
2620
+ "v2:/generations",
2621
+ {
2622
+ public: false,
2623
+ model,
2624
+ parameters: {
2625
+ prompt,
2626
+ duration: 8,
2627
+ mode: opts.resolution,
2628
+ prompt_enhance: opts.enhance ? "ON" : "OFF",
2629
+ width,
2630
+ height
2631
+ }
2632
+ },
2633
+ logger
2634
+ );
2635
+ jobId = v2Res.generate.generationId;
2636
+ } else {
2637
+ const res = await client.post(
2638
+ "/generations-text-to-video",
2639
+ {
2640
+ prompt,
2641
+ model,
2642
+ resolution: opts.resolution,
2643
+ promptEnhance: !!opts.enhance,
2644
+ frameInterpolation: !!opts.frameInterpolation
2645
+ },
2646
+ logger
742
2647
  );
2648
+ jobId = res.motionVideoGenerationJob?.generationId;
743
2649
  }
744
- if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
745
- throw new ValidationError(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
2650
+ if (!jobId) {
2651
+ logger.error("No generationId returned by Leonardo");
2652
+ process.exit(1);
746
2653
  }
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
2654
+ const shouldWait = !!(opts.wait || opts.download);
2655
+ if (!shouldWait) {
2656
+ console.log(jobId);
2657
+ console.error(`Poll with: multix leonardo status ${jobId}`);
2658
+ return;
2659
+ }
2660
+ await pollAndDownload(client, jobId, {
2661
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
2662
+ download: !!opts.download,
2663
+ output: opts.output,
2664
+ thumb: opts.thumb !== false,
2665
+ logger,
2666
+ basename: "leonardo-video"
2667
+ });
2668
+ }
2669
+ );
2670
+ }
2671
+ async function pollAndDownload(client, jobId, opts) {
2672
+ const { logger } = opts;
2673
+ let res;
2674
+ try {
2675
+ res = await poll({
2676
+ fetch: () => client.get(`/generations/${jobId}`, void 0, logger),
2677
+ done: (v) => v.generations_by_pk?.status === "COMPLETE",
2678
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2679
+ intervalMs: 5e3,
2680
+ maxAttempts: Math.max(1, Math.ceil(opts.waitTimeoutMs / 5e3)),
2681
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
2682
+ });
2683
+ } catch (e) {
2684
+ if (e instanceof PollTimeoutError) {
2685
+ logger.error(`Timed out \u2014 multix leonardo status ${jobId}`);
2686
+ process.exit(1);
2687
+ }
2688
+ if (e instanceof PollFailedError) {
2689
+ logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
2690
+ process.exit(1);
2691
+ }
2692
+ throw e;
2693
+ }
2694
+ const gen = res.generations_by_pk;
2695
+ if (!gen) {
2696
+ logger.error("No generation in response");
2697
+ process.exit(1);
2698
+ }
2699
+ const videoUrl = gen.generated_images?.find(
2700
+ (img) => typeof img.url === "string" && /\.(mp4|webm|mov)(\?|$)/i.test(img.url)
2701
+ )?.url ?? gen.generated_images?.[0]?.motionMP4URL;
2702
+ if (!opts.download) {
2703
+ if (typeof videoUrl === "string") console.log(videoUrl);
2704
+ return;
2705
+ }
2706
+ if (typeof videoUrl !== "string") {
2707
+ logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
2708
+ process.exit(1);
2709
+ }
2710
+ const outPath = opts.output ?? path22.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
2711
+ await downloadFile(videoUrl, outPath);
2712
+ logger.success(`Saved ${outPath}`);
2713
+ await maybeDownloadThumb(gen, outPath, {
2714
+ skip: !opts.thumb,
2715
+ copyTo: opts.output,
2716
+ logger
2717
+ });
2718
+ console.log(`
2719
+ Job: ${jobId}`);
2720
+ console.log(`Saved: ${outPath}`);
2721
+ }
2722
+
2723
+ // src/providers/leonardo/commands/image-to-video.ts
2724
+ function registerLeonardoImageToVideoCommand(parent) {
2725
+ 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(
2726
+ "--resolution <r>",
2727
+ "Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
2728
+ "RESOLUTION_720"
2729
+ ).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(
2730
+ async (imageId, opts) => {
2731
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2732
+ const imageType = opts.imageType.toUpperCase();
2733
+ if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
2734
+ logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
2735
+ process.exit(2);
2736
+ }
2737
+ const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
2738
+ const client = createLeonardoClient();
2739
+ logger.info(`Submitting image-to-video (${model}, image=${imageId}, ${imageType})`);
2740
+ const body = {
2741
+ prompt: opts.prompt,
2742
+ imageId,
2743
+ imageType,
2744
+ model,
2745
+ resolution: opts.resolution,
2746
+ promptEnhance: !!opts.enhance,
2747
+ frameInterpolation: !!opts.frameInterpolation
754
2748
  };
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}`);
2749
+ if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
2750
+ if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
2751
+ if (opts.negative) body.negativePrompt = opts.negative;
2752
+ const res = await client.post(
2753
+ "/generations-image-to-video",
2754
+ body,
2755
+ logger
2756
+ );
2757
+ const jobId = res.motionVideoGenerationJob?.generationId;
2758
+ if (!jobId) {
2759
+ logger.error(`No generationId returned by Leonardo: ${JSON.stringify(res)}`);
2760
+ process.exit(1);
2761
+ }
2762
+ const shouldWait = !!(opts.wait || opts.download);
2763
+ if (!shouldWait) {
2764
+ console.log(jobId);
2765
+ console.error(`Poll with: multix leonardo status ${jobId}`);
2766
+ return;
2767
+ }
2768
+ await pollAndDownload(client, jobId, {
2769
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
2770
+ download: !!opts.download,
2771
+ output: opts.output,
2772
+ thumb: opts.thumb !== false,
2773
+ logger,
2774
+ basename: "leonardo-i2v"
2775
+ });
2776
+ }
2777
+ );
2778
+ }
2779
+
2780
+ // src/providers/leonardo/commands/me.ts
2781
+ function registerLeonardoMeCommand(parent) {
2782
+ parent.command("me").description("Show authenticated Leonardo user info and remaining API credits").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2783
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2784
+ const client = createLeonardoClient();
2785
+ const info = await client.get("/me", void 0, logger);
2786
+ const u = info.user_details?.[0];
2787
+ if (!u) {
2788
+ logger.warn("No user info returned.");
2789
+ return;
2790
+ }
2791
+ logger.success(`User: ${u.user.username} (${u.user.id})`);
2792
+ console.log(`Tokens: ${u.subscriptionTokens} | API credit: ${u.apiCredit ?? "n/a"}`);
2793
+ });
2794
+ }
2795
+
2796
+ // src/providers/leonardo/commands/models.ts
2797
+ function registerLeonardoModelsCommand(parent) {
2798
+ parent.command("models").description("List Leonardo platform image models").option("--limit <n>", "Limit number of results").option("-v, --verbose", "Verbose logging").action(async (opts) => {
2799
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2800
+ const client = createLeonardoClient();
2801
+ const res = await client.get(
2802
+ "/platformModels",
2803
+ void 0,
2804
+ logger
2805
+ );
2806
+ const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
2807
+ const models = limit ? res.custom_models.slice(0, limit) : res.custom_models;
2808
+ for (const m of models) {
2809
+ console.log(`${m.id} ${m.name}`);
2810
+ }
2811
+ logger.debug(`${models.length} model(s)`);
2812
+ });
2813
+ }
2814
+
2815
+ // src/providers/leonardo/commands/status.ts
2816
+ init_http_client();
2817
+ import path23 from "path";
2818
+ var VIDEO_KEYS = ["motionMP4URL", "url"];
2819
+ var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
2820
+ var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
2821
+ function collectAssetUrls(gen) {
2822
+ const videos = [];
2823
+ const images = [];
2824
+ for (const img of gen.generated_images ?? []) {
2825
+ if (!img.url) continue;
2826
+ if (VIDEO_EXT_RE.test(img.url)) videos.push(img.url);
2827
+ else if (IMAGE_EXT_RE2.test(img.url)) images.push(img.url);
2828
+ else images.push(img.url);
2829
+ for (const k of VIDEO_KEYS) {
2830
+ const extra = img[k];
2831
+ if (typeof extra === "string" && VIDEO_EXT_RE.test(extra)) videos.push(extra);
2832
+ }
2833
+ }
2834
+ return { videos, images };
2835
+ }
2836
+ function registerLeonardoStatusCommand(parent) {
2837
+ 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(
2838
+ async (generationId, opts) => {
2839
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2840
+ const client = createLeonardoClient();
2841
+ const shouldWait = !!(opts.wait || opts.download);
2842
+ const fetchOnce = () => client.get(`/generations/${generationId}`, void 0, logger);
2843
+ let res;
2844
+ try {
2845
+ res = shouldWait ? await poll({
2846
+ fetch: fetchOnce,
2847
+ done: (v) => v.generations_by_pk?.status === "COMPLETE",
2848
+ failed: (v) => v.generations_by_pk?.status === "FAILED",
2849
+ intervalMs: 5e3,
2850
+ maxAttempts: Math.max(
2851
+ 1,
2852
+ Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
2853
+ ),
2854
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
2855
+ }) : await fetchOnce();
2856
+ } catch (e) {
2857
+ if (e instanceof PollTimeoutError) {
2858
+ logger.error(`Timed out waiting for generation ${generationId}`);
2859
+ process.exit(1);
2860
+ }
2861
+ if (e instanceof PollFailedError) {
2862
+ logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
782
2863
  process.exit(1);
783
2864
  }
2865
+ throw e;
784
2866
  }
785
- if (savedFiles.length === 0) {
786
- logger.error("No images were generated");
787
- process.exit(1);
2867
+ const gen = res.generations_by_pk;
2868
+ if (!gen) {
2869
+ logger.warn("No generation found.");
2870
+ return;
788
2871
  }
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}`);
2872
+ console.error(`status: ${gen.status}`);
2873
+ const { videos, images } = collectAssetUrls(gen);
2874
+ for (const u of [...videos, ...images]) console.log(u);
2875
+ if (!opts.download || gen.status !== "COMPLETE") {
2876
+ if (opts.download) logger.warn(`Cannot download \u2014 status is '${gen.status}'`);
2877
+ return;
2878
+ }
2879
+ const outDir = getOutputDir();
2880
+ let idx = 0;
2881
+ for (const url of videos) {
2882
+ const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path23.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
2883
+ await downloadFile(url, dest);
2884
+ logger.success(`Saved ${dest}`);
2885
+ await maybeDownloadThumb(gen, dest, {
2886
+ skip: opts.thumb === false,
2887
+ copyTo: opts.output,
2888
+ logger
2889
+ });
2890
+ idx++;
2891
+ }
2892
+ for (const url of images) {
2893
+ const m = url.match(IMAGE_EXT_RE2);
2894
+ const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
2895
+ const dest = path23.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
2896
+ await downloadFile(url, dest);
2897
+ logger.success(`Saved ${dest}`);
2898
+ idx++;
793
2899
  }
794
- console.log(`
795
- Generated ${savedFiles.length} image(s):`);
796
- for (const f of savedFiles) console.log(` ${f}`);
797
2900
  }
798
2901
  );
799
2902
  }
800
2903
 
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) => {
2904
+ // src/providers/leonardo/commands/upscale.ts
2905
+ function registerLeonardoUpscaleCommands(parent) {
2906
+ 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(
2907
+ async (generatedImageId, opts) => {
807
2908
  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);
2909
+ const client = createLeonardoClient();
2910
+ logger.info(`Submitting upscale for image ${generatedImageId}`);
2911
+ const res = await client.post(
2912
+ "/variations/universal-upscaler",
2913
+ {
2914
+ generatedImageId,
2915
+ upscalerStyle: opts.style,
2916
+ creativityStrength: Number.parseFloat(opts.strength),
2917
+ upscaleMultiplier: Number.parseFloat(opts.multiplier)
2918
+ },
2919
+ logger
2920
+ );
2921
+ const variationId = res.universalUpscaler.id;
2922
+ logger.success(`variationId: ${variationId}`);
2923
+ console.log(variationId);
2924
+ console.error(`Use: multix leonardo variation ${variationId}`);
847
2925
  }
848
2926
  );
2927
+ parent.command("variation <variationId>").description("Fetch a Leonardo variation/upscale result by ID").option("-v, --verbose", "Verbose logging").action(async (variationId, opts) => {
2928
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2929
+ const client = createLeonardoClient();
2930
+ const res = await client.get(
2931
+ `/variations/${variationId}`,
2932
+ void 0,
2933
+ logger
2934
+ );
2935
+ for (const v of res.generated_image_variation_generic ?? []) {
2936
+ console.log(`${v.status} ${v.url ?? ""}`);
2937
+ }
2938
+ });
849
2939
  }
850
2940
 
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);
2941
+ // src/providers/leonardo/commands/video-models.ts
2942
+ function registerLeonardoVideoModelsCommand(parent) {
2943
+ parent.command("video-models").description("List supported Leonardo video models (static enum from Leonardo docs)").action(() => {
2944
+ for (const m of LEONARDO_VIDEO_MODELS) {
2945
+ console.log(`${m.id} ${m.name} [${m.modes.join(",")}]`);
2946
+ }
2947
+ console.error(`
2948
+ Resolutions: ${LEONARDO_VIDEO_RESOLUTIONS.join(", ")}`);
2949
+ console.error("Source: https://docs.leonardo.ai/llms.txt");
2950
+ });
2951
+ }
2952
+
2953
+ // src/providers/leonardo/commands/index.ts
2954
+ function registerLeonardoCommands(program2) {
2955
+ const leonardo = program2.command("leonardo").description("Leonardo.Ai: image/video generation, upscaling, account info");
2956
+ registerLeonardoGenerateCommand(leonardo);
2957
+ registerLeonardoImageToImageCommand(leonardo);
2958
+ registerLeonardoVideoCommand(leonardo);
2959
+ registerLeonardoImageToVideoCommand(leonardo);
2960
+ registerLeonardoVideoModelsCommand(leonardo);
2961
+ registerLeonardoUpscaleCommands(leonardo);
2962
+ registerLeonardoStatusCommand(leonardo);
2963
+ registerLeonardoModelsCommand(leonardo);
2964
+ registerLeonardoMeCommand(leonardo);
859
2965
  }
860
2966
 
861
2967
  // src/providers/minimax/commands/generate-music.ts
@@ -918,7 +3024,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
918
3024
  if (status === "Failed" || status === "Error") {
919
3025
  throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
920
3026
  }
921
- await sleep3(intervalMs);
3027
+ await sleep4(intervalMs);
922
3028
  elapsed += intervalMs;
923
3029
  }
924
3030
  throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
@@ -937,14 +3043,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
937
3043
  await downloadFile(downloadUrl, dest);
938
3044
  return dest;
939
3045
  }
940
- function sleep3(ms) {
3046
+ function sleep4(ms) {
941
3047
  return new Promise((r) => setTimeout(r, ms));
942
3048
  }
943
3049
 
944
3050
  // src/providers/minimax/generators/music.ts
945
3051
  init_http_client();
946
- import fs11 from "fs";
947
- import path9 from "path";
3052
+ import fs19 from "fs";
3053
+ import path24 from "path";
948
3054
  async function generateMinimaxMusic(opts) {
949
3055
  const {
950
3056
  apiKey,
@@ -989,14 +3095,14 @@ async function generateMinimaxMusic(opts) {
989
3095
  }
990
3096
  const durationMs = resp.extra_info?.music_duration ?? 0;
991
3097
  const outDir = getOutputDir();
992
- const dest = path9.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
3098
+ const dest = path24.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
993
3099
  try {
994
3100
  if (audioData.startsWith("http")) {
995
3101
  const bytes = await fetchBytes(audioData);
996
- fs11.writeFileSync(dest, bytes);
3102
+ fs19.writeFileSync(dest, bytes);
997
3103
  } else {
998
3104
  const audioBytes = Buffer.from(audioData, "hex");
999
- fs11.writeFileSync(dest, audioBytes);
3105
+ fs19.writeFileSync(dest, audioBytes);
1000
3106
  }
1001
3107
  } catch (e) {
1002
3108
  return {
@@ -1007,8 +3113,8 @@ async function generateMinimaxMusic(opts) {
1007
3113
  const durSec = durationMs / 1e3;
1008
3114
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
1009
3115
  if (output) {
1010
- fs11.mkdirSync(path9.dirname(path9.resolve(output)), { recursive: true });
1011
- fs11.copyFileSync(dest, output);
3116
+ fs19.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
3117
+ fs19.copyFileSync(dest, output);
1012
3118
  }
1013
3119
  return { status: "success", generatedAudio: dest, durationMs, model };
1014
3120
  }
@@ -1082,8 +3188,8 @@ Generated music: ${result.generatedAudio}`);
1082
3188
  init_errors();
1083
3189
 
1084
3190
  // src/providers/minimax/generators/speech.ts
1085
- import fs12 from "fs";
1086
- import path10 from "path";
3191
+ import fs20 from "fs";
3192
+ import path25 from "path";
1087
3193
  async function generateMinimaxSpeech(opts) {
1088
3194
  const {
1089
3195
  apiKey,
@@ -1130,12 +3236,12 @@ async function generateMinimaxSpeech(opts) {
1130
3236
  const audioBytes = Buffer.from(audioHex, "hex");
1131
3237
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
1132
3238
  const outDir = getOutputDir();
1133
- const dest = path10.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
1134
- fs12.writeFileSync(dest, audioBytes);
3239
+ const dest = path25.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
3240
+ fs20.writeFileSync(dest, audioBytes);
1135
3241
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
1136
3242
  if (output) {
1137
- fs12.mkdirSync(path10.dirname(path10.resolve(output)), { recursive: true });
1138
- fs12.copyFileSync(dest, output);
3243
+ fs20.mkdirSync(path25.dirname(path25.resolve(output)), { recursive: true });
3244
+ fs20.copyFileSync(dest, output);
1139
3245
  }
1140
3246
  return { status: "success", generatedAudio: dest, model };
1141
3247
  }
@@ -1182,8 +3288,8 @@ Generated audio: ${result.generatedAudio}`);
1182
3288
  init_errors();
1183
3289
 
1184
3290
  // src/providers/minimax/generators/video.ts
1185
- import fs13 from "fs";
1186
- import path11 from "path";
3291
+ import fs21 from "fs";
3292
+ import path26 from "path";
1187
3293
  async function generateMinimaxVideo(opts) {
1188
3294
  const {
1189
3295
  apiKey,
@@ -1193,6 +3299,7 @@ async function generateMinimaxVideo(opts) {
1193
3299
  resolution = "1080P",
1194
3300
  firstFrame,
1195
3301
  output,
3302
+ thumb,
1196
3303
  logger
1197
3304
  } = opts;
1198
3305
  const payload = { prompt, model, duration, resolution };
@@ -1228,19 +3335,24 @@ async function generateMinimaxVideo(opts) {
1228
3335
  return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
1229
3336
  }
1230
3337
  const outDir = getOutputDir();
1231
- const dest = path11.join(outDir, `minimax_video_${Date.now()}.mp4`);
3338
+ const dest = path26.join(outDir, `minimax_video_${Date.now()}.mp4`);
1232
3339
  try {
1233
3340
  await downloadMinimaxFile(fileId, apiKey, dest, logger);
1234
3341
  } catch (e) {
1235
3342
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
1236
3343
  }
1237
3344
  const elapsed = (Date.now() - start) / 1e3;
1238
- const stat = fs13.statSync(dest);
3345
+ const stat = fs21.statSync(dest);
1239
3346
  const fileSizeMb = stat.size / (1024 * 1024);
1240
3347
  if (output) {
1241
- fs13.mkdirSync(path11.dirname(path11.resolve(output)), { recursive: true });
1242
- fs13.copyFileSync(dest, output);
3348
+ fs21.mkdirSync(path26.dirname(path26.resolve(output)), { recursive: true });
3349
+ fs21.copyFileSync(dest, output);
1243
3350
  }
3351
+ await maybeDownloadThumb(pollResult, dest, {
3352
+ skip: thumb === false,
3353
+ copyTo: output,
3354
+ logger
3355
+ });
1244
3356
  logger?.debug(`Generated in ${elapsed.toFixed(1)}s, size: ${fileSizeMb.toFixed(2)} MB`);
1245
3357
  return {
1246
3358
  status: "success",
@@ -1253,7 +3365,7 @@ async function generateMinimaxVideo(opts) {
1253
3365
 
1254
3366
  // src/providers/minimax/commands/generate-video.ts
1255
3367
  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(
3368
+ 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
3369
  async (opts) => {
1258
3370
  const logger = createLogger({ verbose: opts.verbose ?? false });
1259
3371
  const apiKey = requireMinimaxKey();
@@ -1278,6 +3390,7 @@ function registerMinimaxGenerateVideoCommand(parent) {
1278
3390
  resolution,
1279
3391
  firstFrame: opts.firstFrame,
1280
3392
  output: opts.output,
3393
+ thumb: opts.thumb,
1281
3394
  logger
1282
3395
  });
1283
3396
  if (result.status === "error") {
@@ -1296,8 +3409,8 @@ Generated video: ${result.generatedVideo}`);
1296
3409
  // src/providers/minimax/generators/image.ts
1297
3410
  init_errors();
1298
3411
  init_http_client();
1299
- import fs14 from "fs";
1300
- import path12 from "path";
3412
+ import fs22 from "fs";
3413
+ import path27 from "path";
1301
3414
  async function generateMinimaxImage(opts) {
1302
3415
  const {
1303
3416
  apiKey,
@@ -1335,8 +3448,8 @@ async function generateMinimaxImage(opts) {
1335
3448
  try {
1336
3449
  const bytes = await fetchBytes(url);
1337
3450
  const fname = `minimax_image_${Date.now()}_${i}.png`;
1338
- const dest = path12.join(outDir, fname);
1339
- fs14.writeFileSync(dest, bytes);
3451
+ const dest = path27.join(outDir, fname);
3452
+ fs22.writeFileSync(dest, bytes);
1340
3453
  savedFiles.push(dest);
1341
3454
  logger?.success(`Saved: ${dest}`);
1342
3455
  } catch (e) {
@@ -1347,8 +3460,8 @@ async function generateMinimaxImage(opts) {
1347
3460
  }
1348
3461
  }
1349
3462
  if (output && savedFiles[0]) {
1350
- fs14.mkdirSync(path12.dirname(path12.resolve(output)), { recursive: true });
1351
- fs14.copyFileSync(savedFiles[0], output);
3463
+ fs22.mkdirSync(path27.dirname(path27.resolve(output)), { recursive: true });
3464
+ fs22.copyFileSync(savedFiles[0], output);
1352
3465
  }
1353
3466
  return { status: "success", generatedImages: savedFiles, model };
1354
3467
  }
@@ -1381,18 +3494,101 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
1381
3494
  );
1382
3495
  }
1383
3496
 
3497
+ // src/providers/minimax/commands/image-to-image.ts
3498
+ init_errors();
3499
+ init_http_client();
3500
+ import fs23 from "fs";
3501
+ import path28 from "path";
3502
+ function registerMinimaxImageToImageCommand(parent) {
3503
+ parent.command("image-to-image").alias("i2i").description(
3504
+ "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."
3505
+ ).requiredOption("--prompt <text>", "Prompt describing the desired NEW scene/composition").requiredOption(
3506
+ "--ref <path>",
3507
+ "Subject reference image (URL or local path) \u2014 character/face reference only, single image"
3508
+ ).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(
3509
+ async (opts) => {
3510
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3511
+ const apiKey = requireMinimaxKey();
3512
+ const model = opts.model ?? TASK_DEFAULTS.image;
3513
+ const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
3514
+ logger.warn(
3515
+ "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."
3516
+ );
3517
+ const resolved = await resolveImageInput(opts.ref, { logger });
3518
+ const imageFile = refUrl(resolved);
3519
+ const payload = {
3520
+ model,
3521
+ prompt: opts.prompt,
3522
+ aspect_ratio: opts.aspectRatio,
3523
+ n: numImages,
3524
+ response_format: "url",
3525
+ prompt_optimizer: true,
3526
+ subject_reference: [
3527
+ {
3528
+ type: "character",
3529
+ image_file: imageFile
3530
+ }
3531
+ ]
3532
+ };
3533
+ logger.debug(`MiniMax i2i: model=${model}, ref=${opts.ref}, n=${numImages}`);
3534
+ let resp;
3535
+ try {
3536
+ resp = await apiPost("image_generation", payload, apiKey, {
3537
+ logger
3538
+ });
3539
+ } catch (e) {
3540
+ logger.error(e instanceof Error ? e.message : String(e));
3541
+ process.exit(1);
3542
+ }
3543
+ const imageUrls = resp.data?.image_urls ?? [];
3544
+ if (imageUrls.length === 0) {
3545
+ logger.error("No images in response");
3546
+ process.exit(1);
3547
+ }
3548
+ const outDir = getOutputDir();
3549
+ const ts = Date.now();
3550
+ const saved = [];
3551
+ for (let i = 0; i < imageUrls.length; i++) {
3552
+ const url = imageUrls[i];
3553
+ if (!url) continue;
3554
+ try {
3555
+ const bytes = await fetchBytes(url);
3556
+ const dest = path28.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
3557
+ fs23.writeFileSync(dest, bytes);
3558
+ saved.push(dest);
3559
+ logger.success(`Saved: ${dest}`);
3560
+ } catch (e) {
3561
+ throw new ProviderError(
3562
+ `Failed to download image: ${e instanceof Error ? e.message : String(e)}`,
3563
+ "minimax"
3564
+ );
3565
+ }
3566
+ }
3567
+ if (opts.output && saved[0]) {
3568
+ fs23.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
3569
+ fs23.copyFileSync(saved[0], opts.output);
3570
+ logger.success(`Copied to: ${opts.output}`);
3571
+ }
3572
+ console.log(`
3573
+ Generated ${saved.length} image(s):`);
3574
+ for (const f of saved) console.log(` ${f}`);
3575
+ }
3576
+ );
3577
+ }
3578
+
1384
3579
  // src/providers/minimax/commands/index.ts
1385
3580
  function registerMinimaxCommands(program2) {
1386
3581
  const minimax = program2.command("minimax").description("MiniMax generation: images, video, speech, and music");
1387
3582
  registerMinimaxGenerateCommand(minimax);
3583
+ registerMinimaxImageToImageCommand(minimax);
1388
3584
  registerMinimaxGenerateVideoCommand(minimax);
1389
3585
  registerMinimaxGenerateSpeechCommand(minimax);
1390
3586
  registerMinimaxGenerateMusicCommand(minimax);
1391
3587
  }
1392
3588
 
1393
3589
  // src/providers/openrouter/client.ts
1394
- import fs15 from "fs";
1395
- import path13 from "path";
3590
+ import fs24 from "fs";
3591
+ import path29 from "path";
1396
3592
  init_errors();
1397
3593
  init_http_client();
1398
3594
  var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
@@ -1481,8 +3677,8 @@ async function generateOpenRouterImage(opts) {
1481
3677
  if (!url) continue;
1482
3678
  try {
1483
3679
  const bytes = await fetchBytes(url);
1484
- const dest = path13.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
1485
- fs15.writeFileSync(dest, bytes);
3680
+ const dest = path29.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
3681
+ fs24.writeFileSync(dest, bytes);
1486
3682
  savedFiles.push(dest);
1487
3683
  logger?.success(`Saved: ${dest}`);
1488
3684
  } catch (e) {
@@ -1493,8 +3689,8 @@ async function generateOpenRouterImage(opts) {
1493
3689
  }
1494
3690
  }
1495
3691
  if (output && savedFiles[0]) {
1496
- fs15.mkdirSync(path13.dirname(path13.resolve(output)), { recursive: true });
1497
- fs15.writeFileSync(output, fs15.readFileSync(savedFiles[0]));
3692
+ fs24.mkdirSync(path29.dirname(path29.resolve(output)), { recursive: true });
3693
+ fs24.writeFileSync(output, fs24.readFileSync(savedFiles[0]));
1498
3694
  }
1499
3695
  return { status: "success", generatedImages: savedFiles, model: usedModel };
1500
3696
  }
@@ -1528,10 +3724,328 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
1528
3724
  );
1529
3725
  }
1530
3726
 
3727
+ // src/providers/openrouter/commands/image-to-image.ts
3728
+ import fs25 from "fs";
3729
+ import path30 from "path";
3730
+ init_errors();
3731
+ init_http_client();
3732
+ var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
3733
+ var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
3734
+ var HARD_LIMIT_BYTES2 = 8 * 1024 * 1024;
3735
+ function registerOpenRouterImageToImageCommand(parent) {
3736
+ 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(
3737
+ "--ref <path>",
3738
+ "Reference image (URL or local path); repeatable (most models accept 1)",
3739
+ collect5,
3740
+ []
3741
+ ).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(
3742
+ async (opts) => {
3743
+ if (!opts.ref || opts.ref.length === 0) {
3744
+ throw new Error("At least one --ref is required for image-to-image.");
3745
+ }
3746
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3747
+ const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_MODEL;
3748
+ const resolvedRefs = [];
3749
+ for (const r of opts.ref) {
3750
+ const resolved = await resolveImageInput(r, {
3751
+ logger,
3752
+ softLimitBytes: SOFT_LIMIT_BYTES2,
3753
+ hardLimitBytes: HARD_LIMIT_BYTES2
3754
+ });
3755
+ resolvedRefs.push(refUrl(resolved));
3756
+ }
3757
+ logger.debug(`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}`);
3758
+ const saved = await runImageToImage({
3759
+ prompt: opts.prompt,
3760
+ model,
3761
+ refs: resolvedRefs,
3762
+ output: opts.output,
3763
+ logger
3764
+ });
3765
+ console.log(`
3766
+ Generated ${saved.length} image(s):`);
3767
+ for (const f of saved) console.log(` ${f}`);
3768
+ console.log(`Model used: ${model}`);
3769
+ }
3770
+ );
3771
+ }
3772
+ function collect5(value, prev) {
3773
+ return [...prev, value];
3774
+ }
3775
+ async function runImageToImage(opts) {
3776
+ const apiKey = requireOpenRouterKey();
3777
+ const headers = {
3778
+ Authorization: `Bearer ${apiKey}`,
3779
+ "Content-Type": "application/json"
3780
+ };
3781
+ const referer = resolveKey("OPENROUTER_SITE_URL");
3782
+ const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
3783
+ if (referer) headers["HTTP-Referer"] = referer;
3784
+ if (title) headers["X-Title"] = title;
3785
+ const content = opts.refs.map((url) => ({
3786
+ type: "image_url",
3787
+ image_url: { url }
3788
+ }));
3789
+ content.push({ type: "text", text: opts.prompt });
3790
+ const payload = {
3791
+ model: opts.model,
3792
+ messages: [{ role: "user", content }],
3793
+ modalities: opts.model.includes("gemini") ? ["image", "text"] : ["image"]
3794
+ };
3795
+ const data = await httpJson({
3796
+ url: OPENROUTER_API_URL,
3797
+ method: "POST",
3798
+ headers,
3799
+ body: payload,
3800
+ timeoutMs: 24e4
3801
+ });
3802
+ const messageImages = data.choices?.[0]?.message?.images ?? [];
3803
+ if (messageImages.length === 0) {
3804
+ throw new ProviderError(
3805
+ `No images in response \u2014 model '${opts.model}' may not support image-to-image`,
3806
+ "openrouter"
3807
+ );
3808
+ }
3809
+ const outDir = getOutputDir();
3810
+ const ts = Date.now();
3811
+ const saved = [];
3812
+ for (let i = 0; i < messageImages.length; i++) {
3813
+ const url = messageImages[i]?.image_url?.url;
3814
+ if (!url) continue;
3815
+ const bytes = await fetchBytes(url);
3816
+ const dest = path30.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
3817
+ fs25.writeFileSync(dest, bytes);
3818
+ saved.push(dest);
3819
+ opts.logger.success(`Saved: ${dest}`);
3820
+ }
3821
+ if (opts.output && saved[0]) {
3822
+ fs25.mkdirSync(path30.dirname(path30.resolve(opts.output)), { recursive: true });
3823
+ fs25.copyFileSync(saved[0], opts.output);
3824
+ opts.logger.success(`Copied to: ${opts.output}`);
3825
+ }
3826
+ return saved;
3827
+ }
3828
+
3829
+ // src/providers/openrouter/video-client.ts
3830
+ init_http_client();
3831
+ var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
3832
+ function authHeaders3(apiKey) {
3833
+ const headers = {
3834
+ Authorization: `Bearer ${apiKey}`,
3835
+ "Content-Type": "application/json"
3836
+ };
3837
+ const referer = resolveKey("OPENROUTER_SITE_URL");
3838
+ const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
3839
+ if (referer) headers["HTTP-Referer"] = referer;
3840
+ if (title) headers["X-Title"] = title;
3841
+ return headers;
3842
+ }
3843
+ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
3844
+ return httpJson({
3845
+ url: OPENROUTER_VIDEOS_URL,
3846
+ method: "POST",
3847
+ headers: authHeaders3(apiKey),
3848
+ body,
3849
+ timeoutMs: 12e4
3850
+ });
3851
+ }
3852
+ async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
3853
+ return httpJson({
3854
+ url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
3855
+ headers: authHeaders3(apiKey)
3856
+ });
3857
+ }
3858
+ function videoContentUrl(jobId) {
3859
+ return `${OPENROUTER_VIDEOS_URL}/${jobId}/content`;
3860
+ }
3861
+ async function listVideoModels(apiKey = requireOpenRouterKey()) {
3862
+ return httpJson({
3863
+ url: `${OPENROUTER_VIDEOS_URL}/models`,
3864
+ headers: authHeaders3(apiKey)
3865
+ });
3866
+ }
3867
+
3868
+ // src/providers/openrouter/commands/video-status.ts
3869
+ import fs26 from "fs";
3870
+ import path31 from "path";
3871
+ async function downloadOpenRouterVideo(opts) {
3872
+ const { jobId, logger, source } = opts;
3873
+ const apiKey = requireOpenRouterKey();
3874
+ const url = videoContentUrl(jobId);
3875
+ const dest = path31.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
3876
+ const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
3877
+ if (!r.ok) {
3878
+ logger.error(`Download HTTP ${r.status}`);
3879
+ process.exit(1);
3880
+ }
3881
+ if (!r.body) {
3882
+ logger.error("Empty response body");
3883
+ process.exit(1);
3884
+ }
3885
+ fs26.mkdirSync(path31.dirname(dest), { recursive: true });
3886
+ const ws = fs26.createWriteStream(dest);
3887
+ const { pipeline: pipeline2 } = await import("stream/promises");
3888
+ await pipeline2(r.body, ws);
3889
+ logger.success(`Saved ${dest}`);
3890
+ if (opts.output) {
3891
+ fs26.mkdirSync(path31.dirname(path31.resolve(opts.output)), { recursive: true });
3892
+ fs26.copyFileSync(dest, opts.output);
3893
+ logger.success(`Copied to ${opts.output}`);
3894
+ }
3895
+ await maybeDownloadThumb(source, dest, {
3896
+ skip: opts.thumb === false,
3897
+ copyTo: opts.output,
3898
+ logger
3899
+ });
3900
+ return dest;
3901
+ }
3902
+ function registerOpenRouterVideoStatusCommand(parent) {
3903
+ 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(
3904
+ async (jobId, opts) => {
3905
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3906
+ const shouldWait = !!(opts.wait || opts.download);
3907
+ let res;
3908
+ try {
3909
+ res = shouldWait ? await poll({
3910
+ fetch: () => pollVideoJob(jobId),
3911
+ done: (v) => v.status === "completed",
3912
+ failed: (v) => v.status === "failed",
3913
+ intervalMs: 5e3,
3914
+ maxAttempts: Math.max(
3915
+ 1,
3916
+ Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
3917
+ ),
3918
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
3919
+ }) : await pollVideoJob(jobId);
3920
+ } catch (e) {
3921
+ if (e instanceof PollTimeoutError) {
3922
+ logger.error(`Timed out waiting for job ${jobId}`);
3923
+ process.exit(1);
3924
+ }
3925
+ if (e instanceof PollFailedError) {
3926
+ logger.error(`Job failed: ${JSON.stringify(e.value)}`);
3927
+ process.exit(1);
3928
+ }
3929
+ throw e;
3930
+ }
3931
+ console.error(`status: ${res.status}`);
3932
+ for (const u of res.unsigned_urls ?? []) console.log(u);
3933
+ if (opts.download && res.status === "completed") {
3934
+ await downloadOpenRouterVideo({
3935
+ jobId,
3936
+ output: opts.output,
3937
+ thumb: opts.thumb,
3938
+ logger,
3939
+ source: res
3940
+ });
3941
+ } else if (opts.download) {
3942
+ logger.warn(`Cannot download \u2014 status is '${res.status}', not 'completed'`);
3943
+ }
3944
+ if (res.usage?.cost !== void 0) {
3945
+ logger.debug(`cost: ${res.usage.cost}${res.usage.is_byok ? " (BYOK)" : ""}`);
3946
+ }
3947
+ }
3948
+ );
3949
+ }
3950
+
3951
+ // src/providers/openrouter/commands/image-to-video.ts
3952
+ function registerOpenRouterImageToVideoCommand(parent) {
3953
+ 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(
3954
+ async (opts) => {
3955
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3956
+ const model = opts.model ?? resolveKey("OPENROUTER_VIDEO_MODEL") ?? "google/veo-3.1";
3957
+ const frame_images = [
3958
+ {
3959
+ type: "image_url",
3960
+ image_url: { url: opts.imageUrl },
3961
+ frame_type: "first_frame"
3962
+ }
3963
+ ];
3964
+ if (opts.lastFrameUrl) {
3965
+ frame_images.push({
3966
+ type: "image_url",
3967
+ image_url: { url: opts.lastFrameUrl },
3968
+ frame_type: "last_frame"
3969
+ });
3970
+ }
3971
+ const body = {
3972
+ model,
3973
+ prompt: opts.prompt,
3974
+ frame_images
3975
+ };
3976
+ if (opts.resolution) body.resolution = opts.resolution;
3977
+ if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
3978
+ if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
3979
+ if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
3980
+ logger.info(`Submitting image-to-video job (model=${model})`);
3981
+ const res = await submitVideoJob(body);
3982
+ logger.success(`Job ${res.id} (status: ${res.status})`);
3983
+ const shouldWait = !!(opts.wait || opts.download);
3984
+ if (!shouldWait) {
3985
+ console.log(res.id);
3986
+ console.error(`Poll with: multix openrouter video-status ${res.id}`);
3987
+ return;
3988
+ }
3989
+ let final;
3990
+ try {
3991
+ final = await poll({
3992
+ fetch: () => pollVideoJob(res.id),
3993
+ done: (v) => v.status === "completed",
3994
+ failed: (v) => v.status === "failed",
3995
+ intervalMs: 5e3,
3996
+ maxAttempts: Math.max(
3997
+ 1,
3998
+ Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
3999
+ ),
4000
+ onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
4001
+ });
4002
+ } catch (e) {
4003
+ if (e instanceof PollTimeoutError) {
4004
+ logger.error(`Timed out \u2014 multix openrouter video-status ${res.id}`);
4005
+ process.exit(1);
4006
+ }
4007
+ if (e instanceof PollFailedError) {
4008
+ logger.error(`Job failed: ${JSON.stringify(e.value)}`);
4009
+ process.exit(1);
4010
+ }
4011
+ throw e;
4012
+ }
4013
+ console.error(`status: ${final.status}`);
4014
+ for (const u of final.unsigned_urls ?? []) console.log(u);
4015
+ if (opts.download) {
4016
+ await downloadOpenRouterVideo({
4017
+ jobId: res.id,
4018
+ output: opts.output,
4019
+ thumb: opts.thumb,
4020
+ logger,
4021
+ source: final
4022
+ });
4023
+ }
4024
+ }
4025
+ );
4026
+ }
4027
+
4028
+ // src/providers/openrouter/commands/video-models.ts
4029
+ function registerOpenRouterVideoModelsCommand(parent) {
4030
+ parent.command("video-models").description("List available OpenRouter video models").option("-v, --verbose", "Verbose logging").action(async (opts) => {
4031
+ const logger = createLogger({ verbose: opts.verbose ?? false });
4032
+ const res = await listVideoModels();
4033
+ const list = res.data ?? res.models ?? [];
4034
+ for (const m of list) {
4035
+ console.log(`${m.id} ${m.name ?? ""}`);
4036
+ }
4037
+ logger.debug(`${list.length} model(s)`);
4038
+ });
4039
+ }
4040
+
1531
4041
  // src/providers/openrouter/commands/index.ts
1532
4042
  function registerOpenRouterCommands(program2) {
1533
- const openrouter = program2.command("openrouter").description("OpenRouter image generation via chat completions API");
4043
+ const openrouter = program2.command("openrouter").description("OpenRouter image + video generation");
1534
4044
  registerOpenRouterGenerateCommand(openrouter);
4045
+ registerOpenRouterImageToImageCommand(openrouter);
4046
+ registerOpenRouterImageToVideoCommand(openrouter);
4047
+ registerOpenRouterVideoStatusCommand(openrouter);
4048
+ registerOpenRouterVideoModelsCommand(openrouter);
1535
4049
  }
1536
4050
 
1537
4051
  // src/commands/check-helpers/binary-check.ts
@@ -1585,6 +4099,8 @@ To configure multix, set at least one provider API key:
1585
4099
  Gemini / AI Studio : https://aistudio.google.com/apikey
1586
4100
  OpenRouter : https://openrouter.ai/settings/keys
1587
4101
  MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
4102
+ Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
4103
+ BytePlus : https://console.byteplus.com/auth/api-keys
1588
4104
 
1589
4105
  Option A \u2014 user global config (recommended):
1590
4106
  echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
@@ -1604,6 +4120,48 @@ var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands u
1604
4120
  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
4121
 
1606
4122
  // src/commands/check.ts
4123
+ var PROVIDERS = [
4124
+ {
4125
+ name: "Gemini",
4126
+ envPrimary: "GEMINI_API_KEY",
4127
+ optional: false,
4128
+ link: "https://aistudio.google.com/apikey"
4129
+ },
4130
+ {
4131
+ name: "OpenRouter",
4132
+ envPrimary: "OPENROUTER_API_KEY",
4133
+ optional: true,
4134
+ link: "https://openrouter.ai/settings/keys"
4135
+ },
4136
+ {
4137
+ name: "MiniMax",
4138
+ envPrimary: "MINIMAX_API_KEY",
4139
+ optional: true,
4140
+ link: "https://platform.minimax.io/user-center/basic-information/interface-key"
4141
+ },
4142
+ {
4143
+ name: "Leonardo.Ai",
4144
+ envPrimary: "LEONARDO_API_KEY",
4145
+ optional: true,
4146
+ link: "https://app.leonardo.ai/settings/api-keys"
4147
+ },
4148
+ {
4149
+ name: "BytePlus",
4150
+ envPrimary: "BYTEPLUS_API_KEY",
4151
+ envFallback: "ARK_API_KEY",
4152
+ optional: true,
4153
+ link: "https://console.byteplus.com/auth/api-keys"
4154
+ }
4155
+ ];
4156
+ function resolveProviderKey(p) {
4157
+ const primary = resolveKey(p.envPrimary);
4158
+ if (primary) return { key: primary, envUsed: p.envPrimary };
4159
+ if (p.envFallback) {
4160
+ const fb = resolveKey(p.envFallback);
4161
+ if (fb) return { key: fb, envUsed: p.envFallback };
4162
+ }
4163
+ return {};
4164
+ }
1607
4165
  function registerCheckCommand(program2) {
1608
4166
  program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
1609
4167
  const logger = createLogger({ verbose: opts.verbose ?? false });
@@ -1624,29 +4182,28 @@ function registerCheckCommand(program2) {
1624
4182
  if (opts.verbose) console.log(` ${MAGICK_HINT}`);
1625
4183
  }
1626
4184
  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)");
4185
+ let anyKey = false;
4186
+ const resolved = /* @__PURE__ */ new Map();
4187
+ for (const p of PROVIDERS) {
4188
+ const r = resolveProviderKey(p);
4189
+ resolved.set(p.name, r);
4190
+ if (r.key) anyKey = true;
4191
+ const envLabel = r.envUsed ?? p.envPrimary;
4192
+ if (r.key) {
4193
+ logger.success(`${envLabel} found \u2014 ${redact(r.key)}`);
4194
+ } else if (p.optional) {
4195
+ const fbNote = p.envFallback ? ` (or ${p.envFallback})` : "";
4196
+ logger.info(`${p.envPrimary}${fbNote} not set (optional)`);
4197
+ } else {
4198
+ logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
4199
+ }
1644
4200
  }
1645
- if (!geminiKey && !openrouterKey && !minimaxKey) {
4201
+ if (!anyKey) {
1646
4202
  logger.header("No Provider Keys Configured");
1647
4203
  console.log(SETUP_HINTS);
1648
4204
  process.exit(1);
1649
4205
  }
4206
+ const geminiKey = resolved.get("Gemini")?.key;
1650
4207
  if (geminiKey) {
1651
4208
  logger.header("Gemini Connectivity");
1652
4209
  logger.info("Pinging Gemini models endpoint...");
@@ -1668,7 +4225,7 @@ function registerCheckCommand(program2) {
1668
4225
  " Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
1669
4226
  );
1670
4227
  } else {
1671
- console.log(" Image generation via OpenRouter/MiniMax available");
4228
+ console.log(" Image generation via OpenRouter/MiniMax/Leonardo/BytePlus available");
1672
4229
  console.log(" Add GEMINI_API_KEY for analysis, transcription, and doc conversion");
1673
4230
  }
1674
4231
  } else {
@@ -1680,8 +4237,8 @@ function registerCheckCommand(program2) {
1680
4237
 
1681
4238
  // src/commands/doc/convert.ts
1682
4239
  init_errors();
1683
- import fs16 from "fs";
1684
- import path14 from "path";
4240
+ import fs27 from "fs";
4241
+ import path32 from "path";
1685
4242
 
1686
4243
  // src/commands/doc/default-prompt.ts
1687
4244
  var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
@@ -1702,18 +4259,18 @@ async function convertDocuments(opts) {
1702
4259
  const model = opts.model ?? DOC_MODEL_DEFAULT;
1703
4260
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
1704
4261
  for (const f of inputs) {
1705
- if (!fs16.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
4262
+ if (!fs27.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
1706
4263
  }
1707
4264
  let outputPath;
1708
4265
  if (output) {
1709
- outputPath = path14.resolve(output);
4266
+ outputPath = path32.resolve(output);
1710
4267
  } 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`);
4268
+ const base = path32.basename(inputs[0], path32.extname(inputs[0]));
4269
+ outputPath = path32.join(getOutputDir(), `${base}-extraction.md`);
1713
4270
  } else {
1714
- outputPath = path14.join(getOutputDir(), "document-extraction.md");
4271
+ outputPath = path32.join(getOutputDir(), "document-extraction.md");
1715
4272
  }
1716
- fs16.mkdirSync(path14.dirname(outputPath), { recursive: true });
4273
+ fs27.mkdirSync(path32.dirname(outputPath), { recursive: true });
1717
4274
  const results = [];
1718
4275
  for (let i = 0; i < inputs.length; i++) {
1719
4276
  const filePath = inputs[i];
@@ -1745,7 +4302,7 @@ async function convertDocuments(opts) {
1745
4302
  if (attempt < 2) {
1746
4303
  const wait = 2 ** attempt * 1e3;
1747
4304
  logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
1748
- await sleep4(wait);
4305
+ await sleep5(wait);
1749
4306
  }
1750
4307
  }
1751
4308
  }
@@ -1764,7 +4321,7 @@ async function convertDocuments(opts) {
1764
4321
  `);
1765
4322
  for (const r of results) {
1766
4323
  lines.push(`
1767
- ## ${path14.basename(r.file)}
4324
+ ## ${path32.basename(r.file)}
1768
4325
  `);
1769
4326
  if (r.status === "success" && r.markdown) {
1770
4327
  lines.push(`${r.markdown}
@@ -1777,7 +4334,7 @@ async function convertDocuments(opts) {
1777
4334
  }
1778
4335
  lines.push("\n---\n");
1779
4336
  }
1780
- fs16.writeFileSync(outputPath, lines.join("\n"), "utf8");
4337
+ fs27.writeFileSync(outputPath, lines.join("\n"), "utf8");
1781
4338
  const succeeded = results.filter((r) => r.status === "success").length;
1782
4339
  console.log(`
1783
4340
  ${"=".repeat(50)}`);
@@ -1793,7 +4350,7 @@ ${"=".repeat(50)}`);
1793
4350
  const failed = results.filter((r) => r.status === "error").length;
1794
4351
  if (failed > 0) process.exit(1);
1795
4352
  }
1796
- function sleep4(ms) {
4353
+ function sleep5(ms) {
1797
4354
  return new Promise((r) => setTimeout(r, ms));
1798
4355
  }
1799
4356
 
@@ -1823,12 +4380,12 @@ function registerDocCommands(program2) {
1823
4380
 
1824
4381
  // src/commands/media/index.ts
1825
4382
  init_errors();
1826
- import fs19 from "fs";
1827
- import path17 from "path";
4383
+ import fs30 from "fs";
4384
+ import path35 from "path";
1828
4385
 
1829
4386
  // src/commands/media/batch.ts
1830
- import fs17 from "fs";
1831
- import path15 from "path";
4387
+ import fs28 from "fs";
4388
+ import path33 from "path";
1832
4389
 
1833
4390
  // src/commands/media/detect-type.ts
1834
4391
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
@@ -2024,10 +4581,10 @@ async function batchOptimize(opts) {
2024
4581
  verbose,
2025
4582
  logger
2026
4583
  } = opts;
2027
- fs17.mkdirSync(outputDir, { recursive: true });
2028
- const entries = fs17.readdirSync(inputDir);
4584
+ fs28.mkdirSync(outputDir, { recursive: true });
4585
+ const entries = fs28.readdirSync(inputDir);
2029
4586
  const mediaFiles = entries.filter((f) => {
2030
- const ext = path15.extname(f).toLowerCase();
4587
+ const ext = path33.extname(f).toLowerCase();
2031
4588
  return ALL_EXTS.has(ext);
2032
4589
  });
2033
4590
  if (mediaFiles.length === 0) {
@@ -2038,9 +4595,9 @@ async function batchOptimize(opts) {
2038
4595
  let succeeded = 0;
2039
4596
  const errors = [];
2040
4597
  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();
4598
+ const inputPath = path33.join(inputDir, file);
4599
+ const outputPath = path33.join(outputDir, file);
4600
+ const ext = path33.extname(file).toLowerCase();
2044
4601
  const kind = extToKind(ext);
2045
4602
  logger?.debug(`Processing: ${file}`);
2046
4603
  try {
@@ -2077,8 +4634,8 @@ async function batchOptimize(opts) {
2077
4634
 
2078
4635
  // src/commands/media/split-video.ts
2079
4636
  init_errors();
2080
- import fs18 from "fs";
2081
- import path16 from "path";
4637
+ import fs29 from "fs";
4638
+ import path34 from "path";
2082
4639
  async function splitVideo(opts) {
2083
4640
  await assertFfmpeg();
2084
4641
  const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
@@ -2089,10 +4646,10 @@ async function splitVideo(opts) {
2089
4646
  logger?.info("Video is short enough \u2014 no splitting needed");
2090
4647
  return [input];
2091
4648
  }
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}`);
4649
+ fs29.mkdirSync(outputDir, { recursive: true });
4650
+ const ext = path34.extname(input) || ".mp4";
4651
+ const baseName = path34.basename(input, ext);
4652
+ const outputPattern = path34.join(outputDir, `${baseName}_chunk_%03d${ext}`);
2096
4653
  logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
2097
4654
  const args = [
2098
4655
  "-i",
@@ -2111,8 +4668,8 @@ async function splitVideo(opts) {
2111
4668
  await runFfmpeg(args, { verbose, logger });
2112
4669
  const files = [];
2113
4670
  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);
4671
+ const fname = path34.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
4672
+ if (fs29.existsSync(fname)) files.push(fname);
2116
4673
  }
2117
4674
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
2118
4675
  return files;
@@ -2126,9 +4683,9 @@ function registerMediaCommands(program2) {
2126
4683
  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
4684
  async (opts) => {
2128
4685
  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();
4686
+ if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
4687
+ fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
4688
+ const ext = path35.extname(opts.input).toLowerCase();
2132
4689
  const kind = extToKind(ext);
2133
4690
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
2134
4691
  const quality = Number.parseInt(opts.quality, 10);
@@ -2168,7 +4725,7 @@ function registerMediaCommands(program2) {
2168
4725
  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
4726
  async (opts) => {
2170
4727
  const logger = createLogger({ verbose: opts.verbose ?? false });
2171
- if (!fs19.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
4728
+ if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
2172
4729
  const chunks = await splitVideo({
2173
4730
  input: opts.input,
2174
4731
  outputDir: opts.outputDir,
@@ -2184,7 +4741,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
2184
4741
  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
4742
  async (opts) => {
2186
4743
  const logger = createLogger({ verbose: opts.verbose ?? false });
2187
- if (!fs19.existsSync(opts.inputDir))
4744
+ if (!fs30.existsSync(opts.inputDir))
2188
4745
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
2189
4746
  const result = await batchOptimize({
2190
4747
  inputDir: opts.inputDir,
@@ -2211,6 +4768,8 @@ function registerCommands(program2) {
2211
4768
  registerGeminiCommands(program2);
2212
4769
  registerMinimaxCommands(program2);
2213
4770
  registerOpenRouterCommands(program2);
4771
+ registerLeonardoCommands(program2);
4772
+ registerBytePlusCommands(program2);
2214
4773
  registerMediaCommands(program2);
2215
4774
  registerDocCommands(program2);
2216
4775
  }