@mrgoonie/multix 0.7.2 → 0.8.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 +14 -0
- package/README.md +5 -0
- package/dist/cli.js +671 -531
- package/package.json +1 -1
- package/skill/SKILL.md +2 -2
package/dist/cli.js
CHANGED
|
@@ -95,151 +95,6 @@ var init_errors = __esm({
|
|
|
95
95
|
}
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
// src/core/fetch-json.ts
|
|
99
|
-
function parseRetryAfter(value, now = Date.now()) {
|
|
100
|
-
if (!value) return void 0;
|
|
101
|
-
const trimmed = value.trim();
|
|
102
|
-
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
|
|
103
|
-
const date = Date.parse(trimmed);
|
|
104
|
-
if (Number.isNaN(date)) return void 0;
|
|
105
|
-
return Math.max(0, date - now);
|
|
106
|
-
}
|
|
107
|
-
async function httpErrorFromResponse(response, url) {
|
|
108
|
-
let snippet = "";
|
|
109
|
-
try {
|
|
110
|
-
const text = await response.text();
|
|
111
|
-
snippet = text.slice(0, SNIPPET_MAX);
|
|
112
|
-
} catch {
|
|
113
|
-
snippet = "(could not read body)";
|
|
114
|
-
}
|
|
115
|
-
return new HttpError(
|
|
116
|
-
response.status,
|
|
117
|
-
snippet,
|
|
118
|
-
url,
|
|
119
|
-
parseRetryAfter(response.headers.get("retry-after"))
|
|
120
|
-
);
|
|
121
|
-
}
|
|
122
|
-
async function fetchJson(opts) {
|
|
123
|
-
const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
124
|
-
const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
|
|
125
|
-
const init = {
|
|
126
|
-
method,
|
|
127
|
-
headers: {
|
|
128
|
-
"Content-Type": "application/json",
|
|
129
|
-
...headers
|
|
130
|
-
},
|
|
131
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
132
|
-
};
|
|
133
|
-
if (body !== void 0) {
|
|
134
|
-
init.body = JSON.stringify(body);
|
|
135
|
-
}
|
|
136
|
-
let response;
|
|
137
|
-
try {
|
|
138
|
-
response = await doFetch(url, init);
|
|
139
|
-
} catch (cause) {
|
|
140
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
141
|
-
throw new HttpError(0, msg, url);
|
|
142
|
-
}
|
|
143
|
-
if (!response.ok) {
|
|
144
|
-
throw await httpErrorFromResponse(response, url);
|
|
145
|
-
}
|
|
146
|
-
let text;
|
|
147
|
-
try {
|
|
148
|
-
text = await response.text();
|
|
149
|
-
} catch (cause) {
|
|
150
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
151
|
-
throw new HttpError(0, `Failed to read response body: ${msg}`, url);
|
|
152
|
-
}
|
|
153
|
-
if (text.trim() === "") return void 0;
|
|
154
|
-
try {
|
|
155
|
-
return JSON.parse(text);
|
|
156
|
-
} catch {
|
|
157
|
-
throw new HttpError(
|
|
158
|
-
response.status,
|
|
159
|
-
`Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
|
|
160
|
-
url
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
var DEFAULT_TIMEOUT_MS, SNIPPET_MAX;
|
|
165
|
-
var init_fetch_json = __esm({
|
|
166
|
-
"src/core/fetch-json.ts"() {
|
|
167
|
-
"use strict";
|
|
168
|
-
init_errors();
|
|
169
|
-
DEFAULT_TIMEOUT_MS = 12e4;
|
|
170
|
-
SNIPPET_MAX = 500;
|
|
171
|
-
}
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// src/core/http-client.ts
|
|
175
|
-
var http_client_exports = {};
|
|
176
|
-
__export(http_client_exports, {
|
|
177
|
-
downloadFile: () => downloadFile,
|
|
178
|
-
fetchBytes: () => fetchBytes,
|
|
179
|
-
httpJson: () => httpJson
|
|
180
|
-
});
|
|
181
|
-
import { once } from "events";
|
|
182
|
-
import fs4 from "fs";
|
|
183
|
-
import path4 from "path";
|
|
184
|
-
import { pipeline } from "stream/promises";
|
|
185
|
-
async function httpJson(opts) {
|
|
186
|
-
return fetchJson(opts);
|
|
187
|
-
}
|
|
188
|
-
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers) {
|
|
189
|
-
fs4.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
190
|
-
let response;
|
|
191
|
-
try {
|
|
192
|
-
response = await globalThis.fetch(url, {
|
|
193
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
194
|
-
...headers ? { headers } : {}
|
|
195
|
-
});
|
|
196
|
-
} catch (cause) {
|
|
197
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
198
|
-
throw new HttpError(0, msg, url);
|
|
199
|
-
}
|
|
200
|
-
if (!response.ok) {
|
|
201
|
-
throw new HttpError(response.status, "(download failed)", url);
|
|
202
|
-
}
|
|
203
|
-
if (!response.body) {
|
|
204
|
-
throw new HttpError(0, "Response body is null", url);
|
|
205
|
-
}
|
|
206
|
-
const fileStream = fs4.createWriteStream(dest);
|
|
207
|
-
try {
|
|
208
|
-
await pipeline(response.body, fileStream);
|
|
209
|
-
} catch (err2) {
|
|
210
|
-
if (!fileStream.closed) await once(fileStream, "close");
|
|
211
|
-
throw err2;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
async function fetchBytes(urlOrDataUri) {
|
|
215
|
-
if (urlOrDataUri.startsWith("data:")) {
|
|
216
|
-
const commaIdx = urlOrDataUri.indexOf(",");
|
|
217
|
-
if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
|
|
218
|
-
const encoded = urlOrDataUri.slice(commaIdx + 1);
|
|
219
|
-
return Buffer.from(encoded, "base64");
|
|
220
|
-
}
|
|
221
|
-
let response;
|
|
222
|
-
try {
|
|
223
|
-
response = await globalThis.fetch(urlOrDataUri, {
|
|
224
|
-
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
225
|
-
});
|
|
226
|
-
} catch (cause) {
|
|
227
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
228
|
-
throw new HttpError(0, msg, urlOrDataUri);
|
|
229
|
-
}
|
|
230
|
-
if (!response.ok) {
|
|
231
|
-
throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
|
|
232
|
-
}
|
|
233
|
-
return Buffer.from(await response.arrayBuffer());
|
|
234
|
-
}
|
|
235
|
-
var init_http_client = __esm({
|
|
236
|
-
"src/core/http-client.ts"() {
|
|
237
|
-
"use strict";
|
|
238
|
-
init_errors();
|
|
239
|
-
init_fetch_json();
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
|
|
243
98
|
// src/cli.ts
|
|
244
99
|
import { createRequire as createRequire2 } from "module";
|
|
245
100
|
import { Command } from "commander";
|
|
@@ -447,7 +302,135 @@ function resolveKey(name) {
|
|
|
447
302
|
|
|
448
303
|
// src/providers/byteplus/client.ts
|
|
449
304
|
init_errors();
|
|
450
|
-
|
|
305
|
+
|
|
306
|
+
// src/core/http-client.ts
|
|
307
|
+
init_errors();
|
|
308
|
+
import { once } from "events";
|
|
309
|
+
import fs4 from "fs";
|
|
310
|
+
import path4 from "path";
|
|
311
|
+
import { pipeline } from "stream/promises";
|
|
312
|
+
|
|
313
|
+
// src/core/fetch-json.ts
|
|
314
|
+
init_errors();
|
|
315
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
316
|
+
var SNIPPET_MAX = 500;
|
|
317
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
318
|
+
if (!value) return void 0;
|
|
319
|
+
const trimmed = value.trim();
|
|
320
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
|
|
321
|
+
const date = Date.parse(trimmed);
|
|
322
|
+
if (Number.isNaN(date)) return void 0;
|
|
323
|
+
return Math.max(0, date - now);
|
|
324
|
+
}
|
|
325
|
+
async function httpErrorFromResponse(response, url) {
|
|
326
|
+
let snippet = "";
|
|
327
|
+
try {
|
|
328
|
+
const text = await response.text();
|
|
329
|
+
snippet = text.slice(0, SNIPPET_MAX);
|
|
330
|
+
} catch {
|
|
331
|
+
snippet = "(could not read body)";
|
|
332
|
+
}
|
|
333
|
+
return new HttpError(
|
|
334
|
+
response.status,
|
|
335
|
+
snippet,
|
|
336
|
+
url,
|
|
337
|
+
parseRetryAfter(response.headers.get("retry-after"))
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
async function fetchJson(opts) {
|
|
341
|
+
const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
342
|
+
const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
|
|
343
|
+
const init = {
|
|
344
|
+
method,
|
|
345
|
+
headers: {
|
|
346
|
+
"Content-Type": "application/json",
|
|
347
|
+
...headers2
|
|
348
|
+
},
|
|
349
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
350
|
+
};
|
|
351
|
+
if (body !== void 0) {
|
|
352
|
+
init.body = JSON.stringify(body);
|
|
353
|
+
}
|
|
354
|
+
let response;
|
|
355
|
+
try {
|
|
356
|
+
response = await doFetch(url, init);
|
|
357
|
+
} catch (cause) {
|
|
358
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
359
|
+
throw new HttpError(0, msg, url);
|
|
360
|
+
}
|
|
361
|
+
if (!response.ok) {
|
|
362
|
+
throw await httpErrorFromResponse(response, url);
|
|
363
|
+
}
|
|
364
|
+
let text;
|
|
365
|
+
try {
|
|
366
|
+
text = await response.text();
|
|
367
|
+
} catch (cause) {
|
|
368
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
369
|
+
throw new HttpError(0, `Failed to read response body: ${msg}`, url);
|
|
370
|
+
}
|
|
371
|
+
if (text.trim() === "") return void 0;
|
|
372
|
+
try {
|
|
373
|
+
return JSON.parse(text);
|
|
374
|
+
} catch {
|
|
375
|
+
throw new HttpError(
|
|
376
|
+
response.status,
|
|
377
|
+
`Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
|
|
378
|
+
url
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// src/core/http-client.ts
|
|
384
|
+
async function httpJson(opts) {
|
|
385
|
+
return fetchJson(opts);
|
|
386
|
+
}
|
|
387
|
+
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers2) {
|
|
388
|
+
fs4.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
389
|
+
let response;
|
|
390
|
+
try {
|
|
391
|
+
response = await globalThis.fetch(url, {
|
|
392
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
393
|
+
...headers2 ? { headers: headers2 } : {}
|
|
394
|
+
});
|
|
395
|
+
} catch (cause) {
|
|
396
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
397
|
+
throw new HttpError(0, msg, url);
|
|
398
|
+
}
|
|
399
|
+
if (!response.ok) {
|
|
400
|
+
throw new HttpError(response.status, "(download failed)", url);
|
|
401
|
+
}
|
|
402
|
+
if (!response.body) {
|
|
403
|
+
throw new HttpError(0, "Response body is null", url);
|
|
404
|
+
}
|
|
405
|
+
const fileStream = fs4.createWriteStream(dest);
|
|
406
|
+
try {
|
|
407
|
+
await pipeline(response.body, fileStream);
|
|
408
|
+
} catch (err2) {
|
|
409
|
+
if (!fileStream.closed) await once(fileStream, "close");
|
|
410
|
+
throw err2;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
async function fetchBytes(urlOrDataUri) {
|
|
414
|
+
if (urlOrDataUri.startsWith("data:")) {
|
|
415
|
+
const commaIdx = urlOrDataUri.indexOf(",");
|
|
416
|
+
if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
|
|
417
|
+
const encoded = urlOrDataUri.slice(commaIdx + 1);
|
|
418
|
+
return Buffer.from(encoded, "base64");
|
|
419
|
+
}
|
|
420
|
+
let response;
|
|
421
|
+
try {
|
|
422
|
+
response = await globalThis.fetch(urlOrDataUri, {
|
|
423
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
424
|
+
});
|
|
425
|
+
} catch (cause) {
|
|
426
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
427
|
+
throw new HttpError(0, msg, urlOrDataUri);
|
|
428
|
+
}
|
|
429
|
+
if (!response.ok) {
|
|
430
|
+
throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
|
|
431
|
+
}
|
|
432
|
+
return Buffer.from(await response.arrayBuffer());
|
|
433
|
+
}
|
|
451
434
|
|
|
452
435
|
// src/providers/byteplus/models.ts
|
|
453
436
|
var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
|
|
@@ -527,7 +510,6 @@ function createBytePlusClient() {
|
|
|
527
510
|
}
|
|
528
511
|
|
|
529
512
|
// src/providers/byteplus/generators/three-d.ts
|
|
530
|
-
init_http_client();
|
|
531
513
|
function buildThreeDTaskBody(opts) {
|
|
532
514
|
const promptText = (opts.prompt ?? "").trim();
|
|
533
515
|
const flagPart = opts.flags?.trim() ?? "";
|
|
@@ -835,7 +817,6 @@ function finalizeGeneratedImages(files, opts) {
|
|
|
835
817
|
}
|
|
836
818
|
|
|
837
819
|
// src/providers/byteplus/generators/image.ts
|
|
838
|
-
init_http_client();
|
|
839
820
|
import fs7 from "fs";
|
|
840
821
|
import path7 from "path";
|
|
841
822
|
var ASPECT_TO_SIZE = {
|
|
@@ -999,7 +980,6 @@ function collect2(value, prev) {
|
|
|
999
980
|
import path9 from "path";
|
|
1000
981
|
|
|
1001
982
|
// src/core/video-thumb.ts
|
|
1002
|
-
init_http_client();
|
|
1003
983
|
import fs8 from "fs";
|
|
1004
984
|
import path8 from "path";
|
|
1005
985
|
var THUMB_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -1084,7 +1064,6 @@ async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
|
1084
1064
|
}
|
|
1085
1065
|
|
|
1086
1066
|
// src/providers/byteplus/generators/video.ts
|
|
1087
|
-
init_http_client();
|
|
1088
1067
|
function buildFlagSuffix(opts) {
|
|
1089
1068
|
const parts = [];
|
|
1090
1069
|
if (opts.resolution) parts.push(`--rs ${opts.resolution}`);
|
|
@@ -1895,7 +1874,6 @@ function registerCloudflareCommands(program2) {
|
|
|
1895
1874
|
import fs10 from "fs";
|
|
1896
1875
|
import path14 from "path";
|
|
1897
1876
|
init_errors();
|
|
1898
|
-
init_http_client();
|
|
1899
1877
|
|
|
1900
1878
|
// src/providers/elevenlabs/models.ts
|
|
1901
1879
|
var ELEVENLABS_BASE_URL = "https://api.elevenlabs.io/v1";
|
|
@@ -2004,19 +1982,19 @@ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
|
|
|
2004
1982
|
if (opts.query) {
|
|
2005
1983
|
for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
2006
1984
|
}
|
|
2007
|
-
const
|
|
1985
|
+
const headers2 = authHeaders(apiKey);
|
|
2008
1986
|
let init;
|
|
2009
1987
|
if (body instanceof FormData) {
|
|
2010
1988
|
init = {
|
|
2011
1989
|
method: "POST",
|
|
2012
|
-
headers,
|
|
1990
|
+
headers: headers2,
|
|
2013
1991
|
body,
|
|
2014
1992
|
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
2015
1993
|
};
|
|
2016
1994
|
} else {
|
|
2017
1995
|
init = {
|
|
2018
1996
|
method: "POST",
|
|
2019
|
-
headers: { ...
|
|
1997
|
+
headers: { ...headers2, "Content-Type": "application/json" },
|
|
2020
1998
|
body: JSON.stringify(body),
|
|
2021
1999
|
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
2022
2000
|
};
|
|
@@ -2185,7 +2163,6 @@ function registerCloneCommand(parent) {
|
|
|
2185
2163
|
|
|
2186
2164
|
// src/providers/elevenlabs/commands/dub.ts
|
|
2187
2165
|
init_errors();
|
|
2188
|
-
init_http_client();
|
|
2189
2166
|
import fs12 from "fs";
|
|
2190
2167
|
import path16 from "path";
|
|
2191
2168
|
function registerDubCommand(parent) {
|
|
@@ -2669,7 +2646,6 @@ var FAL_DEFAULTS = {
|
|
|
2669
2646
|
};
|
|
2670
2647
|
|
|
2671
2648
|
// src/providers/fal/run-helpers.ts
|
|
2672
|
-
init_http_client();
|
|
2673
2649
|
import fs16 from "fs";
|
|
2674
2650
|
import path19 from "path";
|
|
2675
2651
|
|
|
@@ -2706,7 +2682,6 @@ init_redact();
|
|
|
2706
2682
|
// src/providers/fal/client.ts
|
|
2707
2683
|
init_errors();
|
|
2708
2684
|
init_errors();
|
|
2709
|
-
init_http_client();
|
|
2710
2685
|
init_redact();
|
|
2711
2686
|
function requireFalKey() {
|
|
2712
2687
|
const key = resolveKey("FAL_KEY");
|
|
@@ -3115,10 +3090,72 @@ init_errors();
|
|
|
3115
3090
|
import fs20 from "fs";
|
|
3116
3091
|
|
|
3117
3092
|
// src/providers/gemini/client.ts
|
|
3093
|
+
init_errors();
|
|
3118
3094
|
import fs18 from "fs";
|
|
3119
3095
|
import path22 from "path";
|
|
3096
|
+
|
|
3097
|
+
// src/providers/gemini/key-pool.ts
|
|
3120
3098
|
init_errors();
|
|
3121
|
-
|
|
3099
|
+
var PRIMARY_KEY_ENV = "GEMINI_API_KEY";
|
|
3100
|
+
var DEFAULT_COOLDOWN_MS = 6e4;
|
|
3101
|
+
var cooldownUntil = /* @__PURE__ */ new Map();
|
|
3102
|
+
function listGeminiKeys(env = process.env) {
|
|
3103
|
+
const numbered = Object.keys(env).map((name) => ({ name, match: /^GEMINI_API_KEY_(\d+)$/.exec(name) })).filter((e) => e.match !== null).sort((a, b) => Number(a.match[1]) - Number(b.match[1])).map((e) => e.name);
|
|
3104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3105
|
+
const keys = [];
|
|
3106
|
+
for (const name of [PRIMARY_KEY_ENV, ...numbered]) {
|
|
3107
|
+
const value = env[name]?.trim();
|
|
3108
|
+
if (!value || seen.has(value)) continue;
|
|
3109
|
+
seen.add(value);
|
|
3110
|
+
keys.push({ name, value });
|
|
3111
|
+
}
|
|
3112
|
+
return keys;
|
|
3113
|
+
}
|
|
3114
|
+
function isGeminiRateLimitError(error) {
|
|
3115
|
+
if (!(error instanceof HttpError)) return false;
|
|
3116
|
+
if (error.status === 429) return true;
|
|
3117
|
+
return (error.status === 400 || error.status === 403) && error.snippet.includes("RESOURCE_EXHAUSTED");
|
|
3118
|
+
}
|
|
3119
|
+
function cooldownMs(error) {
|
|
3120
|
+
if (!(error instanceof HttpError)) return DEFAULT_COOLDOWN_MS;
|
|
3121
|
+
if (error.retryAfterMs !== void 0 && error.retryAfterMs > 0) return error.retryAfterMs;
|
|
3122
|
+
const delay = /"retryDelay"\s*:\s*"(\d+(?:\.\d+)?)s"/.exec(error.snippet);
|
|
3123
|
+
if (delay?.[1]) return Math.ceil(Number(delay[1]) * 1e3);
|
|
3124
|
+
return DEFAULT_COOLDOWN_MS;
|
|
3125
|
+
}
|
|
3126
|
+
function keysToTry(keys, now) {
|
|
3127
|
+
const until = (k) => cooldownUntil.get(k.value) ?? 0;
|
|
3128
|
+
const ready = keys.filter((k) => until(k) <= now);
|
|
3129
|
+
if (ready.length > 0) return ready;
|
|
3130
|
+
const soonest = [...keys].sort((a, b) => until(a) - until(b))[0];
|
|
3131
|
+
return soonest ? [soonest] : [];
|
|
3132
|
+
}
|
|
3133
|
+
async function withGeminiKey(fn, opts = {}) {
|
|
3134
|
+
const now = opts.now ?? Date.now;
|
|
3135
|
+
const keys = listGeminiKeys(opts.env);
|
|
3136
|
+
if (keys.length === 0) {
|
|
3137
|
+
throw new ConfigError(
|
|
3138
|
+
"GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
|
|
3139
|
+
);
|
|
3140
|
+
}
|
|
3141
|
+
const ordered = keysToTry(keys, now());
|
|
3142
|
+
let lastError;
|
|
3143
|
+
for (const [i, key] of ordered.entries()) {
|
|
3144
|
+
try {
|
|
3145
|
+
return await fn(key.value, key.name);
|
|
3146
|
+
} catch (error) {
|
|
3147
|
+
if (!isGeminiRateLimitError(error)) throw error;
|
|
3148
|
+
lastError = error;
|
|
3149
|
+
cooldownUntil.set(key.value, now() + cooldownMs(error));
|
|
3150
|
+
const next = ordered[i + 1];
|
|
3151
|
+
if (next) opts.logger?.warn(`${key.name} is rate-limited; retrying with ${next.name}`);
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
if (keys.length > 1) {
|
|
3155
|
+
opts.logger?.warn(`All ${keys.length} Gemini keys are rate-limited; try again later`);
|
|
3156
|
+
}
|
|
3157
|
+
throw lastError;
|
|
3158
|
+
}
|
|
3122
3159
|
|
|
3123
3160
|
// src/providers/gemini/task-resolver.ts
|
|
3124
3161
|
import path21 from "path";
|
|
@@ -3173,19 +3210,16 @@ function requiresProcessingWait(mimeType) {
|
|
|
3173
3210
|
var BASE = "https://generativelanguage.googleapis.com";
|
|
3174
3211
|
var FILES_BASE = `${BASE}/upload/v1beta/files`;
|
|
3175
3212
|
var GENERATE_BASE = `${BASE}/v1beta/models`;
|
|
3176
|
-
function
|
|
3177
|
-
|
|
3178
|
-
if (!key)
|
|
3179
|
-
throw new ConfigError(
|
|
3180
|
-
"GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
|
|
3181
|
-
);
|
|
3182
|
-
return key;
|
|
3213
|
+
function withKey(opts, fn) {
|
|
3214
|
+
return opts.apiKey ? fn(opts.apiKey) : withGeminiKey(fn, { logger: opts.logger });
|
|
3183
3215
|
}
|
|
3184
3216
|
function authHeaders2(apiKey) {
|
|
3185
3217
|
return { "x-goog-api-key": apiKey };
|
|
3186
3218
|
}
|
|
3187
3219
|
async function uploadFile(filePath, opts = {}) {
|
|
3188
|
-
|
|
3220
|
+
return withKey(opts, (apiKey) => uploadFileWithKey(filePath, apiKey, opts));
|
|
3221
|
+
}
|
|
3222
|
+
async function uploadFileWithKey(filePath, apiKey, opts) {
|
|
3189
3223
|
const { timeoutMs = 3e5, logger } = opts;
|
|
3190
3224
|
const mimeType = getMimeType(filePath);
|
|
3191
3225
|
const fileBytes = fs18.readFileSync(filePath);
|
|
@@ -3221,10 +3255,7 @@ Content-Type: ${mimeType}\r
|
|
|
3221
3255
|
});
|
|
3222
3256
|
if (!resp.ok) {
|
|
3223
3257
|
const txt = await resp.text();
|
|
3224
|
-
throw new
|
|
3225
|
-
`File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
|
|
3226
|
-
"gemini"
|
|
3227
|
-
);
|
|
3258
|
+
throw new HttpError(resp.status, `File upload failed: ${txt.slice(0, 500)}`, FILES_BASE);
|
|
3228
3259
|
}
|
|
3229
3260
|
const data = await resp.json();
|
|
3230
3261
|
let fileRef = data.file;
|
|
@@ -3246,26 +3277,30 @@ Content-Type: ${mimeType}\r
|
|
|
3246
3277
|
logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
|
|
3247
3278
|
return fileRef;
|
|
3248
3279
|
}
|
|
3249
|
-
async function generateContent(req) {
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3280
|
+
async function generateContent(req, opts = {}) {
|
|
3281
|
+
return withKey(
|
|
3282
|
+
opts,
|
|
3283
|
+
(apiKey) => httpJson({
|
|
3284
|
+
url: `${GENERATE_BASE}/${req.model}:generateContent`,
|
|
3285
|
+
method: "POST",
|
|
3286
|
+
headers: authHeaders2(apiKey),
|
|
3287
|
+
body: {
|
|
3288
|
+
contents: req.contents,
|
|
3289
|
+
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
3290
|
+
}
|
|
3291
|
+
})
|
|
3292
|
+
);
|
|
3260
3293
|
}
|
|
3261
|
-
async function createInteraction(body) {
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3294
|
+
async function createInteraction(body, opts = {}) {
|
|
3295
|
+
return withKey(
|
|
3296
|
+
opts,
|
|
3297
|
+
(apiKey) => httpJson({
|
|
3298
|
+
url: `${BASE}/v1beta/interactions`,
|
|
3299
|
+
method: "POST",
|
|
3300
|
+
headers: authHeaders2(apiKey),
|
|
3301
|
+
body
|
|
3302
|
+
})
|
|
3303
|
+
);
|
|
3269
3304
|
}
|
|
3270
3305
|
function extractInteractionAudio(resp) {
|
|
3271
3306
|
let found = null;
|
|
@@ -3444,22 +3479,30 @@ function registerAnalyzeCommand(parent) {
|
|
|
3444
3479
|
const filePath = opts.files[i];
|
|
3445
3480
|
logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
|
|
3446
3481
|
try {
|
|
3447
|
-
const
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3482
|
+
const resp = await withGeminiKey(
|
|
3483
|
+
async (apiKey) => {
|
|
3484
|
+
const useApi = shouldUseFileApi(filePath);
|
|
3485
|
+
let contentParts;
|
|
3486
|
+
if (useApi) {
|
|
3487
|
+
logger.debug("File >20MB \u2014 using Files API");
|
|
3488
|
+
const ref = await uploadFile(filePath, { logger, apiKey });
|
|
3489
|
+
contentParts = [
|
|
3490
|
+
{ text: opts.prompt },
|
|
3491
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
3492
|
+
];
|
|
3493
|
+
} else {
|
|
3494
|
+
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
3495
|
+
}
|
|
3496
|
+
return generateContent(
|
|
3497
|
+
{
|
|
3498
|
+
model,
|
|
3499
|
+
contents: [{ parts: contentParts }]
|
|
3500
|
+
},
|
|
3501
|
+
{ apiKey }
|
|
3502
|
+
);
|
|
3503
|
+
},
|
|
3504
|
+
{ logger }
|
|
3505
|
+
);
|
|
3463
3506
|
results.push({
|
|
3464
3507
|
file: filePath,
|
|
3465
3508
|
status: "success",
|
|
@@ -3498,22 +3541,30 @@ function registerExtractCommand(parent) {
|
|
|
3498
3541
|
const filePath = opts.files[i];
|
|
3499
3542
|
logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
|
|
3500
3543
|
try {
|
|
3501
|
-
const
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3544
|
+
const resp = await withGeminiKey(
|
|
3545
|
+
async (apiKey) => {
|
|
3546
|
+
const useApi = shouldUseFileApi(filePath);
|
|
3547
|
+
let contentParts;
|
|
3548
|
+
if (useApi) {
|
|
3549
|
+
const ref = await uploadFile(filePath, { logger, apiKey });
|
|
3550
|
+
contentParts = [
|
|
3551
|
+
{ text: opts.prompt },
|
|
3552
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
3553
|
+
];
|
|
3554
|
+
} else {
|
|
3555
|
+
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
3556
|
+
}
|
|
3557
|
+
return generateContent(
|
|
3558
|
+
{
|
|
3559
|
+
model,
|
|
3560
|
+
contents: [{ parts: contentParts }],
|
|
3561
|
+
generationConfig
|
|
3562
|
+
},
|
|
3563
|
+
{ apiKey }
|
|
3564
|
+
);
|
|
3565
|
+
},
|
|
3566
|
+
{ logger }
|
|
3567
|
+
);
|
|
3517
3568
|
results.push({ file: filePath, status: "success", response: extractText(resp) });
|
|
3518
3569
|
} catch (e) {
|
|
3519
3570
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -3617,11 +3668,14 @@ async function generateGeminiSpeech(opts) {
|
|
|
3617
3668
|
logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
|
|
3618
3669
|
let resp;
|
|
3619
3670
|
try {
|
|
3620
|
-
resp = await generateContent(
|
|
3621
|
-
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3671
|
+
resp = await generateContent(
|
|
3672
|
+
{
|
|
3673
|
+
model,
|
|
3674
|
+
contents: [{ parts: [{ text }] }],
|
|
3675
|
+
generationConfig
|
|
3676
|
+
},
|
|
3677
|
+
{ logger }
|
|
3678
|
+
);
|
|
3625
3679
|
} catch (e) {
|
|
3626
3680
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
3627
3681
|
}
|
|
@@ -3657,7 +3711,7 @@ async function generateInteractionsSpeech(opts) {
|
|
|
3657
3711
|
}
|
|
3658
3712
|
let resp;
|
|
3659
3713
|
try {
|
|
3660
|
-
resp = await createInteraction(buildInteractionsSpeechBody(opts));
|
|
3714
|
+
resp = await createInteraction(buildInteractionsSpeechBody(opts), { logger });
|
|
3661
3715
|
} catch (e) {
|
|
3662
3716
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
3663
3717
|
}
|
|
@@ -3813,20 +3867,194 @@ function collectSpeakers(value, prev) {
|
|
|
3813
3867
|
}
|
|
3814
3868
|
|
|
3815
3869
|
// src/providers/gemini/commands/generate-video.ts
|
|
3816
|
-
|
|
3870
|
+
init_errors();
|
|
3871
|
+
import fs24 from "fs";
|
|
3817
3872
|
import path25 from "path";
|
|
3873
|
+
|
|
3874
|
+
// src/providers/gemini/veo.ts
|
|
3875
|
+
import fs23 from "fs";
|
|
3876
|
+
|
|
3877
|
+
// src/core/async-poll.ts
|
|
3878
|
+
init_errors();
|
|
3879
|
+
var PollTimeoutError2 = class extends MultixError {
|
|
3880
|
+
constructor(message, lastStatus) {
|
|
3881
|
+
super(message, "POLL_TIMEOUT");
|
|
3882
|
+
this.lastStatus = lastStatus;
|
|
3883
|
+
this.name = "PollTimeoutError";
|
|
3884
|
+
}
|
|
3885
|
+
lastStatus;
|
|
3886
|
+
};
|
|
3887
|
+
function isTransientHttpError(error) {
|
|
3888
|
+
if (!(error instanceof HttpError)) return false;
|
|
3889
|
+
return error.status === 0 || error.status === 429 || error.status >= 500;
|
|
3890
|
+
}
|
|
3891
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3892
|
+
async function pollUntil2(opts) {
|
|
3893
|
+
const now = opts.now ?? Date.now;
|
|
3894
|
+
const sleep5 = opts.sleep ?? defaultSleep;
|
|
3895
|
+
const random = opts.random ?? Math.random;
|
|
3896
|
+
const isTransient = opts.isTransientError ?? isTransientHttpError;
|
|
3897
|
+
const maxInterval = opts.maxIntervalMs ?? 15e3;
|
|
3898
|
+
const minInterval = opts.minIntervalMs ?? 500;
|
|
3899
|
+
const factor = opts.backoffFactor ?? 1.5;
|
|
3900
|
+
const jitter = opts.jitter ?? true;
|
|
3901
|
+
const deadline = now() + opts.timeoutMs;
|
|
3902
|
+
let interval = opts.intervalMs ?? 3e3;
|
|
3903
|
+
let attempt = 0;
|
|
3904
|
+
let last;
|
|
3905
|
+
let lastError;
|
|
3906
|
+
for (; ; ) {
|
|
3907
|
+
attempt++;
|
|
3908
|
+
let retryAfterMs;
|
|
3909
|
+
try {
|
|
3910
|
+
last = await opts.fetchStatus({ remainingMs: Math.max(1, deadline - now()), attempt });
|
|
3911
|
+
lastError = void 0;
|
|
3912
|
+
opts.onPoll?.(last, attempt);
|
|
3913
|
+
if (opts.isTerminal(last)) return last;
|
|
3914
|
+
} catch (error) {
|
|
3915
|
+
if (!isTransient(error)) throw error;
|
|
3916
|
+
lastError = error;
|
|
3917
|
+
if (error instanceof HttpError) retryAfterMs = error.retryAfterMs;
|
|
3918
|
+
}
|
|
3919
|
+
const remaining = deadline - now();
|
|
3920
|
+
if (remaining <= 0) {
|
|
3921
|
+
const hint = opts.resumeHint ? ` ${opts.resumeHint}` : "";
|
|
3922
|
+
const cause = lastError instanceof Error ? ` Last error: ${lastError.message}.` : "";
|
|
3923
|
+
throw new PollTimeoutError2(
|
|
3924
|
+
`Timed out after ${opts.timeoutMs}ms waiting for job.${cause}${hint}`,
|
|
3925
|
+
last
|
|
3926
|
+
);
|
|
3927
|
+
}
|
|
3928
|
+
let delay = retryAfterMs ?? interval;
|
|
3929
|
+
if (jitter && retryAfterMs === void 0) delay *= 0.8 + random() * 0.4;
|
|
3930
|
+
await sleep5(Math.min(Math.max(delay, minInterval), remaining));
|
|
3931
|
+
interval = Math.min(interval * factor, maxInterval);
|
|
3932
|
+
}
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
// src/providers/gemini/veo.ts
|
|
3818
3936
|
init_errors();
|
|
3819
|
-
|
|
3820
|
-
var BASE2 =
|
|
3937
|
+
var API_HOST = "generativelanguage.googleapis.com";
|
|
3938
|
+
var BASE2 = `https://${API_HOST}/v1beta`;
|
|
3939
|
+
var VEO_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
3940
|
+
var VEO_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3941
|
+
function veoImageFromFile(imgPath) {
|
|
3942
|
+
if (!fs23.existsSync(imgPath)) throw new ValidationError(`Image not found: ${imgPath}`);
|
|
3943
|
+
const detected = getMimeType(imgPath);
|
|
3944
|
+
const mimeType = detected === "application/octet-stream" ? "image/jpeg" : detected;
|
|
3945
|
+
if (!VEO_IMAGE_TYPES.has(mimeType)) {
|
|
3946
|
+
throw new ValidationError(`Veo accepts JPEG, PNG or WebP images: ${imgPath} (${mimeType})`);
|
|
3947
|
+
}
|
|
3948
|
+
return { bytesBase64Encoded: fs23.readFileSync(imgPath).toString("base64"), mimeType };
|
|
3949
|
+
}
|
|
3950
|
+
function veoFramesFromPaths(paths = []) {
|
|
3951
|
+
if (paths.length > 2) {
|
|
3952
|
+
throw new ValidationError(
|
|
3953
|
+
"--reference-images accepts at most two images (opening frame, closing frame)"
|
|
3954
|
+
);
|
|
3955
|
+
}
|
|
3956
|
+
const [first, last] = paths;
|
|
3957
|
+
return {
|
|
3958
|
+
image: first ? veoImageFromFile(first) : void 0,
|
|
3959
|
+
lastFrame: last ? veoImageFromFile(last) : void 0
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
function modelPath(model) {
|
|
3963
|
+
return model.replace(/^models\//, "");
|
|
3964
|
+
}
|
|
3965
|
+
function buildVeoRequest(opts) {
|
|
3966
|
+
const instance = { prompt: opts.prompt };
|
|
3967
|
+
if (opts.image) instance.image = opts.image;
|
|
3968
|
+
if (opts.lastFrame) instance.lastFrame = opts.lastFrame;
|
|
3969
|
+
return {
|
|
3970
|
+
instances: [instance],
|
|
3971
|
+
parameters: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
|
|
3972
|
+
};
|
|
3973
|
+
}
|
|
3974
|
+
function extractVeoVideoUri(op) {
|
|
3975
|
+
const r = op.response ?? {};
|
|
3976
|
+
return r.generateVideoResponse?.generatedSamples?.[0]?.video?.uri ?? r.generatedSamples?.[0]?.video?.uri ?? r.generatedVideos?.[0]?.video?.uri;
|
|
3977
|
+
}
|
|
3978
|
+
function headers(apiKey) {
|
|
3979
|
+
return { "x-goog-api-key": apiKey };
|
|
3980
|
+
}
|
|
3981
|
+
async function submitVeo(apiKey, model, body) {
|
|
3982
|
+
try {
|
|
3983
|
+
return await httpJson({
|
|
3984
|
+
url: `${BASE2}/models/${modelPath(model)}:predictLongRunning`,
|
|
3985
|
+
method: "POST",
|
|
3986
|
+
headers: headers(apiKey),
|
|
3987
|
+
body
|
|
3988
|
+
});
|
|
3989
|
+
} catch (e) {
|
|
3990
|
+
if (e instanceof HttpError) throw e;
|
|
3991
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
3992
|
+
throw new ProviderError(`Veo submit failed: ${msg}`, "gemini");
|
|
3993
|
+
}
|
|
3994
|
+
}
|
|
3995
|
+
async function waitForVeo(apiKey, operation, logger, opts = {}) {
|
|
3996
|
+
if (operation.done) return checkVeoResult(operation);
|
|
3997
|
+
const finalOp = await pollUntil2({
|
|
3998
|
+
fetchStatus: ({ remainingMs }) => httpJson({
|
|
3999
|
+
url: `${BASE2}/${operation.name}`,
|
|
4000
|
+
headers: headers(apiKey),
|
|
4001
|
+
timeoutMs: Math.min(remainingMs, 6e4)
|
|
4002
|
+
}),
|
|
4003
|
+
isTerminal: (op) => op.done === true,
|
|
4004
|
+
timeoutMs: opts.timeoutMs ?? VEO_TIMEOUT_MS,
|
|
4005
|
+
intervalMs: opts.intervalMs ?? 1e4,
|
|
4006
|
+
maxIntervalMs: 2e4,
|
|
4007
|
+
sleep: opts.sleep,
|
|
4008
|
+
onPoll: (op, attempt) => {
|
|
4009
|
+
if (!op.done) logger.debug(`Still generating... (poll ${attempt})`);
|
|
4010
|
+
},
|
|
4011
|
+
resumeHint: `Operation (still running server-side): ${operation.name}`
|
|
4012
|
+
});
|
|
4013
|
+
return checkVeoResult(finalOp);
|
|
4014
|
+
}
|
|
4015
|
+
function checkVeoResult(op) {
|
|
4016
|
+
if (op.error) throw new ProviderError(`Veo error: ${JSON.stringify(op.error)}`, "gemini");
|
|
4017
|
+
return op;
|
|
4018
|
+
}
|
|
4019
|
+
async function downloadVeoVideo(apiKey, uri, dest) {
|
|
4020
|
+
let host = "";
|
|
4021
|
+
try {
|
|
4022
|
+
host = new URL(uri).hostname;
|
|
4023
|
+
} catch {
|
|
4024
|
+
throw new ProviderError("Veo returned an invalid video URI", "gemini");
|
|
4025
|
+
}
|
|
4026
|
+
await downloadFile(uri, dest, 3e5, host === API_HOST ? headers(apiKey) : void 0);
|
|
4027
|
+
}
|
|
4028
|
+
async function runVeo(model, request, dest, logger) {
|
|
4029
|
+
const body = buildVeoRequest(request);
|
|
4030
|
+
const { apiKey, operation } = await withGeminiKey(
|
|
4031
|
+
async (key) => ({ apiKey: key, operation: await submitVeo(key, model, body) }),
|
|
4032
|
+
{ logger }
|
|
4033
|
+
);
|
|
4034
|
+
logger.debug(`Operation: ${operation.name}`);
|
|
4035
|
+
const finalOp = await waitForVeo(apiKey, operation, logger);
|
|
4036
|
+
const uri = extractVeoVideoUri(finalOp);
|
|
4037
|
+
if (!uri) {
|
|
4038
|
+
throw new ProviderError(
|
|
4039
|
+
`Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
|
|
4040
|
+
"gemini"
|
|
4041
|
+
);
|
|
4042
|
+
}
|
|
4043
|
+
await downloadVeoVideo(apiKey, uri, dest);
|
|
4044
|
+
return finalOp;
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
// src/providers/gemini/commands/generate-video.ts
|
|
3821
4048
|
function registerGenerateVideoCommand(parent) {
|
|
3822
4049
|
parent.command("generate-video").description("Generate video with Veo via Gemini API [EXPERIMENTAL]").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", "Veo model to use").option("--resolution <res>", "Video resolution: 720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option(
|
|
3823
4050
|
"--reference-images <paths...>",
|
|
3824
4051
|
"Reference images (first=opening frame, second=closing)"
|
|
3825
|
-
).option("--upload-timeout <ms>", "
|
|
4052
|
+
).option("--upload-timeout <ms>", "Deprecated and ignored (images are sent inline)").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
3826
4053
|
async (opts) => {
|
|
3827
4054
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3828
|
-
|
|
3829
|
-
|
|
4055
|
+
if (opts.uploadTimeout !== void 0) {
|
|
4056
|
+
logger.warn("--upload-timeout is deprecated and ignored (images are sent inline)");
|
|
4057
|
+
}
|
|
3830
4058
|
const model = opts.model ?? getDefaultModel("generate-video");
|
|
3831
4059
|
if (!model.startsWith("veo-")) {
|
|
3832
4060
|
throw new ValidationError(
|
|
@@ -3841,72 +4069,23 @@ function registerGenerateVideoCommand(parent) {
|
|
|
3841
4069
|
}
|
|
3842
4070
|
logger.debug(`Submitting video generation with ${model}...`);
|
|
3843
4071
|
logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
|
|
3844
|
-
const
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
}
|
|
3858
|
-
}
|
|
3859
|
-
const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
|
|
3860
|
-
let operation;
|
|
3861
|
-
try {
|
|
3862
|
-
operation = await httpJson({
|
|
3863
|
-
url: `${BASE2}/v1beta/models/${model}:generateVideos`,
|
|
3864
|
-
method: "POST",
|
|
3865
|
-
headers,
|
|
3866
|
-
body: {
|
|
3867
|
-
contents: [{ parts: contentParts }],
|
|
3868
|
-
generationConfig: config
|
|
3869
|
-
}
|
|
3870
|
-
});
|
|
3871
|
-
} catch (e) {
|
|
3872
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3873
|
-
throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
|
|
3874
|
-
}
|
|
3875
|
-
const operationName = operation.name;
|
|
3876
|
-
logger.debug(`Operation: ${operationName}`);
|
|
3877
|
-
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
3878
|
-
let done = false;
|
|
3879
|
-
let finalOp = operation;
|
|
3880
|
-
while (!done && Date.now() < deadline) {
|
|
3881
|
-
await sleep3(1e4);
|
|
3882
|
-
finalOp = await httpJson({
|
|
3883
|
-
url: `${BASE2}/v1beta/${operationName}`,
|
|
3884
|
-
headers
|
|
3885
|
-
});
|
|
3886
|
-
done = finalOp.done === true;
|
|
3887
|
-
if (!done)
|
|
3888
|
-
logger.debug(
|
|
3889
|
-
`Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
|
|
3890
|
-
);
|
|
3891
|
-
}
|
|
3892
|
-
if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
|
|
3893
|
-
if (finalOp.error)
|
|
3894
|
-
throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
|
|
3895
|
-
const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
|
|
3896
|
-
if (!videoUri) {
|
|
3897
|
-
throw new ProviderError(
|
|
3898
|
-
`Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
|
|
3899
|
-
"gemini"
|
|
3900
|
-
);
|
|
3901
|
-
}
|
|
3902
|
-
const outDir = getOutputDir();
|
|
3903
|
-
const dest = path25.join(outDir, `veo_generated_${Date.now()}.mp4`);
|
|
3904
|
-
const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
|
|
3905
|
-
await downloadFile2(videoUri, dest);
|
|
4072
|
+
const frames = veoFramesFromPaths(opts.referenceImages);
|
|
4073
|
+
const dest = path25.join(getOutputDir(), `veo_generated_${Date.now()}.mp4`);
|
|
4074
|
+
const finalOp = await runVeo(
|
|
4075
|
+
model,
|
|
4076
|
+
{
|
|
4077
|
+
prompt: opts.prompt,
|
|
4078
|
+
...frames,
|
|
4079
|
+
aspectRatio: opts.aspectRatio,
|
|
4080
|
+
resolution: opts.resolution
|
|
4081
|
+
},
|
|
4082
|
+
dest,
|
|
4083
|
+
logger
|
|
4084
|
+
);
|
|
3906
4085
|
logger.success(`Saved video: ${dest}`);
|
|
3907
4086
|
if (opts.output) {
|
|
3908
|
-
|
|
3909
|
-
|
|
4087
|
+
fs24.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
|
|
4088
|
+
fs24.copyFileSync(dest, opts.output);
|
|
3910
4089
|
logger.success(`Copied to: ${opts.output}`);
|
|
3911
4090
|
}
|
|
3912
4091
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -3919,13 +4098,10 @@ Generated video: ${dest}`);
|
|
|
3919
4098
|
}
|
|
3920
4099
|
);
|
|
3921
4100
|
}
|
|
3922
|
-
function sleep3(ms) {
|
|
3923
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
3924
|
-
}
|
|
3925
4101
|
|
|
3926
4102
|
// src/providers/gemini/commands/generate.ts
|
|
3927
4103
|
init_errors();
|
|
3928
|
-
import
|
|
4104
|
+
import fs25 from "fs";
|
|
3929
4105
|
import path26 from "path";
|
|
3930
4106
|
function registerGenerateCommand(parent) {
|
|
3931
4107
|
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(
|
|
@@ -3952,11 +4128,14 @@ function registerGenerateCommand(parent) {
|
|
|
3952
4128
|
const savedFiles = [];
|
|
3953
4129
|
for (let i = 0; i < numImages; i++) {
|
|
3954
4130
|
try {
|
|
3955
|
-
const resp = await generateContent(
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
4131
|
+
const resp = await generateContent(
|
|
4132
|
+
{
|
|
4133
|
+
model,
|
|
4134
|
+
contents: [{ parts: [{ text: opts.prompt }] }],
|
|
4135
|
+
generationConfig
|
|
4136
|
+
},
|
|
4137
|
+
{ logger }
|
|
4138
|
+
);
|
|
3960
4139
|
const images = extractImages(resp);
|
|
3961
4140
|
if (images.length === 0) {
|
|
3962
4141
|
logger.warn(
|
|
@@ -3969,7 +4148,7 @@ function registerGenerateCommand(parent) {
|
|
|
3969
4148
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
3970
4149
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
3971
4150
|
const dest = path26.join(outDir, fname);
|
|
3972
|
-
|
|
4151
|
+
fs25.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
3973
4152
|
savedFiles.push(dest);
|
|
3974
4153
|
logger.success(`Saved: ${dest}`);
|
|
3975
4154
|
}
|
|
@@ -3984,8 +4163,8 @@ function registerGenerateCommand(parent) {
|
|
|
3984
4163
|
process.exit(1);
|
|
3985
4164
|
}
|
|
3986
4165
|
if (opts.output && savedFiles[0]) {
|
|
3987
|
-
|
|
3988
|
-
|
|
4166
|
+
fs25.mkdirSync(path26.dirname(path26.resolve(opts.output)), { recursive: true });
|
|
4167
|
+
fs25.copyFileSync(savedFiles[0], opts.output);
|
|
3989
4168
|
logger.success(`Copied to: ${opts.output}`);
|
|
3990
4169
|
}
|
|
3991
4170
|
const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
|
|
@@ -3997,7 +4176,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
3997
4176
|
}
|
|
3998
4177
|
|
|
3999
4178
|
// src/providers/gemini/commands/image-to-image.ts
|
|
4000
|
-
import
|
|
4179
|
+
import fs26 from "fs";
|
|
4001
4180
|
import path27 from "path";
|
|
4002
4181
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
4003
4182
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
@@ -4044,7 +4223,7 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
4044
4223
|
contents: [{ parts }],
|
|
4045
4224
|
generationConfig: { responseModalities: ["IMAGE"] }
|
|
4046
4225
|
};
|
|
4047
|
-
const resp = await generateContent(req);
|
|
4226
|
+
const resp = await generateContent(req, { logger });
|
|
4048
4227
|
const images = extractImages(resp);
|
|
4049
4228
|
if (images.length === 0) {
|
|
4050
4229
|
logger.error(
|
|
@@ -4060,13 +4239,13 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
4060
4239
|
if (!img) continue;
|
|
4061
4240
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
4062
4241
|
const dest = path27.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
4063
|
-
|
|
4242
|
+
fs26.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
4064
4243
|
saved.push(dest);
|
|
4065
4244
|
logger.success(`Saved: ${dest}`);
|
|
4066
4245
|
}
|
|
4067
4246
|
if (opts.output && saved[0]) {
|
|
4068
|
-
|
|
4069
|
-
|
|
4247
|
+
fs26.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
|
|
4248
|
+
fs26.copyFileSync(saved[0], opts.output);
|
|
4070
4249
|
logger.success(`Copied to: ${opts.output}`);
|
|
4071
4250
|
}
|
|
4072
4251
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4102,29 +4281,13 @@ function sniffImageMime(buf, url) {
|
|
|
4102
4281
|
}
|
|
4103
4282
|
|
|
4104
4283
|
// src/providers/gemini/commands/image-to-video.ts
|
|
4105
|
-
import fs26 from "fs";
|
|
4106
|
-
import path28 from "path";
|
|
4107
4284
|
init_errors();
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
function imagePart(imgPath) {
|
|
4111
|
-
if (!fs26.existsSync(imgPath)) {
|
|
4112
|
-
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
4113
|
-
}
|
|
4114
|
-
const data = fs26.readFileSync(imgPath).toString("base64");
|
|
4115
|
-
const ext = path28.extname(imgPath).toLowerCase();
|
|
4116
|
-
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
4117
|
-
return { inlineData: { mimeType, data } };
|
|
4118
|
-
}
|
|
4119
|
-
function sleep4(ms) {
|
|
4120
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
4121
|
-
}
|
|
4285
|
+
import fs27 from "fs";
|
|
4286
|
+
import path28 from "path";
|
|
4122
4287
|
function registerGeminiImageToVideoCommand(parent) {
|
|
4123
4288
|
parent.command("image-to-video <imagePath>").alias("i2v").description("Generate video from a starting image with Veo (image-to-video) [EXPERIMENTAL]").requiredOption("--prompt <text>", "Motion/scene prompt").option("--last-frame <path>", "Optional closing frame image (Veo 3.1)").option("--model <id>", "Veo model id").option("--resolution <res>", "720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
4124
4289
|
async (imagePath, opts) => {
|
|
4125
4290
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4126
|
-
const apiKey = resolveKey("GEMINI_API_KEY");
|
|
4127
|
-
if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
|
|
4128
4291
|
const model = opts.model ?? getDefaultModel("generate-video");
|
|
4129
4292
|
if (!model.startsWith("veo-")) {
|
|
4130
4293
|
throw new ValidationError(
|
|
@@ -4137,60 +4300,26 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
4137
4300
|
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
4138
4301
|
throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
|
|
4139
4302
|
}
|
|
4140
|
-
const contentParts = [
|
|
4141
|
-
{ text: opts.prompt },
|
|
4142
|
-
imagePart(imagePath)
|
|
4143
|
-
];
|
|
4144
|
-
if (opts.lastFrame) contentParts.push(imagePart(opts.lastFrame));
|
|
4145
|
-
const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
|
|
4146
4303
|
logger.info(
|
|
4147
4304
|
"Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
|
|
4148
4305
|
);
|
|
4149
|
-
let operation;
|
|
4150
|
-
try {
|
|
4151
|
-
operation = await httpJson({
|
|
4152
|
-
url: `${BASE3}/v1beta/models/${model}:generateVideos`,
|
|
4153
|
-
method: "POST",
|
|
4154
|
-
headers,
|
|
4155
|
-
body: {
|
|
4156
|
-
contents: [{ parts: contentParts }],
|
|
4157
|
-
generationConfig: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
|
|
4158
|
-
}
|
|
4159
|
-
});
|
|
4160
|
-
} catch (e) {
|
|
4161
|
-
throw new ProviderError(
|
|
4162
|
-
`Submit failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
4163
|
-
"gemini"
|
|
4164
|
-
);
|
|
4165
|
-
}
|
|
4166
|
-
const operationName = operation.name;
|
|
4167
|
-
logger.debug(`Operation: ${operationName}`);
|
|
4168
|
-
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
4169
|
-
let finalOp = operation;
|
|
4170
|
-
while (!finalOp.done && Date.now() < deadline) {
|
|
4171
|
-
await sleep4(1e4);
|
|
4172
|
-
finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
|
|
4173
|
-
if (!finalOp.done) {
|
|
4174
|
-
logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
|
|
4175
|
-
}
|
|
4176
|
-
}
|
|
4177
|
-
if (!finalOp.done) throw new ProviderError("Veo timed out after 10 minutes", "gemini");
|
|
4178
|
-
if (finalOp.error) {
|
|
4179
|
-
throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
|
|
4180
|
-
}
|
|
4181
|
-
const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
|
|
4182
|
-
if (!videoUri) {
|
|
4183
|
-
throw new ProviderError(
|
|
4184
|
-
`Unexpected Veo response: ${JSON.stringify(finalOp.response)}`,
|
|
4185
|
-
"gemini"
|
|
4186
|
-
);
|
|
4187
|
-
}
|
|
4188
4306
|
const dest = path28.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
4189
|
-
await
|
|
4307
|
+
const finalOp = await runVeo(
|
|
4308
|
+
model,
|
|
4309
|
+
{
|
|
4310
|
+
prompt: opts.prompt,
|
|
4311
|
+
image: veoImageFromFile(imagePath),
|
|
4312
|
+
lastFrame: opts.lastFrame ? veoImageFromFile(opts.lastFrame) : void 0,
|
|
4313
|
+
aspectRatio: opts.aspectRatio,
|
|
4314
|
+
resolution: opts.resolution
|
|
4315
|
+
},
|
|
4316
|
+
dest,
|
|
4317
|
+
logger
|
|
4318
|
+
);
|
|
4190
4319
|
logger.success(`Saved video: ${dest}`);
|
|
4191
4320
|
if (opts.output) {
|
|
4192
|
-
|
|
4193
|
-
|
|
4321
|
+
fs27.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
|
|
4322
|
+
fs27.copyFileSync(dest, opts.output);
|
|
4194
4323
|
logger.success(`Copied to: ${opts.output}`);
|
|
4195
4324
|
}
|
|
4196
4325
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -4206,36 +4335,44 @@ Generated video: ${dest}`);
|
|
|
4206
4335
|
|
|
4207
4336
|
// src/providers/gemini/commands/transcribe.ts
|
|
4208
4337
|
init_errors();
|
|
4209
|
-
import
|
|
4338
|
+
import fs28 from "fs";
|
|
4210
4339
|
function registerTranscribeCommand2(parent) {
|
|
4211
4340
|
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(
|
|
4212
4341
|
async (opts) => {
|
|
4213
4342
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4214
4343
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
4215
4344
|
for (const f of opts.files) {
|
|
4216
|
-
if (!
|
|
4345
|
+
if (!fs28.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
4217
4346
|
}
|
|
4218
4347
|
const results = [];
|
|
4219
4348
|
for (let i = 0; i < opts.files.length; i++) {
|
|
4220
4349
|
const filePath = opts.files[i];
|
|
4221
4350
|
logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
|
|
4222
4351
|
try {
|
|
4223
|
-
const
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4352
|
+
const resp = await withGeminiKey(
|
|
4353
|
+
async (apiKey) => {
|
|
4354
|
+
const useApi = shouldUseFileApi(filePath);
|
|
4355
|
+
let contentParts;
|
|
4356
|
+
if (useApi) {
|
|
4357
|
+
logger.debug("File >20MB \u2014 using Files API");
|
|
4358
|
+
const ref = await uploadFile(filePath, { logger, apiKey });
|
|
4359
|
+
contentParts = [
|
|
4360
|
+
{ text: opts.prompt },
|
|
4361
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
4362
|
+
];
|
|
4363
|
+
} else {
|
|
4364
|
+
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
4365
|
+
}
|
|
4366
|
+
return generateContent(
|
|
4367
|
+
{
|
|
4368
|
+
model,
|
|
4369
|
+
contents: [{ parts: contentParts }]
|
|
4370
|
+
},
|
|
4371
|
+
{ apiKey }
|
|
4372
|
+
);
|
|
4373
|
+
},
|
|
4374
|
+
{ logger }
|
|
4375
|
+
);
|
|
4239
4376
|
results.push({
|
|
4240
4377
|
file: filePath,
|
|
4241
4378
|
status: "success",
|
|
@@ -4270,13 +4407,11 @@ function registerGeminiCommands(program2) {
|
|
|
4270
4407
|
}
|
|
4271
4408
|
|
|
4272
4409
|
// src/providers/leonardo/commands/generate.ts
|
|
4273
|
-
|
|
4274
|
-
import fs28 from "fs";
|
|
4410
|
+
import fs29 from "fs";
|
|
4275
4411
|
import path29 from "path";
|
|
4276
4412
|
|
|
4277
4413
|
// src/providers/leonardo/client.ts
|
|
4278
4414
|
init_errors();
|
|
4279
|
-
init_http_client();
|
|
4280
4415
|
|
|
4281
4416
|
// src/providers/leonardo/models.ts
|
|
4282
4417
|
var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
|
|
@@ -4483,8 +4618,8 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
4483
4618
|
}
|
|
4484
4619
|
if (opts.output && saved[0]) {
|
|
4485
4620
|
const dest = path29.resolve(opts.output);
|
|
4486
|
-
|
|
4487
|
-
|
|
4621
|
+
fs29.mkdirSync(path29.dirname(dest), { recursive: true });
|
|
4622
|
+
fs29.copyFileSync(saved[0], dest);
|
|
4488
4623
|
logger.success(`Copied to ${dest}`);
|
|
4489
4624
|
}
|
|
4490
4625
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4496,8 +4631,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4496
4631
|
}
|
|
4497
4632
|
|
|
4498
4633
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
4499
|
-
|
|
4500
|
-
import fs29 from "fs";
|
|
4634
|
+
import fs30 from "fs";
|
|
4501
4635
|
import path30 from "path";
|
|
4502
4636
|
function extFromUrl3(url, fallback = ".png") {
|
|
4503
4637
|
try {
|
|
@@ -4574,8 +4708,8 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
4574
4708
|
}
|
|
4575
4709
|
if (opts.output && saved[0]) {
|
|
4576
4710
|
const dest = path30.resolve(opts.output);
|
|
4577
|
-
|
|
4578
|
-
|
|
4711
|
+
fs30.mkdirSync(path30.dirname(dest), { recursive: true });
|
|
4712
|
+
fs30.copyFileSync(saved[0], dest);
|
|
4579
4713
|
logger.success(`Copied to ${dest}`);
|
|
4580
4714
|
}
|
|
4581
4715
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4587,7 +4721,6 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4587
4721
|
}
|
|
4588
4722
|
|
|
4589
4723
|
// src/providers/leonardo/commands/video.ts
|
|
4590
|
-
init_http_client();
|
|
4591
4724
|
import path31 from "path";
|
|
4592
4725
|
function v2DimsFor(resolution) {
|
|
4593
4726
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
@@ -4805,7 +4938,6 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
4805
4938
|
}
|
|
4806
4939
|
|
|
4807
4940
|
// src/providers/leonardo/commands/status.ts
|
|
4808
|
-
init_http_client();
|
|
4809
4941
|
import path32 from "path";
|
|
4810
4942
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
4811
4943
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
@@ -4963,7 +5095,6 @@ init_errors();
|
|
|
4963
5095
|
|
|
4964
5096
|
// src/providers/minimax/client.ts
|
|
4965
5097
|
init_errors();
|
|
4966
|
-
init_http_client();
|
|
4967
5098
|
var MINIMAX_BASE_URL = "https://api.minimax.io/v1";
|
|
4968
5099
|
function requireMinimaxKey() {
|
|
4969
5100
|
const key = resolveKey("MINIMAX_API_KEY");
|
|
@@ -5018,7 +5149,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
|
5018
5149
|
if (status === "Failed" || status === "Error") {
|
|
5019
5150
|
throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
|
|
5020
5151
|
}
|
|
5021
|
-
await
|
|
5152
|
+
await sleep3(intervalMs);
|
|
5022
5153
|
elapsed += intervalMs;
|
|
5023
5154
|
}
|
|
5024
5155
|
throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
|
|
@@ -5037,13 +5168,12 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
|
|
|
5037
5168
|
await downloadFile(downloadUrl, dest);
|
|
5038
5169
|
return dest;
|
|
5039
5170
|
}
|
|
5040
|
-
function
|
|
5171
|
+
function sleep3(ms) {
|
|
5041
5172
|
return new Promise((r) => setTimeout(r, ms));
|
|
5042
5173
|
}
|
|
5043
5174
|
|
|
5044
5175
|
// src/providers/minimax/generators/music.ts
|
|
5045
|
-
|
|
5046
|
-
import fs30 from "fs";
|
|
5176
|
+
import fs31 from "fs";
|
|
5047
5177
|
import path33 from "path";
|
|
5048
5178
|
async function generateMinimaxMusic(opts) {
|
|
5049
5179
|
const {
|
|
@@ -5093,10 +5223,10 @@ async function generateMinimaxMusic(opts) {
|
|
|
5093
5223
|
try {
|
|
5094
5224
|
if (audioData.startsWith("http")) {
|
|
5095
5225
|
const bytes = await fetchBytes(audioData);
|
|
5096
|
-
|
|
5226
|
+
fs31.writeFileSync(dest, bytes);
|
|
5097
5227
|
} else {
|
|
5098
5228
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
5099
|
-
|
|
5229
|
+
fs31.writeFileSync(dest, audioBytes);
|
|
5100
5230
|
}
|
|
5101
5231
|
} catch (e) {
|
|
5102
5232
|
return {
|
|
@@ -5107,8 +5237,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
5107
5237
|
const durSec = durationMs / 1e3;
|
|
5108
5238
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
5109
5239
|
if (output) {
|
|
5110
|
-
|
|
5111
|
-
|
|
5240
|
+
fs31.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
|
|
5241
|
+
fs31.copyFileSync(dest, output);
|
|
5112
5242
|
}
|
|
5113
5243
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
5114
5244
|
}
|
|
@@ -5182,7 +5312,7 @@ Generated music: ${result.generatedAudio}`);
|
|
|
5182
5312
|
init_errors();
|
|
5183
5313
|
|
|
5184
5314
|
// src/providers/minimax/generators/speech.ts
|
|
5185
|
-
import
|
|
5315
|
+
import fs32 from "fs";
|
|
5186
5316
|
import path34 from "path";
|
|
5187
5317
|
async function generateMinimaxSpeech(opts) {
|
|
5188
5318
|
const {
|
|
@@ -5231,11 +5361,11 @@ async function generateMinimaxSpeech(opts) {
|
|
|
5231
5361
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
5232
5362
|
const outDir = getOutputDir();
|
|
5233
5363
|
const dest = path34.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
5234
|
-
|
|
5364
|
+
fs32.writeFileSync(dest, audioBytes);
|
|
5235
5365
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
5236
5366
|
if (output) {
|
|
5237
|
-
|
|
5238
|
-
|
|
5367
|
+
fs32.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
|
|
5368
|
+
fs32.copyFileSync(dest, output);
|
|
5239
5369
|
}
|
|
5240
5370
|
return { status: "success", generatedAudio: dest, model };
|
|
5241
5371
|
}
|
|
@@ -5282,7 +5412,7 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
5282
5412
|
init_errors();
|
|
5283
5413
|
|
|
5284
5414
|
// src/providers/minimax/generators/video.ts
|
|
5285
|
-
import
|
|
5415
|
+
import fs33 from "fs";
|
|
5286
5416
|
import path35 from "path";
|
|
5287
5417
|
async function generateMinimaxVideo(opts) {
|
|
5288
5418
|
const {
|
|
@@ -5336,11 +5466,11 @@ async function generateMinimaxVideo(opts) {
|
|
|
5336
5466
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
5337
5467
|
}
|
|
5338
5468
|
const elapsed = (Date.now() - start) / 1e3;
|
|
5339
|
-
const stat =
|
|
5469
|
+
const stat = fs33.statSync(dest);
|
|
5340
5470
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
5341
5471
|
if (output) {
|
|
5342
|
-
|
|
5343
|
-
|
|
5472
|
+
fs33.mkdirSync(path35.dirname(path35.resolve(output)), { recursive: true });
|
|
5473
|
+
fs33.copyFileSync(dest, output);
|
|
5344
5474
|
}
|
|
5345
5475
|
await maybeDownloadThumb(pollResult, dest, {
|
|
5346
5476
|
skip: thumb === false,
|
|
@@ -5402,8 +5532,7 @@ Generated video: ${result.generatedVideo}`);
|
|
|
5402
5532
|
|
|
5403
5533
|
// src/providers/minimax/generators/image.ts
|
|
5404
5534
|
init_errors();
|
|
5405
|
-
|
|
5406
|
-
import fs33 from "fs";
|
|
5535
|
+
import fs34 from "fs";
|
|
5407
5536
|
import path36 from "path";
|
|
5408
5537
|
async function generateMinimaxImage(opts) {
|
|
5409
5538
|
const {
|
|
@@ -5443,7 +5572,7 @@ async function generateMinimaxImage(opts) {
|
|
|
5443
5572
|
const bytes = await fetchBytes(url);
|
|
5444
5573
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
5445
5574
|
const dest = path36.join(outDir, fname);
|
|
5446
|
-
|
|
5575
|
+
fs34.writeFileSync(dest, bytes);
|
|
5447
5576
|
savedFiles.push(dest);
|
|
5448
5577
|
logger?.success(`Saved: ${dest}`);
|
|
5449
5578
|
} catch (e) {
|
|
@@ -5454,8 +5583,8 @@ async function generateMinimaxImage(opts) {
|
|
|
5454
5583
|
}
|
|
5455
5584
|
}
|
|
5456
5585
|
if (output && savedFiles[0]) {
|
|
5457
|
-
|
|
5458
|
-
|
|
5586
|
+
fs34.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
|
|
5587
|
+
fs34.copyFileSync(savedFiles[0], output);
|
|
5459
5588
|
}
|
|
5460
5589
|
return { status: "success", generatedImages: savedFiles, model };
|
|
5461
5590
|
}
|
|
@@ -5494,8 +5623,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
5494
5623
|
|
|
5495
5624
|
// src/providers/minimax/commands/image-to-image.ts
|
|
5496
5625
|
init_errors();
|
|
5497
|
-
|
|
5498
|
-
import fs34 from "fs";
|
|
5626
|
+
import fs35 from "fs";
|
|
5499
5627
|
import path37 from "path";
|
|
5500
5628
|
function registerMinimaxImageToImageCommand(parent) {
|
|
5501
5629
|
parent.command("image-to-image").alias("i2i").description(
|
|
@@ -5552,7 +5680,7 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5552
5680
|
try {
|
|
5553
5681
|
const bytes = await fetchBytes(url);
|
|
5554
5682
|
const dest = path37.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
5555
|
-
|
|
5683
|
+
fs35.writeFileSync(dest, bytes);
|
|
5556
5684
|
saved.push(dest);
|
|
5557
5685
|
logger.success(`Saved: ${dest}`);
|
|
5558
5686
|
} catch (e) {
|
|
@@ -5563,8 +5691,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5563
5691
|
}
|
|
5564
5692
|
}
|
|
5565
5693
|
if (opts.output && saved[0]) {
|
|
5566
|
-
|
|
5567
|
-
|
|
5694
|
+
fs35.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
|
|
5695
|
+
fs35.copyFileSync(saved[0], opts.output);
|
|
5568
5696
|
logger.success(`Copied to: ${opts.output}`);
|
|
5569
5697
|
}
|
|
5570
5698
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -5698,15 +5826,13 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
5698
5826
|
}
|
|
5699
5827
|
|
|
5700
5828
|
// src/providers/openai/openai-audio.ts
|
|
5701
|
-
|
|
5702
|
-
import fs36 from "fs";
|
|
5829
|
+
import fs37 from "fs";
|
|
5703
5830
|
import path39 from "path";
|
|
5704
5831
|
|
|
5705
5832
|
// src/providers/openai/client.ts
|
|
5706
|
-
import
|
|
5833
|
+
import fs36 from "fs";
|
|
5707
5834
|
import path38 from "path";
|
|
5708
5835
|
init_errors();
|
|
5709
|
-
init_http_client();
|
|
5710
5836
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
5711
5837
|
function requireOpenAIKey() {
|
|
5712
5838
|
const key = resolveKey("OPENAI_API_KEY");
|
|
@@ -5772,8 +5898,8 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
5772
5898
|
}
|
|
5773
5899
|
function fileFromPath(filePath) {
|
|
5774
5900
|
const abs = path38.resolve(filePath);
|
|
5775
|
-
if (!
|
|
5776
|
-
const bytes =
|
|
5901
|
+
if (!fs36.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
5902
|
+
const bytes = fs36.readFileSync(abs);
|
|
5777
5903
|
return new File([new Uint8Array(bytes)], path38.basename(abs));
|
|
5778
5904
|
}
|
|
5779
5905
|
|
|
@@ -5833,11 +5959,11 @@ async function generateOpenAISpeech(opts) {
|
|
|
5833
5959
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
5834
5960
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
5835
5961
|
const dest = path39.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
5836
|
-
|
|
5962
|
+
fs37.writeFileSync(dest, bytes);
|
|
5837
5963
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
5838
5964
|
if (opts.output) {
|
|
5839
|
-
|
|
5840
|
-
|
|
5965
|
+
fs37.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
|
|
5966
|
+
fs37.copyFileSync(dest, opts.output);
|
|
5841
5967
|
}
|
|
5842
5968
|
return dest;
|
|
5843
5969
|
}
|
|
@@ -5860,8 +5986,8 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
5860
5986
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
5861
5987
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
5862
5988
|
const dest = opts.output ? path39.resolve(opts.output) : path39.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
|
|
5863
|
-
|
|
5864
|
-
|
|
5989
|
+
fs37.mkdirSync(path39.dirname(dest), { recursive: true });
|
|
5990
|
+
fs37.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
5865
5991
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
5866
5992
|
return { path: dest, text };
|
|
5867
5993
|
}
|
|
@@ -5888,7 +6014,7 @@ Generated audio: ${dest}`);
|
|
|
5888
6014
|
}
|
|
5889
6015
|
|
|
5890
6016
|
// src/providers/openai/codex-image-driver.ts
|
|
5891
|
-
import
|
|
6017
|
+
import fs38 from "fs";
|
|
5892
6018
|
import os2 from "os";
|
|
5893
6019
|
import path40 from "path";
|
|
5894
6020
|
import { execa } from "execa";
|
|
@@ -5940,11 +6066,11 @@ async function runCodexImageGeneration(opts) {
|
|
|
5940
6066
|
const runner = opts.runner ?? defaultRunner;
|
|
5941
6067
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
5942
6068
|
const dest = opts.output ? path40.resolve(opts.output) : path40.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
|
|
5943
|
-
|
|
5944
|
-
const tmp =
|
|
6069
|
+
fs38.mkdirSync(path40.dirname(dest), { recursive: true });
|
|
6070
|
+
const tmp = fs38.mkdtempSync(path40.join(os2.tmpdir(), "multix-codex-image-"));
|
|
5945
6071
|
const schema = path40.join(tmp, "schema.json");
|
|
5946
6072
|
const result = path40.join(tmp, "result.json");
|
|
5947
|
-
|
|
6073
|
+
fs38.writeFileSync(
|
|
5948
6074
|
schema,
|
|
5949
6075
|
JSON.stringify({
|
|
5950
6076
|
type: "object",
|
|
@@ -5974,7 +6100,7 @@ async function runCodexImageGeneration(opts) {
|
|
|
5974
6100
|
if (res.exitCode !== 0) {
|
|
5975
6101
|
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
5976
6102
|
}
|
|
5977
|
-
if (!
|
|
6103
|
+
if (!fs38.existsSync(dest) || fs38.statSync(dest).size === 0) {
|
|
5978
6104
|
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
5979
6105
|
}
|
|
5980
6106
|
return { path: dest };
|
|
@@ -5991,8 +6117,7 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
5991
6117
|
|
|
5992
6118
|
// src/providers/openai/openai-image.ts
|
|
5993
6119
|
init_errors();
|
|
5994
|
-
|
|
5995
|
-
import fs38 from "fs";
|
|
6120
|
+
import fs39 from "fs";
|
|
5996
6121
|
import path41 from "path";
|
|
5997
6122
|
function buildImageGenerationBody(opts) {
|
|
5998
6123
|
return {
|
|
@@ -6027,13 +6152,13 @@ function saveOpenAIImages(opts) {
|
|
|
6027
6152
|
const b64 = items[i]?.b64_json;
|
|
6028
6153
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
6029
6154
|
const dest = path41.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
6030
|
-
|
|
6155
|
+
fs39.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
6031
6156
|
saved.push(dest);
|
|
6032
6157
|
opts.logger?.success(`Saved: ${dest}`);
|
|
6033
6158
|
}
|
|
6034
6159
|
if (opts.output && saved[0]) {
|
|
6035
|
-
|
|
6036
|
-
|
|
6160
|
+
fs39.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
|
|
6161
|
+
fs39.copyFileSync(saved[0], opts.output);
|
|
6037
6162
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
6038
6163
|
}
|
|
6039
6164
|
return saved;
|
|
@@ -6231,10 +6356,9 @@ function registerOpenAICommands(program2) {
|
|
|
6231
6356
|
}
|
|
6232
6357
|
|
|
6233
6358
|
// src/providers/openrouter/client.ts
|
|
6234
|
-
import
|
|
6359
|
+
import fs40 from "fs";
|
|
6235
6360
|
import path42 from "path";
|
|
6236
6361
|
init_errors();
|
|
6237
|
-
init_http_client();
|
|
6238
6362
|
|
|
6239
6363
|
// src/providers/openrouter/payload.ts
|
|
6240
6364
|
var IMAGE_ONLY_MODEL_PREFIXES = [
|
|
@@ -6247,15 +6371,15 @@ function resolveModalities(model) {
|
|
|
6247
6371
|
return IMAGE_ONLY_MODEL_PREFIXES.some((p) => lower.startsWith(p)) ? ["image"] : ["image", "text"];
|
|
6248
6372
|
}
|
|
6249
6373
|
function buildOpenRouterHeaders(apiKey) {
|
|
6250
|
-
const
|
|
6374
|
+
const headers2 = {
|
|
6251
6375
|
Authorization: `Bearer ${apiKey}`,
|
|
6252
6376
|
"Content-Type": "application/json"
|
|
6253
6377
|
};
|
|
6254
6378
|
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
6255
6379
|
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
6256
|
-
if (referer)
|
|
6257
|
-
if (title)
|
|
6258
|
-
return
|
|
6380
|
+
if (referer) headers2["HTTP-Referer"] = referer;
|
|
6381
|
+
if (title) headers2["X-Title"] = title;
|
|
6382
|
+
return headers2;
|
|
6259
6383
|
}
|
|
6260
6384
|
function extractImagesFromResponse(data) {
|
|
6261
6385
|
const out = { urls: [] };
|
|
@@ -6398,7 +6522,7 @@ async function generateOpenRouterImage(opts) {
|
|
|
6398
6522
|
try {
|
|
6399
6523
|
const bytes = await fetchBytes(url);
|
|
6400
6524
|
const dest = path42.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
6401
|
-
|
|
6525
|
+
fs40.writeFileSync(dest, bytes);
|
|
6402
6526
|
savedFiles.push(dest);
|
|
6403
6527
|
logger?.success(`Saved: ${dest}`);
|
|
6404
6528
|
} catch (e) {
|
|
@@ -6409,8 +6533,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
6409
6533
|
}
|
|
6410
6534
|
}
|
|
6411
6535
|
if (output && savedFiles[0]) {
|
|
6412
|
-
|
|
6413
|
-
|
|
6536
|
+
fs40.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
|
|
6537
|
+
fs40.writeFileSync(output, fs40.readFileSync(savedFiles[0]));
|
|
6414
6538
|
}
|
|
6415
6539
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
6416
6540
|
}
|
|
@@ -6451,10 +6575,9 @@ Generated ${finalImages.length} image(s):`);
|
|
|
6451
6575
|
}
|
|
6452
6576
|
|
|
6453
6577
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
6454
|
-
import
|
|
6578
|
+
import fs41 from "fs";
|
|
6455
6579
|
import path43 from "path";
|
|
6456
6580
|
init_errors();
|
|
6457
|
-
init_http_client();
|
|
6458
6581
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
6459
6582
|
var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
|
|
6460
6583
|
var HARD_LIMIT_BYTES2 = 8 * 1024 * 1024;
|
|
@@ -6519,7 +6642,7 @@ function parseStrength(raw) {
|
|
|
6519
6642
|
}
|
|
6520
6643
|
async function runImageToImage(opts) {
|
|
6521
6644
|
const apiKey = requireOpenRouterKey();
|
|
6522
|
-
const
|
|
6645
|
+
const headers2 = buildOpenRouterHeaders(apiKey);
|
|
6523
6646
|
const payload = buildI2IPayload({
|
|
6524
6647
|
prompt: opts.prompt,
|
|
6525
6648
|
model: opts.model,
|
|
@@ -6530,7 +6653,7 @@ async function runImageToImage(opts) {
|
|
|
6530
6653
|
const data = await httpJson({
|
|
6531
6654
|
url: OPENROUTER_API_URL,
|
|
6532
6655
|
method: "POST",
|
|
6533
|
-
headers,
|
|
6656
|
+
headers: headers2,
|
|
6534
6657
|
body: payload,
|
|
6535
6658
|
timeoutMs: 24e4
|
|
6536
6659
|
});
|
|
@@ -6546,31 +6669,30 @@ async function runImageToImage(opts) {
|
|
|
6546
6669
|
if (!url) continue;
|
|
6547
6670
|
const bytes = await fetchBytes(url);
|
|
6548
6671
|
const dest = path43.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
6549
|
-
|
|
6672
|
+
fs41.writeFileSync(dest, bytes);
|
|
6550
6673
|
saved.push(dest);
|
|
6551
6674
|
opts.logger.success(`Saved: ${dest}`);
|
|
6552
6675
|
}
|
|
6553
6676
|
if (opts.output && saved[0]) {
|
|
6554
|
-
|
|
6555
|
-
|
|
6677
|
+
fs41.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
|
|
6678
|
+
fs41.copyFileSync(saved[0], opts.output);
|
|
6556
6679
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
6557
6680
|
}
|
|
6558
6681
|
return saved;
|
|
6559
6682
|
}
|
|
6560
6683
|
|
|
6561
6684
|
// src/providers/openrouter/video-client.ts
|
|
6562
|
-
init_http_client();
|
|
6563
6685
|
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
6564
6686
|
function authHeaders5(apiKey) {
|
|
6565
|
-
const
|
|
6687
|
+
const headers2 = {
|
|
6566
6688
|
Authorization: `Bearer ${apiKey}`,
|
|
6567
6689
|
"Content-Type": "application/json"
|
|
6568
6690
|
};
|
|
6569
6691
|
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
6570
6692
|
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
6571
|
-
if (referer)
|
|
6572
|
-
if (title)
|
|
6573
|
-
return
|
|
6693
|
+
if (referer) headers2["HTTP-Referer"] = referer;
|
|
6694
|
+
if (title) headers2["X-Title"] = title;
|
|
6695
|
+
return headers2;
|
|
6574
6696
|
}
|
|
6575
6697
|
async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
6576
6698
|
return httpJson({
|
|
@@ -6598,7 +6720,7 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
6598
6720
|
}
|
|
6599
6721
|
|
|
6600
6722
|
// src/providers/openrouter/commands/video-status.ts
|
|
6601
|
-
import
|
|
6723
|
+
import fs42 from "fs";
|
|
6602
6724
|
import path44 from "path";
|
|
6603
6725
|
async function downloadOpenRouterVideo(opts) {
|
|
6604
6726
|
const { jobId, logger, source } = opts;
|
|
@@ -6614,14 +6736,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
6614
6736
|
logger.error("Empty response body");
|
|
6615
6737
|
process.exit(1);
|
|
6616
6738
|
}
|
|
6617
|
-
|
|
6618
|
-
const ws =
|
|
6739
|
+
fs42.mkdirSync(path44.dirname(dest), { recursive: true });
|
|
6740
|
+
const ws = fs42.createWriteStream(dest);
|
|
6619
6741
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
6620
6742
|
await pipeline2(r.body, ws);
|
|
6621
6743
|
logger.success(`Saved ${dest}`);
|
|
6622
6744
|
if (opts.output) {
|
|
6623
|
-
|
|
6624
|
-
|
|
6745
|
+
fs42.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
|
|
6746
|
+
fs42.copyFileSync(dest, opts.output);
|
|
6625
6747
|
logger.success(`Copied to ${opts.output}`);
|
|
6626
6748
|
}
|
|
6627
6749
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -6807,7 +6929,6 @@ async function checkBinary(binary, versionFlag = "--version") {
|
|
|
6807
6929
|
|
|
6808
6930
|
// src/commands/check-helpers/gemini-ping.ts
|
|
6809
6931
|
init_errors();
|
|
6810
|
-
init_http_client();
|
|
6811
6932
|
var MODELS_URL = "https://generativelanguage.googleapis.com/v1beta/models";
|
|
6812
6933
|
async function pingGemini(apiKey) {
|
|
6813
6934
|
try {
|
|
@@ -6912,6 +7033,10 @@ var PROVIDERS = [
|
|
|
6912
7033
|
}
|
|
6913
7034
|
];
|
|
6914
7035
|
function resolveProviderKey(p) {
|
|
7036
|
+
if (p.envPrimary === "GEMINI_API_KEY") {
|
|
7037
|
+
const first = listGeminiKeys()[0];
|
|
7038
|
+
return first ? { key: first.value, envUsed: first.name } : {};
|
|
7039
|
+
}
|
|
6915
7040
|
const primary = resolveKey(p.envPrimary);
|
|
6916
7041
|
if (primary) return { key: primary, envUsed: p.envPrimary };
|
|
6917
7042
|
if (p.envFallback) {
|
|
@@ -6963,6 +7088,14 @@ function registerCheckCommand(program2) {
|
|
|
6963
7088
|
const envLabel = r.envUsed ?? p.envPrimary;
|
|
6964
7089
|
if (r.key) {
|
|
6965
7090
|
logger.success(`${envLabel} found \u2014 ${redact(r.key)}`);
|
|
7091
|
+
if (p.name === "Gemini") {
|
|
7092
|
+
const pool = listGeminiKeys();
|
|
7093
|
+
if (pool.length > 1) {
|
|
7094
|
+
logger.info(
|
|
7095
|
+
`Gemini key rotation: ${pool.length} keys (${pool.map((k) => k.name).join(", ")})`
|
|
7096
|
+
);
|
|
7097
|
+
}
|
|
7098
|
+
}
|
|
6966
7099
|
} else if (p.optional) {
|
|
6967
7100
|
const fbNote = p.envFallback ? ` (or ${p.envFallback})` : "";
|
|
6968
7101
|
logger.info(`${p.envPrimary}${fbNote} not set (optional)`);
|
|
@@ -7008,14 +7141,18 @@ function registerCheckCommand(program2) {
|
|
|
7008
7141
|
if (geminiKey) {
|
|
7009
7142
|
logger.header("Gemini Connectivity");
|
|
7010
7143
|
logger.info("Pinging Gemini models endpoint...");
|
|
7011
|
-
const
|
|
7012
|
-
|
|
7013
|
-
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7144
|
+
for (const key of listGeminiKeys()) {
|
|
7145
|
+
const ping = await pingGemini(key.value);
|
|
7146
|
+
if (ping.status === "success") {
|
|
7147
|
+
logger.success(
|
|
7148
|
+
`${key.name}: Gemini API reachable \u2014 ${ping.modelCount ?? 0} models available`
|
|
7149
|
+
);
|
|
7150
|
+
} else if (ping.status === "auth_error") {
|
|
7151
|
+
logger.error(`Gemini auth failed \u2014 check ${key.name}: ${ping.error ?? ""}`);
|
|
7152
|
+
exitCode = 1;
|
|
7153
|
+
} else {
|
|
7154
|
+
logger.warn(`${key.name}: Gemini network error (API may be down): ${ping.error ?? ""}`);
|
|
7155
|
+
}
|
|
7019
7156
|
}
|
|
7020
7157
|
}
|
|
7021
7158
|
logger.header("Summary");
|
|
@@ -7040,7 +7177,7 @@ function registerCheckCommand(program2) {
|
|
|
7040
7177
|
|
|
7041
7178
|
// src/commands/doc/convert.ts
|
|
7042
7179
|
init_errors();
|
|
7043
|
-
import
|
|
7180
|
+
import fs43 from "fs";
|
|
7044
7181
|
import path45 from "path";
|
|
7045
7182
|
|
|
7046
7183
|
// src/commands/doc/default-prompt.ts
|
|
@@ -7062,7 +7199,7 @@ async function convertDocuments(opts) {
|
|
|
7062
7199
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
7063
7200
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
7064
7201
|
for (const f of inputs) {
|
|
7065
|
-
if (!
|
|
7202
|
+
if (!fs43.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
7066
7203
|
}
|
|
7067
7204
|
let outputPath;
|
|
7068
7205
|
if (output) {
|
|
@@ -7073,7 +7210,7 @@ async function convertDocuments(opts) {
|
|
|
7073
7210
|
} else {
|
|
7074
7211
|
outputPath = path45.join(getOutputDir(), "document-extraction.md");
|
|
7075
7212
|
}
|
|
7076
|
-
|
|
7213
|
+
fs43.mkdirSync(path45.dirname(outputPath), { recursive: true });
|
|
7077
7214
|
const results = [];
|
|
7078
7215
|
for (let i = 0; i < inputs.length; i++) {
|
|
7079
7216
|
const filePath = inputs[i];
|
|
@@ -7082,30 +7219,39 @@ async function convertDocuments(opts) {
|
|
|
7082
7219
|
let errorMsg;
|
|
7083
7220
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
7084
7221
|
try {
|
|
7085
|
-
const
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7100
|
-
|
|
7222
|
+
const resp = await withGeminiKey(
|
|
7223
|
+
async (apiKey) => {
|
|
7224
|
+
const useApi = shouldUseFileApi(filePath);
|
|
7225
|
+
let contentParts;
|
|
7226
|
+
if (useApi) {
|
|
7227
|
+
logger?.debug("File >20MB \u2014 using Files API");
|
|
7228
|
+
const ref = await uploadFile(filePath, { logger, apiKey });
|
|
7229
|
+
contentParts = [
|
|
7230
|
+
{ text: prompt },
|
|
7231
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
7232
|
+
];
|
|
7233
|
+
} else {
|
|
7234
|
+
contentParts = [{ text: prompt }, inlinePart(filePath)];
|
|
7235
|
+
}
|
|
7236
|
+
return generateContent(
|
|
7237
|
+
{
|
|
7238
|
+
model,
|
|
7239
|
+
contents: [{ parts: contentParts }]
|
|
7240
|
+
},
|
|
7241
|
+
{ apiKey }
|
|
7242
|
+
);
|
|
7243
|
+
},
|
|
7244
|
+
{ logger }
|
|
7245
|
+
);
|
|
7101
7246
|
markdown = extractText(resp);
|
|
7102
7247
|
break;
|
|
7103
7248
|
} catch (e) {
|
|
7104
7249
|
errorMsg = e instanceof Error ? e.message : String(e);
|
|
7250
|
+
if (isGeminiRateLimitError(e)) break;
|
|
7105
7251
|
if (attempt < 2) {
|
|
7106
7252
|
const wait = 2 ** attempt * 1e3;
|
|
7107
7253
|
logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
|
|
7108
|
-
await
|
|
7254
|
+
await sleep4(wait);
|
|
7109
7255
|
}
|
|
7110
7256
|
}
|
|
7111
7257
|
}
|
|
@@ -7137,7 +7283,7 @@ async function convertDocuments(opts) {
|
|
|
7137
7283
|
}
|
|
7138
7284
|
lines.push("\n---\n");
|
|
7139
7285
|
}
|
|
7140
|
-
|
|
7286
|
+
fs43.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
7141
7287
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
7142
7288
|
console.log(`
|
|
7143
7289
|
${"=".repeat(50)}`);
|
|
@@ -7153,7 +7299,7 @@ ${"=".repeat(50)}`);
|
|
|
7153
7299
|
const failed = results.filter((r) => r.status === "error").length;
|
|
7154
7300
|
if (failed > 0) process.exit(1);
|
|
7155
7301
|
}
|
|
7156
|
-
function
|
|
7302
|
+
function sleep4(ms) {
|
|
7157
7303
|
return new Promise((r) => setTimeout(r, ms));
|
|
7158
7304
|
}
|
|
7159
7305
|
|
|
@@ -7185,11 +7331,11 @@ function registerDocCommands(program2) {
|
|
|
7185
7331
|
|
|
7186
7332
|
// src/commands/media/index.ts
|
|
7187
7333
|
init_errors();
|
|
7188
|
-
import
|
|
7334
|
+
import fs46 from "fs";
|
|
7189
7335
|
import path48 from "path";
|
|
7190
7336
|
|
|
7191
7337
|
// src/commands/media/batch.ts
|
|
7192
|
-
import
|
|
7338
|
+
import fs44 from "fs";
|
|
7193
7339
|
import path46 from "path";
|
|
7194
7340
|
|
|
7195
7341
|
// src/commands/media/detect-type.ts
|
|
@@ -7386,8 +7532,8 @@ async function batchOptimize(opts) {
|
|
|
7386
7532
|
verbose,
|
|
7387
7533
|
logger
|
|
7388
7534
|
} = opts;
|
|
7389
|
-
|
|
7390
|
-
const entries =
|
|
7535
|
+
fs44.mkdirSync(outputDir, { recursive: true });
|
|
7536
|
+
const entries = fs44.readdirSync(inputDir);
|
|
7391
7537
|
const mediaFiles = entries.filter((f) => {
|
|
7392
7538
|
const ext = path46.extname(f).toLowerCase();
|
|
7393
7539
|
return ALL_EXTS.has(ext);
|
|
@@ -7439,7 +7585,7 @@ async function batchOptimize(opts) {
|
|
|
7439
7585
|
|
|
7440
7586
|
// src/commands/media/split-video.ts
|
|
7441
7587
|
init_errors();
|
|
7442
|
-
import
|
|
7588
|
+
import fs45 from "fs";
|
|
7443
7589
|
import path47 from "path";
|
|
7444
7590
|
async function splitVideo(opts) {
|
|
7445
7591
|
await assertFfmpeg();
|
|
@@ -7451,7 +7597,7 @@ async function splitVideo(opts) {
|
|
|
7451
7597
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
7452
7598
|
return [input];
|
|
7453
7599
|
}
|
|
7454
|
-
|
|
7600
|
+
fs45.mkdirSync(outputDir, { recursive: true });
|
|
7455
7601
|
const ext = path47.extname(input) || ".mp4";
|
|
7456
7602
|
const baseName = path47.basename(input, ext);
|
|
7457
7603
|
const outputPattern = path47.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
@@ -7474,7 +7620,7 @@ async function splitVideo(opts) {
|
|
|
7474
7620
|
const files = [];
|
|
7475
7621
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
7476
7622
|
const fname = path47.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
7477
|
-
if (
|
|
7623
|
+
if (fs45.existsSync(fname)) files.push(fname);
|
|
7478
7624
|
}
|
|
7479
7625
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
7480
7626
|
return files;
|
|
@@ -7488,8 +7634,8 @@ function registerMediaCommands(program2) {
|
|
|
7488
7634
|
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(
|
|
7489
7635
|
async (opts) => {
|
|
7490
7636
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7491
|
-
if (!
|
|
7492
|
-
|
|
7637
|
+
if (!fs46.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7638
|
+
fs46.mkdirSync(path48.dirname(path48.resolve(opts.output)), { recursive: true });
|
|
7493
7639
|
const ext = path48.extname(opts.input).toLowerCase();
|
|
7494
7640
|
const kind = extToKind(ext);
|
|
7495
7641
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
@@ -7530,7 +7676,7 @@ function registerMediaCommands(program2) {
|
|
|
7530
7676
|
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(
|
|
7531
7677
|
async (opts) => {
|
|
7532
7678
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7533
|
-
if (!
|
|
7679
|
+
if (!fs46.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7534
7680
|
const chunks = await splitVideo({
|
|
7535
7681
|
input: opts.input,
|
|
7536
7682
|
outputDir: opts.outputDir,
|
|
@@ -7546,7 +7692,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
7546
7692
|
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(
|
|
7547
7693
|
async (opts) => {
|
|
7548
7694
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7549
|
-
if (!
|
|
7695
|
+
if (!fs46.existsSync(opts.inputDir))
|
|
7550
7696
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
7551
7697
|
const result = await batchOptimize({
|
|
7552
7698
|
inputDir: opts.inputDir,
|
|
@@ -7569,7 +7715,6 @@ Processed: ${result.succeeded}/${result.total} files`);
|
|
|
7569
7715
|
|
|
7570
7716
|
// src/commands/update.ts
|
|
7571
7717
|
init_errors();
|
|
7572
|
-
init_http_client();
|
|
7573
7718
|
import { createRequire } from "module";
|
|
7574
7719
|
import { execa as execa5 } from "execa";
|
|
7575
7720
|
|
|
@@ -7734,11 +7879,6 @@ function registerCommands(program2) {
|
|
|
7734
7879
|
// src/core/index.ts
|
|
7735
7880
|
init_errors();
|
|
7736
7881
|
init_redact();
|
|
7737
|
-
init_http_client();
|
|
7738
|
-
init_fetch_json();
|
|
7739
|
-
|
|
7740
|
-
// src/core/async-poll.ts
|
|
7741
|
-
init_errors();
|
|
7742
7882
|
|
|
7743
7883
|
// src/cli.ts
|
|
7744
7884
|
loadEnv();
|