@mrgoonie/multix 0.6.0 → 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 +8 -0
- package/README.md +49 -4
- package/dist/cli.js +952 -233
- package/package.json +4 -2
- package/skill/SKILL.md +50 -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() {
|
|
@@ -1185,10 +1191,10 @@ function toRef(kind, resolved, role) {
|
|
|
1185
1191
|
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
1186
1192
|
}
|
|
1187
1193
|
async function resolveRefSpec(kind, spec, logger) {
|
|
1188
|
-
const { path:
|
|
1189
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
1190
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
1191
|
-
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);
|
|
1192
1198
|
}
|
|
1193
1199
|
async function resolveRefSpecs(specs, logger) {
|
|
1194
1200
|
const out = [];
|
|
@@ -2575,18 +2581,455 @@ function registerElevenLabsCommands(program2) {
|
|
|
2575
2581
|
registerAccountCommands(el);
|
|
2576
2582
|
}
|
|
2577
2583
|
|
|
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
|
|
2600
|
+
init_errors();
|
|
2601
|
+
init_http_client();
|
|
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
|
+
|
|
2578
3021
|
// src/providers/gemini/commands/analyze.ts
|
|
2579
3022
|
init_errors();
|
|
2580
|
-
import
|
|
3023
|
+
import fs19 from "fs";
|
|
2581
3024
|
|
|
2582
3025
|
// src/providers/gemini/client.ts
|
|
2583
|
-
import
|
|
2584
|
-
import
|
|
3026
|
+
import fs17 from "fs";
|
|
3027
|
+
import path22 from "path";
|
|
2585
3028
|
init_errors();
|
|
2586
3029
|
init_http_client();
|
|
2587
3030
|
|
|
2588
3031
|
// src/providers/gemini/task-resolver.ts
|
|
2589
|
-
import
|
|
3032
|
+
import path21 from "path";
|
|
2590
3033
|
var MIME_MAP = {
|
|
2591
3034
|
// Audio
|
|
2592
3035
|
".mp3": "audio/mp3",
|
|
@@ -2627,7 +3070,7 @@ var MIME_MAP = {
|
|
|
2627
3070
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
2628
3071
|
};
|
|
2629
3072
|
function getMimeType(filePath) {
|
|
2630
|
-
const ext =
|
|
3073
|
+
const ext = path21.extname(filePath).toLowerCase();
|
|
2631
3074
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
2632
3075
|
}
|
|
2633
3076
|
function requiresProcessingWait(mimeType) {
|
|
@@ -2653,8 +3096,8 @@ async function uploadFile(filePath, opts = {}) {
|
|
|
2653
3096
|
const apiKey = requireApiKey();
|
|
2654
3097
|
const { timeoutMs = 3e5, logger } = opts;
|
|
2655
3098
|
const mimeType = getMimeType(filePath);
|
|
2656
|
-
const fileBytes =
|
|
2657
|
-
const displayName =
|
|
3099
|
+
const fileBytes = fs17.readFileSync(filePath);
|
|
3100
|
+
const displayName = path22.basename(filePath);
|
|
2658
3101
|
const boundary = `multix_${Date.now()}`;
|
|
2659
3102
|
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
2660
3103
|
const body = Buffer.concat([
|
|
@@ -2696,7 +3139,7 @@ Content-Type: ${mimeType}\r
|
|
|
2696
3139
|
if (requiresProcessingWait(mimeType)) {
|
|
2697
3140
|
const deadline = Date.now() + timeoutMs;
|
|
2698
3141
|
while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
|
|
2699
|
-
await
|
|
3142
|
+
await sleep2(2e3);
|
|
2700
3143
|
fileRef = await httpJson({
|
|
2701
3144
|
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
2702
3145
|
headers: authHeaders2(apiKey)
|
|
@@ -2723,6 +3166,33 @@ async function generateContent(req) {
|
|
|
2723
3166
|
}
|
|
2724
3167
|
});
|
|
2725
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
|
+
}
|
|
2726
3196
|
function extractText(resp) {
|
|
2727
3197
|
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
|
2728
3198
|
return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
|
|
@@ -2743,15 +3213,15 @@ function extractImages(resp) {
|
|
|
2743
3213
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
2744
3214
|
}
|
|
2745
3215
|
function inlinePart(filePath) {
|
|
2746
|
-
const data =
|
|
3216
|
+
const data = fs17.readFileSync(filePath).toString("base64");
|
|
2747
3217
|
const mimeType = getMimeType(filePath);
|
|
2748
3218
|
return { inlineData: { mimeType, data } };
|
|
2749
3219
|
}
|
|
2750
3220
|
function shouldUseFileApi(filePath) {
|
|
2751
|
-
const stat =
|
|
3221
|
+
const stat = fs17.statSync(filePath);
|
|
2752
3222
|
return stat.size > 20 * 1024 * 1024;
|
|
2753
3223
|
}
|
|
2754
|
-
function
|
|
3224
|
+
function sleep2(ms) {
|
|
2755
3225
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2756
3226
|
}
|
|
2757
3227
|
|
|
@@ -2761,7 +3231,7 @@ var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
|
|
|
2761
3231
|
var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
|
|
2762
3232
|
var TEXT_MODEL_DEFAULT = "gemini-3.6-flash";
|
|
2763
3233
|
var ANALYSIS_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
|
|
2764
|
-
var TTS_MODEL_DEFAULT = "gemini-3.
|
|
3234
|
+
var TTS_MODEL_DEFAULT = "gemini-3.8-flash-lite-tts";
|
|
2765
3235
|
var DOC_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
|
|
2766
3236
|
var IMAGEN_MODELS = /* @__PURE__ */ new Set([
|
|
2767
3237
|
"imagen-4.0-generate-001",
|
|
@@ -2802,13 +3272,13 @@ function getDefaultModel(task) {
|
|
|
2802
3272
|
}
|
|
2803
3273
|
|
|
2804
3274
|
// src/providers/gemini/result-formatter.ts
|
|
2805
|
-
import
|
|
2806
|
-
import
|
|
3275
|
+
import fs18 from "fs";
|
|
3276
|
+
import path23 from "path";
|
|
2807
3277
|
function formatResults(results, format, outputPath) {
|
|
2808
3278
|
const content = renderFormat(results, format);
|
|
2809
3279
|
if (outputPath) {
|
|
2810
|
-
|
|
2811
|
-
|
|
3280
|
+
fs18.mkdirSync(path23.dirname(path23.resolve(outputPath)), { recursive: true });
|
|
3281
|
+
fs18.writeFileSync(outputPath, content, "utf8");
|
|
2812
3282
|
} else {
|
|
2813
3283
|
process.stdout.write(content);
|
|
2814
3284
|
}
|
|
@@ -2875,7 +3345,7 @@ function registerAnalyzeCommand(parent) {
|
|
|
2875
3345
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2876
3346
|
const model = opts.model ?? getDefaultModel("analyze");
|
|
2877
3347
|
for (const f of opts.files) {
|
|
2878
|
-
if (!
|
|
3348
|
+
if (!fs19.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2879
3349
|
}
|
|
2880
3350
|
const results = [];
|
|
2881
3351
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -2921,14 +3391,14 @@ function registerAnalyzeCommand(parent) {
|
|
|
2921
3391
|
|
|
2922
3392
|
// src/providers/gemini/commands/extract.ts
|
|
2923
3393
|
init_errors();
|
|
2924
|
-
import
|
|
3394
|
+
import fs20 from "fs";
|
|
2925
3395
|
function registerExtractCommand(parent) {
|
|
2926
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(
|
|
2927
3397
|
async (opts) => {
|
|
2928
3398
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2929
3399
|
const model = opts.model ?? getDefaultModel("extract");
|
|
2930
3400
|
for (const f of opts.files) {
|
|
2931
|
-
if (!
|
|
3401
|
+
if (!fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2932
3402
|
}
|
|
2933
3403
|
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
2934
3404
|
const results = [];
|
|
@@ -2970,8 +3440,8 @@ function registerExtractCommand(parent) {
|
|
|
2970
3440
|
init_errors();
|
|
2971
3441
|
|
|
2972
3442
|
// src/providers/gemini/generators/speech.ts
|
|
2973
|
-
import
|
|
2974
|
-
import
|
|
3443
|
+
import fs21 from "fs";
|
|
3444
|
+
import path24 from "path";
|
|
2975
3445
|
|
|
2976
3446
|
// src/providers/gemini/voices.ts
|
|
2977
3447
|
var GEMINI_TTS_VOICES = [
|
|
@@ -3007,10 +3477,16 @@ var GEMINI_TTS_VOICES = [
|
|
|
3007
3477
|
"Sulafat"
|
|
3008
3478
|
];
|
|
3009
3479
|
var GEMINI_TTS_MODELS = /* @__PURE__ */ new Set([
|
|
3480
|
+
"gemini-3.8-flash-lite-tts",
|
|
3481
|
+
"gemini-3.8-flash-tts",
|
|
3010
3482
|
"gemini-3.1-flash-tts-preview",
|
|
3011
3483
|
"gemini-2.5-flash-preview-tts",
|
|
3012
3484
|
"gemini-2.5-pro-preview-tts"
|
|
3013
3485
|
]);
|
|
3486
|
+
var GEMINI_INTERACTIONS_TTS_MODELS = /* @__PURE__ */ new Set([
|
|
3487
|
+
"gemini-3.8-flash-lite-tts",
|
|
3488
|
+
"gemini-3.8-flash-tts"
|
|
3489
|
+
]);
|
|
3014
3490
|
var TTS_VOICE_DEFAULT = "Kore";
|
|
3015
3491
|
var TTS_PCM_SAMPLE_RATE = 24e3;
|
|
3016
3492
|
var TTS_PCM_CHANNELS = 1;
|
|
@@ -3019,10 +3495,19 @@ var TTS_OUTPUT_FORMATS = ["wav", "pcm"];
|
|
|
3019
3495
|
function isValidGeminiVoice(name) {
|
|
3020
3496
|
return GEMINI_TTS_VOICES.includes(name);
|
|
3021
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
|
+
}
|
|
3022
3504
|
|
|
3023
3505
|
// src/providers/gemini/generators/speech.ts
|
|
3024
3506
|
async function generateGeminiSpeech(opts) {
|
|
3025
3507
|
const { text, model, voice, speakers, outputFormat, output, logger } = opts;
|
|
3508
|
+
if (GEMINI_INTERACTIONS_TTS_MODELS.has(model)) {
|
|
3509
|
+
return generateInteractionsSpeech(opts);
|
|
3510
|
+
}
|
|
3026
3511
|
const speechConfig = speakers && speakers.length > 0 ? {
|
|
3027
3512
|
multiSpeakerVoiceConfig: {
|
|
3028
3513
|
speakerVoiceConfigs: speakers.map((s) => ({
|
|
@@ -3053,16 +3538,81 @@ async function generateGeminiSpeech(opts) {
|
|
|
3053
3538
|
const pcmBytes = Buffer.from(audio.data, "base64");
|
|
3054
3539
|
const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
|
|
3055
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) {
|
|
3056
3606
|
const outDir = getOutputDir();
|
|
3057
|
-
const dest =
|
|
3058
|
-
|
|
3607
|
+
const dest = path24.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
3608
|
+
fs21.writeFileSync(dest, outBytes);
|
|
3059
3609
|
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
3060
3610
|
if (output) {
|
|
3061
|
-
|
|
3062
|
-
|
|
3611
|
+
fs21.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
|
|
3612
|
+
fs21.copyFileSync(dest, output);
|
|
3063
3613
|
logger?.success(`Copied to: ${output}`);
|
|
3064
3614
|
}
|
|
3065
|
-
return
|
|
3615
|
+
return dest;
|
|
3066
3616
|
}
|
|
3067
3617
|
function parseSampleRate(mimeType) {
|
|
3068
3618
|
const m = mimeType.match(/rate=(\d+)/i);
|
|
@@ -3091,7 +3641,14 @@ function wrapPcmInWav(pcm, sampleRate, channels, bitsPerSample) {
|
|
|
3091
3641
|
|
|
3092
3642
|
// src/providers/gemini/commands/generate-speech.ts
|
|
3093
3643
|
function registerGeminiGenerateSpeechCommand(parent) {
|
|
3094
|
-
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(
|
|
3095
3652
|
"--speaker <name:voice>",
|
|
3096
3653
|
"Speaker mapping (repeatable, max 2 \u2014 switches to multi-speaker mode)",
|
|
3097
3654
|
collectSpeakers,
|
|
@@ -3114,13 +3671,18 @@ function registerGeminiGenerateSpeechCommand(parent) {
|
|
|
3114
3671
|
if (speakers.length > 2) {
|
|
3115
3672
|
throw new ValidationError("Gemini TTS supports max 2 speakers in multi-speaker mode");
|
|
3116
3673
|
}
|
|
3117
|
-
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)) {
|
|
3118
3680
|
throw new ValidationError(
|
|
3119
3681
|
`Invalid --voice "${opts.voice}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
|
|
3120
3682
|
);
|
|
3121
3683
|
}
|
|
3122
3684
|
for (const sv of speakers) {
|
|
3123
|
-
if (!
|
|
3685
|
+
if (!isVoiceAllowedForModel(sv.voice, model)) {
|
|
3124
3686
|
throw new ValidationError(
|
|
3125
3687
|
`Invalid voice "${sv.voice}" for speaker "${sv.speaker}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
|
|
3126
3688
|
);
|
|
@@ -3131,6 +3693,7 @@ function registerGeminiGenerateSpeechCommand(parent) {
|
|
|
3131
3693
|
model,
|
|
3132
3694
|
voice: speakers.length === 0 ? opts.voice : void 0,
|
|
3133
3695
|
speakers: speakers.length > 0 ? speakers : void 0,
|
|
3696
|
+
style: opts.style,
|
|
3134
3697
|
outputFormat: opts.outputFormat,
|
|
3135
3698
|
output: opts.output,
|
|
3136
3699
|
logger
|
|
@@ -3158,8 +3721,8 @@ function collectSpeakers(value, prev) {
|
|
|
3158
3721
|
}
|
|
3159
3722
|
|
|
3160
3723
|
// src/providers/gemini/commands/generate-video.ts
|
|
3161
|
-
import
|
|
3162
|
-
import
|
|
3724
|
+
import fs22 from "fs";
|
|
3725
|
+
import path25 from "path";
|
|
3163
3726
|
init_errors();
|
|
3164
3727
|
init_http_client();
|
|
3165
3728
|
var BASE2 = "https://generativelanguage.googleapis.com";
|
|
@@ -3193,10 +3756,10 @@ function registerGenerateVideoCommand(parent) {
|
|
|
3193
3756
|
const contentParts = [{ text: opts.prompt }];
|
|
3194
3757
|
if (opts.referenceImages?.length) {
|
|
3195
3758
|
for (const imgPath of opts.referenceImages.slice(0, 2)) {
|
|
3196
|
-
if (!
|
|
3759
|
+
if (!fs22.existsSync(imgPath))
|
|
3197
3760
|
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
3198
|
-
const data =
|
|
3199
|
-
const ext =
|
|
3761
|
+
const data = fs22.readFileSync(imgPath).toString("base64");
|
|
3762
|
+
const ext = path25.extname(imgPath).toLowerCase();
|
|
3200
3763
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
3201
3764
|
contentParts.push({ inlineData: { mimeType, data } });
|
|
3202
3765
|
}
|
|
@@ -3223,7 +3786,7 @@ function registerGenerateVideoCommand(parent) {
|
|
|
3223
3786
|
let done = false;
|
|
3224
3787
|
let finalOp = operation;
|
|
3225
3788
|
while (!done && Date.now() < deadline) {
|
|
3226
|
-
await
|
|
3789
|
+
await sleep3(1e4);
|
|
3227
3790
|
finalOp = await httpJson({
|
|
3228
3791
|
url: `${BASE2}/v1beta/${operationName}`,
|
|
3229
3792
|
headers
|
|
@@ -3245,13 +3808,13 @@ function registerGenerateVideoCommand(parent) {
|
|
|
3245
3808
|
);
|
|
3246
3809
|
}
|
|
3247
3810
|
const outDir = getOutputDir();
|
|
3248
|
-
const dest =
|
|
3811
|
+
const dest = path25.join(outDir, `veo_generated_${Date.now()}.mp4`);
|
|
3249
3812
|
const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
|
|
3250
3813
|
await downloadFile2(videoUri, dest);
|
|
3251
3814
|
logger.success(`Saved video: ${dest}`);
|
|
3252
3815
|
if (opts.output) {
|
|
3253
|
-
|
|
3254
|
-
|
|
3816
|
+
fs22.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
|
|
3817
|
+
fs22.copyFileSync(dest, opts.output);
|
|
3255
3818
|
logger.success(`Copied to: ${opts.output}`);
|
|
3256
3819
|
}
|
|
3257
3820
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -3264,14 +3827,14 @@ Generated video: ${dest}`);
|
|
|
3264
3827
|
}
|
|
3265
3828
|
);
|
|
3266
3829
|
}
|
|
3267
|
-
function
|
|
3830
|
+
function sleep3(ms) {
|
|
3268
3831
|
return new Promise((r) => setTimeout(r, ms));
|
|
3269
3832
|
}
|
|
3270
3833
|
|
|
3271
3834
|
// src/providers/gemini/commands/generate.ts
|
|
3272
3835
|
init_errors();
|
|
3273
|
-
import
|
|
3274
|
-
import
|
|
3836
|
+
import fs23 from "fs";
|
|
3837
|
+
import path26 from "path";
|
|
3275
3838
|
function registerGenerateCommand(parent) {
|
|
3276
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(
|
|
3277
3840
|
async (opts) => {
|
|
@@ -3313,8 +3876,8 @@ function registerGenerateCommand(parent) {
|
|
|
3313
3876
|
const outDir = getOutputDir();
|
|
3314
3877
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
3315
3878
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
3316
|
-
const dest =
|
|
3317
|
-
|
|
3879
|
+
const dest = path26.join(outDir, fname);
|
|
3880
|
+
fs23.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
3318
3881
|
savedFiles.push(dest);
|
|
3319
3882
|
logger.success(`Saved: ${dest}`);
|
|
3320
3883
|
}
|
|
@@ -3329,8 +3892,8 @@ function registerGenerateCommand(parent) {
|
|
|
3329
3892
|
process.exit(1);
|
|
3330
3893
|
}
|
|
3331
3894
|
if (opts.output && savedFiles[0]) {
|
|
3332
|
-
|
|
3333
|
-
|
|
3895
|
+
fs23.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
|
|
3896
|
+
fs23.copyFileSync(savedFiles[0], opts.output);
|
|
3334
3897
|
logger.success(`Copied to: ${opts.output}`);
|
|
3335
3898
|
}
|
|
3336
3899
|
const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
|
|
@@ -3342,8 +3905,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
3342
3905
|
}
|
|
3343
3906
|
|
|
3344
3907
|
// src/providers/gemini/commands/image-to-image.ts
|
|
3345
|
-
import
|
|
3346
|
-
import
|
|
3908
|
+
import fs24 from "fs";
|
|
3909
|
+
import path27 from "path";
|
|
3347
3910
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
3348
3911
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
3349
3912
|
function registerGeminiImageToImageCommand(parent) {
|
|
@@ -3404,14 +3967,14 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
3404
3967
|
const img = images[i];
|
|
3405
3968
|
if (!img) continue;
|
|
3406
3969
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
3407
|
-
const dest =
|
|
3408
|
-
|
|
3970
|
+
const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
3971
|
+
fs24.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
3409
3972
|
saved.push(dest);
|
|
3410
3973
|
logger.success(`Saved: ${dest}`);
|
|
3411
3974
|
}
|
|
3412
3975
|
if (opts.output && saved[0]) {
|
|
3413
|
-
|
|
3414
|
-
|
|
3976
|
+
fs24.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
|
|
3977
|
+
fs24.copyFileSync(saved[0], opts.output);
|
|
3415
3978
|
logger.success(`Copied to: ${opts.output}`);
|
|
3416
3979
|
}
|
|
3417
3980
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -3439,7 +4002,7 @@ function sniffImageMime(buf, url) {
|
|
|
3439
4002
|
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
3440
4003
|
return "image/webp";
|
|
3441
4004
|
}
|
|
3442
|
-
const ext =
|
|
4005
|
+
const ext = path27.extname(new URL(url).pathname).toLowerCase();
|
|
3443
4006
|
if (ext === ".png") return "image/png";
|
|
3444
4007
|
if (ext === ".webp") return "image/webp";
|
|
3445
4008
|
if (ext === ".gif") return "image/gif";
|
|
@@ -3447,21 +4010,21 @@ function sniffImageMime(buf, url) {
|
|
|
3447
4010
|
}
|
|
3448
4011
|
|
|
3449
4012
|
// src/providers/gemini/commands/image-to-video.ts
|
|
3450
|
-
import
|
|
3451
|
-
import
|
|
4013
|
+
import fs25 from "fs";
|
|
4014
|
+
import path28 from "path";
|
|
3452
4015
|
init_errors();
|
|
3453
4016
|
init_http_client();
|
|
3454
4017
|
var BASE3 = "https://generativelanguage.googleapis.com";
|
|
3455
4018
|
function imagePart(imgPath) {
|
|
3456
|
-
if (!
|
|
4019
|
+
if (!fs25.existsSync(imgPath)) {
|
|
3457
4020
|
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
3458
4021
|
}
|
|
3459
|
-
const data =
|
|
3460
|
-
const ext =
|
|
4022
|
+
const data = fs25.readFileSync(imgPath).toString("base64");
|
|
4023
|
+
const ext = path28.extname(imgPath).toLowerCase();
|
|
3461
4024
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
3462
4025
|
return { inlineData: { mimeType, data } };
|
|
3463
4026
|
}
|
|
3464
|
-
function
|
|
4027
|
+
function sleep4(ms) {
|
|
3465
4028
|
return new Promise((r) => setTimeout(r, ms));
|
|
3466
4029
|
}
|
|
3467
4030
|
function registerGeminiImageToVideoCommand(parent) {
|
|
@@ -3513,7 +4076,7 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
3513
4076
|
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
3514
4077
|
let finalOp = operation;
|
|
3515
4078
|
while (!finalOp.done && Date.now() < deadline) {
|
|
3516
|
-
await
|
|
4079
|
+
await sleep4(1e4);
|
|
3517
4080
|
finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
|
|
3518
4081
|
if (!finalOp.done) {
|
|
3519
4082
|
logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
|
|
@@ -3530,12 +4093,12 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
3530
4093
|
"gemini"
|
|
3531
4094
|
);
|
|
3532
4095
|
}
|
|
3533
|
-
const dest =
|
|
4096
|
+
const dest = path28.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
3534
4097
|
await downloadFile(videoUri, dest);
|
|
3535
4098
|
logger.success(`Saved video: ${dest}`);
|
|
3536
4099
|
if (opts.output) {
|
|
3537
|
-
|
|
3538
|
-
|
|
4100
|
+
fs25.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
|
|
4101
|
+
fs25.copyFileSync(dest, opts.output);
|
|
3539
4102
|
logger.success(`Copied to: ${opts.output}`);
|
|
3540
4103
|
}
|
|
3541
4104
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -3551,14 +4114,14 @@ Generated video: ${dest}`);
|
|
|
3551
4114
|
|
|
3552
4115
|
// src/providers/gemini/commands/transcribe.ts
|
|
3553
4116
|
init_errors();
|
|
3554
|
-
import
|
|
4117
|
+
import fs26 from "fs";
|
|
3555
4118
|
function registerTranscribeCommand2(parent) {
|
|
3556
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(
|
|
3557
4120
|
async (opts) => {
|
|
3558
4121
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3559
4122
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
3560
4123
|
for (const f of opts.files) {
|
|
3561
|
-
if (!
|
|
4124
|
+
if (!fs26.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
3562
4125
|
}
|
|
3563
4126
|
const results = [];
|
|
3564
4127
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -3616,8 +4179,8 @@ function registerGeminiCommands(program2) {
|
|
|
3616
4179
|
|
|
3617
4180
|
// src/providers/leonardo/commands/generate.ts
|
|
3618
4181
|
init_http_client();
|
|
3619
|
-
import
|
|
3620
|
-
import
|
|
4182
|
+
import fs27 from "fs";
|
|
4183
|
+
import path29 from "path";
|
|
3621
4184
|
|
|
3622
4185
|
// src/providers/leonardo/client.ts
|
|
3623
4186
|
init_errors();
|
|
@@ -3691,11 +4254,11 @@ var LeonardoClient = class {
|
|
|
3691
4254
|
}
|
|
3692
4255
|
apiKey;
|
|
3693
4256
|
baseUrl;
|
|
3694
|
-
async request(
|
|
4257
|
+
async request(path49, opts = {}) {
|
|
3695
4258
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
3696
|
-
const v2Override =
|
|
4259
|
+
const v2Override = path49.startsWith("v2:");
|
|
3697
4260
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
3698
|
-
const finalPath = v2Override ?
|
|
4261
|
+
const finalPath = v2Override ? path49.slice(3) : path49;
|
|
3699
4262
|
const url = new URL(finalBase + finalPath);
|
|
3700
4263
|
if (opts.query) {
|
|
3701
4264
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -3714,14 +4277,14 @@ var LeonardoClient = class {
|
|
|
3714
4277
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
3715
4278
|
});
|
|
3716
4279
|
}
|
|
3717
|
-
get(
|
|
3718
|
-
return this.request(
|
|
4280
|
+
get(path49, query, logger) {
|
|
4281
|
+
return this.request(path49, { method: "GET", query, logger });
|
|
3719
4282
|
}
|
|
3720
|
-
post(
|
|
3721
|
-
return this.request(
|
|
4283
|
+
post(path49, body, logger) {
|
|
4284
|
+
return this.request(path49, { method: "POST", body, logger });
|
|
3722
4285
|
}
|
|
3723
|
-
delete(
|
|
3724
|
-
return this.request(
|
|
4286
|
+
delete(path49, logger) {
|
|
4287
|
+
return this.request(path49, { method: "DELETE", logger });
|
|
3725
4288
|
}
|
|
3726
4289
|
};
|
|
3727
4290
|
function createLeonardoClient() {
|
|
@@ -3729,10 +4292,10 @@ function createLeonardoClient() {
|
|
|
3729
4292
|
}
|
|
3730
4293
|
|
|
3731
4294
|
// src/providers/leonardo/commands/generate.ts
|
|
3732
|
-
function
|
|
4295
|
+
function extFromUrl2(url, fallback = ".png") {
|
|
3733
4296
|
try {
|
|
3734
4297
|
const u = new URL(url);
|
|
3735
|
-
const e =
|
|
4298
|
+
const e = path29.extname(u.pathname);
|
|
3736
4299
|
return e.length > 0 ? e : fallback;
|
|
3737
4300
|
} catch {
|
|
3738
4301
|
return fallback;
|
|
@@ -3815,9 +4378,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
3815
4378
|
for (let i = 0; i < images.length; i++) {
|
|
3816
4379
|
const img = images[i];
|
|
3817
4380
|
if (!img) continue;
|
|
3818
|
-
const ext =
|
|
4381
|
+
const ext = extFromUrl2(img.url);
|
|
3819
4382
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
3820
|
-
const filepath =
|
|
4383
|
+
const filepath = path29.join(outDir, filename);
|
|
3821
4384
|
try {
|
|
3822
4385
|
await downloadFile(img.url, filepath);
|
|
3823
4386
|
saved.push(filepath);
|
|
@@ -3827,9 +4390,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
3827
4390
|
}
|
|
3828
4391
|
}
|
|
3829
4392
|
if (opts.output && saved[0]) {
|
|
3830
|
-
const dest =
|
|
3831
|
-
|
|
3832
|
-
|
|
4393
|
+
const dest = path29.resolve(opts.output);
|
|
4394
|
+
fs27.mkdirSync(path29.dirname(dest), { recursive: true });
|
|
4395
|
+
fs27.copyFileSync(saved[0], dest);
|
|
3833
4396
|
logger.success(`Copied to ${dest}`);
|
|
3834
4397
|
}
|
|
3835
4398
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -3842,11 +4405,11 @@ Generated ${finalImages.length} image(s):`);
|
|
|
3842
4405
|
|
|
3843
4406
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
3844
4407
|
init_http_client();
|
|
3845
|
-
import
|
|
3846
|
-
import
|
|
3847
|
-
function
|
|
4408
|
+
import fs28 from "fs";
|
|
4409
|
+
import path30 from "path";
|
|
4410
|
+
function extFromUrl3(url, fallback = ".png") {
|
|
3848
4411
|
try {
|
|
3849
|
-
return
|
|
4412
|
+
return path30.extname(new URL(url).pathname) || fallback;
|
|
3850
4413
|
} catch {
|
|
3851
4414
|
return fallback;
|
|
3852
4415
|
}
|
|
@@ -3906,9 +4469,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
3906
4469
|
for (let i = 0; i < images.length; i++) {
|
|
3907
4470
|
const img = images[i];
|
|
3908
4471
|
if (!img) continue;
|
|
3909
|
-
const ext =
|
|
4472
|
+
const ext = extFromUrl3(img.url);
|
|
3910
4473
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
3911
|
-
const filepath =
|
|
4474
|
+
const filepath = path30.join(outDir, filename);
|
|
3912
4475
|
try {
|
|
3913
4476
|
await downloadFile(img.url, filepath);
|
|
3914
4477
|
saved.push(filepath);
|
|
@@ -3918,9 +4481,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
3918
4481
|
}
|
|
3919
4482
|
}
|
|
3920
4483
|
if (opts.output && saved[0]) {
|
|
3921
|
-
const dest =
|
|
3922
|
-
|
|
3923
|
-
|
|
4484
|
+
const dest = path30.resolve(opts.output);
|
|
4485
|
+
fs28.mkdirSync(path30.dirname(dest), { recursive: true });
|
|
4486
|
+
fs28.copyFileSync(saved[0], dest);
|
|
3924
4487
|
logger.success(`Copied to ${dest}`);
|
|
3925
4488
|
}
|
|
3926
4489
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -3933,7 +4496,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
3933
4496
|
|
|
3934
4497
|
// src/providers/leonardo/commands/video.ts
|
|
3935
4498
|
init_http_client();
|
|
3936
|
-
import
|
|
4499
|
+
import path31 from "path";
|
|
3937
4500
|
function v2DimsFor(resolution) {
|
|
3938
4501
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
3939
4502
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -4044,7 +4607,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
4044
4607
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
4045
4608
|
process.exit(1);
|
|
4046
4609
|
}
|
|
4047
|
-
const outPath = opts.output ??
|
|
4610
|
+
const outPath = opts.output ?? path31.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
4048
4611
|
await downloadFile(videoUrl2, outPath);
|
|
4049
4612
|
logger.success(`Saved ${outPath}`);
|
|
4050
4613
|
await maybeDownloadThumb(gen, outPath, {
|
|
@@ -4151,7 +4714,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
4151
4714
|
|
|
4152
4715
|
// src/providers/leonardo/commands/status.ts
|
|
4153
4716
|
init_http_client();
|
|
4154
|
-
import
|
|
4717
|
+
import path32 from "path";
|
|
4155
4718
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
4156
4719
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
4157
4720
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -4216,7 +4779,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
4216
4779
|
const outDir = getOutputDir();
|
|
4217
4780
|
let idx = 0;
|
|
4218
4781
|
for (const url of videos) {
|
|
4219
|
-
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`);
|
|
4220
4783
|
await downloadFile(url, dest);
|
|
4221
4784
|
logger.success(`Saved ${dest}`);
|
|
4222
4785
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -4229,7 +4792,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
4229
4792
|
for (const url of images) {
|
|
4230
4793
|
const m = url.match(IMAGE_EXT_RE2);
|
|
4231
4794
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
4232
|
-
const dest =
|
|
4795
|
+
const dest = path32.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
4233
4796
|
await downloadFile(url, dest);
|
|
4234
4797
|
logger.success(`Saved ${dest}`);
|
|
4235
4798
|
idx++;
|
|
@@ -4363,7 +4926,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
|
4363
4926
|
if (status === "Failed" || status === "Error") {
|
|
4364
4927
|
throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
|
|
4365
4928
|
}
|
|
4366
|
-
await
|
|
4929
|
+
await sleep5(intervalMs);
|
|
4367
4930
|
elapsed += intervalMs;
|
|
4368
4931
|
}
|
|
4369
4932
|
throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
|
|
@@ -4382,14 +4945,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
|
|
|
4382
4945
|
await downloadFile(downloadUrl, dest);
|
|
4383
4946
|
return dest;
|
|
4384
4947
|
}
|
|
4385
|
-
function
|
|
4948
|
+
function sleep5(ms) {
|
|
4386
4949
|
return new Promise((r) => setTimeout(r, ms));
|
|
4387
4950
|
}
|
|
4388
4951
|
|
|
4389
4952
|
// src/providers/minimax/generators/music.ts
|
|
4390
4953
|
init_http_client();
|
|
4391
|
-
import
|
|
4392
|
-
import
|
|
4954
|
+
import fs29 from "fs";
|
|
4955
|
+
import path33 from "path";
|
|
4393
4956
|
async function generateMinimaxMusic(opts) {
|
|
4394
4957
|
const {
|
|
4395
4958
|
apiKey,
|
|
@@ -4434,14 +4997,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
4434
4997
|
}
|
|
4435
4998
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
4436
4999
|
const outDir = getOutputDir();
|
|
4437
|
-
const dest =
|
|
5000
|
+
const dest = path33.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
4438
5001
|
try {
|
|
4439
5002
|
if (audioData.startsWith("http")) {
|
|
4440
5003
|
const bytes = await fetchBytes(audioData);
|
|
4441
|
-
|
|
5004
|
+
fs29.writeFileSync(dest, bytes);
|
|
4442
5005
|
} else {
|
|
4443
5006
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
4444
|
-
|
|
5007
|
+
fs29.writeFileSync(dest, audioBytes);
|
|
4445
5008
|
}
|
|
4446
5009
|
} catch (e) {
|
|
4447
5010
|
return {
|
|
@@ -4452,8 +5015,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
4452
5015
|
const durSec = durationMs / 1e3;
|
|
4453
5016
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
4454
5017
|
if (output) {
|
|
4455
|
-
|
|
4456
|
-
|
|
5018
|
+
fs29.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
|
|
5019
|
+
fs29.copyFileSync(dest, output);
|
|
4457
5020
|
}
|
|
4458
5021
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
4459
5022
|
}
|
|
@@ -4527,8 +5090,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
4527
5090
|
init_errors();
|
|
4528
5091
|
|
|
4529
5092
|
// src/providers/minimax/generators/speech.ts
|
|
4530
|
-
import
|
|
4531
|
-
import
|
|
5093
|
+
import fs30 from "fs";
|
|
5094
|
+
import path34 from "path";
|
|
4532
5095
|
async function generateMinimaxSpeech(opts) {
|
|
4533
5096
|
const {
|
|
4534
5097
|
apiKey,
|
|
@@ -4575,12 +5138,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
4575
5138
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
4576
5139
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
4577
5140
|
const outDir = getOutputDir();
|
|
4578
|
-
const dest =
|
|
4579
|
-
|
|
5141
|
+
const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
5142
|
+
fs30.writeFileSync(dest, audioBytes);
|
|
4580
5143
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
4581
5144
|
if (output) {
|
|
4582
|
-
|
|
4583
|
-
|
|
5145
|
+
fs30.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
|
|
5146
|
+
fs30.copyFileSync(dest, output);
|
|
4584
5147
|
}
|
|
4585
5148
|
return { status: "success", generatedAudio: dest, model };
|
|
4586
5149
|
}
|
|
@@ -4627,8 +5190,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
4627
5190
|
init_errors();
|
|
4628
5191
|
|
|
4629
5192
|
// src/providers/minimax/generators/video.ts
|
|
4630
|
-
import
|
|
4631
|
-
import
|
|
5193
|
+
import fs31 from "fs";
|
|
5194
|
+
import path35 from "path";
|
|
4632
5195
|
async function generateMinimaxVideo(opts) {
|
|
4633
5196
|
const {
|
|
4634
5197
|
apiKey,
|
|
@@ -4674,18 +5237,18 @@ async function generateMinimaxVideo(opts) {
|
|
|
4674
5237
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
4675
5238
|
}
|
|
4676
5239
|
const outDir = getOutputDir();
|
|
4677
|
-
const dest =
|
|
5240
|
+
const dest = path35.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
4678
5241
|
try {
|
|
4679
5242
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
4680
5243
|
} catch (e) {
|
|
4681
5244
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
4682
5245
|
}
|
|
4683
5246
|
const elapsed = (Date.now() - start) / 1e3;
|
|
4684
|
-
const stat =
|
|
5247
|
+
const stat = fs31.statSync(dest);
|
|
4685
5248
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
4686
5249
|
if (output) {
|
|
4687
|
-
|
|
4688
|
-
|
|
5250
|
+
fs31.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
|
|
5251
|
+
fs31.copyFileSync(dest, output);
|
|
4689
5252
|
}
|
|
4690
5253
|
await maybeDownloadThumb(pollResult, dest, {
|
|
4691
5254
|
skip: thumb === false,
|
|
@@ -4748,8 +5311,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
4748
5311
|
// src/providers/minimax/generators/image.ts
|
|
4749
5312
|
init_errors();
|
|
4750
5313
|
init_http_client();
|
|
4751
|
-
import
|
|
4752
|
-
import
|
|
5314
|
+
import fs32 from "fs";
|
|
5315
|
+
import path36 from "path";
|
|
4753
5316
|
async function generateMinimaxImage(opts) {
|
|
4754
5317
|
const {
|
|
4755
5318
|
apiKey,
|
|
@@ -4787,8 +5350,8 @@ async function generateMinimaxImage(opts) {
|
|
|
4787
5350
|
try {
|
|
4788
5351
|
const bytes = await fetchBytes(url);
|
|
4789
5352
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
4790
|
-
const dest =
|
|
4791
|
-
|
|
5353
|
+
const dest = path36.join(outDir, fname);
|
|
5354
|
+
fs32.writeFileSync(dest, bytes);
|
|
4792
5355
|
savedFiles.push(dest);
|
|
4793
5356
|
logger?.success(`Saved: ${dest}`);
|
|
4794
5357
|
} catch (e) {
|
|
@@ -4799,8 +5362,8 @@ async function generateMinimaxImage(opts) {
|
|
|
4799
5362
|
}
|
|
4800
5363
|
}
|
|
4801
5364
|
if (output && savedFiles[0]) {
|
|
4802
|
-
|
|
4803
|
-
|
|
5365
|
+
fs32.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
|
|
5366
|
+
fs32.copyFileSync(savedFiles[0], output);
|
|
4804
5367
|
}
|
|
4805
5368
|
return { status: "success", generatedImages: savedFiles, model };
|
|
4806
5369
|
}
|
|
@@ -4840,8 +5403,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4840
5403
|
// src/providers/minimax/commands/image-to-image.ts
|
|
4841
5404
|
init_errors();
|
|
4842
5405
|
init_http_client();
|
|
4843
|
-
import
|
|
4844
|
-
import
|
|
5406
|
+
import fs33 from "fs";
|
|
5407
|
+
import path37 from "path";
|
|
4845
5408
|
function registerMinimaxImageToImageCommand(parent) {
|
|
4846
5409
|
parent.command("image-to-image").alias("i2i").description(
|
|
4847
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."
|
|
@@ -4896,8 +5459,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
4896
5459
|
if (!url) continue;
|
|
4897
5460
|
try {
|
|
4898
5461
|
const bytes = await fetchBytes(url);
|
|
4899
|
-
const dest =
|
|
4900
|
-
|
|
5462
|
+
const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
5463
|
+
fs33.writeFileSync(dest, bytes);
|
|
4901
5464
|
saved.push(dest);
|
|
4902
5465
|
logger.success(`Saved: ${dest}`);
|
|
4903
5466
|
} catch (e) {
|
|
@@ -4908,8 +5471,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
4908
5471
|
}
|
|
4909
5472
|
}
|
|
4910
5473
|
if (opts.output && saved[0]) {
|
|
4911
|
-
|
|
4912
|
-
|
|
5474
|
+
fs33.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
|
|
5475
|
+
fs33.copyFileSync(saved[0], opts.output);
|
|
4913
5476
|
logger.success(`Copied to: ${opts.output}`);
|
|
4914
5477
|
}
|
|
4915
5478
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -5044,12 +5607,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
5044
5607
|
|
|
5045
5608
|
// src/providers/openai/openai-audio.ts
|
|
5046
5609
|
init_http_client();
|
|
5047
|
-
import
|
|
5048
|
-
import
|
|
5610
|
+
import fs35 from "fs";
|
|
5611
|
+
import path39 from "path";
|
|
5049
5612
|
|
|
5050
5613
|
// src/providers/openai/client.ts
|
|
5051
|
-
import
|
|
5052
|
-
import
|
|
5614
|
+
import fs34 from "fs";
|
|
5615
|
+
import path38 from "path";
|
|
5053
5616
|
init_errors();
|
|
5054
5617
|
init_http_client();
|
|
5055
5618
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -5116,10 +5679,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
5116
5679
|
return Buffer.from(await res.arrayBuffer());
|
|
5117
5680
|
}
|
|
5118
5681
|
function fileFromPath(filePath) {
|
|
5119
|
-
const abs =
|
|
5120
|
-
if (!
|
|
5121
|
-
const bytes =
|
|
5122
|
-
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));
|
|
5123
5686
|
}
|
|
5124
5687
|
|
|
5125
5688
|
// src/providers/openai/openai-audio.ts
|
|
@@ -5158,7 +5721,7 @@ async function resolveAudioReferenceDataUrl(ref) {
|
|
|
5158
5721
|
}
|
|
5159
5722
|
function audioMimeFromReference(ref) {
|
|
5160
5723
|
const pathname = new URL(ref).pathname;
|
|
5161
|
-
const ext =
|
|
5724
|
+
const ext = path39.extname(pathname).toLowerCase();
|
|
5162
5725
|
switch (ext) {
|
|
5163
5726
|
case ".mp3":
|
|
5164
5727
|
return "audio/mpeg";
|
|
@@ -5177,12 +5740,12 @@ function audioMimeFromReference(ref) {
|
|
|
5177
5740
|
async function generateOpenAISpeech(opts) {
|
|
5178
5741
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
5179
5742
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
5180
|
-
const dest =
|
|
5181
|
-
|
|
5743
|
+
const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
5744
|
+
fs35.writeFileSync(dest, bytes);
|
|
5182
5745
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
5183
5746
|
if (opts.output) {
|
|
5184
|
-
|
|
5185
|
-
|
|
5747
|
+
fs35.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
|
|
5748
|
+
fs35.copyFileSync(dest, opts.output);
|
|
5186
5749
|
}
|
|
5187
5750
|
return dest;
|
|
5188
5751
|
}
|
|
@@ -5204,9 +5767,9 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
5204
5767
|
);
|
|
5205
5768
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
5206
5769
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
5207
|
-
const dest = opts.output ?
|
|
5208
|
-
|
|
5209
|
-
|
|
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));
|
|
5210
5773
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
5211
5774
|
return { path: dest, text };
|
|
5212
5775
|
}
|
|
@@ -5233,9 +5796,9 @@ Generated audio: ${dest}`);
|
|
|
5233
5796
|
}
|
|
5234
5797
|
|
|
5235
5798
|
// src/providers/openai/codex-image-driver.ts
|
|
5236
|
-
import
|
|
5799
|
+
import fs36 from "fs";
|
|
5237
5800
|
import os2 from "os";
|
|
5238
|
-
import
|
|
5801
|
+
import path40 from "path";
|
|
5239
5802
|
import { execa } from "execa";
|
|
5240
5803
|
init_errors();
|
|
5241
5804
|
var defaultRunner = async (cmd, args, opts) => {
|
|
@@ -5284,12 +5847,12 @@ function sanitizeCodexEnv() {
|
|
|
5284
5847
|
async function runCodexImageGeneration(opts) {
|
|
5285
5848
|
const runner = opts.runner ?? defaultRunner;
|
|
5286
5849
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
5287
|
-
const dest = opts.output ?
|
|
5288
|
-
|
|
5289
|
-
const tmp =
|
|
5290
|
-
const schema =
|
|
5291
|
-
const result =
|
|
5292
|
-
|
|
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(
|
|
5293
5856
|
schema,
|
|
5294
5857
|
JSON.stringify({
|
|
5295
5858
|
type: "object",
|
|
@@ -5319,7 +5882,7 @@ async function runCodexImageGeneration(opts) {
|
|
|
5319
5882
|
if (res.exitCode !== 0) {
|
|
5320
5883
|
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
5321
5884
|
}
|
|
5322
|
-
if (!
|
|
5885
|
+
if (!fs36.existsSync(dest) || fs36.statSync(dest).size === 0) {
|
|
5323
5886
|
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
5324
5887
|
}
|
|
5325
5888
|
return { path: dest };
|
|
@@ -5337,8 +5900,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
5337
5900
|
// src/providers/openai/openai-image.ts
|
|
5338
5901
|
init_errors();
|
|
5339
5902
|
init_http_client();
|
|
5340
|
-
import
|
|
5341
|
-
import
|
|
5903
|
+
import fs37 from "fs";
|
|
5904
|
+
import path41 from "path";
|
|
5342
5905
|
function buildImageGenerationBody(opts) {
|
|
5343
5906
|
return {
|
|
5344
5907
|
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
@@ -5371,14 +5934,14 @@ function saveOpenAIImages(opts) {
|
|
|
5371
5934
|
for (let i = 0; i < items.length; i++) {
|
|
5372
5935
|
const b64 = items[i]?.b64_json;
|
|
5373
5936
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
5374
|
-
const dest =
|
|
5375
|
-
|
|
5937
|
+
const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
5938
|
+
fs37.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
5376
5939
|
saved.push(dest);
|
|
5377
5940
|
opts.logger?.success(`Saved: ${dest}`);
|
|
5378
5941
|
}
|
|
5379
5942
|
if (opts.output && saved[0]) {
|
|
5380
|
-
|
|
5381
|
-
|
|
5943
|
+
fs37.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
|
|
5944
|
+
fs37.copyFileSync(saved[0], opts.output);
|
|
5382
5945
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
5383
5946
|
}
|
|
5384
5947
|
return saved;
|
|
@@ -5387,7 +5950,7 @@ async function resolveImageRefBytes(ref) {
|
|
|
5387
5950
|
if (isHttpUrl(ref)) {
|
|
5388
5951
|
const bytes2 = await fetchBytes(ref);
|
|
5389
5952
|
const pathname = new URL(ref).pathname;
|
|
5390
|
-
const filename =
|
|
5953
|
+
const filename = path41.basename(pathname) || "reference.png";
|
|
5391
5954
|
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
5392
5955
|
}
|
|
5393
5956
|
const file = fileFromPath(ref);
|
|
@@ -5432,7 +5995,7 @@ function sniffImageMime2(buf, name) {
|
|
|
5432
5995
|
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
5433
5996
|
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
5434
5997
|
return "image/webp";
|
|
5435
|
-
const ext =
|
|
5998
|
+
const ext = path41.extname(name).toLowerCase();
|
|
5436
5999
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
5437
6000
|
if (ext === ".webp") return "image/webp";
|
|
5438
6001
|
return "image/png";
|
|
@@ -5576,8 +6139,8 @@ function registerOpenAICommands(program2) {
|
|
|
5576
6139
|
}
|
|
5577
6140
|
|
|
5578
6141
|
// src/providers/openrouter/client.ts
|
|
5579
|
-
import
|
|
5580
|
-
import
|
|
6142
|
+
import fs38 from "fs";
|
|
6143
|
+
import path42 from "path";
|
|
5581
6144
|
init_errors();
|
|
5582
6145
|
init_http_client();
|
|
5583
6146
|
|
|
@@ -5742,8 +6305,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
5742
6305
|
if (!url) continue;
|
|
5743
6306
|
try {
|
|
5744
6307
|
const bytes = await fetchBytes(url);
|
|
5745
|
-
const dest =
|
|
5746
|
-
|
|
6308
|
+
const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
6309
|
+
fs38.writeFileSync(dest, bytes);
|
|
5747
6310
|
savedFiles.push(dest);
|
|
5748
6311
|
logger?.success(`Saved: ${dest}`);
|
|
5749
6312
|
} catch (e) {
|
|
@@ -5754,8 +6317,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
5754
6317
|
}
|
|
5755
6318
|
}
|
|
5756
6319
|
if (output && savedFiles[0]) {
|
|
5757
|
-
|
|
5758
|
-
|
|
6320
|
+
fs38.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
|
|
6321
|
+
fs38.writeFileSync(output, fs38.readFileSync(savedFiles[0]));
|
|
5759
6322
|
}
|
|
5760
6323
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
5761
6324
|
}
|
|
@@ -5796,8 +6359,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
5796
6359
|
}
|
|
5797
6360
|
|
|
5798
6361
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
5799
|
-
import
|
|
5800
|
-
import
|
|
6362
|
+
import fs39 from "fs";
|
|
6363
|
+
import path43 from "path";
|
|
5801
6364
|
init_errors();
|
|
5802
6365
|
init_http_client();
|
|
5803
6366
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -5890,14 +6453,14 @@ async function runImageToImage(opts) {
|
|
|
5890
6453
|
const url = parsed.urls[i];
|
|
5891
6454
|
if (!url) continue;
|
|
5892
6455
|
const bytes = await fetchBytes(url);
|
|
5893
|
-
const dest =
|
|
5894
|
-
|
|
6456
|
+
const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
6457
|
+
fs39.writeFileSync(dest, bytes);
|
|
5895
6458
|
saved.push(dest);
|
|
5896
6459
|
opts.logger.success(`Saved: ${dest}`);
|
|
5897
6460
|
}
|
|
5898
6461
|
if (opts.output && saved[0]) {
|
|
5899
|
-
|
|
5900
|
-
|
|
6462
|
+
fs39.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
|
|
6463
|
+
fs39.copyFileSync(saved[0], opts.output);
|
|
5901
6464
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
5902
6465
|
}
|
|
5903
6466
|
return saved;
|
|
@@ -5943,13 +6506,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
5943
6506
|
}
|
|
5944
6507
|
|
|
5945
6508
|
// src/providers/openrouter/commands/video-status.ts
|
|
5946
|
-
import
|
|
5947
|
-
import
|
|
6509
|
+
import fs40 from "fs";
|
|
6510
|
+
import path44 from "path";
|
|
5948
6511
|
async function downloadOpenRouterVideo(opts) {
|
|
5949
6512
|
const { jobId, logger, source } = opts;
|
|
5950
6513
|
const apiKey = requireOpenRouterKey();
|
|
5951
6514
|
const url = videoContentUrl(jobId);
|
|
5952
|
-
const dest =
|
|
6515
|
+
const dest = path44.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
5953
6516
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
5954
6517
|
if (!r.ok) {
|
|
5955
6518
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -5959,14 +6522,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
5959
6522
|
logger.error("Empty response body");
|
|
5960
6523
|
process.exit(1);
|
|
5961
6524
|
}
|
|
5962
|
-
|
|
5963
|
-
const ws =
|
|
6525
|
+
fs40.mkdirSync(path44.dirname(dest), { recursive: true });
|
|
6526
|
+
const ws = fs40.createWriteStream(dest);
|
|
5964
6527
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
5965
6528
|
await pipeline2(r.body, ws);
|
|
5966
6529
|
logger.success(`Saved ${dest}`);
|
|
5967
6530
|
if (opts.output) {
|
|
5968
|
-
|
|
5969
|
-
|
|
6531
|
+
fs40.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
|
|
6532
|
+
fs40.copyFileSync(dest, opts.output);
|
|
5970
6533
|
logger.success(`Copied to ${opts.output}`);
|
|
5971
6534
|
}
|
|
5972
6535
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -6184,6 +6747,7 @@ To configure multix, set at least one provider API key:
|
|
|
6184
6747
|
MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
|
|
6185
6748
|
Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
|
|
6186
6749
|
BytePlus : https://console.byteplus.com/auth/api-keys
|
|
6750
|
+
fal.ai : https://fal.ai/dashboard/keys
|
|
6187
6751
|
|
|
6188
6752
|
Option A \u2014 user global config (recommended):
|
|
6189
6753
|
echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
|
|
@@ -6246,6 +6810,12 @@ var PROVIDERS = [
|
|
|
6246
6810
|
envPrimary: "ELEVENLABS_API_KEY",
|
|
6247
6811
|
optional: true,
|
|
6248
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"
|
|
6249
6819
|
}
|
|
6250
6820
|
];
|
|
6251
6821
|
function resolveProviderKey(p) {
|
|
@@ -6377,8 +6947,8 @@ function registerCheckCommand(program2) {
|
|
|
6377
6947
|
|
|
6378
6948
|
// src/commands/doc/convert.ts
|
|
6379
6949
|
init_errors();
|
|
6380
|
-
import
|
|
6381
|
-
import
|
|
6950
|
+
import fs41 from "fs";
|
|
6951
|
+
import path45 from "path";
|
|
6382
6952
|
|
|
6383
6953
|
// src/commands/doc/default-prompt.ts
|
|
6384
6954
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -6399,18 +6969,18 @@ async function convertDocuments(opts) {
|
|
|
6399
6969
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
6400
6970
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
6401
6971
|
for (const f of inputs) {
|
|
6402
|
-
if (!
|
|
6972
|
+
if (!fs41.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
6403
6973
|
}
|
|
6404
6974
|
let outputPath;
|
|
6405
6975
|
if (output) {
|
|
6406
|
-
outputPath =
|
|
6976
|
+
outputPath = path45.resolve(output);
|
|
6407
6977
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
6408
|
-
const base =
|
|
6409
|
-
outputPath =
|
|
6978
|
+
const base = path45.basename(inputs[0], path45.extname(inputs[0]));
|
|
6979
|
+
outputPath = path45.join(getOutputDir(), `${base}-extraction.md`);
|
|
6410
6980
|
} else {
|
|
6411
|
-
outputPath =
|
|
6981
|
+
outputPath = path45.join(getOutputDir(), "document-extraction.md");
|
|
6412
6982
|
}
|
|
6413
|
-
|
|
6983
|
+
fs41.mkdirSync(path45.dirname(outputPath), { recursive: true });
|
|
6414
6984
|
const results = [];
|
|
6415
6985
|
for (let i = 0; i < inputs.length; i++) {
|
|
6416
6986
|
const filePath = inputs[i];
|
|
@@ -6442,7 +7012,7 @@ async function convertDocuments(opts) {
|
|
|
6442
7012
|
if (attempt < 2) {
|
|
6443
7013
|
const wait = 2 ** attempt * 1e3;
|
|
6444
7014
|
logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
|
|
6445
|
-
await
|
|
7015
|
+
await sleep6(wait);
|
|
6446
7016
|
}
|
|
6447
7017
|
}
|
|
6448
7018
|
}
|
|
@@ -6461,7 +7031,7 @@ async function convertDocuments(opts) {
|
|
|
6461
7031
|
`);
|
|
6462
7032
|
for (const r of results) {
|
|
6463
7033
|
lines.push(`
|
|
6464
|
-
## ${
|
|
7034
|
+
## ${path45.basename(r.file)}
|
|
6465
7035
|
`);
|
|
6466
7036
|
if (r.status === "success" && r.markdown) {
|
|
6467
7037
|
lines.push(`${r.markdown}
|
|
@@ -6474,7 +7044,7 @@ async function convertDocuments(opts) {
|
|
|
6474
7044
|
}
|
|
6475
7045
|
lines.push("\n---\n");
|
|
6476
7046
|
}
|
|
6477
|
-
|
|
7047
|
+
fs41.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
6478
7048
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
6479
7049
|
console.log(`
|
|
6480
7050
|
${"=".repeat(50)}`);
|
|
@@ -6490,7 +7060,7 @@ ${"=".repeat(50)}`);
|
|
|
6490
7060
|
const failed = results.filter((r) => r.status === "error").length;
|
|
6491
7061
|
if (failed > 0) process.exit(1);
|
|
6492
7062
|
}
|
|
6493
|
-
function
|
|
7063
|
+
function sleep6(ms) {
|
|
6494
7064
|
return new Promise((r) => setTimeout(r, ms));
|
|
6495
7065
|
}
|
|
6496
7066
|
|
|
@@ -6522,12 +7092,12 @@ function registerDocCommands(program2) {
|
|
|
6522
7092
|
|
|
6523
7093
|
// src/commands/media/index.ts
|
|
6524
7094
|
init_errors();
|
|
6525
|
-
import
|
|
6526
|
-
import
|
|
7095
|
+
import fs44 from "fs";
|
|
7096
|
+
import path48 from "path";
|
|
6527
7097
|
|
|
6528
7098
|
// src/commands/media/batch.ts
|
|
6529
|
-
import
|
|
6530
|
-
import
|
|
7099
|
+
import fs42 from "fs";
|
|
7100
|
+
import path46 from "path";
|
|
6531
7101
|
|
|
6532
7102
|
// src/commands/media/detect-type.ts
|
|
6533
7103
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -6723,10 +7293,10 @@ async function batchOptimize(opts) {
|
|
|
6723
7293
|
verbose,
|
|
6724
7294
|
logger
|
|
6725
7295
|
} = opts;
|
|
6726
|
-
|
|
6727
|
-
const entries =
|
|
7296
|
+
fs42.mkdirSync(outputDir, { recursive: true });
|
|
7297
|
+
const entries = fs42.readdirSync(inputDir);
|
|
6728
7298
|
const mediaFiles = entries.filter((f) => {
|
|
6729
|
-
const ext =
|
|
7299
|
+
const ext = path46.extname(f).toLowerCase();
|
|
6730
7300
|
return ALL_EXTS.has(ext);
|
|
6731
7301
|
});
|
|
6732
7302
|
if (mediaFiles.length === 0) {
|
|
@@ -6737,9 +7307,9 @@ async function batchOptimize(opts) {
|
|
|
6737
7307
|
let succeeded = 0;
|
|
6738
7308
|
const errors = [];
|
|
6739
7309
|
for (const file of mediaFiles) {
|
|
6740
|
-
const inputPath =
|
|
6741
|
-
const outputPath =
|
|
6742
|
-
const ext =
|
|
7310
|
+
const inputPath = path46.join(inputDir, file);
|
|
7311
|
+
const outputPath = path46.join(outputDir, file);
|
|
7312
|
+
const ext = path46.extname(file).toLowerCase();
|
|
6743
7313
|
const kind = extToKind(ext);
|
|
6744
7314
|
logger?.debug(`Processing: ${file}`);
|
|
6745
7315
|
try {
|
|
@@ -6776,8 +7346,8 @@ async function batchOptimize(opts) {
|
|
|
6776
7346
|
|
|
6777
7347
|
// src/commands/media/split-video.ts
|
|
6778
7348
|
init_errors();
|
|
6779
|
-
import
|
|
6780
|
-
import
|
|
7349
|
+
import fs43 from "fs";
|
|
7350
|
+
import path47 from "path";
|
|
6781
7351
|
async function splitVideo(opts) {
|
|
6782
7352
|
await assertFfmpeg();
|
|
6783
7353
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -6788,10 +7358,10 @@ async function splitVideo(opts) {
|
|
|
6788
7358
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
6789
7359
|
return [input];
|
|
6790
7360
|
}
|
|
6791
|
-
|
|
6792
|
-
const ext =
|
|
6793
|
-
const baseName =
|
|
6794
|
-
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}`);
|
|
6795
7365
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
6796
7366
|
const args = [
|
|
6797
7367
|
"-i",
|
|
@@ -6810,8 +7380,8 @@ async function splitVideo(opts) {
|
|
|
6810
7380
|
await runFfmpeg(args, { verbose, logger });
|
|
6811
7381
|
const files = [];
|
|
6812
7382
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
6813
|
-
const fname =
|
|
6814
|
-
if (
|
|
7383
|
+
const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
7384
|
+
if (fs43.existsSync(fname)) files.push(fname);
|
|
6815
7385
|
}
|
|
6816
7386
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
6817
7387
|
return files;
|
|
@@ -6825,9 +7395,9 @@ function registerMediaCommands(program2) {
|
|
|
6825
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(
|
|
6826
7396
|
async (opts) => {
|
|
6827
7397
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6828
|
-
if (!
|
|
6829
|
-
|
|
6830
|
-
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();
|
|
6831
7401
|
const kind = extToKind(ext);
|
|
6832
7402
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
6833
7403
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -6867,7 +7437,7 @@ function registerMediaCommands(program2) {
|
|
|
6867
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(
|
|
6868
7438
|
async (opts) => {
|
|
6869
7439
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6870
|
-
if (!
|
|
7440
|
+
if (!fs44.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
6871
7441
|
const chunks = await splitVideo({
|
|
6872
7442
|
input: opts.input,
|
|
6873
7443
|
outputDir: opts.outputDir,
|
|
@@ -6883,7 +7453,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
6883
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(
|
|
6884
7454
|
async (opts) => {
|
|
6885
7455
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6886
|
-
if (!
|
|
7456
|
+
if (!fs44.existsSync(opts.inputDir))
|
|
6887
7457
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
6888
7458
|
const result = await batchOptimize({
|
|
6889
7459
|
inputDir: opts.inputDir,
|
|
@@ -6904,9 +7474,157 @@ Processed: ${result.succeeded}/${result.total} files`);
|
|
|
6904
7474
|
);
|
|
6905
7475
|
}
|
|
6906
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
|
+
|
|
6907
7624
|
// src/commands/index.ts
|
|
6908
7625
|
function registerCommands(program2) {
|
|
6909
7626
|
registerCheckCommand(program2);
|
|
7627
|
+
registerUpdateCommand(program2);
|
|
6910
7628
|
registerGeminiCommands(program2);
|
|
6911
7629
|
registerMinimaxCommands(program2);
|
|
6912
7630
|
registerOpenAICommands(program2);
|
|
@@ -6915,6 +7633,7 @@ function registerCommands(program2) {
|
|
|
6915
7633
|
registerBytePlusCommands(program2);
|
|
6916
7634
|
registerCloudflareCommands(program2);
|
|
6917
7635
|
registerElevenLabsCommands(program2);
|
|
7636
|
+
registerFalCommands(program2);
|
|
6918
7637
|
registerMediaCommands(program2);
|
|
6919
7638
|
registerDocCommands(program2);
|
|
6920
7639
|
}
|
|
@@ -6925,10 +7644,10 @@ init_http_client();
|
|
|
6925
7644
|
|
|
6926
7645
|
// src/cli.ts
|
|
6927
7646
|
loadEnv();
|
|
6928
|
-
var
|
|
6929
|
-
var pkg =
|
|
7647
|
+
var require3 = createRequire2(import.meta.url);
|
|
7648
|
+
var pkg = require3("../package.json");
|
|
6930
7649
|
var program = new Command().name("multix").description(
|
|
6931
|
-
"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"
|
|
6932
7651
|
).version(pkg.version);
|
|
6933
7652
|
registerCommands(program);
|
|
6934
7653
|
program.parseAsync(process.argv).catch((err2) => {
|