@mrgoonie/multix 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -75,6 +75,7 @@ __export(http_client_exports, {
75
75
  fetchBytes: () => fetchBytes,
76
76
  httpJson: () => httpJson
77
77
  });
78
+ import { once } from "events";
78
79
  import fs4 from "fs";
79
80
  import path4 from "path";
80
81
  import { pipeline } from "stream/promises";
@@ -126,7 +127,12 @@ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
126
127
  throw new HttpError(0, "Response body is null", url);
127
128
  }
128
129
  const fileStream = fs4.createWriteStream(dest);
129
- await pipeline(response.body, fileStream);
130
+ try {
131
+ await pipeline(response.body, fileStream);
132
+ } catch (err2) {
133
+ if (!fileStream.closed) await once(fileStream, "close");
134
+ throw err2;
135
+ }
130
136
  }
131
137
  async function fetchBytes(urlOrDataUri) {
132
138
  if (urlOrDataUri.startsWith("data:")) {
@@ -160,7 +166,7 @@ var init_http_client = __esm({
160
166
  });
161
167
 
162
168
  // src/cli.ts
163
- import { createRequire } from "module";
169
+ import { createRequire as createRequire2 } from "module";
164
170
  import { Command } from "commander";
165
171
 
166
172
  // src/providers/byteplus/commands/generate-3d.ts
@@ -414,9 +420,9 @@ var BytePlusClient = class {
414
420
  }
415
421
  apiKey;
416
422
  baseUrl;
417
- async request(path46, opts = {}) {
423
+ async request(path49, opts = {}) {
418
424
  const base = this.baseUrl.replace(/\/$/, "");
419
- const url = new URL(base + path46);
425
+ const url = new URL(base + path49);
420
426
  if (opts.query) {
421
427
  for (const [k, v] of Object.entries(opts.query)) {
422
428
  if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
@@ -434,14 +440,14 @@ var BytePlusClient = class {
434
440
  timeoutMs: opts.timeoutMs ?? 12e4
435
441
  });
436
442
  }
437
- get(path46, query, logger) {
438
- return this.request(path46, { method: "GET", query, logger });
443
+ get(path49, query, logger) {
444
+ return this.request(path49, { method: "GET", query, logger });
439
445
  }
440
- post(path46, body, logger) {
441
- return this.request(path46, { method: "POST", body, logger });
446
+ post(path49, body, logger) {
447
+ return this.request(path49, { method: "POST", body, logger });
442
448
  }
443
- delete(path46, logger) {
444
- return this.request(path46, { method: "DELETE", logger });
449
+ delete(path49, logger) {
450
+ return this.request(path49, { method: "DELETE", logger });
445
451
  }
446
452
  };
447
453
  function createBytePlusClient() {
@@ -569,10 +575,197 @@ Task: ${taskId}`);
569
575
  );
570
576
  }
571
577
 
578
+ // src/core/image-webp-finalize.ts
579
+ init_errors();
580
+ import { spawnSync } from "child_process";
581
+ import fs6 from "fs";
582
+ import path6 from "path";
583
+ import { Option } from "commander";
584
+
585
+ // src/core/image-header.ts
586
+ import fs5 from "fs";
587
+ function sniffImageKind(buf) {
588
+ if (buf.length >= 8 && buf.readUInt32BE(0) === 2303741511) return "png";
589
+ if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpeg";
590
+ if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP")
591
+ return "webp";
592
+ return void 0;
593
+ }
594
+ function readPng(buf) {
595
+ if (buf.length < 33) return void 0;
596
+ const width = buf.readUInt32BE(16);
597
+ const height = buf.readUInt32BE(20);
598
+ const colorType = buf[25];
599
+ let hasAlpha = colorType === 4 || colorType === 6;
600
+ let offset = 8;
601
+ while (!hasAlpha && offset + 8 <= buf.length) {
602
+ const len = buf.readUInt32BE(offset);
603
+ const type = buf.toString("ascii", offset + 4, offset + 8);
604
+ if (type === "tRNS") hasAlpha = true;
605
+ if (type === "IDAT" || type === "IEND") break;
606
+ offset += 12 + len;
607
+ }
608
+ return { kind: "png", width, height, hasAlpha };
609
+ }
610
+ function readJpeg(buf) {
611
+ let offset = 2;
612
+ while (offset + 9 < buf.length) {
613
+ if (buf[offset] !== 255) {
614
+ offset++;
615
+ continue;
616
+ }
617
+ const marker = buf[offset + 1] ?? 0;
618
+ if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
619
+ return {
620
+ kind: "jpeg",
621
+ height: buf.readUInt16BE(offset + 5),
622
+ width: buf.readUInt16BE(offset + 7),
623
+ hasAlpha: false
624
+ };
625
+ }
626
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
627
+ offset += 2;
628
+ continue;
629
+ }
630
+ offset += 2 + buf.readUInt16BE(offset + 2);
631
+ }
632
+ return void 0;
633
+ }
634
+ function readWebp(buf) {
635
+ if (buf.length < 30) return void 0;
636
+ const chunk = buf.toString("ascii", 12, 16);
637
+ if (chunk === "VP8X") {
638
+ const flags = buf[20] ?? 0;
639
+ return {
640
+ kind: "webp",
641
+ width: 1 + buf.readUIntLE(24, 3),
642
+ height: 1 + buf.readUIntLE(27, 3),
643
+ hasAlpha: (flags & 16) !== 0
644
+ };
645
+ }
646
+ if (chunk === "VP8L") {
647
+ const bits = buf.readUInt32LE(21);
648
+ return {
649
+ kind: "webp",
650
+ width: 1 + (bits & 16383),
651
+ height: 1 + (bits >>> 14 & 16383),
652
+ hasAlpha: (bits >>> 28 & 1) === 1
653
+ };
654
+ }
655
+ if (chunk === "VP8 ") {
656
+ return {
657
+ kind: "webp",
658
+ width: buf.readUInt16LE(26) & 16383,
659
+ height: buf.readUInt16LE(28) & 16383,
660
+ hasAlpha: false
661
+ };
662
+ }
663
+ return void 0;
664
+ }
665
+ function readImageHeader(buf) {
666
+ const kind = sniffImageKind(buf);
667
+ if (kind === "png") return readPng(buf);
668
+ if (kind === "jpeg") return readJpeg(buf);
669
+ if (kind === "webp") return readWebp(buf);
670
+ return void 0;
671
+ }
672
+ function readImageHeaderFromFile(file) {
673
+ return readImageHeader(fs5.readFileSync(file));
674
+ }
675
+
676
+ // src/core/image-webp-finalize.ts
677
+ var CWEBP_ARGS = ["-q", "85", "-m", "6", "-metadata", "none"];
678
+ var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"]);
679
+ function imageFormatOption() {
680
+ return new Option(
681
+ "--image-format <mode>",
682
+ "Final image format: webp (default, cwebp -q 85) | original (keep provider format/quality)"
683
+ );
684
+ }
685
+ function noWebpOption() {
686
+ return new Option("--no-webp", "Keep the provider's original image format and quality");
687
+ }
688
+ function resolveImageFormatMode(opts, explicitProviderFormat = false) {
689
+ if (opts.webp === false && opts.imageFormat === void 0) return "original";
690
+ const raw = opts.imageFormat ?? process.env.MULTIX_IMAGE_FORMAT;
691
+ if (raw !== void 0 && raw !== "") {
692
+ const mode = raw.toLowerCase();
693
+ if (mode !== "webp" && mode !== "original") {
694
+ throw new ValidationError(`Invalid --image-format: ${raw}. Valid: webp, original`);
695
+ }
696
+ return mode;
697
+ }
698
+ if (explicitProviderFormat) return "original";
699
+ const ext = opts.output ? path6.extname(opts.output).toLowerCase() : "";
700
+ if (ext && ext !== ".webp" && IMAGE_EXTS.has(ext)) return "original";
701
+ return "webp";
702
+ }
703
+ function hasCwebp() {
704
+ const res = spawnSync("cwebp", ["-version"], { stdio: "ignore" });
705
+ return !res.error && res.status === 0;
706
+ }
707
+ function webpTarget(file) {
708
+ const ext = path6.extname(file);
709
+ return ext.toLowerCase() === ".webp" ? file : `${file.slice(0, -ext.length || void 0)}.webp`;
710
+ }
711
+ function convertToWebp(file, logger, keepPath = false) {
712
+ const src = readImageHeaderFromFile(file);
713
+ if (!src) {
714
+ logger?.warn(`Skipping WebP conversion (unrecognized image): ${file}`);
715
+ return file;
716
+ }
717
+ if (src.kind === "webp") {
718
+ const target2 = keepPath ? file : webpTarget(file);
719
+ if (target2 !== file) fs6.renameSync(file, target2);
720
+ return target2;
721
+ }
722
+ const target = keepPath ? file : webpTarget(file);
723
+ const tmp = `${target}.tmp-${process.pid}.webp`;
724
+ const res = spawnSync("cwebp", [...CWEBP_ARGS, file, "-o", tmp], { encoding: "utf8" });
725
+ if (res.error || res.status !== 0) {
726
+ fs6.rmSync(tmp, { force: true });
727
+ logger?.warn(`cwebp failed, keeping original ${file}: ${res.stderr || res.error?.message}`);
728
+ return file;
729
+ }
730
+ const out = fs6.existsSync(tmp) ? readImageHeaderFromFile(tmp) : void 0;
731
+ const problem = !out ? "output is not a valid WebP" : out.width !== src.width || out.height !== src.height ? `size changed ${src.width}x${src.height} -> ${out.width}x${out.height}` : src.hasAlpha && !out.hasAlpha ? "transparency lost" : void 0;
732
+ if (problem) {
733
+ fs6.rmSync(tmp, { force: true });
734
+ logger?.warn(`WebP verification failed (${problem}), keeping original ${file}`);
735
+ return file;
736
+ }
737
+ if (target !== file) fs6.rmSync(file, { force: true });
738
+ fs6.renameSync(tmp, target);
739
+ logger?.debug(`WebP verified ${out?.width}x${out?.height} alpha=${out?.hasAlpha}: ${target}`);
740
+ return target;
741
+ }
742
+ function finalizeGeneratedImages(files, opts) {
743
+ const mode = resolveImageFormatMode(opts, opts.explicitProviderFormat);
744
+ if (mode === "original" || files.length === 0) return files;
745
+ if (!hasCwebp()) {
746
+ opts.logger?.warn(
747
+ "cwebp not found on PATH; keeping original image format (install libwebp, or pass --image-format original)"
748
+ );
749
+ return files;
750
+ }
751
+ const outputAbs = opts.output ? path6.resolve(opts.output) : void 0;
752
+ const wroteOutputDirectly = outputAbs !== void 0 && files.some((f) => path6.resolve(f) === outputAbs);
753
+ const finals = files.map(
754
+ (f) => convertToWebp(f, opts.logger, outputAbs !== void 0 && path6.resolve(f) === outputAbs)
755
+ );
756
+ if (opts.output && outputAbs && !wroteOutputDirectly && finals[0] && fs6.existsSync(outputAbs)) {
757
+ const outExt = path6.extname(outputAbs).toLowerCase();
758
+ if ((outExt === ".webp" || outExt === "") && path6.extname(finals[0]).toLowerCase() === ".webp") {
759
+ fs6.copyFileSync(finals[0], outputAbs);
760
+ }
761
+ }
762
+ return finals;
763
+ }
764
+
572
765
  // src/providers/byteplus/generators/image.ts
573
766
  init_http_client();
574
- import fs5 from "fs";
575
- import path6 from "path";
767
+ import fs7 from "fs";
768
+ import path7 from "path";
576
769
  var ASPECT_TO_SIZE = {
577
770
  "1:1": "2048x2048",
578
771
  "16:9": "2560x1440",
@@ -636,14 +829,14 @@ async function generateSeedream(client, opts) {
636
829
  }
637
830
  const ext = (() => {
638
831
  try {
639
- const e = path6.extname(new URL(url).pathname);
832
+ const e = path7.extname(new URL(url).pathname);
640
833
  return e || ".jpg";
641
834
  } catch {
642
835
  return ".jpg";
643
836
  }
644
837
  })();
645
- const filename = opts.output && items.length === 1 ? path6.resolve(opts.output) : path6.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
646
- fs5.mkdirSync(path6.dirname(filename), { recursive: true });
838
+ const filename = opts.output && items.length === 1 ? path7.resolve(opts.output) : path7.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
839
+ fs7.mkdirSync(path7.dirname(filename), { recursive: true });
647
840
  await downloadFile(url, filename);
648
841
  saved.push({ path: filename, url, seed: item.seed });
649
842
  logger?.success(`Saved ${filename}`);
@@ -658,7 +851,7 @@ function registerBytePlusGenerateCommand(parent) {
658
851
  "Reference image (URL or local path); repeatable",
659
852
  collect,
660
853
  []
661
- ).option("--output <path>", "Save single image to this path").option("-v, --verbose", "Verbose logging").action(
854
+ ).option("--output <path>", "Save single image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
662
855
  async (opts) => {
663
856
  const logger = createLogger({ verbose: opts.verbose ?? false });
664
857
  const client = createBytePlusClient();
@@ -674,9 +867,13 @@ function registerBytePlusGenerateCommand(parent) {
674
867
  output: opts.output,
675
868
  logger
676
869
  });
870
+ const finalImages = finalizeGeneratedImages(
871
+ saved.map((f) => f.path),
872
+ { ...opts, logger }
873
+ );
677
874
  console.log(`
678
- Generated ${saved.length} image(s):`);
679
- for (const f of saved) console.log(` ${f.path}`);
875
+ Generated ${finalImages.length} image(s):`);
876
+ for (const f of finalImages) console.log(` ${f}`);
680
877
  }
681
878
  );
682
879
  }
@@ -693,7 +890,7 @@ function registerBytePlusImageToImageCommand(parent) {
693
890
  "Reference image (URL or local path); repeatable for multi-ref",
694
891
  collect2,
695
892
  []
696
- ).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(
893
+ ).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").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
697
894
  async (opts) => {
698
895
  if (!opts.ref || opts.ref.length === 0) {
699
896
  throw new Error("At least one --ref is required for image-to-image.");
@@ -712,9 +909,13 @@ function registerBytePlusImageToImageCommand(parent) {
712
909
  output: opts.output,
713
910
  logger
714
911
  });
912
+ const finalImages = finalizeGeneratedImages(
913
+ saved.map((f) => f.path),
914
+ { ...opts, logger }
915
+ );
715
916
  console.log(`
716
- Generated ${saved.length} image(s):`);
717
- for (const f of saved) console.log(` ${f.path}`);
917
+ Generated ${finalImages.length} image(s):`);
918
+ for (const f of finalImages) console.log(` ${f}`);
718
919
  }
719
920
  );
720
921
  }
@@ -723,12 +924,12 @@ function collect2(value, prev) {
723
924
  }
724
925
 
725
926
  // src/providers/byteplus/commands/image-to-video.ts
726
- import path8 from "path";
927
+ import path9 from "path";
727
928
 
728
929
  // src/core/video-thumb.ts
729
930
  init_http_client();
730
- import fs6 from "fs";
731
- import path7 from "path";
931
+ import fs8 from "fs";
932
+ import path8 from "path";
732
933
  var THUMB_KEYS = /* @__PURE__ */ new Set([
733
934
  "cover_image_url",
734
935
  "coverImageUrl",
@@ -778,9 +979,9 @@ function detectThumbUrl(obj) {
778
979
  async function downloadThumbBeside(thumbUrl, videoPath, logger) {
779
980
  const match = thumbUrl.match(IMAGE_EXT_RE);
780
981
  const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
781
- const dir = path7.dirname(videoPath);
782
- const base = path7.basename(videoPath, path7.extname(videoPath));
783
- const dest = path7.join(dir, `${base}_thumb.${ext}`);
982
+ const dir = path8.dirname(videoPath);
983
+ const base = path8.basename(videoPath, path8.extname(videoPath));
984
+ const dest = path8.join(dir, `${base}_thumb.${ext}`);
784
985
  try {
785
986
  await downloadFile(thumbUrl, dest);
786
987
  logger?.success(`Saved thumb ${dest}`);
@@ -799,12 +1000,12 @@ async function maybeDownloadThumb(source, videoPath, opts = {}) {
799
1000
  }
800
1001
  const saved = await downloadThumbBeside(url, videoPath, opts.logger);
801
1002
  if (saved && opts.copyTo) {
802
- const copyExt = path7.extname(saved);
803
- const copyBase = path7.basename(opts.copyTo, path7.extname(opts.copyTo));
804
- const copyDir = path7.dirname(path7.resolve(opts.copyTo));
805
- const copyDest = path7.join(copyDir, `${copyBase}_thumb${copyExt}`);
806
- fs6.mkdirSync(copyDir, { recursive: true });
807
- fs6.copyFileSync(saved, copyDest);
1003
+ const copyExt = path8.extname(saved);
1004
+ const copyBase = path8.basename(opts.copyTo, path8.extname(opts.copyTo));
1005
+ const copyDir = path8.dirname(path8.resolve(opts.copyTo));
1006
+ const copyDest = path8.join(copyDir, `${copyBase}_thumb${copyExt}`);
1007
+ fs8.mkdirSync(copyDir, { recursive: true });
1008
+ fs8.copyFileSync(saved, copyDest);
808
1009
  opts.logger?.success(`Copied thumb to ${copyDest}`);
809
1010
  }
810
1011
  return saved;
@@ -932,7 +1133,7 @@ function registerBytePlusImageToVideoCommand(parent) {
932
1133
  logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
933
1134
  process.exit(1);
934
1135
  }
935
- const outPath = opts.output ?? path8.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1136
+ const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
936
1137
  await downloadVideo(url, outPath, logger);
937
1138
  await maybeDownloadThumb(final, outPath, {
938
1139
  skip: opts.thumb === false,
@@ -958,7 +1159,7 @@ Task: ${taskId}`);
958
1159
  }
959
1160
 
960
1161
  // src/providers/byteplus/commands/reference-to-video.ts
961
- import path9 from "path";
1162
+ import path10 from "path";
962
1163
 
963
1164
  // src/providers/byteplus/reference-input.ts
964
1165
  var MAX_TOTAL_REFS = 12;
@@ -990,10 +1191,10 @@ function toRef(kind, resolved, role) {
990
1191
  return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
991
1192
  }
992
1193
  async function resolveRefSpec(kind, spec, logger) {
993
- const { path: path46, role } = parseRefSpec(spec);
994
- if (kind === "image") return toRef(kind, await resolveImageInput(path46, logger), role);
995
- if (kind === "video") return toRef(kind, await resolveVideoInput(path46, logger), role);
996
- return toRef(kind, await resolveAudioInput(path46, logger), role);
1194
+ const { path: path49, role } = parseRefSpec(spec);
1195
+ if (kind === "image") return toRef(kind, await resolveImageInput(path49, logger), role);
1196
+ if (kind === "video") return toRef(kind, await resolveVideoInput(path49, logger), role);
1197
+ return toRef(kind, await resolveAudioInput(path49, logger), role);
997
1198
  }
998
1199
  async function resolveRefSpecs(specs, logger) {
999
1200
  const out = [];
@@ -1090,7 +1291,7 @@ function registerBytePlusReferenceToVideoCommand(parent) {
1090
1291
  logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
1091
1292
  process.exit(1);
1092
1293
  }
1093
- const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1294
+ const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1094
1295
  await downloadVideo(url, outPath, logger);
1095
1296
  await maybeDownloadThumb(final, outPath, {
1096
1297
  skip: opts.thumb === false,
@@ -1116,7 +1317,7 @@ Task: ${taskId}`);
1116
1317
  }
1117
1318
 
1118
1319
  // src/providers/byteplus/commands/status.ts
1119
- import path10 from "path";
1320
+ import path11 from "path";
1120
1321
  function registerBytePlusStatusCommand(parent) {
1121
1322
  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(
1122
1323
  async (taskId, opts) => {
@@ -1134,7 +1335,7 @@ function registerBytePlusStatusCommand(parent) {
1134
1335
  const videoUrl2 = content?.video_url;
1135
1336
  const fileUrl = content?.file_url;
1136
1337
  if (videoUrl2) {
1137
- const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
1338
+ const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
1138
1339
  await downloadVideo(videoUrl2, outPath, logger);
1139
1340
  await maybeDownloadThumb(status, outPath, {
1140
1341
  skip: opts.thumb === false,
@@ -1144,7 +1345,7 @@ function registerBytePlusStatusCommand(parent) {
1144
1345
  console.log(`Saved: ${outPath}`);
1145
1346
  } else if (fileUrl) {
1146
1347
  const ext = inferModelFileExt(fileUrl);
1147
- const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
1348
+ const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
1148
1349
  await downloadModelFile(fileUrl, outPath, logger);
1149
1350
  console.log(`Saved: ${outPath}`);
1150
1351
  } else {
@@ -1168,7 +1369,7 @@ function registerBytePlusStatusCommand(parent) {
1168
1369
  }
1169
1370
 
1170
1371
  // src/providers/byteplus/commands/video.ts
1171
- import path11 from "path";
1372
+ import path12 from "path";
1172
1373
  function registerBytePlusVideoCommand(parent) {
1173
1374
  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(
1174
1375
  "--aspect-ratio <r>",
@@ -1210,7 +1411,7 @@ function registerBytePlusVideoCommand(parent) {
1210
1411
  logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
1211
1412
  process.exit(1);
1212
1413
  }
1213
- const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1414
+ const outPath = opts.output ?? path12.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
1214
1415
  await downloadVideo(url, outPath, logger);
1215
1416
  await maybeDownloadThumb(final, outPath, {
1216
1417
  skip: opts.thumb === false,
@@ -1428,15 +1629,15 @@ async function downloadVideo2(url) {
1428
1629
 
1429
1630
  // src/providers/cloudflare/media-output.ts
1430
1631
  init_errors();
1431
- import fs7 from "fs";
1432
- import path12 from "path";
1632
+ import fs9 from "fs";
1633
+ import path13 from "path";
1433
1634
  function destination(filename, output) {
1434
- return output ? path12.resolve(output) : path12.join(getOutputDir(), filename);
1635
+ return output ? path13.resolve(output) : path13.join(getOutputDir(), filename);
1435
1636
  }
1436
1637
  function saveBytes(bytes, filename, output) {
1437
1638
  const dest = destination(filename, output);
1438
- fs7.mkdirSync(path12.dirname(dest), { recursive: true });
1439
- fs7.writeFileSync(dest, bytes);
1639
+ fs9.mkdirSync(path13.dirname(dest), { recursive: true });
1640
+ fs9.writeFileSync(dest, bytes);
1440
1641
  return dest;
1441
1642
  }
1442
1643
  function saveBase64Image(image, output) {
@@ -1553,7 +1754,7 @@ function registerCloudflareVideoCommand(parent) {
1553
1754
  // src/providers/cloudflare/commands/generate.ts
1554
1755
  init_errors();
1555
1756
  function registerCloudflareGenerateCommand(parent) {
1556
- parent.command("generate").description("Generate an image with Cloudflare Workers AI FLUX.1 Schnell").requiredOption("--prompt <text>", "Image prompt").option("--model <id>", "Cloudflare image model").option("--steps <n>", "Inference steps (1-8)", "4").option("--seed <n>", "Optional non-negative integer seed").option("--output <path>", "Save image at this path").option("-v, --verbose", "Verbose logging").action(
1757
+ parent.command("generate").description("Generate an image with Cloudflare Workers AI FLUX.1 Schnell").requiredOption("--prompt <text>", "Image prompt").option("--model <id>", "Cloudflare image model").option("--steps <n>", "Inference steps (1-8)", "4").option("--seed <n>", "Optional non-negative integer seed").option("--output <path>", "Save image at this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
1557
1758
  async (opts) => {
1558
1759
  if (!opts.prompt.trim()) throw new ValidationError("--prompt must not be empty");
1559
1760
  const logger = createLogger({ verbose: opts.verbose ?? false });
@@ -1575,8 +1776,9 @@ function registerCloudflareGenerateCommand(parent) {
1575
1776
  if (typeof result.image !== "string") {
1576
1777
  throw new ProviderError("Cloudflare returned no image result", "Cloudflare");
1577
1778
  }
1578
- const destination2 = saveBase64Image(result.image, opts.output);
1579
- logger.success(`Saved ${destination2}`);
1779
+ const saved = saveBase64Image(result.image, opts.output);
1780
+ logger.success(`Saved ${saved}`);
1781
+ const [destination2] = finalizeGeneratedImages([saved], { ...opts, logger });
1580
1782
  console.log(destination2);
1581
1783
  }
1582
1784
  );
@@ -1618,8 +1820,8 @@ function registerCloudflareCommands(program2) {
1618
1820
  }
1619
1821
 
1620
1822
  // src/providers/elevenlabs/client.ts
1621
- import fs8 from "fs";
1622
- import path13 from "path";
1823
+ import fs10 from "fs";
1824
+ import path14 from "path";
1623
1825
  init_errors();
1624
1826
  init_http_client();
1625
1827
 
@@ -1781,10 +1983,10 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
1781
1983
  return res.json();
1782
1984
  }
1783
1985
  function readFileAsBlob(filePath, fieldFilename) {
1784
- const abs = path13.resolve(filePath);
1785
- if (!fs8.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
1786
- const buf = fs8.readFileSync(abs);
1787
- const name = fieldFilename ?? path13.basename(abs);
1986
+ const abs = path14.resolve(filePath);
1987
+ if (!fs10.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
1988
+ const buf = fs10.readFileSync(abs);
1989
+ const name = fieldFilename ?? path14.basename(abs);
1788
1990
  return new File([new Uint8Array(buf)], name);
1789
1991
  }
1790
1992
  async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
@@ -1808,12 +2010,12 @@ async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
1808
2010
  function saveBytes2(opts) {
1809
2011
  const { bytes, task, ext, outputCopy, logger } = opts;
1810
2012
  const dir = getOutputDir();
1811
- const dest = path13.join(dir, `elevenlabs_${task}_${Date.now()}.${ext}`);
1812
- fs8.writeFileSync(dest, bytes);
2013
+ const dest = path14.join(dir, `elevenlabs_${task}_${Date.now()}.${ext}`);
2014
+ fs10.writeFileSync(dest, bytes);
1813
2015
  logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
1814
2016
  if (outputCopy) {
1815
- fs8.mkdirSync(path13.dirname(path13.resolve(outputCopy)), { recursive: true });
1816
- fs8.copyFileSync(dest, outputCopy);
2017
+ fs10.mkdirSync(path14.dirname(path14.resolve(outputCopy)), { recursive: true });
2018
+ fs10.copyFileSync(dest, outputCopy);
1817
2019
  }
1818
2020
  return dest;
1819
2021
  }
@@ -1852,14 +2054,14 @@ ${models.length} models`);
1852
2054
  }
1853
2055
 
1854
2056
  // src/providers/elevenlabs/commands/align.ts
1855
- import fs9 from "fs";
1856
- import path14 from "path";
2057
+ import fs11 from "fs";
2058
+ import path15 from "path";
1857
2059
  function registerAlignCommand(parent) {
1858
2060
  parent.command("align").description("Forced alignment of audio to a transcript (returns word/char timestamps)").requiredOption("--input <path>", "Source audio file").requiredOption("--text <str>", "Transcript text (or pass --text-file)").option("--text-file <path>", "Read transcript from file").option("--output <path>", "Save JSON to this path (default: multix-output)").option("-v, --verbose", "Verbose logging").action(async (opts) => {
1859
2061
  const logger = createLogger({ verbose: opts.verbose ?? false });
1860
2062
  const apiKey = requireElevenLabsKey();
1861
2063
  let text = opts.text;
1862
- if (opts.textFile) text = fs9.readFileSync(path14.resolve(opts.textFile), "utf8");
2064
+ if (opts.textFile) text = fs11.readFileSync(path15.resolve(opts.textFile), "utf8");
1863
2065
  const form = new FormData();
1864
2066
  form.append("file", readFileAsBlob(opts.input));
1865
2067
  form.append("text", text);
@@ -1867,9 +2069,9 @@ function registerAlignCommand(parent) {
1867
2069
  const data = await apiPostMultipart("forced-alignment", form, apiKey, {
1868
2070
  timeoutMs: 6e5
1869
2071
  });
1870
- const dest = opts.output ? path14.resolve(opts.output) : path14.join(getOutputDir(), `elevenlabs_align_${Date.now()}.json`);
1871
- fs9.mkdirSync(path14.dirname(dest), { recursive: true });
1872
- fs9.writeFileSync(dest, JSON.stringify(data, null, 2));
2072
+ const dest = opts.output ? path15.resolve(opts.output) : path15.join(getOutputDir(), `elevenlabs_align_${Date.now()}.json`);
2073
+ fs11.mkdirSync(path15.dirname(dest), { recursive: true });
2074
+ fs11.writeFileSync(dest, JSON.stringify(data, null, 2));
1873
2075
  logger.success(`Saved alignment: ${dest}`);
1874
2076
  if (data.words) console.log(`Aligned ${data.words.length} words (loss=${data.loss ?? "?"})`);
1875
2077
  });
@@ -1912,8 +2114,8 @@ function registerCloneCommand(parent) {
1912
2114
  // src/providers/elevenlabs/commands/dub.ts
1913
2115
  init_errors();
1914
2116
  init_http_client();
1915
- import fs10 from "fs";
1916
- import path15 from "path";
2117
+ import fs12 from "fs";
2118
+ import path16 from "path";
1917
2119
  function registerDubCommand(parent) {
1918
2120
  parent.command("dub").description("Dub a video/audio file into target languages (async)").option("--input <path>", "Source media file (use --input or --source-url)").option("--source-url <url>", "Public URL of source media").requiredOption("--target-lang <code>", "Target language ISO code (e.g. es, fr, de)").option("--source-lang <code>", "Source language (auto if omitted)").option("--num-speakers <n>", "Speaker count (auto if omitted)").option("--watermark", "Add ElevenLabs watermark", false).option("--start-time <s>", "Start time in seconds").option("--end-time <s>", "End time in seconds").option("--highest-resolution", "Use highest resolution video", false).option("--name <n>", "Project display name").option("--async", "Return job id immediately, do not wait", false).option("--wait", "Wait until status=dubbed (implied by --download)", false).option("--download", "Download dubbed media when ready", false).option("--poll-interval <s>", "Poll interval seconds", "10").option("--wait-timeout <ms>", "Poll timeout", "1800000").option("--output <path>", "Output path for downloaded dub").option("-v, --verbose", "Verbose logging").action(async (opts) => {
1919
2121
  const logger = createLogger({ verbose: opts.verbose ?? false });
@@ -1956,7 +2158,7 @@ function registerDubCommand(parent) {
1956
2158
  );
1957
2159
  logger.success(`Dub complete: ${start.dubbing_id}`);
1958
2160
  if (opts.download) {
1959
- const dest = opts.output ? path15.resolve(opts.output) : path15.join(getOutputDir(), `elevenlabs_dub_${start.dubbing_id}_${opts.targetLang}.mp4`);
2161
+ const dest = opts.output ? path16.resolve(opts.output) : path16.join(getOutputDir(), `elevenlabs_dub_${start.dubbing_id}_${opts.targetLang}.mp4`);
1960
2162
  const url = `${ELEVENLABS_BASE_URL}/dubbing/${start.dubbing_id}/audio/${opts.targetLang}`;
1961
2163
  await downloadDub(url, apiKey, dest);
1962
2164
  logger.success(`Saved: ${dest}`);
@@ -1971,7 +2173,7 @@ Dubbed file: ${dest}`);
1971
2173
  const status = await apiGet(`dubbing/${dubbingId}`, apiKey);
1972
2174
  console.log(JSON.stringify(status, null, 2));
1973
2175
  if (opts.download) {
1974
- const dest = opts.output ? path15.resolve(opts.output) : path15.join(getOutputDir(), `elevenlabs_dub_${dubbingId}_${opts.download}.mp4`);
2176
+ const dest = opts.output ? path16.resolve(opts.output) : path16.join(getOutputDir(), `elevenlabs_dub_${dubbingId}_${opts.download}.mp4`);
1975
2177
  const url = `${ELEVENLABS_BASE_URL}/dubbing/${dubbingId}/audio/${opts.download}`;
1976
2178
  await downloadDub(url, apiKey, dest);
1977
2179
  console.log(`Saved: ${dest}`);
@@ -1979,7 +2181,7 @@ Dubbed file: ${dest}`);
1979
2181
  });
1980
2182
  }
1981
2183
  async function downloadDub(url, apiKey, dest) {
1982
- fs10.mkdirSync(path15.dirname(dest), { recursive: true });
2184
+ fs12.mkdirSync(path16.dirname(dest), { recursive: true });
1983
2185
  const res = await globalThis.fetch(url, {
1984
2186
  headers: { "xi-api-key": apiKey },
1985
2187
  signal: AbortSignal.timeout(6e5)
@@ -1988,7 +2190,7 @@ async function downloadDub(url, apiKey, dest) {
1988
2190
  throw new Error(`Failed to download dub: HTTP ${res.status}`);
1989
2191
  }
1990
2192
  const arr = new Uint8Array(await res.arrayBuffer());
1991
- fs10.writeFileSync(dest, arr);
2193
+ fs12.writeFileSync(dest, arr);
1992
2194
  void downloadFile;
1993
2195
  }
1994
2196
 
@@ -2017,8 +2219,8 @@ Isolated audio: ${dest}`);
2017
2219
 
2018
2220
  // src/providers/elevenlabs/commands/music.ts
2019
2221
  init_errors();
2020
- import fs11 from "fs";
2021
- import path16 from "path";
2222
+ import fs13 from "fs";
2223
+ import path17 from "path";
2022
2224
  function registerMusicCommand(parent) {
2023
2225
  parent.command("music").description("Generate music from a text prompt or composition plan").option("--prompt <str>", "Music prompt (mutually exclusive with --plan)").option("--plan <path>", "Path to composition plan JSON file").option("--music-length-ms <n>", "Total length in milliseconds (default 30000)").option("--model <id>", "Music model", "music_v1").option(
2024
2226
  "--format <fmt>",
@@ -2033,9 +2235,9 @@ function registerMusicCommand(parent) {
2033
2235
  const payload = { model_id: opts.model };
2034
2236
  if (opts.prompt) payload.prompt = opts.prompt;
2035
2237
  if (opts.plan) {
2036
- const planPath = path16.resolve(opts.plan);
2037
- if (!fs11.existsSync(planPath)) throw new ValidationError(`Plan file not found: ${planPath}`);
2038
- const planJson = JSON.parse(fs11.readFileSync(planPath, "utf8"));
2238
+ const planPath = path17.resolve(opts.plan);
2239
+ if (!fs13.existsSync(planPath)) throw new ValidationError(`Plan file not found: ${planPath}`);
2240
+ const planJson = JSON.parse(fs13.readFileSync(planPath, "utf8"));
2039
2241
  payload.composition_plan = planJson;
2040
2242
  }
2041
2243
  if (opts.musicLengthMs) payload.music_length_ms = Number.parseInt(opts.musicLengthMs, 10);
@@ -2094,8 +2296,8 @@ Generated SFX: ${dest}`);
2094
2296
  }
2095
2297
 
2096
2298
  // src/providers/elevenlabs/commands/transcribe.ts
2097
- import fs12 from "fs";
2098
- import path17 from "path";
2299
+ import fs14 from "fs";
2300
+ import path18 from "path";
2099
2301
  function registerTranscribeCommand(parent) {
2100
2302
  parent.command("transcribe").description("Speech-to-text transcription with Scribe").requiredOption("--input <path>", "Audio or video file to transcribe").option(
2101
2303
  "--model <id>",
@@ -2118,7 +2320,7 @@ function registerTranscribeCommand(parent) {
2118
2320
  });
2119
2321
  const ext = opts.format === "json" ? "json" : opts.format === "srt" ? "srt" : opts.format === "vtt" ? "vtt" : "txt";
2120
2322
  const dir = getOutputDir();
2121
- const dest = opts.output ? path17.resolve(opts.output) : path17.join(dir, `elevenlabs_transcribe_${Date.now()}.${ext}`);
2323
+ const dest = opts.output ? path18.resolve(opts.output) : path18.join(dir, `elevenlabs_transcribe_${Date.now()}.${ext}`);
2122
2324
  let body;
2123
2325
  switch (opts.format) {
2124
2326
  case "json":
@@ -2133,8 +2335,8 @@ function registerTranscribeCommand(parent) {
2133
2335
  default:
2134
2336
  body = data.text ?? "";
2135
2337
  }
2136
- fs12.mkdirSync(path17.dirname(dest), { recursive: true });
2137
- fs12.writeFileSync(dest, body);
2338
+ fs14.mkdirSync(path18.dirname(dest), { recursive: true });
2339
+ fs14.writeFileSync(dest, body);
2138
2340
  logger.success(`Saved transcript: ${dest}`);
2139
2341
  if (data.language_code) {
2140
2342
  logger.info(
@@ -2379,18 +2581,455 @@ function registerElevenLabsCommands(program2) {
2379
2581
  registerAccountCommands(el);
2380
2582
  }
2381
2583
 
2382
- // src/providers/gemini/commands/analyze.ts
2584
+ // src/providers/fal/commands/image.ts
2585
+ import fs16 from "fs";
2586
+ import path20 from "path";
2587
+
2588
+ // src/providers/fal/models.ts
2589
+ var DEFAULT_FAL_BASE_URL = "https://queue.fal.run";
2590
+ var FAL_DEFAULTS = {
2591
+ // FLUX.1 [schnell] — fast, low-cost default text-to-image model.
2592
+ imageModel: process.env.FAL_IMAGE_MODEL ?? "fal-ai/flux/schnell",
2593
+ // Stable, widely available default text-to-video model.
2594
+ videoModel: process.env.FAL_VIDEO_MODEL ?? "fal-ai/kling-video/v1.6/standard/text-to-video",
2595
+ // Matching image-to-video model — used when --image-url is passed without -m.
2596
+ videoImageModel: process.env.FAL_VIDEO_IMAGE_MODEL ?? "fal-ai/kling-video/v1.6/standard/image-to-video"
2597
+ };
2598
+
2599
+ // src/providers/fal/run-helpers.ts
2383
2600
  init_errors();
2601
+ init_http_client();
2384
2602
  import fs15 from "fs";
2603
+ import path19 from "path";
2604
+
2605
+ // src/providers/fal/client.ts
2606
+ init_errors();
2607
+ init_errors();
2608
+ init_http_client();
2609
+ function requireFalKey() {
2610
+ const key = resolveKey("FAL_KEY");
2611
+ if (!key) {
2612
+ throw new ConfigError("FAL_KEY is not set. Get one at https://fal.ai/dashboard/keys");
2613
+ }
2614
+ return key;
2615
+ }
2616
+ function falBaseUrl() {
2617
+ return resolveKey("FAL_BASE_URL") ?? DEFAULT_FAL_BASE_URL;
2618
+ }
2619
+ var FalClient = class {
2620
+ constructor(apiKey, baseUrl = falBaseUrl()) {
2621
+ this.apiKey = apiKey;
2622
+ this.baseUrl = baseUrl;
2623
+ }
2624
+ apiKey;
2625
+ baseUrl;
2626
+ /** `path` is appended verbatim to the base queue URL, e.g. "/fal-ai/flux/schnell". */
2627
+ async request(path49, opts = {}) {
2628
+ const base = this.baseUrl.replace(/\/$/, "");
2629
+ const suffix = path49.startsWith("/") ? path49 : `/${path49}`;
2630
+ return this.requestAbsolute(base + suffix, opts);
2631
+ }
2632
+ /** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
2633
+ async requestAbsolute(url, opts = {}) {
2634
+ opts.logger?.debug(`${opts.method ?? "GET"} ${url}`);
2635
+ return httpJson({
2636
+ url,
2637
+ method: opts.method ?? "GET",
2638
+ headers: {
2639
+ accept: "application/json",
2640
+ authorization: `Key ${this.apiKey}`
2641
+ },
2642
+ body: opts.body,
2643
+ timeoutMs: opts.timeoutMs ?? 12e4
2644
+ });
2645
+ }
2646
+ get(path49, logger) {
2647
+ return this.request(path49, { method: "GET", logger });
2648
+ }
2649
+ post(path49, body, logger) {
2650
+ return this.request(path49, { method: "POST", body, logger });
2651
+ }
2652
+ getAbsolute(url, logger) {
2653
+ return this.requestAbsolute(url, { method: "GET", logger });
2654
+ }
2655
+ };
2656
+ function createFalClient() {
2657
+ return new FalClient(requireFalKey(), falBaseUrl());
2658
+ }
2659
+ function submitPath(model) {
2660
+ return `/${model.replace(/^\/+/, "")}`;
2661
+ }
2662
+ function appIdFromModel(model) {
2663
+ const segments = model.replace(/^\/+|\/+$/g, "").split("/");
2664
+ if (segments.length < 2 || !segments[0] || !segments[1]) {
2665
+ throw new ValidationError(
2666
+ `Invalid fal model id "${model}" \u2014 expected at least "owner/alias" (e.g. fal-ai/flux)`
2667
+ );
2668
+ }
2669
+ return `${segments[0]}/${segments[1]}`;
2670
+ }
2671
+ function statusPath(model, requestId) {
2672
+ return `/${appIdFromModel(model)}/requests/${requestId}/status`;
2673
+ }
2674
+ function resultPath(model, requestId) {
2675
+ return `/${appIdFromModel(model)}/requests/${requestId}`;
2676
+ }
2677
+
2678
+ // src/providers/fal/run-helpers.ts
2679
+ function parseInputArg(raw) {
2680
+ const text = raw.startsWith("@") ? fs15.readFileSync(raw.slice(1), "utf8") : raw;
2681
+ try {
2682
+ const parsed = JSON.parse(text);
2683
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
2684
+ throw new Error("input must be a JSON object");
2685
+ }
2686
+ return parsed;
2687
+ } catch (e) {
2688
+ const msg = e instanceof Error ? e.message : String(e);
2689
+ throw new ValidationError(`Invalid --input JSON: ${msg}`);
2690
+ }
2691
+ }
2692
+ function isTransientError(e) {
2693
+ if (e instanceof HttpError) return e.status === 0 || e.status >= 500;
2694
+ return false;
2695
+ }
2696
+ function sleep(ms, signal) {
2697
+ return new Promise((resolve, reject) => {
2698
+ const t = setTimeout(resolve, ms);
2699
+ signal?.addEventListener("abort", () => {
2700
+ clearTimeout(t);
2701
+ reject(new Error("Aborted"));
2702
+ });
2703
+ });
2704
+ }
2705
+ async function submitAndWait(opts) {
2706
+ const { model, input, logger } = opts;
2707
+ const client = createFalClient();
2708
+ const submitRes = await client.post(submitPath(model), input, logger);
2709
+ const requestId = submitRes.request_id;
2710
+ logger.debug(`request_id: ${requestId}`);
2711
+ logger.info("Polling for completion...");
2712
+ const waitTimeoutMs = opts.waitTimeoutMs ?? 48e4;
2713
+ const intervalMs = opts.pollIntervalMs ?? 3e3;
2714
+ const deadline = Date.now() + waitTimeoutMs;
2715
+ const resumeHint = `Resume with: multix fal status ${model} ${requestId} or multix fal result ${model} ${requestId}`;
2716
+ let attempt = 0;
2717
+ for (; ; ) {
2718
+ if (Date.now() >= deadline) {
2719
+ throw new Error(
2720
+ `Timed out waiting for fal request ${requestId} after ${waitTimeoutMs}ms. ${resumeHint}`
2721
+ );
2722
+ }
2723
+ attempt++;
2724
+ let statusRes;
2725
+ try {
2726
+ statusRes = await client.getAbsolute(submitRes.status_url, logger);
2727
+ } catch (e) {
2728
+ if (isTransientError(e)) {
2729
+ const msg2 = e instanceof Error ? e.message : String(e);
2730
+ logger.debug(`attempt ${attempt} \u2014 transient error, retrying: ${msg2}`);
2731
+ await sleep(intervalMs);
2732
+ continue;
2733
+ }
2734
+ const msg = e instanceof Error ? e.message : String(e);
2735
+ throw new Error(`Failed to poll status for fal request ${requestId}: ${msg}. ${resumeHint}`);
2736
+ }
2737
+ logger.debug(`attempt ${attempt} \u2014 status: ${statusRes.status}`);
2738
+ if (statusRes.status === "COMPLETED") break;
2739
+ await sleep(intervalMs);
2740
+ }
2741
+ try {
2742
+ const result = await client.getAbsolute(submitRes.response_url, logger);
2743
+ return { requestId, result };
2744
+ } catch (e) {
2745
+ const msg = e instanceof Error ? e.message : String(e);
2746
+ throw new Error(`Failed to fetch result for fal request ${requestId}: ${msg}. ${resumeHint}`);
2747
+ }
2748
+ }
2749
+ var MEDIA_URL_RE = /^https?:\/\//i;
2750
+ function isHttpUrl2(v) {
2751
+ return typeof v === "string" && MEDIA_URL_RE.test(v);
2752
+ }
2753
+ function urlOf(item) {
2754
+ if (isHttpUrl2(item)) return item;
2755
+ if (item !== null && typeof item === "object" && "url" in item) {
2756
+ const u = item.url;
2757
+ if (isHttpUrl2(u)) return u;
2758
+ }
2759
+ return void 0;
2760
+ }
2761
+ function extractKnownMediaUrls(result) {
2762
+ const urls = [];
2763
+ const images = result.images;
2764
+ if (Array.isArray(images)) {
2765
+ for (const img of images) {
2766
+ const u = urlOf(img);
2767
+ if (u) urls.push(u);
2768
+ }
2769
+ }
2770
+ for (const key of ["video", "image", "audio"]) {
2771
+ const u = urlOf(result[key]);
2772
+ if (u) urls.push(u);
2773
+ }
2774
+ return urls;
2775
+ }
2776
+ function extractMediaUrlsGeneric(value, acc = [], seen = /* @__PURE__ */ new Set()) {
2777
+ if (value === null || typeof value !== "object") return acc;
2778
+ if (seen.has(value)) return acc;
2779
+ seen.add(value);
2780
+ if (Array.isArray(value)) {
2781
+ for (const item of value) extractMediaUrlsGeneric(item, acc, seen);
2782
+ return acc;
2783
+ }
2784
+ for (const [key, val] of Object.entries(value)) {
2785
+ if (key === "url" && isHttpUrl2(val)) {
2786
+ acc.push(val);
2787
+ } else if (typeof val === "object" && val !== null) {
2788
+ extractMediaUrlsGeneric(val, acc, seen);
2789
+ }
2790
+ }
2791
+ return acc;
2792
+ }
2793
+ function extractMediaUrls(result) {
2794
+ if (result === null || typeof result !== "object") return [];
2795
+ const known = extractKnownMediaUrls(result);
2796
+ if (known.length > 0) return known;
2797
+ return extractMediaUrlsGeneric(result);
2798
+ }
2799
+ function extFromUrl(url, fallback = ".bin") {
2800
+ try {
2801
+ const u = new URL(url);
2802
+ const e = path19.extname(u.pathname);
2803
+ return e.length > 0 ? e : fallback;
2804
+ } catch {
2805
+ return fallback;
2806
+ }
2807
+ }
2808
+ async function downloadResultMedia(urls, outDir, requestId, logger) {
2809
+ const saved = [];
2810
+ for (let i = 0; i < urls.length; i++) {
2811
+ const url = urls[i];
2812
+ if (!url) continue;
2813
+ const ext = extFromUrl(url);
2814
+ const filename = `fal-${requestId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
2815
+ const filepath = path19.join(outDir, filename);
2816
+ try {
2817
+ await downloadFile(url, filepath);
2818
+ saved.push(filepath);
2819
+ logger.success(`Saved ${filepath}`);
2820
+ } catch (e) {
2821
+ fs15.rmSync(filepath, { force: true });
2822
+ logger.warn(`Download failed for ${url}: ${e instanceof Error ? e.message : e}`);
2823
+ }
2824
+ }
2825
+ return saved;
2826
+ }
2827
+
2828
+ // src/providers/fal/commands/image.ts
2829
+ function registerFalImageCommand(parent) {
2830
+ parent.command("image <prompt>").description(
2831
+ `Generate images from a text prompt via fal.ai (default: ${FAL_DEFAULTS.imageModel})`
2832
+ ).option("-m, --model <id>", "fal model id", FAL_DEFAULTS.imageModel).option("--image-size <size>", "e.g. square_hd, portrait_16_9, landscape_16_9, or WxH").option("-n, --num-images <n>", "Number of images", "1").option("--seed <n>", "Seed").option("--negative-prompt <text>", "Negative prompt").option("--output <path>", "Copy first image to this path (single-image only)").addOption(imageFormatOption()).addOption(noWebpOption()).option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
2833
+ async (prompt, opts) => {
2834
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2835
+ const input = {
2836
+ prompt,
2837
+ num_images: Number.parseInt(opts.numImages, 10) || 1,
2838
+ ...opts.imageSize ? { image_size: opts.imageSize } : {},
2839
+ ...opts.seed ? { seed: Number.parseInt(opts.seed, 10) } : {},
2840
+ ...opts.negativePrompt ? { negative_prompt: opts.negativePrompt } : {}
2841
+ };
2842
+ logger.info(`Submitting to ${opts.model}...`);
2843
+ const { requestId, result } = await submitAndWait({
2844
+ model: opts.model,
2845
+ input,
2846
+ logger,
2847
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 48e4
2848
+ });
2849
+ const urls = extractMediaUrls(result);
2850
+ if (urls.length === 0) {
2851
+ logger.warn("No image URLs found in result.");
2852
+ console.log(JSON.stringify(result, null, 2));
2853
+ return;
2854
+ }
2855
+ if (opts.download === false) {
2856
+ for (const u of urls) console.log(u);
2857
+ return;
2858
+ }
2859
+ const outDir = getOutputDir();
2860
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
2861
+ if (saved.length === 0) {
2862
+ logger.error(`Found ${urls.length} image URL(s) but none downloaded successfully.`);
2863
+ process.exit(1);
2864
+ }
2865
+ if (opts.output && saved[0]) {
2866
+ const dest = path20.resolve(opts.output);
2867
+ fs16.mkdirSync(path20.dirname(dest), { recursive: true });
2868
+ fs16.copyFileSync(saved[0], dest);
2869
+ logger.success(`Copied to ${dest}`);
2870
+ }
2871
+ const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
2872
+ console.log(`
2873
+ Generated ${finalImages.length} image(s):`);
2874
+ for (const f of finalImages) console.log(` ${f}`);
2875
+ }
2876
+ );
2877
+ }
2878
+
2879
+ // src/providers/fal/commands/result.ts
2880
+ function registerFalResultCommand(parent) {
2881
+ parent.command("result <model> <requestId>").description("Fetch a completed fal.ai request's result").option("--download", "Download media URLs found in the result to the output dir").option("-v, --verbose", "Verbose logging").action(
2882
+ async (model, requestId, opts) => {
2883
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2884
+ const client = createFalClient();
2885
+ let result;
2886
+ try {
2887
+ result = await client.get(resultPath(model, requestId), logger);
2888
+ } catch (e) {
2889
+ const msg = e instanceof Error ? e.message : String(e);
2890
+ logger.error(`Failed to fetch result for fal request ${requestId}: ${msg}`);
2891
+ process.exit(1);
2892
+ }
2893
+ if (!opts.download) {
2894
+ console.log(JSON.stringify(result, null, 2));
2895
+ return;
2896
+ }
2897
+ const urls = extractMediaUrls(result);
2898
+ if (urls.length === 0) {
2899
+ logger.warn("No media URLs found in result.");
2900
+ console.log(JSON.stringify(result, null, 2));
2901
+ return;
2902
+ }
2903
+ const outDir = getOutputDir();
2904
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
2905
+ if (saved.length === 0) {
2906
+ logger.error(`Found ${urls.length} media URL(s) but none downloaded successfully.`);
2907
+ process.exit(1);
2908
+ }
2909
+ console.log(`
2910
+ Downloaded ${saved.length} file(s):`);
2911
+ for (const f of saved) console.log(` ${f}`);
2912
+ }
2913
+ );
2914
+ }
2915
+
2916
+ // src/providers/fal/commands/run.ts
2917
+ function registerFalRunCommand(parent) {
2918
+ parent.command("run <model>").description("Run any fal.ai model by id (e.g. fal-ai/flux/schnell)").requiredOption("--input <json>", "JSON input, or @path/to/file.json").option("--no-download", "Skip downloading media; print result JSON only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
2919
+ async (model, opts) => {
2920
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2921
+ const input = parseInputArg(opts.input);
2922
+ logger.info(`Submitting to ${model}...`);
2923
+ const { requestId, result } = await submitAndWait({
2924
+ model,
2925
+ input,
2926
+ logger,
2927
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 48e4
2928
+ });
2929
+ if (opts.download === false) {
2930
+ console.log(JSON.stringify(result, null, 2));
2931
+ return;
2932
+ }
2933
+ const urls = extractMediaUrls(result);
2934
+ if (urls.length === 0) {
2935
+ logger.warn("No media URLs found in result; printing raw JSON.");
2936
+ console.log(JSON.stringify(result, null, 2));
2937
+ return;
2938
+ }
2939
+ const outDir = getOutputDir();
2940
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
2941
+ if (saved.length === 0) {
2942
+ logger.error(`Found ${urls.length} media URL(s) but none downloaded successfully.`);
2943
+ process.exit(1);
2944
+ }
2945
+ console.log(`
2946
+ Downloaded ${saved.length} file(s):`);
2947
+ for (const f of saved) console.log(` ${f}`);
2948
+ }
2949
+ );
2950
+ }
2951
+
2952
+ // src/providers/fal/commands/status.ts
2953
+ function registerFalStatusCommand(parent) {
2954
+ parent.command("status <model> <requestId>").description("Get a fal.ai request's queue status").option("-v, --verbose", "Verbose logging").action(async (model, requestId, opts) => {
2955
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2956
+ const client = createFalClient();
2957
+ const res = await client.get(statusPath(model, requestId), logger);
2958
+ console.log(JSON.stringify(res, null, 2));
2959
+ });
2960
+ }
2961
+
2962
+ // src/providers/fal/commands/video.ts
2963
+ function registerFalVideoCommand(parent) {
2964
+ parent.command("video <prompt>").description(
2965
+ `Generate video via fal.ai (default text-to-video: ${FAL_DEFAULTS.videoModel}; default image-to-video: ${FAL_DEFAULTS.videoImageModel})`
2966
+ ).option(
2967
+ "-m, --model <id>",
2968
+ "fal model id (default depends on --image-url: text-to-video vs. image-to-video default)"
2969
+ ).option("--image-url <url>", "First-frame image URL \u2014 switches to image-to-video input").option("--duration <n>", "Duration in seconds (model-dependent)").option("--aspect-ratio <r>", "Aspect ratio hint (e.g. 16:9, 9:16)").option("--seed <n>", "Seed").option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "900000").option("-v, --verbose", "Verbose logging").action(
2970
+ async (prompt, opts) => {
2971
+ const logger = createLogger({ verbose: opts.verbose ?? false });
2972
+ const model = opts.model ?? (opts.imageUrl ? FAL_DEFAULTS.videoImageModel : FAL_DEFAULTS.videoModel);
2973
+ appIdFromModel(model);
2974
+ const input = {
2975
+ prompt,
2976
+ ...opts.imageUrl ? { image_url: opts.imageUrl } : {},
2977
+ ...opts.duration ? { duration: opts.duration } : {},
2978
+ ...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
2979
+ ...opts.seed ? { seed: Number.parseInt(opts.seed, 10) } : {}
2980
+ };
2981
+ logger.info(`Submitting to ${model}...`);
2982
+ const { requestId, result } = await submitAndWait({
2983
+ model,
2984
+ input,
2985
+ logger,
2986
+ waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 9e5
2987
+ });
2988
+ const urls = extractMediaUrls(result);
2989
+ if (urls.length === 0) {
2990
+ logger.warn("No video URLs found in result.");
2991
+ console.log(JSON.stringify(result, null, 2));
2992
+ return;
2993
+ }
2994
+ if (opts.download === false) {
2995
+ for (const u of urls) console.log(u);
2996
+ return;
2997
+ }
2998
+ const outDir = getOutputDir();
2999
+ const saved = await downloadResultMedia(urls, outDir, requestId, logger);
3000
+ if (saved.length === 0) {
3001
+ logger.error(`Found ${urls.length} video URL(s) but none downloaded successfully.`);
3002
+ process.exit(1);
3003
+ }
3004
+ console.log(`
3005
+ Generated ${saved.length} file(s):`);
3006
+ for (const f of saved) console.log(` ${f}`);
3007
+ }
3008
+ );
3009
+ }
3010
+
3011
+ // src/providers/fal/commands/index.ts
3012
+ function registerFalCommands(program2) {
3013
+ const fal = program2.command("fal").description("fal.ai: generic model runner, image, video, and queue status/result");
3014
+ registerFalRunCommand(fal);
3015
+ registerFalImageCommand(fal);
3016
+ registerFalVideoCommand(fal);
3017
+ registerFalStatusCommand(fal);
3018
+ registerFalResultCommand(fal);
3019
+ }
3020
+
3021
+ // src/providers/gemini/commands/analyze.ts
3022
+ init_errors();
3023
+ import fs19 from "fs";
2385
3024
 
2386
3025
  // src/providers/gemini/client.ts
2387
- import fs13 from "fs";
2388
- import path19 from "path";
3026
+ import fs17 from "fs";
3027
+ import path22 from "path";
2389
3028
  init_errors();
2390
3029
  init_http_client();
2391
3030
 
2392
3031
  // src/providers/gemini/task-resolver.ts
2393
- import path18 from "path";
3032
+ import path21 from "path";
2394
3033
  var MIME_MAP = {
2395
3034
  // Audio
2396
3035
  ".mp3": "audio/mp3",
@@ -2431,7 +3070,7 @@ var MIME_MAP = {
2431
3070
  ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
2432
3071
  };
2433
3072
  function getMimeType(filePath) {
2434
- const ext = path18.extname(filePath).toLowerCase();
3073
+ const ext = path21.extname(filePath).toLowerCase();
2435
3074
  return MIME_MAP[ext] ?? "application/octet-stream";
2436
3075
  }
2437
3076
  function requiresProcessingWait(mimeType) {
@@ -2457,8 +3096,8 @@ async function uploadFile(filePath, opts = {}) {
2457
3096
  const apiKey = requireApiKey();
2458
3097
  const { timeoutMs = 3e5, logger } = opts;
2459
3098
  const mimeType = getMimeType(filePath);
2460
- const fileBytes = fs13.readFileSync(filePath);
2461
- const displayName = path19.basename(filePath);
3099
+ const fileBytes = fs17.readFileSync(filePath);
3100
+ const displayName = path22.basename(filePath);
2462
3101
  const boundary = `multix_${Date.now()}`;
2463
3102
  const metaJson = JSON.stringify({ file: { displayName, mimeType } });
2464
3103
  const body = Buffer.concat([
@@ -2500,7 +3139,7 @@ Content-Type: ${mimeType}\r
2500
3139
  if (requiresProcessingWait(mimeType)) {
2501
3140
  const deadline = Date.now() + timeoutMs;
2502
3141
  while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
2503
- await sleep(2e3);
3142
+ await sleep2(2e3);
2504
3143
  fileRef = await httpJson({
2505
3144
  url: `${BASE}/v1beta/${fileRef.name}`,
2506
3145
  headers: authHeaders2(apiKey)
@@ -2527,6 +3166,33 @@ async function generateContent(req) {
2527
3166
  }
2528
3167
  });
2529
3168
  }
3169
+ async function createInteraction(body) {
3170
+ const apiKey = requireApiKey();
3171
+ return httpJson({
3172
+ url: `${BASE}/v1beta/interactions`,
3173
+ method: "POST",
3174
+ headers: authHeaders2(apiKey),
3175
+ body
3176
+ });
3177
+ }
3178
+ function extractInteractionAudio(resp) {
3179
+ let found = null;
3180
+ const walk = (node) => {
3181
+ if (Array.isArray(node)) {
3182
+ for (const n of node) walk(n);
3183
+ return;
3184
+ }
3185
+ if (!node || typeof node !== "object") return;
3186
+ const o = node;
3187
+ if (o.type === "audio" && typeof o.data === "string") {
3188
+ const mime = o.mime_type ?? o.mimeType ?? "audio/wav";
3189
+ found = { mimeType: mime, data: o.data };
3190
+ }
3191
+ for (const v of Object.values(o)) walk(v);
3192
+ };
3193
+ walk(resp);
3194
+ return found;
3195
+ }
2530
3196
  function extractText(resp) {
2531
3197
  const parts = resp.candidates?.[0]?.content?.parts ?? [];
2532
3198
  return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
@@ -2547,15 +3213,15 @@ function extractImages(resp) {
2547
3213
  ).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
2548
3214
  }
2549
3215
  function inlinePart(filePath) {
2550
- const data = fs13.readFileSync(filePath).toString("base64");
3216
+ const data = fs17.readFileSync(filePath).toString("base64");
2551
3217
  const mimeType = getMimeType(filePath);
2552
3218
  return { inlineData: { mimeType, data } };
2553
3219
  }
2554
3220
  function shouldUseFileApi(filePath) {
2555
- const stat = fs13.statSync(filePath);
3221
+ const stat = fs17.statSync(filePath);
2556
3222
  return stat.size > 20 * 1024 * 1024;
2557
3223
  }
2558
- function sleep(ms) {
3224
+ function sleep2(ms) {
2559
3225
  return new Promise((resolve) => setTimeout(resolve, ms));
2560
3226
  }
2561
3227
 
@@ -2565,7 +3231,7 @@ var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
2565
3231
  var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
2566
3232
  var TEXT_MODEL_DEFAULT = "gemini-3.6-flash";
2567
3233
  var ANALYSIS_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
2568
- var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
3234
+ var TTS_MODEL_DEFAULT = "gemini-3.8-flash-lite-tts";
2569
3235
  var DOC_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
2570
3236
  var IMAGEN_MODELS = /* @__PURE__ */ new Set([
2571
3237
  "imagen-4.0-generate-001",
@@ -2606,13 +3272,13 @@ function getDefaultModel(task) {
2606
3272
  }
2607
3273
 
2608
3274
  // src/providers/gemini/result-formatter.ts
2609
- import fs14 from "fs";
2610
- import path20 from "path";
3275
+ import fs18 from "fs";
3276
+ import path23 from "path";
2611
3277
  function formatResults(results, format, outputPath) {
2612
3278
  const content = renderFormat(results, format);
2613
3279
  if (outputPath) {
2614
- fs14.mkdirSync(path20.dirname(path20.resolve(outputPath)), { recursive: true });
2615
- fs14.writeFileSync(outputPath, content, "utf8");
3280
+ fs18.mkdirSync(path23.dirname(path23.resolve(outputPath)), { recursive: true });
3281
+ fs18.writeFileSync(outputPath, content, "utf8");
2616
3282
  } else {
2617
3283
  process.stdout.write(content);
2618
3284
  }
@@ -2679,7 +3345,7 @@ function registerAnalyzeCommand(parent) {
2679
3345
  const logger = createLogger({ verbose: opts.verbose ?? false });
2680
3346
  const model = opts.model ?? getDefaultModel("analyze");
2681
3347
  for (const f of opts.files) {
2682
- if (!fs15.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3348
+ if (!fs19.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2683
3349
  }
2684
3350
  const results = [];
2685
3351
  for (let i = 0; i < opts.files.length; i++) {
@@ -2725,14 +3391,14 @@ function registerAnalyzeCommand(parent) {
2725
3391
 
2726
3392
  // src/providers/gemini/commands/extract.ts
2727
3393
  init_errors();
2728
- import fs16 from "fs";
3394
+ import fs20 from "fs";
2729
3395
  function registerExtractCommand(parent) {
2730
3396
  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(
2731
3397
  async (opts) => {
2732
3398
  const logger = createLogger({ verbose: opts.verbose ?? false });
2733
3399
  const model = opts.model ?? getDefaultModel("extract");
2734
3400
  for (const f of opts.files) {
2735
- if (!fs16.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3401
+ if (!fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2736
3402
  }
2737
3403
  const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
2738
3404
  const results = [];
@@ -2774,8 +3440,8 @@ function registerExtractCommand(parent) {
2774
3440
  init_errors();
2775
3441
 
2776
3442
  // src/providers/gemini/generators/speech.ts
2777
- import fs17 from "fs";
2778
- import path21 from "path";
3443
+ import fs21 from "fs";
3444
+ import path24 from "path";
2779
3445
 
2780
3446
  // src/providers/gemini/voices.ts
2781
3447
  var GEMINI_TTS_VOICES = [
@@ -2811,10 +3477,16 @@ var GEMINI_TTS_VOICES = [
2811
3477
  "Sulafat"
2812
3478
  ];
2813
3479
  var GEMINI_TTS_MODELS = /* @__PURE__ */ new Set([
3480
+ "gemini-3.8-flash-lite-tts",
3481
+ "gemini-3.8-flash-tts",
2814
3482
  "gemini-3.1-flash-tts-preview",
2815
3483
  "gemini-2.5-flash-preview-tts",
2816
3484
  "gemini-2.5-pro-preview-tts"
2817
3485
  ]);
3486
+ var GEMINI_INTERACTIONS_TTS_MODELS = /* @__PURE__ */ new Set([
3487
+ "gemini-3.8-flash-lite-tts",
3488
+ "gemini-3.8-flash-tts"
3489
+ ]);
2818
3490
  var TTS_VOICE_DEFAULT = "Kore";
2819
3491
  var TTS_PCM_SAMPLE_RATE = 24e3;
2820
3492
  var TTS_PCM_CHANNELS = 1;
@@ -2823,10 +3495,19 @@ var TTS_OUTPUT_FORMATS = ["wav", "pcm"];
2823
3495
  function isValidGeminiVoice(name) {
2824
3496
  return GEMINI_TTS_VOICES.includes(name);
2825
3497
  }
3498
+ function isCustomGeminiVoiceId(name) {
3499
+ return /^(voice|voicekey)_[A-Za-z0-9_-]+$/.test(name);
3500
+ }
3501
+ function isVoiceAllowedForModel(voice, model) {
3502
+ return isValidGeminiVoice(voice) || GEMINI_INTERACTIONS_TTS_MODELS.has(model) && isCustomGeminiVoiceId(voice);
3503
+ }
2826
3504
 
2827
3505
  // src/providers/gemini/generators/speech.ts
2828
3506
  async function generateGeminiSpeech(opts) {
2829
3507
  const { text, model, voice, speakers, outputFormat, output, logger } = opts;
3508
+ if (GEMINI_INTERACTIONS_TTS_MODELS.has(model)) {
3509
+ return generateInteractionsSpeech(opts);
3510
+ }
2830
3511
  const speechConfig = speakers && speakers.length > 0 ? {
2831
3512
  multiSpeakerVoiceConfig: {
2832
3513
  speakerVoiceConfigs: speakers.map((s) => ({
@@ -2857,16 +3538,81 @@ async function generateGeminiSpeech(opts) {
2857
3538
  const pcmBytes = Buffer.from(audio.data, "base64");
2858
3539
  const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
2859
3540
  const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
3541
+ const dest = saveSpeech(outBytes, outputFormat, output, logger);
3542
+ return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
3543
+ }
3544
+ function buildInteractionsSpeechBody(opts) {
3545
+ const { text, model, voice, speakers, style } = opts;
3546
+ const speechConfig = speakers && speakers.length > 0 ? { mode: "conversational", speakers: speakers.map((s) => ({ ...s })) } : [{ voice }];
3547
+ const content = { type: "text", text };
3548
+ if (style) content.annotations = [{ type: "speech_metadata", style }];
3549
+ return {
3550
+ model,
3551
+ input: [{ type: "user_input", content: [content] }],
3552
+ response_format: { type: "audio", mime_type: "audio/wav" },
3553
+ generation_config: { speech_config: speechConfig }
3554
+ };
3555
+ }
3556
+ async function generateInteractionsSpeech(opts) {
3557
+ const { model, speakers, outputFormat, output, logger, text, style } = opts;
3558
+ logger?.debug(
3559
+ `Gemini TTS (interactions) model=${model} mode=${speakers?.length ? "multi" : "single"}`
3560
+ );
3561
+ if (!style && /^\s*(say|read|speak)\b[^:\n]{0,80}:/i.test(text)) {
3562
+ logger?.warn(
3563
+ `${model} reads text verbatim; move delivery directions like "Say cheerfully:" into --style`
3564
+ );
3565
+ }
3566
+ let resp;
3567
+ try {
3568
+ resp = await createInteraction(buildInteractionsSpeechBody(opts));
3569
+ } catch (e) {
3570
+ return { status: "error", error: e instanceof Error ? e.message : String(e) };
3571
+ }
3572
+ const interaction = resp;
3573
+ if (typeof interaction?.status === "string" && interaction.status !== "completed") {
3574
+ const detail = typeof interaction.error?.message === "string" ? `: ${interaction.error.message}` : "";
3575
+ return { status: "error", error: `Interaction ${interaction.status}${detail}` };
3576
+ }
3577
+ const audio = extractInteractionAudio(resp);
3578
+ if (!audio) return { status: "error", error: "No audio in response" };
3579
+ const bytes = Buffer.from(audio.data, "base64");
3580
+ const isWav = bytes.subarray(0, 4).toString("ascii") === "RIFF";
3581
+ let outBytes;
3582
+ if (outputFormat === "wav") {
3583
+ outBytes = isWav ? bytes : wrapPcmInWav(
3584
+ bytes,
3585
+ parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE,
3586
+ TTS_PCM_CHANNELS,
3587
+ TTS_PCM_BITS_PER_SAMPLE
3588
+ );
3589
+ } else {
3590
+ outBytes = isWav ? extractWavData(bytes) : bytes;
3591
+ }
3592
+ const dest = saveSpeech(outBytes, outputFormat, output, logger);
3593
+ return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
3594
+ }
3595
+ function extractWavData(wav) {
3596
+ let offset = 12;
3597
+ while (offset + 8 <= wav.length) {
3598
+ const id = wav.subarray(offset, offset + 4).toString("ascii");
3599
+ const size = wav.readUInt32LE(offset + 4);
3600
+ if (id === "data") return wav.subarray(offset + 8, Math.min(offset + 8 + size, wav.length));
3601
+ offset += 8 + size + size % 2;
3602
+ }
3603
+ throw new Error("WAV response has no data chunk");
3604
+ }
3605
+ function saveSpeech(outBytes, outputFormat, output, logger) {
2860
3606
  const outDir = getOutputDir();
2861
- const dest = path21.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
2862
- fs17.writeFileSync(dest, outBytes);
3607
+ const dest = path24.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
3608
+ fs21.writeFileSync(dest, outBytes);
2863
3609
  logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
2864
3610
  if (output) {
2865
- fs17.mkdirSync(path21.dirname(path21.resolve(output)), { recursive: true });
2866
- fs17.copyFileSync(dest, output);
3611
+ fs21.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
3612
+ fs21.copyFileSync(dest, output);
2867
3613
  logger?.success(`Copied to: ${output}`);
2868
3614
  }
2869
- return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
3615
+ return dest;
2870
3616
  }
2871
3617
  function parseSampleRate(mimeType) {
2872
3618
  const m = mimeType.match(/rate=(\d+)/i);
@@ -2895,7 +3641,14 @@ function wrapPcmInWav(pcm, sampleRate, channels, bitsPerSample) {
2895
3641
 
2896
3642
  // src/providers/gemini/commands/generate-speech.ts
2897
3643
  function registerGeminiGenerateSpeechCommand(parent) {
2898
- 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(
3644
+ 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(
3645
+ "--voice <name>",
3646
+ "Prebuilt voice for single-speaker mode (3.8 models also accept voice_/voicekey_ ids)",
3647
+ TTS_VOICE_DEFAULT
3648
+ ).option(
3649
+ "--style <text>",
3650
+ 'Delivery direction for Gemini 3.8 models, e.g. "cheerful and friendly"'
3651
+ ).option(
2899
3652
  "--speaker <name:voice>",
2900
3653
  "Speaker mapping (repeatable, max 2 \u2014 switches to multi-speaker mode)",
2901
3654
  collectSpeakers,
@@ -2918,13 +3671,18 @@ function registerGeminiGenerateSpeechCommand(parent) {
2918
3671
  if (speakers.length > 2) {
2919
3672
  throw new ValidationError("Gemini TTS supports max 2 speakers in multi-speaker mode");
2920
3673
  }
2921
- if (speakers.length === 0 && !isValidGeminiVoice(opts.voice)) {
3674
+ if (opts.style && !GEMINI_INTERACTIONS_TTS_MODELS.has(model)) {
3675
+ throw new ValidationError(
3676
+ `--style requires a Gemini 3.8 TTS model (${[...GEMINI_INTERACTIONS_TTS_MODELS].join(", ")}); put directions in the text for ${model}`
3677
+ );
3678
+ }
3679
+ if (speakers.length === 0 && !isVoiceAllowedForModel(opts.voice, model)) {
2922
3680
  throw new ValidationError(
2923
3681
  `Invalid --voice "${opts.voice}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
2924
3682
  );
2925
3683
  }
2926
3684
  for (const sv of speakers) {
2927
- if (!isValidGeminiVoice(sv.voice)) {
3685
+ if (!isVoiceAllowedForModel(sv.voice, model)) {
2928
3686
  throw new ValidationError(
2929
3687
  `Invalid voice "${sv.voice}" for speaker "${sv.speaker}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
2930
3688
  );
@@ -2935,6 +3693,7 @@ function registerGeminiGenerateSpeechCommand(parent) {
2935
3693
  model,
2936
3694
  voice: speakers.length === 0 ? opts.voice : void 0,
2937
3695
  speakers: speakers.length > 0 ? speakers : void 0,
3696
+ style: opts.style,
2938
3697
  outputFormat: opts.outputFormat,
2939
3698
  output: opts.output,
2940
3699
  logger
@@ -2962,8 +3721,8 @@ function collectSpeakers(value, prev) {
2962
3721
  }
2963
3722
 
2964
3723
  // src/providers/gemini/commands/generate-video.ts
2965
- import fs18 from "fs";
2966
- import path22 from "path";
3724
+ import fs22 from "fs";
3725
+ import path25 from "path";
2967
3726
  init_errors();
2968
3727
  init_http_client();
2969
3728
  var BASE2 = "https://generativelanguage.googleapis.com";
@@ -2997,10 +3756,10 @@ function registerGenerateVideoCommand(parent) {
2997
3756
  const contentParts = [{ text: opts.prompt }];
2998
3757
  if (opts.referenceImages?.length) {
2999
3758
  for (const imgPath of opts.referenceImages.slice(0, 2)) {
3000
- if (!fs18.existsSync(imgPath))
3759
+ if (!fs22.existsSync(imgPath))
3001
3760
  throw new ValidationError(`Reference image not found: ${imgPath}`);
3002
- const data = fs18.readFileSync(imgPath).toString("base64");
3003
- const ext = path22.extname(imgPath).toLowerCase();
3761
+ const data = fs22.readFileSync(imgPath).toString("base64");
3762
+ const ext = path25.extname(imgPath).toLowerCase();
3004
3763
  const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
3005
3764
  contentParts.push({ inlineData: { mimeType, data } });
3006
3765
  }
@@ -3027,7 +3786,7 @@ function registerGenerateVideoCommand(parent) {
3027
3786
  let done = false;
3028
3787
  let finalOp = operation;
3029
3788
  while (!done && Date.now() < deadline) {
3030
- await sleep2(1e4);
3789
+ await sleep3(1e4);
3031
3790
  finalOp = await httpJson({
3032
3791
  url: `${BASE2}/v1beta/${operationName}`,
3033
3792
  headers
@@ -3049,13 +3808,13 @@ function registerGenerateVideoCommand(parent) {
3049
3808
  );
3050
3809
  }
3051
3810
  const outDir = getOutputDir();
3052
- const dest = path22.join(outDir, `veo_generated_${Date.now()}.mp4`);
3811
+ const dest = path25.join(outDir, `veo_generated_${Date.now()}.mp4`);
3053
3812
  const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
3054
3813
  await downloadFile2(videoUri, dest);
3055
3814
  logger.success(`Saved video: ${dest}`);
3056
3815
  if (opts.output) {
3057
- fs18.mkdirSync(path22.dirname(path22.resolve(opts.output)), { recursive: true });
3058
- fs18.copyFileSync(dest, opts.output);
3816
+ fs22.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3817
+ fs22.copyFileSync(dest, opts.output);
3059
3818
  logger.success(`Copied to: ${opts.output}`);
3060
3819
  }
3061
3820
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -3068,16 +3827,16 @@ Generated video: ${dest}`);
3068
3827
  }
3069
3828
  );
3070
3829
  }
3071
- function sleep2(ms) {
3830
+ function sleep3(ms) {
3072
3831
  return new Promise((r) => setTimeout(r, ms));
3073
3832
  }
3074
3833
 
3075
3834
  // src/providers/gemini/commands/generate.ts
3076
3835
  init_errors();
3077
- import fs19 from "fs";
3078
- import path23 from "path";
3836
+ import fs23 from "fs";
3837
+ import path26 from "path";
3079
3838
  function registerGenerateCommand(parent) {
3080
- 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(
3839
+ 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").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
3081
3840
  async (opts) => {
3082
3841
  const logger = createLogger({ verbose: opts.verbose ?? false });
3083
3842
  const model = opts.model ?? getDefaultModel("generate");
@@ -3117,8 +3876,8 @@ function registerGenerateCommand(parent) {
3117
3876
  const outDir = getOutputDir();
3118
3877
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
3119
3878
  const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
3120
- const dest = path23.join(outDir, fname);
3121
- fs19.writeFileSync(dest, Buffer.from(img.data, "base64"));
3879
+ const dest = path26.join(outDir, fname);
3880
+ fs23.writeFileSync(dest, Buffer.from(img.data, "base64"));
3122
3881
  savedFiles.push(dest);
3123
3882
  logger.success(`Saved: ${dest}`);
3124
3883
  }
@@ -3133,20 +3892,21 @@ function registerGenerateCommand(parent) {
3133
3892
  process.exit(1);
3134
3893
  }
3135
3894
  if (opts.output && savedFiles[0]) {
3136
- fs19.mkdirSync(path23.dirname(path23.resolve(opts.output)), { recursive: true });
3137
- fs19.copyFileSync(savedFiles[0], opts.output);
3895
+ fs23.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
3896
+ fs23.copyFileSync(savedFiles[0], opts.output);
3138
3897
  logger.success(`Copied to: ${opts.output}`);
3139
3898
  }
3899
+ const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
3140
3900
  console.log(`
3141
- Generated ${savedFiles.length} image(s):`);
3142
- for (const f of savedFiles) console.log(` ${f}`);
3901
+ Generated ${finalImages.length} image(s):`);
3902
+ for (const f of finalImages) console.log(` ${f}`);
3143
3903
  }
3144
3904
  );
3145
3905
  }
3146
3906
 
3147
3907
  // src/providers/gemini/commands/image-to-image.ts
3148
- import fs20 from "fs";
3149
- import path24 from "path";
3908
+ import fs24 from "fs";
3909
+ import path27 from "path";
3150
3910
  var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
3151
3911
  var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
3152
3912
  function registerGeminiImageToImageCommand(parent) {
@@ -3157,7 +3917,7 @@ function registerGeminiImageToImageCommand(parent) {
3157
3917
  "Reference image (URL or local path); repeatable for multi-ref",
3158
3918
  collect4,
3159
3919
  []
3160
- ).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(
3920
+ ).option("-m, --model <id>", `Gemini image model (default ${IMAGE_MODEL_FALLBACK})`).option("--output <path>", "Save first generated image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
3161
3921
  async (opts) => {
3162
3922
  if (!opts.ref || opts.ref.length === 0) {
3163
3923
  throw new Error("At least one --ref is required for image-to-image.");
@@ -3207,19 +3967,20 @@ function registerGeminiImageToImageCommand(parent) {
3207
3967
  const img = images[i];
3208
3968
  if (!img) continue;
3209
3969
  const ext = img.mimeType.includes("png") ? "png" : "jpg";
3210
- const dest = path24.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
3211
- fs20.writeFileSync(dest, Buffer.from(img.data, "base64"));
3970
+ const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
3971
+ fs24.writeFileSync(dest, Buffer.from(img.data, "base64"));
3212
3972
  saved.push(dest);
3213
3973
  logger.success(`Saved: ${dest}`);
3214
3974
  }
3215
3975
  if (opts.output && saved[0]) {
3216
- fs20.mkdirSync(path24.dirname(path24.resolve(opts.output)), { recursive: true });
3217
- fs20.copyFileSync(saved[0], opts.output);
3976
+ fs24.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
3977
+ fs24.copyFileSync(saved[0], opts.output);
3218
3978
  logger.success(`Copied to: ${opts.output}`);
3219
3979
  }
3980
+ const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
3220
3981
  console.log(`
3221
- Generated ${saved.length} image(s):`);
3222
- for (const f of saved) console.log(` ${f}`);
3982
+ Generated ${finalImages.length} image(s):`);
3983
+ for (const f of finalImages) console.log(` ${f}`);
3223
3984
  }
3224
3985
  );
3225
3986
  }
@@ -3241,7 +4002,7 @@ function sniffImageMime(buf, url) {
3241
4002
  if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
3242
4003
  return "image/webp";
3243
4004
  }
3244
- const ext = path24.extname(new URL(url).pathname).toLowerCase();
4005
+ const ext = path27.extname(new URL(url).pathname).toLowerCase();
3245
4006
  if (ext === ".png") return "image/png";
3246
4007
  if (ext === ".webp") return "image/webp";
3247
4008
  if (ext === ".gif") return "image/gif";
@@ -3249,21 +4010,21 @@ function sniffImageMime(buf, url) {
3249
4010
  }
3250
4011
 
3251
4012
  // src/providers/gemini/commands/image-to-video.ts
3252
- import fs21 from "fs";
3253
- import path25 from "path";
4013
+ import fs25 from "fs";
4014
+ import path28 from "path";
3254
4015
  init_errors();
3255
4016
  init_http_client();
3256
4017
  var BASE3 = "https://generativelanguage.googleapis.com";
3257
4018
  function imagePart(imgPath) {
3258
- if (!fs21.existsSync(imgPath)) {
4019
+ if (!fs25.existsSync(imgPath)) {
3259
4020
  throw new ValidationError(`Image not found: ${imgPath}`);
3260
4021
  }
3261
- const data = fs21.readFileSync(imgPath).toString("base64");
3262
- const ext = path25.extname(imgPath).toLowerCase();
4022
+ const data = fs25.readFileSync(imgPath).toString("base64");
4023
+ const ext = path28.extname(imgPath).toLowerCase();
3263
4024
  const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
3264
4025
  return { inlineData: { mimeType, data } };
3265
4026
  }
3266
- function sleep3(ms) {
4027
+ function sleep4(ms) {
3267
4028
  return new Promise((r) => setTimeout(r, ms));
3268
4029
  }
3269
4030
  function registerGeminiImageToVideoCommand(parent) {
@@ -3315,7 +4076,7 @@ function registerGeminiImageToVideoCommand(parent) {
3315
4076
  const deadline = Date.now() + 10 * 60 * 1e3;
3316
4077
  let finalOp = operation;
3317
4078
  while (!finalOp.done && Date.now() < deadline) {
3318
- await sleep3(1e4);
4079
+ await sleep4(1e4);
3319
4080
  finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
3320
4081
  if (!finalOp.done) {
3321
4082
  logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
@@ -3332,12 +4093,12 @@ function registerGeminiImageToVideoCommand(parent) {
3332
4093
  "gemini"
3333
4094
  );
3334
4095
  }
3335
- const dest = path25.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
4096
+ const dest = path28.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
3336
4097
  await downloadFile(videoUri, dest);
3337
4098
  logger.success(`Saved video: ${dest}`);
3338
4099
  if (opts.output) {
3339
- fs21.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3340
- fs21.copyFileSync(dest, opts.output);
4100
+ fs25.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
4101
+ fs25.copyFileSync(dest, opts.output);
3341
4102
  logger.success(`Copied to: ${opts.output}`);
3342
4103
  }
3343
4104
  await maybeDownloadThumb(finalOp.response, dest, {
@@ -3353,14 +4114,14 @@ Generated video: ${dest}`);
3353
4114
 
3354
4115
  // src/providers/gemini/commands/transcribe.ts
3355
4116
  init_errors();
3356
- import fs22 from "fs";
4117
+ import fs26 from "fs";
3357
4118
  function registerTranscribeCommand2(parent) {
3358
4119
  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(
3359
4120
  async (opts) => {
3360
4121
  const logger = createLogger({ verbose: opts.verbose ?? false });
3361
4122
  const model = opts.model ?? getDefaultModel("transcribe");
3362
4123
  for (const f of opts.files) {
3363
- if (!fs22.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
4124
+ if (!fs26.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3364
4125
  }
3365
4126
  const results = [];
3366
4127
  for (let i = 0; i < opts.files.length; i++) {
@@ -3418,8 +4179,8 @@ function registerGeminiCommands(program2) {
3418
4179
 
3419
4180
  // src/providers/leonardo/commands/generate.ts
3420
4181
  init_http_client();
3421
- import fs23 from "fs";
3422
- import path26 from "path";
4182
+ import fs27 from "fs";
4183
+ import path29 from "path";
3423
4184
 
3424
4185
  // src/providers/leonardo/client.ts
3425
4186
  init_errors();
@@ -3493,11 +4254,11 @@ var LeonardoClient = class {
3493
4254
  }
3494
4255
  apiKey;
3495
4256
  baseUrl;
3496
- async request(path46, opts = {}) {
4257
+ async request(path49, opts = {}) {
3497
4258
  const base = this.baseUrl.replace(/\/$/, "");
3498
- const v2Override = path46.startsWith("v2:");
4259
+ const v2Override = path49.startsWith("v2:");
3499
4260
  const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
3500
- const finalPath = v2Override ? path46.slice(3) : path46;
4261
+ const finalPath = v2Override ? path49.slice(3) : path49;
3501
4262
  const url = new URL(finalBase + finalPath);
3502
4263
  if (opts.query) {
3503
4264
  for (const [k, v] of Object.entries(opts.query)) {
@@ -3516,14 +4277,14 @@ var LeonardoClient = class {
3516
4277
  timeoutMs: opts.timeoutMs ?? 12e4
3517
4278
  });
3518
4279
  }
3519
- get(path46, query, logger) {
3520
- return this.request(path46, { method: "GET", query, logger });
4280
+ get(path49, query, logger) {
4281
+ return this.request(path49, { method: "GET", query, logger });
3521
4282
  }
3522
- post(path46, body, logger) {
3523
- return this.request(path46, { method: "POST", body, logger });
4283
+ post(path49, body, logger) {
4284
+ return this.request(path49, { method: "POST", body, logger });
3524
4285
  }
3525
- delete(path46, logger) {
3526
- return this.request(path46, { method: "DELETE", logger });
4286
+ delete(path49, logger) {
4287
+ return this.request(path49, { method: "DELETE", logger });
3527
4288
  }
3528
4289
  };
3529
4290
  function createLeonardoClient() {
@@ -3531,10 +4292,10 @@ function createLeonardoClient() {
3531
4292
  }
3532
4293
 
3533
4294
  // src/providers/leonardo/commands/generate.ts
3534
- function extFromUrl(url, fallback = ".png") {
4295
+ function extFromUrl2(url, fallback = ".png") {
3535
4296
  try {
3536
4297
  const u = new URL(url);
3537
- const e = path26.extname(u.pathname);
4298
+ const e = path29.extname(u.pathname);
3538
4299
  return e.length > 0 ? e : fallback;
3539
4300
  } catch {
3540
4301
  return fallback;
@@ -3545,7 +4306,7 @@ function autoFilename(generationId, index, ext) {
3545
4306
  return `leonardo-${generationId.slice(0, 8)}-${String(index).padStart(2, "0")}${e}`;
3546
4307
  }
3547
4308
  function registerLeonardoGenerateCommand(parent) {
3548
- 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(
4309
+ 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)").addOption(imageFormatOption()).addOption(noWebpOption()).option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
3549
4310
  async (prompt, opts) => {
3550
4311
  const logger = createLogger({ verbose: opts.verbose ?? false });
3551
4312
  const client = createLeonardoClient();
@@ -3617,9 +4378,9 @@ function registerLeonardoGenerateCommand(parent) {
3617
4378
  for (let i = 0; i < images.length; i++) {
3618
4379
  const img = images[i];
3619
4380
  if (!img) continue;
3620
- const ext = extFromUrl(img.url);
4381
+ const ext = extFromUrl2(img.url);
3621
4382
  const filename = autoFilename(generationId, i + 1, ext);
3622
- const filepath = path26.join(outDir, filename);
4383
+ const filepath = path29.join(outDir, filename);
3623
4384
  try {
3624
4385
  await downloadFile(img.url, filepath);
3625
4386
  saved.push(filepath);
@@ -3629,31 +4390,32 @@ function registerLeonardoGenerateCommand(parent) {
3629
4390
  }
3630
4391
  }
3631
4392
  if (opts.output && saved[0]) {
3632
- const dest = path26.resolve(opts.output);
3633
- fs23.mkdirSync(path26.dirname(dest), { recursive: true });
3634
- fs23.copyFileSync(saved[0], dest);
4393
+ const dest = path29.resolve(opts.output);
4394
+ fs27.mkdirSync(path29.dirname(dest), { recursive: true });
4395
+ fs27.copyFileSync(saved[0], dest);
3635
4396
  logger.success(`Copied to ${dest}`);
3636
4397
  }
4398
+ const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
3637
4399
  console.log(`
3638
- Generated ${saved.length} image(s):`);
3639
- for (const f of saved) console.log(` ${f}`);
4400
+ Generated ${finalImages.length} image(s):`);
4401
+ for (const f of finalImages) console.log(` ${f}`);
3640
4402
  }
3641
4403
  );
3642
4404
  }
3643
4405
 
3644
4406
  // src/providers/leonardo/commands/image-to-image.ts
3645
4407
  init_http_client();
3646
- import fs24 from "fs";
3647
- import path27 from "path";
3648
- function extFromUrl2(url, fallback = ".png") {
4408
+ import fs28 from "fs";
4409
+ import path30 from "path";
4410
+ function extFromUrl3(url, fallback = ".png") {
3649
4411
  try {
3650
- return path27.extname(new URL(url).pathname) || fallback;
4412
+ return path30.extname(new URL(url).pathname) || fallback;
3651
4413
  } catch {
3652
4414
  return fallback;
3653
4415
  }
3654
4416
  }
3655
4417
  function registerLeonardoImageToImageCommand(parent) {
3656
- 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(
4418
+ 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)").addOption(imageFormatOption()).addOption(noWebpOption()).option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
3657
4419
  async (opts) => {
3658
4420
  const logger = createLogger({ verbose: opts.verbose ?? false });
3659
4421
  const imageType = opts.imageType.toUpperCase();
@@ -3707,9 +4469,9 @@ function registerLeonardoImageToImageCommand(parent) {
3707
4469
  for (let i = 0; i < images.length; i++) {
3708
4470
  const img = images[i];
3709
4471
  if (!img) continue;
3710
- const ext = extFromUrl2(img.url);
4472
+ const ext = extFromUrl3(img.url);
3711
4473
  const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
3712
- const filepath = path27.join(outDir, filename);
4474
+ const filepath = path30.join(outDir, filename);
3713
4475
  try {
3714
4476
  await downloadFile(img.url, filepath);
3715
4477
  saved.push(filepath);
@@ -3719,21 +4481,22 @@ function registerLeonardoImageToImageCommand(parent) {
3719
4481
  }
3720
4482
  }
3721
4483
  if (opts.output && saved[0]) {
3722
- const dest = path27.resolve(opts.output);
3723
- fs24.mkdirSync(path27.dirname(dest), { recursive: true });
3724
- fs24.copyFileSync(saved[0], dest);
4484
+ const dest = path30.resolve(opts.output);
4485
+ fs28.mkdirSync(path30.dirname(dest), { recursive: true });
4486
+ fs28.copyFileSync(saved[0], dest);
3725
4487
  logger.success(`Copied to ${dest}`);
3726
4488
  }
4489
+ const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
3727
4490
  console.log(`
3728
- Generated ${saved.length} image(s):`);
3729
- for (const f of saved) console.log(` ${f}`);
4491
+ Generated ${finalImages.length} image(s):`);
4492
+ for (const f of finalImages) console.log(` ${f}`);
3730
4493
  }
3731
4494
  );
3732
4495
  }
3733
4496
 
3734
4497
  // src/providers/leonardo/commands/video.ts
3735
4498
  init_http_client();
3736
- import path28 from "path";
4499
+ import path31 from "path";
3737
4500
  function v2DimsFor(resolution) {
3738
4501
  if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
3739
4502
  if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
@@ -3844,7 +4607,7 @@ async function pollAndDownload(client, jobId, opts) {
3844
4607
  logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
3845
4608
  process.exit(1);
3846
4609
  }
3847
- const outPath = opts.output ?? path28.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
4610
+ const outPath = opts.output ?? path31.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
3848
4611
  await downloadFile(videoUrl2, outPath);
3849
4612
  logger.success(`Saved ${outPath}`);
3850
4613
  await maybeDownloadThumb(gen, outPath, {
@@ -3951,7 +4714,7 @@ function registerLeonardoModelsCommand(parent) {
3951
4714
 
3952
4715
  // src/providers/leonardo/commands/status.ts
3953
4716
  init_http_client();
3954
- import path29 from "path";
4717
+ import path32 from "path";
3955
4718
  var VIDEO_KEYS = ["motionMP4URL", "url"];
3956
4719
  var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
3957
4720
  var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
@@ -4016,7 +4779,7 @@ function registerLeonardoStatusCommand(parent) {
4016
4779
  const outDir = getOutputDir();
4017
4780
  let idx = 0;
4018
4781
  for (const url of videos) {
4019
- const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
4782
+ const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path32.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
4020
4783
  await downloadFile(url, dest);
4021
4784
  logger.success(`Saved ${dest}`);
4022
4785
  await maybeDownloadThumb(gen, dest, {
@@ -4029,7 +4792,7 @@ function registerLeonardoStatusCommand(parent) {
4029
4792
  for (const url of images) {
4030
4793
  const m = url.match(IMAGE_EXT_RE2);
4031
4794
  const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
4032
- const dest = path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
4795
+ const dest = path32.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
4033
4796
  await downloadFile(url, dest);
4034
4797
  logger.success(`Saved ${dest}`);
4035
4798
  idx++;
@@ -4163,7 +4926,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
4163
4926
  if (status === "Failed" || status === "Error") {
4164
4927
  throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
4165
4928
  }
4166
- await sleep4(intervalMs);
4929
+ await sleep5(intervalMs);
4167
4930
  elapsed += intervalMs;
4168
4931
  }
4169
4932
  throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
@@ -4182,14 +4945,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
4182
4945
  await downloadFile(downloadUrl, dest);
4183
4946
  return dest;
4184
4947
  }
4185
- function sleep4(ms) {
4948
+ function sleep5(ms) {
4186
4949
  return new Promise((r) => setTimeout(r, ms));
4187
4950
  }
4188
4951
 
4189
4952
  // src/providers/minimax/generators/music.ts
4190
4953
  init_http_client();
4191
- import fs25 from "fs";
4192
- import path30 from "path";
4954
+ import fs29 from "fs";
4955
+ import path33 from "path";
4193
4956
  async function generateMinimaxMusic(opts) {
4194
4957
  const {
4195
4958
  apiKey,
@@ -4234,14 +4997,14 @@ async function generateMinimaxMusic(opts) {
4234
4997
  }
4235
4998
  const durationMs = resp.extra_info?.music_duration ?? 0;
4236
4999
  const outDir = getOutputDir();
4237
- const dest = path30.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
5000
+ const dest = path33.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
4238
5001
  try {
4239
5002
  if (audioData.startsWith("http")) {
4240
5003
  const bytes = await fetchBytes(audioData);
4241
- fs25.writeFileSync(dest, bytes);
5004
+ fs29.writeFileSync(dest, bytes);
4242
5005
  } else {
4243
5006
  const audioBytes = Buffer.from(audioData, "hex");
4244
- fs25.writeFileSync(dest, audioBytes);
5007
+ fs29.writeFileSync(dest, audioBytes);
4245
5008
  }
4246
5009
  } catch (e) {
4247
5010
  return {
@@ -4252,8 +5015,8 @@ async function generateMinimaxMusic(opts) {
4252
5015
  const durSec = durationMs / 1e3;
4253
5016
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
4254
5017
  if (output) {
4255
- fs25.mkdirSync(path30.dirname(path30.resolve(output)), { recursive: true });
4256
- fs25.copyFileSync(dest, output);
5018
+ fs29.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
5019
+ fs29.copyFileSync(dest, output);
4257
5020
  }
4258
5021
  return { status: "success", generatedAudio: dest, durationMs, model };
4259
5022
  }
@@ -4327,8 +5090,8 @@ Generated music: ${result.generatedAudio}`);
4327
5090
  init_errors();
4328
5091
 
4329
5092
  // src/providers/minimax/generators/speech.ts
4330
- import fs26 from "fs";
4331
- import path31 from "path";
5093
+ import fs30 from "fs";
5094
+ import path34 from "path";
4332
5095
  async function generateMinimaxSpeech(opts) {
4333
5096
  const {
4334
5097
  apiKey,
@@ -4375,12 +5138,12 @@ async function generateMinimaxSpeech(opts) {
4375
5138
  const audioBytes = Buffer.from(audioHex, "hex");
4376
5139
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
4377
5140
  const outDir = getOutputDir();
4378
- const dest = path31.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
4379
- fs26.writeFileSync(dest, audioBytes);
5141
+ const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
5142
+ fs30.writeFileSync(dest, audioBytes);
4380
5143
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
4381
5144
  if (output) {
4382
- fs26.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
4383
- fs26.copyFileSync(dest, output);
5145
+ fs30.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
5146
+ fs30.copyFileSync(dest, output);
4384
5147
  }
4385
5148
  return { status: "success", generatedAudio: dest, model };
4386
5149
  }
@@ -4427,8 +5190,8 @@ Generated audio: ${result.generatedAudio}`);
4427
5190
  init_errors();
4428
5191
 
4429
5192
  // src/providers/minimax/generators/video.ts
4430
- import fs27 from "fs";
4431
- import path32 from "path";
5193
+ import fs31 from "fs";
5194
+ import path35 from "path";
4432
5195
  async function generateMinimaxVideo(opts) {
4433
5196
  const {
4434
5197
  apiKey,
@@ -4474,18 +5237,18 @@ async function generateMinimaxVideo(opts) {
4474
5237
  return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
4475
5238
  }
4476
5239
  const outDir = getOutputDir();
4477
- const dest = path32.join(outDir, `minimax_video_${Date.now()}.mp4`);
5240
+ const dest = path35.join(outDir, `minimax_video_${Date.now()}.mp4`);
4478
5241
  try {
4479
5242
  await downloadMinimaxFile(fileId, apiKey, dest, logger);
4480
5243
  } catch (e) {
4481
5244
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
4482
5245
  }
4483
5246
  const elapsed = (Date.now() - start) / 1e3;
4484
- const stat = fs27.statSync(dest);
5247
+ const stat = fs31.statSync(dest);
4485
5248
  const fileSizeMb = stat.size / (1024 * 1024);
4486
5249
  if (output) {
4487
- fs27.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
4488
- fs27.copyFileSync(dest, output);
5250
+ fs31.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
5251
+ fs31.copyFileSync(dest, output);
4489
5252
  }
4490
5253
  await maybeDownloadThumb(pollResult, dest, {
4491
5254
  skip: thumb === false,
@@ -4548,8 +5311,8 @@ Generated video: ${result.generatedVideo}`);
4548
5311
  // src/providers/minimax/generators/image.ts
4549
5312
  init_errors();
4550
5313
  init_http_client();
4551
- import fs28 from "fs";
4552
- import path33 from "path";
5314
+ import fs32 from "fs";
5315
+ import path36 from "path";
4553
5316
  async function generateMinimaxImage(opts) {
4554
5317
  const {
4555
5318
  apiKey,
@@ -4587,8 +5350,8 @@ async function generateMinimaxImage(opts) {
4587
5350
  try {
4588
5351
  const bytes = await fetchBytes(url);
4589
5352
  const fname = `minimax_image_${Date.now()}_${i}.png`;
4590
- const dest = path33.join(outDir, fname);
4591
- fs28.writeFileSync(dest, bytes);
5353
+ const dest = path36.join(outDir, fname);
5354
+ fs32.writeFileSync(dest, bytes);
4592
5355
  savedFiles.push(dest);
4593
5356
  logger?.success(`Saved: ${dest}`);
4594
5357
  } catch (e) {
@@ -4599,15 +5362,15 @@ async function generateMinimaxImage(opts) {
4599
5362
  }
4600
5363
  }
4601
5364
  if (output && savedFiles[0]) {
4602
- fs28.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
4603
- fs28.copyFileSync(savedFiles[0], output);
5365
+ fs32.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
5366
+ fs32.copyFileSync(savedFiles[0], output);
4604
5367
  }
4605
5368
  return { status: "success", generatedImages: savedFiles, model };
4606
5369
  }
4607
5370
 
4608
5371
  // src/providers/minimax/commands/generate.ts
4609
5372
  function registerMinimaxGenerateCommand(parent) {
4610
- parent.command("generate").description("Generate images with MiniMax").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", `Model (${[...MINIMAX_IMAGE_MODELS].join("|")})`).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(
5373
+ parent.command("generate").description("Generate images with MiniMax").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", `Model (${[...MINIMAX_IMAGE_MODELS].join("|")})`).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").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
4611
5374
  async (opts) => {
4612
5375
  const logger = createLogger({ verbose: opts.verbose ?? false });
4613
5376
  const apiKey = requireMinimaxKey();
@@ -4626,9 +5389,13 @@ function registerMinimaxGenerateCommand(parent) {
4626
5389
  logger.error(result.error ?? "Unknown error");
4627
5390
  process.exit(1);
4628
5391
  }
5392
+ const finalImages = finalizeGeneratedImages(result.generatedImages ?? [], {
5393
+ ...opts,
5394
+ logger
5395
+ });
4629
5396
  console.log(`
4630
- Generated ${result.generatedImages?.length ?? 0} image(s):`);
4631
- for (const f of result.generatedImages ?? []) console.log(` ${f}`);
5397
+ Generated ${finalImages.length} image(s):`);
5398
+ for (const f of finalImages) console.log(` ${f}`);
4632
5399
  }
4633
5400
  );
4634
5401
  }
@@ -4636,15 +5403,15 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
4636
5403
  // src/providers/minimax/commands/image-to-image.ts
4637
5404
  init_errors();
4638
5405
  init_http_client();
4639
- import fs29 from "fs";
4640
- import path34 from "path";
5406
+ import fs33 from "fs";
5407
+ import path37 from "path";
4641
5408
  function registerMinimaxImageToImageCommand(parent) {
4642
5409
  parent.command("image-to-image").alias("i2i").description(
4643
5410
  "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."
4644
5411
  ).requiredOption("--prompt <text>", "Prompt describing the desired NEW scene/composition").requiredOption(
4645
5412
  "--ref <path>",
4646
5413
  "Subject reference image (URL or local path) \u2014 character/face reference only, single image"
4647
- ).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(
5414
+ ).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").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
4648
5415
  async (opts) => {
4649
5416
  const logger = createLogger({ verbose: opts.verbose ?? false });
4650
5417
  const apiKey = requireMinimaxKey();
@@ -4692,8 +5459,8 @@ function registerMinimaxImageToImageCommand(parent) {
4692
5459
  if (!url) continue;
4693
5460
  try {
4694
5461
  const bytes = await fetchBytes(url);
4695
- const dest = path34.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
4696
- fs29.writeFileSync(dest, bytes);
5462
+ const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
5463
+ fs33.writeFileSync(dest, bytes);
4697
5464
  saved.push(dest);
4698
5465
  logger.success(`Saved: ${dest}`);
4699
5466
  } catch (e) {
@@ -4704,13 +5471,14 @@ function registerMinimaxImageToImageCommand(parent) {
4704
5471
  }
4705
5472
  }
4706
5473
  if (opts.output && saved[0]) {
4707
- fs29.mkdirSync(path34.dirname(path34.resolve(opts.output)), { recursive: true });
4708
- fs29.copyFileSync(saved[0], opts.output);
5474
+ fs33.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
5475
+ fs33.copyFileSync(saved[0], opts.output);
4709
5476
  logger.success(`Copied to: ${opts.output}`);
4710
5477
  }
5478
+ const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
4711
5479
  console.log(`
4712
- Generated ${saved.length} image(s):`);
4713
- for (const f of saved) console.log(` ${f}`);
5480
+ Generated ${finalImages.length} image(s):`);
5481
+ for (const f of finalImages) console.log(` ${f}`);
4714
5482
  }
4715
5483
  );
4716
5484
  }
@@ -4839,12 +5607,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
4839
5607
 
4840
5608
  // src/providers/openai/openai-audio.ts
4841
5609
  init_http_client();
4842
- import fs31 from "fs";
4843
- import path36 from "path";
5610
+ import fs35 from "fs";
5611
+ import path39 from "path";
4844
5612
 
4845
5613
  // src/providers/openai/client.ts
4846
- import fs30 from "fs";
4847
- import path35 from "path";
5614
+ import fs34 from "fs";
5615
+ import path38 from "path";
4848
5616
  init_errors();
4849
5617
  init_http_client();
4850
5618
  var DEFAULT_TIMEOUT_MS3 = 24e4;
@@ -4911,10 +5679,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
4911
5679
  return Buffer.from(await res.arrayBuffer());
4912
5680
  }
4913
5681
  function fileFromPath(filePath) {
4914
- const abs = path35.resolve(filePath);
4915
- if (!fs30.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
4916
- const bytes = fs30.readFileSync(abs);
4917
- return new File([new Uint8Array(bytes)], path35.basename(abs));
5682
+ const abs = path38.resolve(filePath);
5683
+ if (!fs34.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
5684
+ const bytes = fs34.readFileSync(abs);
5685
+ return new File([new Uint8Array(bytes)], path38.basename(abs));
4918
5686
  }
4919
5687
 
4920
5688
  // src/providers/openai/openai-audio.ts
@@ -4953,7 +5721,7 @@ async function resolveAudioReferenceDataUrl(ref) {
4953
5721
  }
4954
5722
  function audioMimeFromReference(ref) {
4955
5723
  const pathname = new URL(ref).pathname;
4956
- const ext = path36.extname(pathname).toLowerCase();
5724
+ const ext = path39.extname(pathname).toLowerCase();
4957
5725
  switch (ext) {
4958
5726
  case ".mp3":
4959
5727
  return "audio/mpeg";
@@ -4972,12 +5740,12 @@ function audioMimeFromReference(ref) {
4972
5740
  async function generateOpenAISpeech(opts) {
4973
5741
  const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
4974
5742
  const ext = extFromAudioFormat(opts.outputFormat);
4975
- const dest = path36.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
4976
- fs31.writeFileSync(dest, bytes);
5743
+ const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
5744
+ fs35.writeFileSync(dest, bytes);
4977
5745
  opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
4978
5746
  if (opts.output) {
4979
- fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
4980
- fs31.copyFileSync(dest, opts.output);
5747
+ fs35.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
5748
+ fs35.copyFileSync(dest, opts.output);
4981
5749
  }
4982
5750
  return dest;
4983
5751
  }
@@ -4999,9 +5767,9 @@ async function transcribeOpenAIAudio(opts) {
4999
5767
  );
5000
5768
  const text = typeof data.text === "string" ? data.text : void 0;
5001
5769
  const ext = opts.format === "text" ? "txt" : "json";
5002
- const dest = opts.output ? path36.resolve(opts.output) : path36.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
5003
- fs31.mkdirSync(path36.dirname(dest), { recursive: true });
5004
- fs31.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
5770
+ const dest = opts.output ? path39.resolve(opts.output) : path39.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
5771
+ fs35.mkdirSync(path39.dirname(dest), { recursive: true });
5772
+ fs35.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
5005
5773
  opts.logger?.success(`Saved transcript: ${dest}`);
5006
5774
  return { path: dest, text };
5007
5775
  }
@@ -5028,9 +5796,9 @@ Generated audio: ${dest}`);
5028
5796
  }
5029
5797
 
5030
5798
  // src/providers/openai/codex-image-driver.ts
5031
- import fs32 from "fs";
5799
+ import fs36 from "fs";
5032
5800
  import os2 from "os";
5033
- import path37 from "path";
5801
+ import path40 from "path";
5034
5802
  import { execa } from "execa";
5035
5803
  init_errors();
5036
5804
  var defaultRunner = async (cmd, args, opts) => {
@@ -5079,12 +5847,12 @@ function sanitizeCodexEnv() {
5079
5847
  async function runCodexImageGeneration(opts) {
5080
5848
  const runner = opts.runner ?? defaultRunner;
5081
5849
  const ext = extFromImageFormat(opts.outputFormat);
5082
- const dest = opts.output ? path37.resolve(opts.output) : path37.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
5083
- fs32.mkdirSync(path37.dirname(dest), { recursive: true });
5084
- const tmp = fs32.mkdtempSync(path37.join(os2.tmpdir(), "multix-codex-image-"));
5085
- const schema = path37.join(tmp, "schema.json");
5086
- const result = path37.join(tmp, "result.json");
5087
- fs32.writeFileSync(
5850
+ const dest = opts.output ? path40.resolve(opts.output) : path40.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
5851
+ fs36.mkdirSync(path40.dirname(dest), { recursive: true });
5852
+ const tmp = fs36.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
5853
+ const schema = path40.join(tmp, "schema.json");
5854
+ const result = path40.join(tmp, "result.json");
5855
+ fs36.writeFileSync(
5088
5856
  schema,
5089
5857
  JSON.stringify({
5090
5858
  type: "object",
@@ -5114,7 +5882,7 @@ async function runCodexImageGeneration(opts) {
5114
5882
  if (res.exitCode !== 0) {
5115
5883
  throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
5116
5884
  }
5117
- if (!fs32.existsSync(dest) || fs32.statSync(dest).size === 0) {
5885
+ if (!fs36.existsSync(dest) || fs36.statSync(dest).size === 0) {
5118
5886
  throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
5119
5887
  }
5120
5888
  return { path: dest };
@@ -5132,8 +5900,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
5132
5900
  // src/providers/openai/openai-image.ts
5133
5901
  init_errors();
5134
5902
  init_http_client();
5135
- import fs33 from "fs";
5136
- import path38 from "path";
5903
+ import fs37 from "fs";
5904
+ import path41 from "path";
5137
5905
  function buildImageGenerationBody(opts) {
5138
5906
  return {
5139
5907
  model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
@@ -5166,14 +5934,14 @@ function saveOpenAIImages(opts) {
5166
5934
  for (let i = 0; i < items.length; i++) {
5167
5935
  const b64 = items[i]?.b64_json;
5168
5936
  if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
5169
- const dest = path38.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
5170
- fs33.writeFileSync(dest, Buffer.from(b64, "base64"));
5937
+ const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
5938
+ fs37.writeFileSync(dest, Buffer.from(b64, "base64"));
5171
5939
  saved.push(dest);
5172
5940
  opts.logger?.success(`Saved: ${dest}`);
5173
5941
  }
5174
5942
  if (opts.output && saved[0]) {
5175
- fs33.mkdirSync(path38.dirname(path38.resolve(opts.output)), { recursive: true });
5176
- fs33.copyFileSync(saved[0], opts.output);
5943
+ fs37.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
5944
+ fs37.copyFileSync(saved[0], opts.output);
5177
5945
  opts.logger?.success(`Copied to: ${opts.output}`);
5178
5946
  }
5179
5947
  return saved;
@@ -5182,7 +5950,7 @@ async function resolveImageRefBytes(ref) {
5182
5950
  if (isHttpUrl(ref)) {
5183
5951
  const bytes2 = await fetchBytes(ref);
5184
5952
  const pathname = new URL(ref).pathname;
5185
- const filename = path38.basename(pathname) || "reference.png";
5953
+ const filename = path41.basename(pathname) || "reference.png";
5186
5954
  return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
5187
5955
  }
5188
5956
  const file = fileFromPath(ref);
@@ -5227,7 +5995,7 @@ function sniffImageMime2(buf, name) {
5227
5995
  if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
5228
5996
  if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
5229
5997
  return "image/webp";
5230
- const ext = path38.extname(name).toLowerCase();
5998
+ const ext = path41.extname(name).toLowerCase();
5231
5999
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
5232
6000
  if (ext === ".webp") return "image/webp";
5233
6001
  return "image/png";
@@ -5239,9 +6007,12 @@ function registerOpenAIGenerateCommand(parent) {
5239
6007
  "-m, --model <id>",
5240
6008
  "Image model",
5241
6009
  resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
5242
- ).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option("--format <fmt>", "png|jpeg|webp", "png").option("-n, --num-images <n>", "Number of images for API driver", "1").option("--output <path>", "Save first image to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
6010
+ ).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option(
6011
+ "--format <fmt>",
6012
+ "Provider output format png|jpeg|webp (explicit value keeps it; default png then WebP)"
6013
+ ).option("-n, --num-images <n>", "Number of images for API driver", "1").option("--output <path>", "Save first image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(async (opts) => {
5243
6014
  const logger = createLogger({ verbose: opts.verbose ?? false });
5244
- const outputFormat = parseImageFormat(opts.format);
6015
+ const outputFormat = parseImageFormat(opts.format ?? "png");
5245
6016
  const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
5246
6017
  const saved = driver === "codex" ? [
5247
6018
  (await runCodexImageGeneration({
@@ -5259,9 +6030,14 @@ function registerOpenAIGenerateCommand(parent) {
5259
6030
  output: opts.output,
5260
6031
  logger
5261
6032
  });
6033
+ const finalImages = finalizeGeneratedImages(saved, {
6034
+ ...opts,
6035
+ logger,
6036
+ explicitProviderFormat: opts.format !== void 0
6037
+ });
5262
6038
  console.log(`
5263
- Generated ${saved.length} image(s):`);
5264
- for (const file of saved) console.log(` ${file}`);
6039
+ Generated ${finalImages.length} image(s):`);
6040
+ for (const f of finalImages) console.log(` ${f}`);
5265
6041
  });
5266
6042
  }
5267
6043
 
@@ -5277,12 +6053,15 @@ function registerOpenAIImageToImageCommand(parent) {
5277
6053
  "-m, --model <id>",
5278
6054
  "Image model",
5279
6055
  resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
5280
- ).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option("--format <fmt>", "png|jpeg|webp", "png").option("--output <path>", "Save first image to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
6056
+ ).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option(
6057
+ "--format <fmt>",
6058
+ "Provider output format png|jpeg|webp (explicit value keeps it; default png then WebP)"
6059
+ ).option("--output <path>", "Save first image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(async (opts) => {
5281
6060
  if (!opts.ref || opts.ref.length === 0) {
5282
6061
  throw new ValidationError("At least one --ref is required for image-to-image.");
5283
6062
  }
5284
6063
  const logger = createLogger({ verbose: opts.verbose ?? false });
5285
- const outputFormat = parseImageFormat(opts.format);
6064
+ const outputFormat = parseImageFormat(opts.format ?? "png");
5286
6065
  const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
5287
6066
  const saved = driver === "codex" ? [
5288
6067
  (await runCodexImageGeneration({
@@ -5301,9 +6080,14 @@ function registerOpenAIImageToImageCommand(parent) {
5301
6080
  output: opts.output,
5302
6081
  logger
5303
6082
  });
6083
+ const finalImages = finalizeGeneratedImages(saved, {
6084
+ ...opts,
6085
+ logger,
6086
+ explicitProviderFormat: opts.format !== void 0
6087
+ });
5304
6088
  console.log(`
5305
- Generated ${saved.length} image(s):`);
5306
- for (const file of saved) console.log(` ${file}`);
6089
+ Generated ${finalImages.length} image(s):`);
6090
+ for (const f of finalImages) console.log(` ${f}`);
5307
6091
  });
5308
6092
  }
5309
6093
  function collect5(value, prev) {
@@ -5355,8 +6139,8 @@ function registerOpenAICommands(program2) {
5355
6139
  }
5356
6140
 
5357
6141
  // src/providers/openrouter/client.ts
5358
- import fs34 from "fs";
5359
- import path39 from "path";
6142
+ import fs38 from "fs";
6143
+ import path42 from "path";
5360
6144
  init_errors();
5361
6145
  init_http_client();
5362
6146
 
@@ -5521,8 +6305,8 @@ async function generateOpenRouterImage(opts) {
5521
6305
  if (!url) continue;
5522
6306
  try {
5523
6307
  const bytes = await fetchBytes(url);
5524
- const dest = path39.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
5525
- fs34.writeFileSync(dest, bytes);
6308
+ const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
6309
+ fs38.writeFileSync(dest, bytes);
5526
6310
  savedFiles.push(dest);
5527
6311
  logger?.success(`Saved: ${dest}`);
5528
6312
  } catch (e) {
@@ -5533,8 +6317,8 @@ async function generateOpenRouterImage(opts) {
5533
6317
  }
5534
6318
  }
5535
6319
  if (output && savedFiles[0]) {
5536
- fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
5537
- fs34.writeFileSync(output, fs34.readFileSync(savedFiles[0]));
6320
+ fs38.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
6321
+ fs38.writeFileSync(output, fs38.readFileSync(savedFiles[0]));
5538
6322
  }
5539
6323
  return { status: "success", generatedImages: savedFiles, model: usedModel };
5540
6324
  }
@@ -5543,7 +6327,7 @@ async function generateOpenRouterImage(opts) {
5543
6327
  function registerOpenRouterGenerateCommand(parent) {
5544
6328
  parent.command("generate").description(
5545
6329
  "Generate images via OpenRouter (text-to-image); models: Gemini, GPT-Image, Flux, Recraft, Seedream"
5546
- ).requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "OpenRouter model id (e.g. google/gemini-3.1-flash-image-preview)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9)", "1:1").option("--image-size <size>", "Image size hint (e.g. 1K, 2K)").option("--num-images <n>", "Number of images to generate", "1").option("--output <path>", "Copy first image to this path").option("-v, --verbose", "Verbose logging").action(
6330
+ ).requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "OpenRouter model id (e.g. google/gemini-3.1-flash-image-preview)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9)", "1:1").option("--image-size <size>", "Image size hint (e.g. 1K, 2K)").option("--num-images <n>", "Number of images to generate", "1").option("--output <path>", "Copy first image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
5547
6331
  async (opts) => {
5548
6332
  const logger = createLogger({ verbose: opts.verbose ?? false });
5549
6333
  const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_OPENROUTER_MODEL;
@@ -5562,17 +6346,21 @@ function registerOpenRouterGenerateCommand(parent) {
5562
6346
  logger.error(result.error ?? "Unknown error");
5563
6347
  process.exit(1);
5564
6348
  }
6349
+ const finalImages = finalizeGeneratedImages(result.generatedImages ?? [], {
6350
+ ...opts,
6351
+ logger
6352
+ });
5565
6353
  console.log(`
5566
- Generated ${result.generatedImages?.length ?? 0} image(s):`);
5567
- for (const f of result.generatedImages ?? []) console.log(` ${f}`);
6354
+ Generated ${finalImages.length} image(s):`);
6355
+ for (const f of finalImages) console.log(` ${f}`);
5568
6356
  if (result.model) console.log(`Model used: ${result.model}`);
5569
6357
  }
5570
6358
  );
5571
6359
  }
5572
6360
 
5573
6361
  // src/providers/openrouter/commands/image-to-image.ts
5574
- import fs35 from "fs";
5575
- import path40 from "path";
6362
+ import fs39 from "fs";
6363
+ import path43 from "path";
5576
6364
  init_errors();
5577
6365
  init_http_client();
5578
6366
  var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
@@ -5586,7 +6374,7 @@ function registerOpenRouterImageToImageCommand(parent) {
5586
6374
  "Reference image (URL or local path); repeatable (most models accept 1)",
5587
6375
  collect7,
5588
6376
  []
5589
- ).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--strength <n>", "Recraft init-image strength 0..1 (Recraft models only)").option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
6377
+ ).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--strength <n>", "Recraft init-image strength 0..1 (Recraft models only)").option("--output <path>", "Save first generated image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
5590
6378
  async (opts) => {
5591
6379
  if (!opts.ref || opts.ref.length === 0) {
5592
6380
  throw new ValidationError("At least one --ref is required for image-to-image.");
@@ -5618,9 +6406,10 @@ function registerOpenRouterImageToImageCommand(parent) {
5618
6406
  output: opts.output,
5619
6407
  logger
5620
6408
  });
6409
+ const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
5621
6410
  console.log(`
5622
- Generated ${saved.length} image(s):`);
5623
- for (const f of saved) console.log(` ${f}`);
6411
+ Generated ${finalImages.length} image(s):`);
6412
+ for (const f of finalImages) console.log(` ${f}`);
5624
6413
  console.log(`Model used: ${model}`);
5625
6414
  }
5626
6415
  );
@@ -5664,14 +6453,14 @@ async function runImageToImage(opts) {
5664
6453
  const url = parsed.urls[i];
5665
6454
  if (!url) continue;
5666
6455
  const bytes = await fetchBytes(url);
5667
- const dest = path40.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
5668
- fs35.writeFileSync(dest, bytes);
6456
+ const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
6457
+ fs39.writeFileSync(dest, bytes);
5669
6458
  saved.push(dest);
5670
6459
  opts.logger.success(`Saved: ${dest}`);
5671
6460
  }
5672
6461
  if (opts.output && saved[0]) {
5673
- fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
5674
- fs35.copyFileSync(saved[0], opts.output);
6462
+ fs39.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
6463
+ fs39.copyFileSync(saved[0], opts.output);
5675
6464
  opts.logger.success(`Copied to: ${opts.output}`);
5676
6465
  }
5677
6466
  return saved;
@@ -5717,13 +6506,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
5717
6506
  }
5718
6507
 
5719
6508
  // src/providers/openrouter/commands/video-status.ts
5720
- import fs36 from "fs";
5721
- import path41 from "path";
6509
+ import fs40 from "fs";
6510
+ import path44 from "path";
5722
6511
  async function downloadOpenRouterVideo(opts) {
5723
6512
  const { jobId, logger, source } = opts;
5724
6513
  const apiKey = requireOpenRouterKey();
5725
6514
  const url = videoContentUrl(jobId);
5726
- const dest = path41.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
6515
+ const dest = path44.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
5727
6516
  const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
5728
6517
  if (!r.ok) {
5729
6518
  logger.error(`Download HTTP ${r.status}`);
@@ -5733,14 +6522,14 @@ async function downloadOpenRouterVideo(opts) {
5733
6522
  logger.error("Empty response body");
5734
6523
  process.exit(1);
5735
6524
  }
5736
- fs36.mkdirSync(path41.dirname(dest), { recursive: true });
5737
- const ws = fs36.createWriteStream(dest);
6525
+ fs40.mkdirSync(path44.dirname(dest), { recursive: true });
6526
+ const ws = fs40.createWriteStream(dest);
5738
6527
  const { pipeline: pipeline2 } = await import("stream/promises");
5739
6528
  await pipeline2(r.body, ws);
5740
6529
  logger.success(`Saved ${dest}`);
5741
6530
  if (opts.output) {
5742
- fs36.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
5743
- fs36.copyFileSync(dest, opts.output);
6531
+ fs40.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
6532
+ fs40.copyFileSync(dest, opts.output);
5744
6533
  logger.success(`Copied to ${opts.output}`);
5745
6534
  }
5746
6535
  await maybeDownloadThumb(source, dest, {
@@ -5958,6 +6747,7 @@ To configure multix, set at least one provider API key:
5958
6747
  MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
5959
6748
  Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
5960
6749
  BytePlus : https://console.byteplus.com/auth/api-keys
6750
+ fal.ai : https://fal.ai/dashboard/keys
5961
6751
 
5962
6752
  Option A \u2014 user global config (recommended):
5963
6753
  echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
@@ -6020,6 +6810,12 @@ var PROVIDERS = [
6020
6810
  envPrimary: "ELEVENLABS_API_KEY",
6021
6811
  optional: true,
6022
6812
  link: "https://elevenlabs.io/app/settings/api-keys"
6813
+ },
6814
+ {
6815
+ name: "fal.ai",
6816
+ envPrimary: "FAL_KEY",
6817
+ optional: true,
6818
+ link: "https://fal.ai/dashboard/keys"
6023
6819
  }
6024
6820
  ];
6025
6821
  function resolveProviderKey(p) {
@@ -6151,8 +6947,8 @@ function registerCheckCommand(program2) {
6151
6947
 
6152
6948
  // src/commands/doc/convert.ts
6153
6949
  init_errors();
6154
- import fs37 from "fs";
6155
- import path42 from "path";
6950
+ import fs41 from "fs";
6951
+ import path45 from "path";
6156
6952
 
6157
6953
  // src/commands/doc/default-prompt.ts
6158
6954
  var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
@@ -6173,18 +6969,18 @@ async function convertDocuments(opts) {
6173
6969
  const model = opts.model ?? DOC_MODEL_DEFAULT;
6174
6970
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
6175
6971
  for (const f of inputs) {
6176
- if (!fs37.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
6972
+ if (!fs41.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
6177
6973
  }
6178
6974
  let outputPath;
6179
6975
  if (output) {
6180
- outputPath = path42.resolve(output);
6976
+ outputPath = path45.resolve(output);
6181
6977
  } else if (autoName && inputs.length === 1 && inputs[0]) {
6182
- const base = path42.basename(inputs[0], path42.extname(inputs[0]));
6183
- outputPath = path42.join(getOutputDir(), `${base}-extraction.md`);
6978
+ const base = path45.basename(inputs[0], path45.extname(inputs[0]));
6979
+ outputPath = path45.join(getOutputDir(), `${base}-extraction.md`);
6184
6980
  } else {
6185
- outputPath = path42.join(getOutputDir(), "document-extraction.md");
6981
+ outputPath = path45.join(getOutputDir(), "document-extraction.md");
6186
6982
  }
6187
- fs37.mkdirSync(path42.dirname(outputPath), { recursive: true });
6983
+ fs41.mkdirSync(path45.dirname(outputPath), { recursive: true });
6188
6984
  const results = [];
6189
6985
  for (let i = 0; i < inputs.length; i++) {
6190
6986
  const filePath = inputs[i];
@@ -6216,7 +7012,7 @@ async function convertDocuments(opts) {
6216
7012
  if (attempt < 2) {
6217
7013
  const wait = 2 ** attempt * 1e3;
6218
7014
  logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
6219
- await sleep5(wait);
7015
+ await sleep6(wait);
6220
7016
  }
6221
7017
  }
6222
7018
  }
@@ -6235,7 +7031,7 @@ async function convertDocuments(opts) {
6235
7031
  `);
6236
7032
  for (const r of results) {
6237
7033
  lines.push(`
6238
- ## ${path42.basename(r.file)}
7034
+ ## ${path45.basename(r.file)}
6239
7035
  `);
6240
7036
  if (r.status === "success" && r.markdown) {
6241
7037
  lines.push(`${r.markdown}
@@ -6248,7 +7044,7 @@ async function convertDocuments(opts) {
6248
7044
  }
6249
7045
  lines.push("\n---\n");
6250
7046
  }
6251
- fs37.writeFileSync(outputPath, lines.join("\n"), "utf8");
7047
+ fs41.writeFileSync(outputPath, lines.join("\n"), "utf8");
6252
7048
  const succeeded = results.filter((r) => r.status === "success").length;
6253
7049
  console.log(`
6254
7050
  ${"=".repeat(50)}`);
@@ -6264,7 +7060,7 @@ ${"=".repeat(50)}`);
6264
7060
  const failed = results.filter((r) => r.status === "error").length;
6265
7061
  if (failed > 0) process.exit(1);
6266
7062
  }
6267
- function sleep5(ms) {
7063
+ function sleep6(ms) {
6268
7064
  return new Promise((r) => setTimeout(r, ms));
6269
7065
  }
6270
7066
 
@@ -6296,22 +7092,22 @@ function registerDocCommands(program2) {
6296
7092
 
6297
7093
  // src/commands/media/index.ts
6298
7094
  init_errors();
6299
- import fs40 from "fs";
6300
- import path45 from "path";
7095
+ import fs44 from "fs";
7096
+ import path48 from "path";
6301
7097
 
6302
7098
  // src/commands/media/batch.ts
6303
- import fs38 from "fs";
6304
- import path43 from "path";
7099
+ import fs42 from "fs";
7100
+ import path46 from "path";
6305
7101
 
6306
7102
  // src/commands/media/detect-type.ts
6307
7103
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
6308
7104
  var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg", ".aiff"]);
6309
- var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".gif", ".bmp"]);
7105
+ var IMAGE_EXTS2 = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".gif", ".bmp"]);
6310
7106
  function extToKind(ext) {
6311
7107
  const lower = ext.toLowerCase();
6312
7108
  if (VIDEO_EXTS.has(lower)) return "video";
6313
7109
  if (AUDIO_EXTS.has(lower)) return "audio";
6314
- if (IMAGE_EXTS.has(lower)) return "image";
7110
+ if (IMAGE_EXTS2.has(lower)) return "image";
6315
7111
  return void 0;
6316
7112
  }
6317
7113
 
@@ -6486,7 +7282,7 @@ async function optimizeVideo(opts) {
6486
7282
  }
6487
7283
 
6488
7284
  // src/commands/media/batch.ts
6489
- var ALL_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS, ...AUDIO_EXTS, ...IMAGE_EXTS]);
7285
+ var ALL_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS, ...AUDIO_EXTS, ...IMAGE_EXTS2]);
6490
7286
  async function batchOptimize(opts) {
6491
7287
  const {
6492
7288
  inputDir,
@@ -6497,10 +7293,10 @@ async function batchOptimize(opts) {
6497
7293
  verbose,
6498
7294
  logger
6499
7295
  } = opts;
6500
- fs38.mkdirSync(outputDir, { recursive: true });
6501
- const entries = fs38.readdirSync(inputDir);
7296
+ fs42.mkdirSync(outputDir, { recursive: true });
7297
+ const entries = fs42.readdirSync(inputDir);
6502
7298
  const mediaFiles = entries.filter((f) => {
6503
- const ext = path43.extname(f).toLowerCase();
7299
+ const ext = path46.extname(f).toLowerCase();
6504
7300
  return ALL_EXTS.has(ext);
6505
7301
  });
6506
7302
  if (mediaFiles.length === 0) {
@@ -6511,9 +7307,9 @@ async function batchOptimize(opts) {
6511
7307
  let succeeded = 0;
6512
7308
  const errors = [];
6513
7309
  for (const file of mediaFiles) {
6514
- const inputPath = path43.join(inputDir, file);
6515
- const outputPath = path43.join(outputDir, file);
6516
- const ext = path43.extname(file).toLowerCase();
7310
+ const inputPath = path46.join(inputDir, file);
7311
+ const outputPath = path46.join(outputDir, file);
7312
+ const ext = path46.extname(file).toLowerCase();
6517
7313
  const kind = extToKind(ext);
6518
7314
  logger?.debug(`Processing: ${file}`);
6519
7315
  try {
@@ -6550,8 +7346,8 @@ async function batchOptimize(opts) {
6550
7346
 
6551
7347
  // src/commands/media/split-video.ts
6552
7348
  init_errors();
6553
- import fs39 from "fs";
6554
- import path44 from "path";
7349
+ import fs43 from "fs";
7350
+ import path47 from "path";
6555
7351
  async function splitVideo(opts) {
6556
7352
  await assertFfmpeg();
6557
7353
  const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
@@ -6562,10 +7358,10 @@ async function splitVideo(opts) {
6562
7358
  logger?.info("Video is short enough \u2014 no splitting needed");
6563
7359
  return [input];
6564
7360
  }
6565
- fs39.mkdirSync(outputDir, { recursive: true });
6566
- const ext = path44.extname(input) || ".mp4";
6567
- const baseName = path44.basename(input, ext);
6568
- const outputPattern = path44.join(outputDir, `${baseName}_chunk_%03d${ext}`);
7361
+ fs43.mkdirSync(outputDir, { recursive: true });
7362
+ const ext = path47.extname(input) || ".mp4";
7363
+ const baseName = path47.basename(input, ext);
7364
+ const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
6569
7365
  logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
6570
7366
  const args = [
6571
7367
  "-i",
@@ -6584,8 +7380,8 @@ async function splitVideo(opts) {
6584
7380
  await runFfmpeg(args, { verbose, logger });
6585
7381
  const files = [];
6586
7382
  for (let i = 0; i < numChunks + 1; i++) {
6587
- const fname = path44.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
6588
- if (fs39.existsSync(fname)) files.push(fname);
7383
+ const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
7384
+ if (fs43.existsSync(fname)) files.push(fname);
6589
7385
  }
6590
7386
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
6591
7387
  return files;
@@ -6599,9 +7395,9 @@ function registerMediaCommands(program2) {
6599
7395
  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(
6600
7396
  async (opts) => {
6601
7397
  const logger = createLogger({ verbose: opts.verbose ?? false });
6602
- if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
6603
- fs40.mkdirSync(path45.dirname(path45.resolve(opts.output)), { recursive: true });
6604
- const ext = path45.extname(opts.input).toLowerCase();
7398
+ if (!fs44.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7399
+ fs44.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
7400
+ const ext = path48.extname(opts.input).toLowerCase();
6605
7401
  const kind = extToKind(ext);
6606
7402
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
6607
7403
  const quality = Number.parseInt(opts.quality, 10);
@@ -6641,7 +7437,7 @@ function registerMediaCommands(program2) {
6641
7437
  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(
6642
7438
  async (opts) => {
6643
7439
  const logger = createLogger({ verbose: opts.verbose ?? false });
6644
- if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
7440
+ if (!fs44.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
6645
7441
  const chunks = await splitVideo({
6646
7442
  input: opts.input,
6647
7443
  outputDir: opts.outputDir,
@@ -6657,7 +7453,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
6657
7453
  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(
6658
7454
  async (opts) => {
6659
7455
  const logger = createLogger({ verbose: opts.verbose ?? false });
6660
- if (!fs40.existsSync(opts.inputDir))
7456
+ if (!fs44.existsSync(opts.inputDir))
6661
7457
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
6662
7458
  const result = await batchOptimize({
6663
7459
  inputDir: opts.inputDir,
@@ -6678,9 +7474,157 @@ Processed: ${result.succeeded}/${result.total} files`);
6678
7474
  );
6679
7475
  }
6680
7476
 
7477
+ // src/commands/update.ts
7478
+ init_errors();
7479
+ init_http_client();
7480
+ import { createRequire } from "module";
7481
+ import { execa as execa5 } from "execa";
7482
+
7483
+ // src/core/package-manager.ts
7484
+ var PACKAGE_NAME = "@mrgoonie/multix";
7485
+ function detectPackageManager(execPath, env = process.env) {
7486
+ const normalized = execPath.replace(/\\/g, "/").toLowerCase();
7487
+ if (normalized.includes("/.bun/") || normalized.includes("/bun/install/")) return "bun";
7488
+ if (normalized.includes("/.pnpm/") || normalized.includes("/pnpm/") || normalized.includes("/pnpm-global/"))
7489
+ return "pnpm";
7490
+ if (normalized.includes("/yarn/") || normalized.includes("/.yarn/")) return "yarn";
7491
+ if (normalized.includes("/.npm/") || normalized.includes("/npm/") || normalized.includes("/node_modules/"))
7492
+ return "npm";
7493
+ const userAgent = env.npm_config_user_agent ?? "";
7494
+ if (userAgent.startsWith("bun")) return "bun";
7495
+ if (userAgent.startsWith("pnpm")) return "pnpm";
7496
+ if (userAgent.startsWith("yarn")) return "yarn";
7497
+ if (userAgent.startsWith("npm")) return "npm";
7498
+ return "npm";
7499
+ }
7500
+ function buildUpdateCommand(pm, tag = "latest") {
7501
+ const spec = `${PACKAGE_NAME}@${tag}`;
7502
+ switch (pm) {
7503
+ case "pnpm":
7504
+ return { command: "pnpm", args: ["add", "-g", spec] };
7505
+ case "yarn":
7506
+ return { command: "yarn", args: ["global", "add", spec] };
7507
+ case "bun":
7508
+ return { command: "bun", args: ["add", "-g", spec] };
7509
+ default:
7510
+ return { command: "npm", args: ["i", "-g", spec] };
7511
+ }
7512
+ }
7513
+ function formatUpdateCommand(cmd) {
7514
+ return [cmd.command, ...cmd.args].join(" ");
7515
+ }
7516
+ function compareVersions(a, b) {
7517
+ const parse = (v) => {
7518
+ const [core, prerelease] = v.replace(/^v/, "").split("-", 2);
7519
+ const parts = (core ?? "").split(".").map((n) => Number.parseInt(n, 10) || 0);
7520
+ return { parts, prerelease };
7521
+ };
7522
+ const pa = parse(a);
7523
+ const pb = parse(b);
7524
+ for (let i = 0; i < 3; i++) {
7525
+ const na = pa.parts[i] ?? 0;
7526
+ const nb = pb.parts[i] ?? 0;
7527
+ if (na !== nb) return na < nb ? -1 : 1;
7528
+ }
7529
+ if (pa.prerelease === pb.prerelease) return 0;
7530
+ if (pa.prerelease === void 0) return 1;
7531
+ if (pb.prerelease === void 0) return -1;
7532
+ return pa.prerelease < pb.prerelease ? -1 : 1;
7533
+ }
7534
+
7535
+ // src/commands/update.ts
7536
+ var PACKAGE_NAME2 = "@mrgoonie/multix";
7537
+ var REGISTRY_BASE = "https://registry.npmjs.org";
7538
+ var require2 = createRequire(import.meta.url);
7539
+ function getCurrentVersion() {
7540
+ const pkg2 = require2("../../package.json");
7541
+ return pkg2.version;
7542
+ }
7543
+ async function fetchRegistryVersion(tag = "latest") {
7544
+ try {
7545
+ const doc = await httpJson({
7546
+ url: `${REGISTRY_BASE}/${PACKAGE_NAME2}/${encodeURIComponent(tag)}`
7547
+ });
7548
+ if (!doc.version) {
7549
+ throw new Error(`Registry response for tag "${tag}" is missing a version field`);
7550
+ }
7551
+ return doc.version;
7552
+ } catch (e) {
7553
+ if (e instanceof HttpError) {
7554
+ if (e.status === 404) {
7555
+ throw new Error(`Dist-tag "${tag}" does not exist for ${PACKAGE_NAME2}`);
7556
+ }
7557
+ throw new Error(`Failed to reach npm registry: ${e.message}`);
7558
+ }
7559
+ const msg = e instanceof Error ? e.message : String(e);
7560
+ throw new Error(`Failed to reach npm registry: ${msg}`);
7561
+ }
7562
+ }
7563
+ function registerUpdateCommand(program2) {
7564
+ program2.command("update").description("Update multix to the latest (or a given dist-tag) published version").option("--check", "Only compare current vs. latest version; do not install").option(
7565
+ "--tag <tag>",
7566
+ "Install a specific dist-tag instead of latest (always installs, skipping the up-to-date check)"
7567
+ ).option("--dry-run", "Print the install command instead of running it").option("-v, --verbose", "Verbose logging").action(
7568
+ async (opts) => {
7569
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7570
+ const currentVersion = getCurrentVersion();
7571
+ const explicitTag = opts.tag !== void 0;
7572
+ const tag = opts.tag ?? "latest";
7573
+ logger.info(`Current version: ${currentVersion}`);
7574
+ logger.info(`Checking npm registry for "${tag}"...`);
7575
+ let latestVersion;
7576
+ try {
7577
+ latestVersion = await fetchRegistryVersion(tag);
7578
+ } catch (e) {
7579
+ logger.error(e instanceof Error ? e.message : String(e));
7580
+ process.exit(1);
7581
+ }
7582
+ const cmp = compareVersions(latestVersion, currentVersion);
7583
+ if (opts.check) {
7584
+ if (cmp > 0) {
7585
+ logger.info(`Update available: ${currentVersion} -> ${latestVersion}`);
7586
+ console.log(`Run \`multix update\` to install ${latestVersion}.`);
7587
+ } else {
7588
+ logger.success(`Already up to date (${currentVersion}).`);
7589
+ }
7590
+ return;
7591
+ }
7592
+ if (!explicitTag && cmp <= 0) {
7593
+ logger.success(`Already up to date (${currentVersion}).`);
7594
+ return;
7595
+ }
7596
+ if (explicitTag && cmp <= 0) {
7597
+ logger.info(
7598
+ `Requested tag "${tag}" (${latestVersion}) \u2014 installing regardless of current version (${currentVersion}).`
7599
+ );
7600
+ }
7601
+ const pm = detectPackageManager(process.argv[1] ?? process.execPath);
7602
+ const updateCmd = buildUpdateCommand(pm, tag);
7603
+ const commandStr = formatUpdateCommand(updateCmd);
7604
+ logger.info(`Detected package manager: ${pm}`);
7605
+ logger.info(`Updating ${currentVersion} -> ${latestVersion}`);
7606
+ if (opts.dryRun) {
7607
+ console.log(commandStr);
7608
+ return;
7609
+ }
7610
+ logger.info(`Running: ${commandStr}`);
7611
+ try {
7612
+ await execa5(updateCmd.command, updateCmd.args, { stdio: "inherit" });
7613
+ } catch (e) {
7614
+ const msg = e instanceof Error ? e.message : String(e);
7615
+ logger.error(`Update command failed: ${msg}`);
7616
+ logger.info(`You can run it manually: ${commandStr}`);
7617
+ process.exit(1);
7618
+ }
7619
+ logger.success(`Updated to ${latestVersion}.`);
7620
+ }
7621
+ );
7622
+ }
7623
+
6681
7624
  // src/commands/index.ts
6682
7625
  function registerCommands(program2) {
6683
7626
  registerCheckCommand(program2);
7627
+ registerUpdateCommand(program2);
6684
7628
  registerGeminiCommands(program2);
6685
7629
  registerMinimaxCommands(program2);
6686
7630
  registerOpenAICommands(program2);
@@ -6689,6 +7633,7 @@ function registerCommands(program2) {
6689
7633
  registerBytePlusCommands(program2);
6690
7634
  registerCloudflareCommands(program2);
6691
7635
  registerElevenLabsCommands(program2);
7636
+ registerFalCommands(program2);
6692
7637
  registerMediaCommands(program2);
6693
7638
  registerDocCommands(program2);
6694
7639
  }
@@ -6699,10 +7644,10 @@ init_http_client();
6699
7644
 
6700
7645
  // src/cli.ts
6701
7646
  loadEnv();
6702
- var require2 = createRequire(import.meta.url);
6703
- var pkg = require2("../package.json");
7647
+ var require3 = createRequire2(import.meta.url);
7648
+ var pkg = require3("../package.json");
6704
7649
  var program = new Command().name("multix").description(
6705
- "AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, ffmpeg, and ImageMagick"
7650
+ "AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, ffmpeg, and ImageMagick"
6706
7651
  ).version(pkg.version);
6707
7652
  registerCommands(program);
6708
7653
  program.parseAsync(process.argv).catch((err2) => {