@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/CHANGELOG.md +15 -0
- package/README.md +63 -4
- package/dist/cli.js +1284 -339
- package/package.json +4 -2
- package/skill/SKILL.md +63 -3
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
|
-
|
|
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(
|
|
423
|
+
async request(path49, opts = {}) {
|
|
418
424
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
419
|
-
const url = new URL(base +
|
|
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(
|
|
438
|
-
return this.request(
|
|
443
|
+
get(path49, query, logger) {
|
|
444
|
+
return this.request(path49, { method: "GET", query, logger });
|
|
439
445
|
}
|
|
440
|
-
post(
|
|
441
|
-
return this.request(
|
|
446
|
+
post(path49, body, logger) {
|
|
447
|
+
return this.request(path49, { method: "POST", body, logger });
|
|
442
448
|
}
|
|
443
|
-
delete(
|
|
444
|
-
return this.request(
|
|
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
|
|
575
|
-
import
|
|
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 =
|
|
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 ?
|
|
646
|
-
|
|
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 ${
|
|
679
|
-
for (const f of
|
|
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 ${
|
|
717
|
-
for (const f of
|
|
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
|
|
927
|
+
import path9 from "path";
|
|
727
928
|
|
|
728
929
|
// src/core/video-thumb.ts
|
|
729
930
|
init_http_client();
|
|
730
|
-
import
|
|
731
|
-
import
|
|
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 =
|
|
782
|
-
const base =
|
|
783
|
-
const dest =
|
|
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 =
|
|
803
|
-
const copyBase =
|
|
804
|
-
const copyDir =
|
|
805
|
-
const copyDest =
|
|
806
|
-
|
|
807
|
-
|
|
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 ??
|
|
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
|
|
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:
|
|
994
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
995
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
996
|
-
return toRef(kind, await resolveAudioInput(
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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 ??
|
|
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
|
|
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 ??
|
|
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
|
|
1432
|
-
import
|
|
1632
|
+
import fs9 from "fs";
|
|
1633
|
+
import path13 from "path";
|
|
1433
1634
|
function destination(filename, output) {
|
|
1434
|
-
return output ?
|
|
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
|
-
|
|
1439
|
-
|
|
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
|
|
1579
|
-
logger.success(`Saved ${
|
|
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
|
|
1622
|
-
import
|
|
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 =
|
|
1785
|
-
if (!
|
|
1786
|
-
const buf =
|
|
1787
|
-
const name = fieldFilename ??
|
|
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 =
|
|
1812
|
-
|
|
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
|
-
|
|
1816
|
-
|
|
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
|
|
1856
|
-
import
|
|
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 =
|
|
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 ?
|
|
1871
|
-
|
|
1872
|
-
|
|
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
|
|
1916
|
-
import
|
|
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 ?
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2021
|
-
import
|
|
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 =
|
|
2037
|
-
if (!
|
|
2038
|
-
const planJson = JSON.parse(
|
|
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
|
|
2098
|
-
import
|
|
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 ?
|
|
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
|
-
|
|
2137
|
-
|
|
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/
|
|
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
|
|
2388
|
-
import
|
|
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
|
|
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 =
|
|
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 =
|
|
2461
|
-
const displayName =
|
|
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
|
|
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 =
|
|
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 =
|
|
3221
|
+
const stat = fs17.statSync(filePath);
|
|
2556
3222
|
return stat.size > 20 * 1024 * 1024;
|
|
2557
3223
|
}
|
|
2558
|
-
function
|
|
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.
|
|
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
|
|
2610
|
-
import
|
|
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
|
-
|
|
2615
|
-
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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
|
|
2778
|
-
import
|
|
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 =
|
|
2862
|
-
|
|
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
|
-
|
|
2866
|
-
|
|
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
|
|
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(
|
|
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 (
|
|
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 (!
|
|
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
|
|
2966
|
-
import
|
|
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 (!
|
|
3759
|
+
if (!fs22.existsSync(imgPath))
|
|
3001
3760
|
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
3002
|
-
const data =
|
|
3003
|
-
const ext =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
3058
|
-
|
|
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
|
|
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
|
|
3078
|
-
import
|
|
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 =
|
|
3121
|
-
|
|
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
|
-
|
|
3137
|
-
|
|
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 ${
|
|
3142
|
-
for (const f of
|
|
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
|
|
3149
|
-
import
|
|
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 =
|
|
3211
|
-
|
|
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
|
-
|
|
3217
|
-
|
|
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 ${
|
|
3222
|
-
for (const f of
|
|
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 =
|
|
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
|
|
3253
|
-
import
|
|
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 (!
|
|
4019
|
+
if (!fs25.existsSync(imgPath)) {
|
|
3259
4020
|
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
3260
4021
|
}
|
|
3261
|
-
const data =
|
|
3262
|
-
const ext =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
-
|
|
3340
|
-
|
|
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
|
|
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 (!
|
|
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
|
|
3422
|
-
import
|
|
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(
|
|
4257
|
+
async request(path49, opts = {}) {
|
|
3497
4258
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
3498
|
-
const v2Override =
|
|
4259
|
+
const v2Override = path49.startsWith("v2:");
|
|
3499
4260
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
3500
|
-
const finalPath = v2Override ?
|
|
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(
|
|
3520
|
-
return this.request(
|
|
4280
|
+
get(path49, query, logger) {
|
|
4281
|
+
return this.request(path49, { method: "GET", query, logger });
|
|
3521
4282
|
}
|
|
3522
|
-
post(
|
|
3523
|
-
return this.request(
|
|
4283
|
+
post(path49, body, logger) {
|
|
4284
|
+
return this.request(path49, { method: "POST", body, logger });
|
|
3524
4285
|
}
|
|
3525
|
-
delete(
|
|
3526
|
-
return this.request(
|
|
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
|
|
4295
|
+
function extFromUrl2(url, fallback = ".png") {
|
|
3535
4296
|
try {
|
|
3536
4297
|
const u = new URL(url);
|
|
3537
|
-
const e =
|
|
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 =
|
|
4381
|
+
const ext = extFromUrl2(img.url);
|
|
3621
4382
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
3622
|
-
const filepath =
|
|
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 =
|
|
3633
|
-
|
|
3634
|
-
|
|
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 ${
|
|
3639
|
-
for (const f of
|
|
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
|
|
3647
|
-
import
|
|
3648
|
-
function
|
|
4408
|
+
import fs28 from "fs";
|
|
4409
|
+
import path30 from "path";
|
|
4410
|
+
function extFromUrl3(url, fallback = ".png") {
|
|
3649
4411
|
try {
|
|
3650
|
-
return
|
|
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 =
|
|
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 =
|
|
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 =
|
|
3723
|
-
|
|
3724
|
-
|
|
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 ${
|
|
3729
|
-
for (const f of
|
|
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
|
|
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 ??
|
|
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
|
|
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 :
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
4192
|
-
import
|
|
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 =
|
|
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
|
-
|
|
5004
|
+
fs29.writeFileSync(dest, bytes);
|
|
4242
5005
|
} else {
|
|
4243
5006
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
4244
|
-
|
|
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
|
-
|
|
4256
|
-
|
|
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
|
|
4331
|
-
import
|
|
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 =
|
|
4379
|
-
|
|
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
|
-
|
|
4383
|
-
|
|
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
|
|
4431
|
-
import
|
|
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 =
|
|
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 =
|
|
5247
|
+
const stat = fs31.statSync(dest);
|
|
4485
5248
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
4486
5249
|
if (output) {
|
|
4487
|
-
|
|
4488
|
-
|
|
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
|
|
4552
|
-
import
|
|
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 =
|
|
4591
|
-
|
|
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
|
-
|
|
4603
|
-
|
|
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 ${
|
|
4631
|
-
for (const f of
|
|
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
|
|
4640
|
-
import
|
|
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 =
|
|
4696
|
-
|
|
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
|
-
|
|
4708
|
-
|
|
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 ${
|
|
4713
|
-
for (const f of
|
|
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
|
|
4843
|
-
import
|
|
5610
|
+
import fs35 from "fs";
|
|
5611
|
+
import path39 from "path";
|
|
4844
5612
|
|
|
4845
5613
|
// src/providers/openai/client.ts
|
|
4846
|
-
import
|
|
4847
|
-
import
|
|
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 =
|
|
4915
|
-
if (!
|
|
4916
|
-
const bytes =
|
|
4917
|
-
return new File([new Uint8Array(bytes)],
|
|
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 =
|
|
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 =
|
|
4976
|
-
|
|
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
|
-
|
|
4980
|
-
|
|
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 ?
|
|
5003
|
-
|
|
5004
|
-
|
|
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
|
|
5799
|
+
import fs36 from "fs";
|
|
5032
5800
|
import os2 from "os";
|
|
5033
|
-
import
|
|
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 ?
|
|
5083
|
-
|
|
5084
|
-
const tmp =
|
|
5085
|
-
const schema =
|
|
5086
|
-
const result =
|
|
5087
|
-
|
|
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 (!
|
|
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
|
|
5136
|
-
import
|
|
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 =
|
|
5170
|
-
|
|
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
|
-
|
|
5176
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 ${
|
|
5264
|
-
for (const
|
|
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(
|
|
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 ${
|
|
5306
|
-
for (const
|
|
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
|
|
5359
|
-
import
|
|
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 =
|
|
5525
|
-
|
|
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
|
-
|
|
5537
|
-
|
|
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 ${
|
|
5567
|
-
for (const f of
|
|
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
|
|
5575
|
-
import
|
|
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 ${
|
|
5623
|
-
for (const f of
|
|
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 =
|
|
5668
|
-
|
|
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
|
-
|
|
5674
|
-
|
|
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
|
|
5721
|
-
import
|
|
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 =
|
|
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
|
-
|
|
5737
|
-
const ws =
|
|
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
|
-
|
|
5743
|
-
|
|
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
|
|
6155
|
-
import
|
|
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 (!
|
|
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 =
|
|
6976
|
+
outputPath = path45.resolve(output);
|
|
6181
6977
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
6182
|
-
const base =
|
|
6183
|
-
outputPath =
|
|
6978
|
+
const base = path45.basename(inputs[0], path45.extname(inputs[0]));
|
|
6979
|
+
outputPath = path45.join(getOutputDir(), `${base}-extraction.md`);
|
|
6184
6980
|
} else {
|
|
6185
|
-
outputPath =
|
|
6981
|
+
outputPath = path45.join(getOutputDir(), "document-extraction.md");
|
|
6186
6982
|
}
|
|
6187
|
-
|
|
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
|
|
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
|
-
## ${
|
|
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
|
-
|
|
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
|
|
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
|
|
6300
|
-
import
|
|
7095
|
+
import fs44 from "fs";
|
|
7096
|
+
import path48 from "path";
|
|
6301
7097
|
|
|
6302
7098
|
// src/commands/media/batch.ts
|
|
6303
|
-
import
|
|
6304
|
-
import
|
|
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
|
|
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 (
|
|
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, ...
|
|
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
|
-
|
|
6501
|
-
const entries =
|
|
7296
|
+
fs42.mkdirSync(outputDir, { recursive: true });
|
|
7297
|
+
const entries = fs42.readdirSync(inputDir);
|
|
6502
7298
|
const mediaFiles = entries.filter((f) => {
|
|
6503
|
-
const ext =
|
|
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 =
|
|
6515
|
-
const outputPath =
|
|
6516
|
-
const ext =
|
|
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
|
|
6554
|
-
import
|
|
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
|
-
|
|
6566
|
-
const ext =
|
|
6567
|
-
const baseName =
|
|
6568
|
-
const outputPattern =
|
|
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 =
|
|
6588
|
-
if (
|
|
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 (!
|
|
6603
|
-
|
|
6604
|
-
const ext =
|
|
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 (!
|
|
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 (!
|
|
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
|
|
6703
|
-
var pkg =
|
|
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) => {
|