@mrgoonie/multix 0.0.2 → 0.0.5
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 +22 -0
- package/README.md +136 -2
- package/dist/cli.js +3000 -580
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -155,10 +155,6 @@ var init_http_client = __esm({
|
|
|
155
155
|
import { createRequire } from "module";
|
|
156
156
|
import { Command } from "commander";
|
|
157
157
|
|
|
158
|
-
// src/providers/gemini/commands/analyze.ts
|
|
159
|
-
init_errors();
|
|
160
|
-
import fs5 from "fs";
|
|
161
|
-
|
|
162
158
|
// src/core/logger.ts
|
|
163
159
|
var ANSI = {
|
|
164
160
|
reset: "\x1B[0m",
|
|
@@ -204,10 +200,6 @@ ${line}
|
|
|
204
200
|
}
|
|
205
201
|
var defaultLogger = createLogger({ verbose: false });
|
|
206
202
|
|
|
207
|
-
// src/providers/gemini/client.ts
|
|
208
|
-
import fs3 from "fs";
|
|
209
|
-
import path4 from "path";
|
|
210
|
-
|
|
211
203
|
// src/core/env-loader.ts
|
|
212
204
|
import fs from "fs";
|
|
213
205
|
import os from "os";
|
|
@@ -236,234 +228,1130 @@ function redact(key) {
|
|
|
236
228
|
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
237
229
|
}
|
|
238
230
|
|
|
239
|
-
// src/providers/
|
|
231
|
+
// src/providers/byteplus/client.ts
|
|
240
232
|
init_errors();
|
|
241
233
|
init_http_client();
|
|
242
234
|
|
|
243
|
-
// src/providers/
|
|
235
|
+
// src/providers/byteplus/models.ts
|
|
236
|
+
var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
|
|
237
|
+
var DEFAULT_BYTEPLUS_IMAGE_MODEL = "seedream-4-0-250828";
|
|
238
|
+
var DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-2.0";
|
|
239
|
+
var BYTEPLUS_DEFAULTS = {
|
|
240
|
+
imageModel: process.env.BYTEPLUS_IMAGE_MODEL ?? DEFAULT_BYTEPLUS_IMAGE_MODEL,
|
|
241
|
+
videoModel: process.env.BYTEPLUS_VIDEO_MODEL ?? DEFAULT_BYTEPLUS_VIDEO_MODEL,
|
|
242
|
+
videoResolution: "1080p",
|
|
243
|
+
videoDuration: 8,
|
|
244
|
+
videoAspectRatio: "16:9",
|
|
245
|
+
imageSize: "2K"
|
|
246
|
+
};
|
|
247
|
+
function bytePlusVideoParamsMode() {
|
|
248
|
+
const m = process.env.BYTEPLUS_VIDEO_PARAMS_MODE;
|
|
249
|
+
return m === "structured" ? "structured" : "flags";
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// src/providers/byteplus/client.ts
|
|
253
|
+
function requireBytePlusKey() {
|
|
254
|
+
const key = resolveKey("BYTEPLUS_API_KEY") ?? resolveKey("ARK_API_KEY");
|
|
255
|
+
if (!key) {
|
|
256
|
+
throw new ConfigError(
|
|
257
|
+
"BytePlus API key not set. Set BYTEPLUS_API_KEY or ARK_API_KEY. Get one at https://console.byteplus.com/auth/api-keys"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return key;
|
|
261
|
+
}
|
|
262
|
+
function bytePlusBaseUrl() {
|
|
263
|
+
return resolveKey("BYTEPLUS_BASE_URL") ?? DEFAULT_BYTEPLUS_BASE_URL;
|
|
264
|
+
}
|
|
265
|
+
var BytePlusClient = class {
|
|
266
|
+
constructor(apiKey, baseUrl = bytePlusBaseUrl()) {
|
|
267
|
+
this.apiKey = apiKey;
|
|
268
|
+
this.baseUrl = baseUrl;
|
|
269
|
+
}
|
|
270
|
+
apiKey;
|
|
271
|
+
baseUrl;
|
|
272
|
+
async request(path35, opts = {}) {
|
|
273
|
+
const base = this.baseUrl.replace(/\/$/, "");
|
|
274
|
+
const url = new URL(base + path35);
|
|
275
|
+
if (opts.query) {
|
|
276
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
277
|
+
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
|
|
281
|
+
return httpJson({
|
|
282
|
+
url: url.toString(),
|
|
283
|
+
method: opts.method ?? "GET",
|
|
284
|
+
headers: {
|
|
285
|
+
accept: "application/json",
|
|
286
|
+
authorization: `Bearer ${this.apiKey}`
|
|
287
|
+
},
|
|
288
|
+
body: opts.body,
|
|
289
|
+
timeoutMs: opts.timeoutMs ?? 12e4
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
get(path35, query, logger) {
|
|
293
|
+
return this.request(path35, { method: "GET", query, logger });
|
|
294
|
+
}
|
|
295
|
+
post(path35, body, logger) {
|
|
296
|
+
return this.request(path35, { method: "POST", body, logger });
|
|
297
|
+
}
|
|
298
|
+
delete(path35, logger) {
|
|
299
|
+
return this.request(path35, { method: "DELETE", logger });
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
function createBytePlusClient() {
|
|
303
|
+
return new BytePlusClient(requireBytePlusKey(), bytePlusBaseUrl());
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/providers/byteplus/generators/image.ts
|
|
307
|
+
init_http_client();
|
|
308
|
+
import fs5 from "fs";
|
|
309
|
+
import path5 from "path";
|
|
310
|
+
|
|
311
|
+
// src/core/image-input.ts
|
|
312
|
+
import fs3 from "fs/promises";
|
|
244
313
|
import path3 from "path";
|
|
245
|
-
var
|
|
246
|
-
// Audio
|
|
247
|
-
".mp3": "audio/mp3",
|
|
248
|
-
".wav": "audio/wav",
|
|
249
|
-
".aac": "audio/aac",
|
|
250
|
-
".flac": "audio/flac",
|
|
251
|
-
".ogg": "audio/ogg",
|
|
252
|
-
".aiff": "audio/aiff",
|
|
253
|
-
".m4a": "audio/mp4",
|
|
254
|
-
// Image
|
|
314
|
+
var IMAGE_MIME_BY_EXT = {
|
|
255
315
|
".jpg": "image/jpeg",
|
|
256
316
|
".jpeg": "image/jpeg",
|
|
257
317
|
".png": "image/png",
|
|
258
318
|
".webp": "image/webp",
|
|
259
|
-
".
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
".bmp": "image/bmp",
|
|
263
|
-
// Video
|
|
319
|
+
".gif": "image/gif"
|
|
320
|
+
};
|
|
321
|
+
var VIDEO_MIME_BY_EXT = {
|
|
264
322
|
".mp4": "video/mp4",
|
|
265
|
-
".mpeg": "video/mpeg",
|
|
266
|
-
".mov": "video/quicktime",
|
|
267
|
-
".avi": "video/x-msvideo",
|
|
268
|
-
".flv": "video/x-flv",
|
|
269
|
-
".mpg": "video/mpeg",
|
|
270
323
|
".webm": "video/webm",
|
|
271
|
-
".
|
|
272
|
-
".
|
|
273
|
-
// Document
|
|
274
|
-
".pdf": "application/pdf",
|
|
275
|
-
".txt": "text/plain",
|
|
276
|
-
".html": "text/html",
|
|
277
|
-
".htm": "text/html",
|
|
278
|
-
".md": "text/markdown",
|
|
279
|
-
".csv": "text/csv",
|
|
280
|
-
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
281
|
-
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
282
|
-
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
324
|
+
".mov": "video/quicktime",
|
|
325
|
+
".m4v": "video/x-m4v"
|
|
283
326
|
};
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
327
|
+
var AUDIO_MIME_BY_EXT = {
|
|
328
|
+
".mp3": "audio/mpeg",
|
|
329
|
+
".wav": "audio/wav",
|
|
330
|
+
".m4a": "audio/mp4",
|
|
331
|
+
".flac": "audio/flac",
|
|
332
|
+
".ogg": "audio/ogg"
|
|
333
|
+
};
|
|
334
|
+
var DEFAULT_SOFT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
335
|
+
var DEFAULT_HARD_LIMIT_BYTES = 25 * 1024 * 1024;
|
|
336
|
+
function isHttpUrl(input) {
|
|
337
|
+
return /^https?:\/\//i.test(input);
|
|
290
338
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const key = resolveKey("GEMINI_API_KEY");
|
|
298
|
-
if (!key)
|
|
299
|
-
throw new ConfigError(
|
|
300
|
-
"GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
|
|
339
|
+
async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
|
|
340
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
341
|
+
const mime = mimeTable[ext];
|
|
342
|
+
if (!mime) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`Unsupported ${kindLabel} extension '${ext}'. Allowed: ${Object.keys(mimeTable).join(", ")}`
|
|
301
345
|
);
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
const { timeoutMs = 3e5, logger } = opts;
|
|
310
|
-
const mimeType = getMimeType(filePath);
|
|
311
|
-
const fileBytes = fs3.readFileSync(filePath);
|
|
312
|
-
const displayName = path4.basename(filePath);
|
|
313
|
-
const boundary = `multix_${Date.now()}`;
|
|
314
|
-
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
315
|
-
const body = Buffer.concat([
|
|
316
|
-
Buffer.from(
|
|
317
|
-
`--${boundary}\r
|
|
318
|
-
Content-Type: application/json; charset=UTF-8\r
|
|
319
|
-
\r
|
|
320
|
-
${metaJson}\r
|
|
321
|
-
--${boundary}\r
|
|
322
|
-
Content-Type: ${mimeType}\r
|
|
323
|
-
\r
|
|
324
|
-
`
|
|
325
|
-
),
|
|
326
|
-
fileBytes,
|
|
327
|
-
Buffer.from(`\r
|
|
328
|
-
--${boundary}--`)
|
|
329
|
-
]);
|
|
330
|
-
logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
331
|
-
const resp = await globalThis.fetch(FILES_BASE, {
|
|
332
|
-
method: "POST",
|
|
333
|
-
headers: {
|
|
334
|
-
"x-goog-api-key": apiKey,
|
|
335
|
-
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
336
|
-
"Content-Length": String(body.length),
|
|
337
|
-
"X-Goog-Upload-Protocol": "multipart"
|
|
338
|
-
},
|
|
339
|
-
body,
|
|
340
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
341
|
-
});
|
|
342
|
-
if (!resp.ok) {
|
|
343
|
-
const txt = await resp.text();
|
|
344
|
-
throw new ProviderError(
|
|
345
|
-
`File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
|
|
346
|
-
"gemini"
|
|
346
|
+
}
|
|
347
|
+
const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
|
|
348
|
+
const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
|
|
349
|
+
const buf = await fs3.readFile(filePath);
|
|
350
|
+
if (buf.byteLength > hard) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`${kindLabel} file ${filePath} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds ${(hard / 1024 / 1024).toFixed(0)}MB limit. Upload to a URL instead.`
|
|
347
353
|
);
|
|
348
354
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
|
|
354
|
-
await sleep(2e3);
|
|
355
|
-
fileRef = await httpJson({
|
|
356
|
-
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
357
|
-
headers: authHeaders(apiKey)
|
|
358
|
-
}).then((r) => r.file);
|
|
359
|
-
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
360
|
-
}
|
|
361
|
-
if (fileRef.state === "FAILED")
|
|
362
|
-
throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
|
|
363
|
-
if (fileRef.state === "PROCESSING")
|
|
364
|
-
throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
|
|
355
|
+
if (buf.byteLength > soft) {
|
|
356
|
+
opts.logger?.warn(
|
|
357
|
+
`${kindLabel} payload ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB is large; consider URL input.`
|
|
358
|
+
);
|
|
365
359
|
}
|
|
366
|
-
|
|
367
|
-
return
|
|
360
|
+
const dataUrl = `data:${mime};base64,${buf.toString("base64")}`;
|
|
361
|
+
return { dataUrl, mime, bytes: buf.byteLength };
|
|
368
362
|
}
|
|
369
|
-
async function
|
|
370
|
-
const
|
|
371
|
-
return
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
headers: authHeaders(apiKey),
|
|
375
|
-
body: {
|
|
376
|
-
contents: req.contents,
|
|
377
|
-
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
378
|
-
}
|
|
379
|
-
});
|
|
363
|
+
async function resolveImageInput(input, opts = {}) {
|
|
364
|
+
const o = normalizeOpts(opts);
|
|
365
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
366
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, IMAGE_MIME_BY_EXT, "image", o);
|
|
367
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
380
368
|
}
|
|
381
|
-
function
|
|
382
|
-
const
|
|
383
|
-
|
|
369
|
+
async function resolveVideoInput(input, opts = {}) {
|
|
370
|
+
const o = normalizeOpts(opts);
|
|
371
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
372
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, VIDEO_MIME_BY_EXT, "video", o);
|
|
373
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
384
374
|
}
|
|
385
|
-
function
|
|
386
|
-
const
|
|
387
|
-
return
|
|
388
|
-
|
|
389
|
-
|
|
375
|
+
async function resolveAudioInput(input, opts = {}) {
|
|
376
|
+
const o = normalizeOpts(opts);
|
|
377
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
378
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, AUDIO_MIME_BY_EXT, "audio", o);
|
|
379
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
390
380
|
}
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
const mimeType = getMimeType(filePath);
|
|
394
|
-
return { inlineData: { mimeType, data } };
|
|
381
|
+
function refUrl(r) {
|
|
382
|
+
return r.kind === "url" ? r.url : r.dataUrl;
|
|
395
383
|
}
|
|
396
|
-
function
|
|
397
|
-
|
|
398
|
-
|
|
384
|
+
function normalizeOpts(opts) {
|
|
385
|
+
if (!opts) return {};
|
|
386
|
+
if (typeof opts.info === "function" && typeof opts.warn === "function") {
|
|
387
|
+
return { logger: opts };
|
|
388
|
+
}
|
|
389
|
+
return opts;
|
|
399
390
|
}
|
|
400
|
-
|
|
401
|
-
|
|
391
|
+
|
|
392
|
+
// src/core/output-dir.ts
|
|
393
|
+
import fs4 from "fs";
|
|
394
|
+
import path4 from "path";
|
|
395
|
+
var cachedOutputDir;
|
|
396
|
+
function getOutputDir() {
|
|
397
|
+
if (cachedOutputDir) return cachedOutputDir;
|
|
398
|
+
const dir = process.env.MULTIX_OUTPUT_DIR ? path4.resolve(process.env.MULTIX_OUTPUT_DIR) : path4.resolve(process.cwd(), "multix-output");
|
|
399
|
+
fs4.mkdirSync(dir, { recursive: true });
|
|
400
|
+
cachedOutputDir = dir;
|
|
401
|
+
return dir;
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
-
// src/providers/
|
|
405
|
-
var
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
"
|
|
411
|
-
"
|
|
412
|
-
"
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
...IMAGEN_MODELS
|
|
420
|
-
]);
|
|
421
|
-
var ASPECT_RATIOS = [
|
|
422
|
-
"1:1",
|
|
423
|
-
"2:3",
|
|
424
|
-
"3:2",
|
|
425
|
-
"3:4",
|
|
426
|
-
"4:3",
|
|
427
|
-
"4:5",
|
|
428
|
-
"5:4",
|
|
429
|
-
"9:16",
|
|
430
|
-
"16:9",
|
|
431
|
-
"21:9"
|
|
432
|
-
];
|
|
433
|
-
var IMAGE_SIZES = ["1K", "2K", "4K"];
|
|
434
|
-
function getDefaultModel(task) {
|
|
435
|
-
if (task === "generate") {
|
|
436
|
-
return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
|
|
404
|
+
// src/providers/byteplus/generators/image.ts
|
|
405
|
+
var ASPECT_TO_SIZE = {
|
|
406
|
+
"1:1": "2048x2048",
|
|
407
|
+
"16:9": "2560x1440",
|
|
408
|
+
"9:16": "1440x2560",
|
|
409
|
+
"4:3": "2048x1536",
|
|
410
|
+
"3:4": "1536x2048",
|
|
411
|
+
"21:9": "2560x1080",
|
|
412
|
+
"3:2": "2048x1365",
|
|
413
|
+
"2:3": "1365x2048"
|
|
414
|
+
};
|
|
415
|
+
function resolveSize(opts) {
|
|
416
|
+
if (opts.size && opts.aspectRatio) {
|
|
417
|
+
opts.logger?.warn("Both --size and --aspect-ratio given; using --size.");
|
|
418
|
+
return opts.size;
|
|
437
419
|
}
|
|
438
|
-
if (
|
|
439
|
-
|
|
420
|
+
if (opts.size) return opts.size;
|
|
421
|
+
if (opts.aspectRatio) {
|
|
422
|
+
const mapped = ASPECT_TO_SIZE[opts.aspectRatio];
|
|
423
|
+
if (!mapped) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
`Unsupported --aspect-ratio '${opts.aspectRatio}'. Supported: ${Object.keys(ASPECT_TO_SIZE).join(", ")}`
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
return mapped;
|
|
440
429
|
}
|
|
441
|
-
return
|
|
430
|
+
return BYTEPLUS_DEFAULTS.imageSize;
|
|
442
431
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
fs4.mkdirSync(path5.dirname(path5.resolve(outputPath)), { recursive: true });
|
|
451
|
-
fs4.writeFileSync(outputPath, content, "utf8");
|
|
452
|
-
} else {
|
|
453
|
-
process.stdout.write(content);
|
|
432
|
+
async function generateSeedream(client, opts) {
|
|
433
|
+
const logger = opts.logger;
|
|
434
|
+
const model = opts.model ?? BYTEPLUS_DEFAULTS.imageModel;
|
|
435
|
+
const resolvedInputs = [];
|
|
436
|
+
for (const input of opts.inputImages ?? []) {
|
|
437
|
+
const r = await resolveImageInput(input, logger);
|
|
438
|
+
resolvedInputs.push(refUrl(r));
|
|
454
439
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
440
|
+
const body = {
|
|
441
|
+
model,
|
|
442
|
+
prompt: opts.prompt,
|
|
443
|
+
response_format: "url",
|
|
444
|
+
n: opts.numImages ?? 1,
|
|
445
|
+
watermark: opts.watermark ?? true,
|
|
446
|
+
size: resolveSize(opts)
|
|
447
|
+
};
|
|
448
|
+
if (resolvedInputs.length > 0) body.image = resolvedInputs;
|
|
449
|
+
if (opts.seed !== void 0) body.seed = opts.seed;
|
|
450
|
+
logger?.debug(
|
|
451
|
+
`Seedream body: ${JSON.stringify({ ...body, image: body.image ? `[${body.image.length}]` : void 0 })}`
|
|
452
|
+
);
|
|
453
|
+
const res = await client.post("/images/generations", body, logger);
|
|
454
|
+
const items = res.data ?? [];
|
|
455
|
+
const ts = Date.now();
|
|
456
|
+
const outDir = getOutputDir();
|
|
457
|
+
const saved = [];
|
|
458
|
+
for (let i = 0; i < items.length; i++) {
|
|
459
|
+
const item = items[i];
|
|
460
|
+
if (!item) continue;
|
|
461
|
+
const url = item.url;
|
|
462
|
+
if (!url) {
|
|
463
|
+
logger?.warn(`Image ${i + 1}: no URL in response (b64_json fallback not yet supported).`);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
const ext = (() => {
|
|
467
|
+
try {
|
|
468
|
+
const e = path5.extname(new URL(url).pathname);
|
|
469
|
+
return e || ".jpg";
|
|
470
|
+
} catch {
|
|
471
|
+
return ".jpg";
|
|
472
|
+
}
|
|
473
|
+
})();
|
|
474
|
+
const filename = opts.output && items.length === 1 ? path5.resolve(opts.output) : path5.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
|
|
475
|
+
fs5.mkdirSync(path5.dirname(filename), { recursive: true });
|
|
476
|
+
await downloadFile(url, filename);
|
|
477
|
+
saved.push({ path: filename, url, seed: item.seed });
|
|
478
|
+
logger?.success(`Saved ${filename}`);
|
|
479
|
+
}
|
|
480
|
+
return saved;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// src/providers/byteplus/commands/generate.ts
|
|
484
|
+
function registerBytePlusGenerateCommand(parent) {
|
|
485
|
+
parent.command("generate").alias("gen").description("Generate images with BytePlus Seedream 4.0 (text-to-image, multi-image input)").requiredOption("--prompt <text>", "Text prompt").option("-m, --model <id>", "Model id (default seedream-4-0-250828)").option("--size <size>", "Image size (e.g. 1024x1024, 1K, 2K, 4K)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9, 9:16); maps to size").option("-n, --num-images <n>", "Number of images", "1").option("--seed <n>", "Fixed seed").option("--no-watermark", "Disable watermark").option(
|
|
486
|
+
"--input-image <path>",
|
|
487
|
+
"Reference image (URL or local path); repeatable",
|
|
488
|
+
collect,
|
|
489
|
+
[]
|
|
490
|
+
).option("--output <path>", "Save single image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
491
|
+
async (opts) => {
|
|
492
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
493
|
+
const client = createBytePlusClient();
|
|
494
|
+
const saved = await generateSeedream(client, {
|
|
495
|
+
prompt: opts.prompt,
|
|
496
|
+
model: opts.model,
|
|
497
|
+
size: opts.size,
|
|
498
|
+
aspectRatio: opts.aspectRatio,
|
|
499
|
+
numImages: Number.parseInt(opts.numImages, 10) || 1,
|
|
500
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
501
|
+
watermark: opts.watermark,
|
|
502
|
+
inputImages: opts.inputImage,
|
|
503
|
+
output: opts.output,
|
|
504
|
+
logger
|
|
505
|
+
});
|
|
506
|
+
console.log(`
|
|
507
|
+
Generated ${saved.length} image(s):`);
|
|
508
|
+
for (const f of saved) console.log(` ${f.path}`);
|
|
509
|
+
}
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
function collect(value, prev) {
|
|
513
|
+
return [...prev, value];
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/providers/byteplus/commands/image-to-image.ts
|
|
517
|
+
function registerBytePlusImageToImageCommand(parent) {
|
|
518
|
+
parent.command("image-to-image").alias("i2i").description(
|
|
519
|
+
"Edit/transform image(s) with BytePlus Seedream/Seededit using one or more reference images"
|
|
520
|
+
).requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
521
|
+
"--ref <path>",
|
|
522
|
+
"Reference image (URL or local path); repeatable for multi-ref",
|
|
523
|
+
collect2,
|
|
524
|
+
[]
|
|
525
|
+
).option("-m, --model <id>", "Model id (default seedream-4-0-250828)").option("--size <size>", "Image size (e.g. 1024x1024, 1K, 2K, 4K)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9, 9:16); maps to size").option("-n, --num-images <n>", "Number of images", "1").option("--seed <n>", "Fixed seed").option("--no-watermark", "Disable watermark").option("--output <path>", "Save single image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
526
|
+
async (opts) => {
|
|
527
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
528
|
+
throw new Error("At least one --ref is required for image-to-image.");
|
|
529
|
+
}
|
|
530
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
531
|
+
const client = createBytePlusClient();
|
|
532
|
+
const saved = await generateSeedream(client, {
|
|
533
|
+
prompt: opts.prompt,
|
|
534
|
+
model: opts.model,
|
|
535
|
+
size: opts.size,
|
|
536
|
+
aspectRatio: opts.aspectRatio,
|
|
537
|
+
numImages: Number.parseInt(opts.numImages, 10) || 1,
|
|
538
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
539
|
+
watermark: opts.watermark,
|
|
540
|
+
inputImages: opts.ref,
|
|
541
|
+
output: opts.output,
|
|
542
|
+
logger
|
|
543
|
+
});
|
|
544
|
+
console.log(`
|
|
545
|
+
Generated ${saved.length} image(s):`);
|
|
546
|
+
for (const f of saved) console.log(` ${f.path}`);
|
|
547
|
+
}
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
function collect2(value, prev) {
|
|
551
|
+
return [...prev, value];
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// src/providers/byteplus/commands/image-to-video.ts
|
|
555
|
+
import path7 from "path";
|
|
556
|
+
|
|
557
|
+
// src/core/video-thumb.ts
|
|
558
|
+
init_http_client();
|
|
559
|
+
import fs6 from "fs";
|
|
560
|
+
import path6 from "path";
|
|
561
|
+
var THUMB_KEYS = /* @__PURE__ */ new Set([
|
|
562
|
+
"cover_image_url",
|
|
563
|
+
"coverImageUrl",
|
|
564
|
+
"cover_url",
|
|
565
|
+
"coverUrl",
|
|
566
|
+
"thumbnail_url",
|
|
567
|
+
"thumbnailUrl",
|
|
568
|
+
"thumb_url",
|
|
569
|
+
"thumbUrl",
|
|
570
|
+
"preview_url",
|
|
571
|
+
"previewUrl",
|
|
572
|
+
"first_frame_url",
|
|
573
|
+
"firstFrameUrl",
|
|
574
|
+
"poster_url",
|
|
575
|
+
"posterUrl",
|
|
576
|
+
"image_url",
|
|
577
|
+
"imageUrl"
|
|
578
|
+
]);
|
|
579
|
+
var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
|
|
580
|
+
function isImageUrl(value) {
|
|
581
|
+
if (typeof value !== "string") return false;
|
|
582
|
+
if (!/^https?:\/\//i.test(value)) return false;
|
|
583
|
+
return IMAGE_EXT_RE.test(value);
|
|
584
|
+
}
|
|
585
|
+
function detectThumbUrl(obj) {
|
|
586
|
+
if (obj === null || obj === void 0) return null;
|
|
587
|
+
if (Array.isArray(obj)) {
|
|
588
|
+
for (const item of obj) {
|
|
589
|
+
const found = detectThumbUrl(item);
|
|
590
|
+
if (found) return found;
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
if (typeof obj !== "object") return null;
|
|
595
|
+
const record = obj;
|
|
596
|
+
for (const [key, value] of Object.entries(record)) {
|
|
597
|
+
if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
|
|
598
|
+
}
|
|
599
|
+
for (const value of Object.values(record)) {
|
|
600
|
+
if (value && typeof value === "object") {
|
|
601
|
+
const found = detectThumbUrl(value);
|
|
602
|
+
if (found) return found;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
async function downloadThumbBeside(thumbUrl, videoPath, logger) {
|
|
608
|
+
const match = thumbUrl.match(IMAGE_EXT_RE);
|
|
609
|
+
const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
|
|
610
|
+
const dir = path6.dirname(videoPath);
|
|
611
|
+
const base = path6.basename(videoPath, path6.extname(videoPath));
|
|
612
|
+
const dest = path6.join(dir, `${base}_thumb.${ext}`);
|
|
613
|
+
try {
|
|
614
|
+
await downloadFile(thumbUrl, dest);
|
|
615
|
+
logger?.success(`Saved thumb ${dest}`);
|
|
616
|
+
return dest;
|
|
617
|
+
} catch (e) {
|
|
618
|
+
logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
623
|
+
if (opts.skip) return null;
|
|
624
|
+
const url = detectThumbUrl(source);
|
|
625
|
+
if (!url) {
|
|
626
|
+
opts.logger?.debug("No thumbnail URL detected in provider response");
|
|
627
|
+
return null;
|
|
628
|
+
}
|
|
629
|
+
const saved = await downloadThumbBeside(url, videoPath, opts.logger);
|
|
630
|
+
if (saved && opts.copyTo) {
|
|
631
|
+
const copyExt = path6.extname(saved);
|
|
632
|
+
const copyBase = path6.basename(opts.copyTo, path6.extname(opts.copyTo));
|
|
633
|
+
const copyDir = path6.dirname(path6.resolve(opts.copyTo));
|
|
634
|
+
const copyDest = path6.join(copyDir, `${copyBase}_thumb${copyExt}`);
|
|
635
|
+
fs6.mkdirSync(copyDir, { recursive: true });
|
|
636
|
+
fs6.copyFileSync(saved, copyDest);
|
|
637
|
+
opts.logger?.success(`Copied thumb to ${copyDest}`);
|
|
638
|
+
}
|
|
639
|
+
return saved;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/providers/leonardo/poll.ts
|
|
643
|
+
var PollTimeoutError = class extends Error {
|
|
644
|
+
constructor() {
|
|
645
|
+
super("Polling timed out before the operation completed.");
|
|
646
|
+
this.name = "PollTimeoutError";
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
var PollFailedError = class extends Error {
|
|
650
|
+
constructor(value) {
|
|
651
|
+
super("Operation failed.");
|
|
652
|
+
this.value = value;
|
|
653
|
+
this.name = "PollFailedError";
|
|
654
|
+
}
|
|
655
|
+
value;
|
|
656
|
+
};
|
|
657
|
+
async function poll(opts) {
|
|
658
|
+
const interval = opts.intervalMs ?? 4e3;
|
|
659
|
+
const max = opts.maxAttempts ?? 120;
|
|
660
|
+
for (let attempt = 1; attempt <= max; attempt++) {
|
|
661
|
+
if (opts.signal?.aborted) throw new Error("Aborted");
|
|
662
|
+
const value = await opts.fetch();
|
|
663
|
+
opts.onTick?.(attempt, value);
|
|
664
|
+
if (opts.failed?.(value)) throw new PollFailedError(value);
|
|
665
|
+
if (opts.done(value)) return value;
|
|
666
|
+
await new Promise((resolve, reject) => {
|
|
667
|
+
const t = setTimeout(resolve, interval);
|
|
668
|
+
opts.signal?.addEventListener("abort", () => {
|
|
669
|
+
clearTimeout(t);
|
|
670
|
+
reject(new Error("Aborted"));
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
throw new PollTimeoutError();
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// src/providers/byteplus/generators/video.ts
|
|
678
|
+
init_http_client();
|
|
679
|
+
function buildFlagSuffix(opts) {
|
|
680
|
+
const parts = [];
|
|
681
|
+
if (opts.resolution) parts.push(`--rs ${opts.resolution}`);
|
|
682
|
+
if (opts.duration !== void 0) parts.push(`--dur ${opts.duration}`);
|
|
683
|
+
if (opts.aspectRatio) parts.push(`--rt ${opts.aspectRatio}`);
|
|
684
|
+
if (opts.cameraFixed !== void 0) parts.push(`--cf ${opts.cameraFixed}`);
|
|
685
|
+
if (opts.seed !== void 0) parts.push(`--seed ${opts.seed}`);
|
|
686
|
+
if (opts.audio !== void 0) parts.push(`--wm ${opts.audio}`);
|
|
687
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
688
|
+
}
|
|
689
|
+
function buildVideoTaskBody(opts) {
|
|
690
|
+
const mode = opts.paramsMode ?? bytePlusVideoParamsMode();
|
|
691
|
+
const text = mode === "flags" ? `${opts.prompt}${buildFlagSuffix(opts)}${opts.negativePrompt ? ` negative: "${opts.negativePrompt}"` : ""}` : opts.prompt;
|
|
692
|
+
const content = [{ type: "text", text }];
|
|
693
|
+
for (const img of opts.imageInputs ?? []) {
|
|
694
|
+
content.push({ type: "image_url", image_url: { url: refUrl(img) } });
|
|
695
|
+
}
|
|
696
|
+
for (const ref of opts.references ?? []) {
|
|
697
|
+
if (ref.kind === "image") {
|
|
698
|
+
content.push({
|
|
699
|
+
type: "image_url",
|
|
700
|
+
image_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
|
|
701
|
+
});
|
|
702
|
+
} else if (ref.kind === "video") {
|
|
703
|
+
content.push({
|
|
704
|
+
type: "video_url",
|
|
705
|
+
video_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
|
|
706
|
+
});
|
|
707
|
+
} else {
|
|
708
|
+
content.push({
|
|
709
|
+
type: "audio_url",
|
|
710
|
+
audio_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
|
|
711
|
+
});
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
const body = { model: opts.model, content };
|
|
715
|
+
if (mode === "structured") {
|
|
716
|
+
body.parameters = {
|
|
717
|
+
...opts.resolution ? { resolution: opts.resolution } : {},
|
|
718
|
+
...opts.duration !== void 0 ? { duration: opts.duration } : {},
|
|
719
|
+
...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
|
|
720
|
+
...opts.audio !== void 0 ? { audio: opts.audio } : {},
|
|
721
|
+
...opts.seed !== void 0 ? { seed: opts.seed } : {},
|
|
722
|
+
...opts.negativePrompt ? { negative_prompt: opts.negativePrompt } : {},
|
|
723
|
+
...opts.cameraFixed !== void 0 ? { camera_fixed: opts.cameraFixed } : {}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
return body;
|
|
727
|
+
}
|
|
728
|
+
async function submitVideoTask(client, body, logger) {
|
|
729
|
+
return client.post("/contents/generations/tasks", body, logger);
|
|
730
|
+
}
|
|
731
|
+
async function getVideoTaskStatus(client, id, logger) {
|
|
732
|
+
return client.get(`/contents/generations/tasks/${id}`, void 0, logger);
|
|
733
|
+
}
|
|
734
|
+
async function waitForVideoTask(client, id, opts = {}) {
|
|
735
|
+
const intervalMs = opts.intervalMs ?? 4e3;
|
|
736
|
+
const maxAttempts = opts.maxAttempts ?? (opts.waitTimeoutMs ? Math.max(1, Math.ceil(opts.waitTimeoutMs / intervalMs)) : 120);
|
|
737
|
+
return poll({
|
|
738
|
+
fetch: () => getVideoTaskStatus(client, id, opts.logger),
|
|
739
|
+
done: (v) => v.status === "succeeded",
|
|
740
|
+
failed: (v) => v.status === "failed" || v.status === "cancelled",
|
|
741
|
+
intervalMs,
|
|
742
|
+
maxAttempts,
|
|
743
|
+
onTick: (attempt, v) => opts.logger?.debug(`attempt ${attempt} \u2014 status: ${v.status}`)
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
async function downloadVideo(url, outputPath, logger) {
|
|
747
|
+
await downloadFile(url, outputPath);
|
|
748
|
+
logger?.success(`Saved ${outputPath}`);
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// src/providers/byteplus/commands/image-to-video.ts
|
|
752
|
+
function registerBytePlusImageToVideoCommand(parent) {
|
|
753
|
+
parent.command("image-to-video <image>").alias("i2v").description("Generate a video from an image (URL or local) via BytePlus Seedance 2.0").requiredOption("--prompt <text>", "Motion prompt").option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds", String(BYTEPLUS_DEFAULTS.videoDuration)).option("--aspect-ratio <r>", "Aspect ratio", BYTEPLUS_DEFAULTS.videoAspectRatio).option("--audio", "Enable audio").option("--no-audio", "Disable audio").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--camera-fixed", "Lock camera").option("--last-frame <path>", "Optional last-frame image (URL or local)").option("--async", "Submit task and exit").option("--wait-timeout <ms>", "Poll timeout in ms", "600000").option("--output <path>", "Output MP4 path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
754
|
+
async (image, opts) => {
|
|
755
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
756
|
+
const client = createBytePlusClient();
|
|
757
|
+
const first = await resolveImageInput(image, logger);
|
|
758
|
+
const refs = [];
|
|
759
|
+
if (opts.lastFrame) {
|
|
760
|
+
const last = await resolveImageInput(opts.lastFrame, logger);
|
|
761
|
+
refs.push({
|
|
762
|
+
kind: "image",
|
|
763
|
+
url: last.kind === "url" ? last.url : last.dataUrl,
|
|
764
|
+
mime: last.kind === "data" ? last.mime : void 0,
|
|
765
|
+
role: "last_frame"
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
const body = buildVideoTaskBody({
|
|
769
|
+
model: opts.model,
|
|
770
|
+
prompt: opts.prompt,
|
|
771
|
+
resolution: opts.resolution,
|
|
772
|
+
duration: Number.parseInt(opts.duration, 10),
|
|
773
|
+
aspectRatio: opts.aspectRatio,
|
|
774
|
+
audio: opts.audio,
|
|
775
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
776
|
+
negativePrompt: opts.negative,
|
|
777
|
+
cameraFixed: opts.cameraFixed,
|
|
778
|
+
imageInputs: [first],
|
|
779
|
+
references: refs
|
|
780
|
+
});
|
|
781
|
+
logger.info(`Submitting i2v task (${opts.model})`);
|
|
782
|
+
const submitted = await submitVideoTask(client, body, logger);
|
|
783
|
+
const taskId = submitted.id;
|
|
784
|
+
if (opts.async) {
|
|
785
|
+
console.log(taskId);
|
|
786
|
+
console.error(`Poll with: multix byteplus status ${taskId}`);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
try {
|
|
790
|
+
const final = await waitForVideoTask(client, taskId, {
|
|
791
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
792
|
+
logger
|
|
793
|
+
});
|
|
794
|
+
const url = final.content?.video_url;
|
|
795
|
+
if (!url) {
|
|
796
|
+
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
797
|
+
process.exit(1);
|
|
798
|
+
}
|
|
799
|
+
const outPath = opts.output ?? path7.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
800
|
+
await downloadVideo(url, outPath, logger);
|
|
801
|
+
await maybeDownloadThumb(final, outPath, {
|
|
802
|
+
skip: opts.thumb === false,
|
|
803
|
+
copyTo: opts.output,
|
|
804
|
+
logger
|
|
805
|
+
});
|
|
806
|
+
console.log(`
|
|
807
|
+
Task: ${taskId}`);
|
|
808
|
+
console.log(`Saved: ${outPath}`);
|
|
809
|
+
} catch (e) {
|
|
810
|
+
if (e instanceof PollTimeoutError) {
|
|
811
|
+
logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
if (e instanceof PollFailedError) {
|
|
815
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
816
|
+
process.exit(1);
|
|
817
|
+
}
|
|
818
|
+
throw e;
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/providers/byteplus/commands/reference-to-video.ts
|
|
825
|
+
import path8 from "path";
|
|
826
|
+
|
|
827
|
+
// src/providers/byteplus/reference-input.ts
|
|
828
|
+
var MAX_TOTAL_REFS = 12;
|
|
829
|
+
var MAX_IMAGE_REFS = 9;
|
|
830
|
+
var MAX_VIDEO_REFS = 3;
|
|
831
|
+
var MAX_AUDIO_REFS = 3;
|
|
832
|
+
function parseRefSpec(input) {
|
|
833
|
+
const PLACEHOLDER = "\0";
|
|
834
|
+
const escaped = input.replace(/\\:/g, PLACEHOLDER);
|
|
835
|
+
const lastIdx = escaped.lastIndexOf(":");
|
|
836
|
+
if (lastIdx === -1) {
|
|
837
|
+
return { path: input.replace(/\\:/g, ":") };
|
|
838
|
+
}
|
|
839
|
+
const left = escaped.slice(0, lastIdx);
|
|
840
|
+
const right = escaped.slice(lastIdx + 1);
|
|
841
|
+
if (/^[a-zA-Z]$/.test(left) && /[\\/]/.test(right)) {
|
|
842
|
+
return { path: input.replace(/\\:/g, ":") };
|
|
843
|
+
}
|
|
844
|
+
if (right.length === 0) {
|
|
845
|
+
return { path: left.replace(new RegExp(PLACEHOLDER, "g"), ":") };
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
path: left.replace(new RegExp(PLACEHOLDER, "g"), ":"),
|
|
849
|
+
role: right.replace(new RegExp(PLACEHOLDER, "g"), ":") || void 0
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
function toRef(kind, resolved, role) {
|
|
853
|
+
if (resolved.kind === "url") return { kind, url: resolved.url, role };
|
|
854
|
+
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
855
|
+
}
|
|
856
|
+
async function resolveRefSpec(kind, spec, logger) {
|
|
857
|
+
const { path: path35, role } = parseRefSpec(spec);
|
|
858
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path35, logger), role);
|
|
859
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path35, logger), role);
|
|
860
|
+
return toRef(kind, await resolveAudioInput(path35, logger), role);
|
|
861
|
+
}
|
|
862
|
+
async function resolveRefSpecs(specs, logger) {
|
|
863
|
+
const out = [];
|
|
864
|
+
for (const { kind, spec } of specs) {
|
|
865
|
+
out.push(await resolveRefSpec(kind, spec, logger));
|
|
866
|
+
}
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
function validateRefCounts(refs) {
|
|
870
|
+
if (refs.length > MAX_TOTAL_REFS) {
|
|
871
|
+
throw new Error(`Too many references: ${refs.length} > ${MAX_TOTAL_REFS}`);
|
|
872
|
+
}
|
|
873
|
+
const images = refs.filter((r) => r.kind === "image").length;
|
|
874
|
+
const videos = refs.filter((r) => r.kind === "video").length;
|
|
875
|
+
const audios = refs.filter((r) => r.kind === "audio").length;
|
|
876
|
+
if (images > MAX_IMAGE_REFS)
|
|
877
|
+
throw new Error(`Too many image refs: ${images} > ${MAX_IMAGE_REFS}`);
|
|
878
|
+
if (videos > MAX_VIDEO_REFS)
|
|
879
|
+
throw new Error(`Too many video refs: ${videos} > ${MAX_VIDEO_REFS}`);
|
|
880
|
+
if (audios > MAX_AUDIO_REFS)
|
|
881
|
+
throw new Error(`Too many audio refs: ${audios} > ${MAX_AUDIO_REFS}`);
|
|
882
|
+
}
|
|
883
|
+
var REF_LIMITS = {
|
|
884
|
+
total: MAX_TOTAL_REFS,
|
|
885
|
+
image: MAX_IMAGE_REFS,
|
|
886
|
+
video: MAX_VIDEO_REFS,
|
|
887
|
+
audio: MAX_AUDIO_REFS
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// src/providers/byteplus/commands/reference-to-video.ts
|
|
891
|
+
function collect3(value, prev) {
|
|
892
|
+
return [...prev, value];
|
|
893
|
+
}
|
|
894
|
+
function registerBytePlusReferenceToVideoCommand(parent) {
|
|
895
|
+
parent.command("reference-to-video").alias("r2v").description(
|
|
896
|
+
`Generate video from multimodal references (\u2264${REF_LIMITS.image} images, \u2264${REF_LIMITS.video} videos, \u2264${REF_LIMITS.audio} audio; total \u2264${REF_LIMITS.total}). Use path:role syntax (escape literal colons with \\:).`
|
|
897
|
+
).requiredOption("--prompt <text>", "Text prompt").option(
|
|
898
|
+
"--ref-image <spec>",
|
|
899
|
+
"Image ref (path|url[:role]); repeatable",
|
|
900
|
+
collect3,
|
|
901
|
+
[]
|
|
902
|
+
).option(
|
|
903
|
+
"--ref-video <spec>",
|
|
904
|
+
"Video ref (path|url[:role]); repeatable",
|
|
905
|
+
collect3,
|
|
906
|
+
[]
|
|
907
|
+
).option(
|
|
908
|
+
"--ref-audio <spec>",
|
|
909
|
+
"Audio ref (path|url[:role]); repeatable",
|
|
910
|
+
collect3,
|
|
911
|
+
[]
|
|
912
|
+
).option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds", String(BYTEPLUS_DEFAULTS.videoDuration)).option("--aspect-ratio <r>", "Aspect ratio", BYTEPLUS_DEFAULTS.videoAspectRatio).option("--audio", "Enable audio").option("--no-audio", "Disable audio").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--camera-fixed", "Lock camera").option("--async", "Submit task and exit").option("--wait-timeout <ms>", "Poll timeout in ms", "600000").option("--output <path>", "Output MP4 path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
913
|
+
async (opts) => {
|
|
914
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
915
|
+
const specs = [
|
|
916
|
+
...opts.refImage.map((s) => ({ kind: "image", spec: s })),
|
|
917
|
+
...opts.refVideo.map((s) => ({ kind: "video", spec: s })),
|
|
918
|
+
...opts.refAudio.map((s) => ({ kind: "audio", spec: s }))
|
|
919
|
+
];
|
|
920
|
+
if (specs.length === 0) {
|
|
921
|
+
logger.error("At least one --ref-image / --ref-video / --ref-audio is required");
|
|
922
|
+
process.exit(2);
|
|
923
|
+
}
|
|
924
|
+
const refs = await resolveRefSpecs(specs, logger);
|
|
925
|
+
validateRefCounts(refs);
|
|
926
|
+
const client = createBytePlusClient();
|
|
927
|
+
const body = buildVideoTaskBody({
|
|
928
|
+
model: opts.model,
|
|
929
|
+
prompt: opts.prompt,
|
|
930
|
+
resolution: opts.resolution,
|
|
931
|
+
duration: Number.parseInt(opts.duration, 10),
|
|
932
|
+
aspectRatio: opts.aspectRatio,
|
|
933
|
+
audio: opts.audio,
|
|
934
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
935
|
+
negativePrompt: opts.negative,
|
|
936
|
+
cameraFixed: opts.cameraFixed,
|
|
937
|
+
references: refs
|
|
938
|
+
});
|
|
939
|
+
logger.info(`Submitting r2v task (${opts.model}, ${refs.length} refs)`);
|
|
940
|
+
const submitted = await submitVideoTask(client, body, logger);
|
|
941
|
+
const taskId = submitted.id;
|
|
942
|
+
if (opts.async) {
|
|
943
|
+
console.log(taskId);
|
|
944
|
+
console.error(`Poll with: multix byteplus status ${taskId}`);
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
const final = await waitForVideoTask(client, taskId, {
|
|
949
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
950
|
+
logger
|
|
951
|
+
});
|
|
952
|
+
const url = final.content?.video_url;
|
|
953
|
+
if (!url) {
|
|
954
|
+
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
955
|
+
process.exit(1);
|
|
956
|
+
}
|
|
957
|
+
const outPath = opts.output ?? path8.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
958
|
+
await downloadVideo(url, outPath, logger);
|
|
959
|
+
await maybeDownloadThumb(final, outPath, {
|
|
960
|
+
skip: opts.thumb === false,
|
|
961
|
+
copyTo: opts.output,
|
|
962
|
+
logger
|
|
963
|
+
});
|
|
964
|
+
console.log(`
|
|
965
|
+
Task: ${taskId}`);
|
|
966
|
+
console.log(`Saved: ${outPath}`);
|
|
967
|
+
} catch (e) {
|
|
968
|
+
if (e instanceof PollTimeoutError) {
|
|
969
|
+
logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
|
|
970
|
+
process.exit(1);
|
|
971
|
+
}
|
|
972
|
+
if (e instanceof PollFailedError) {
|
|
973
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
974
|
+
process.exit(1);
|
|
975
|
+
}
|
|
976
|
+
throw e;
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// src/providers/byteplus/commands/status.ts
|
|
983
|
+
import path9 from "path";
|
|
984
|
+
function registerBytePlusStatusCommand(parent) {
|
|
985
|
+
parent.command("status <taskId>").description("Get a Seedance task's status; --wait + --download to fetch the result MP4").option("--wait", "Poll until terminal status").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download MP4 on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
986
|
+
async (taskId, opts) => {
|
|
987
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
988
|
+
const client = createBytePlusClient();
|
|
989
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
990
|
+
try {
|
|
991
|
+
const status = shouldWait ? await waitForVideoTask(client, taskId, {
|
|
992
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
993
|
+
logger
|
|
994
|
+
}) : await getVideoTaskStatus(client, taskId, logger);
|
|
995
|
+
console.log(JSON.stringify(status, null, 2));
|
|
996
|
+
if (opts.download && status.status === "succeeded") {
|
|
997
|
+
const url = status.content?.video_url;
|
|
998
|
+
if (!url) {
|
|
999
|
+
logger.error("Task succeeded but no video_url present");
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
|
|
1003
|
+
await downloadVideo(url, outPath, logger);
|
|
1004
|
+
await maybeDownloadThumb(status, outPath, {
|
|
1005
|
+
skip: opts.thumb === false,
|
|
1006
|
+
copyTo: opts.output,
|
|
1007
|
+
logger
|
|
1008
|
+
});
|
|
1009
|
+
console.log(`Saved: ${outPath}`);
|
|
1010
|
+
}
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
if (e instanceof PollTimeoutError) {
|
|
1013
|
+
logger.error(`Timed out waiting for task ${taskId}`);
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
}
|
|
1016
|
+
if (e instanceof PollFailedError) {
|
|
1017
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1018
|
+
process.exit(1);
|
|
1019
|
+
}
|
|
1020
|
+
throw e;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
// src/providers/byteplus/commands/video.ts
|
|
1027
|
+
import path10 from "path";
|
|
1028
|
+
function registerBytePlusVideoCommand(parent) {
|
|
1029
|
+
parent.command("video").description("Generate a video from a text prompt via BytePlus Seedance 2.0").requiredOption("--prompt <text>", "Text prompt").option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds (4-15)", String(BYTEPLUS_DEFAULTS.videoDuration)).option(
|
|
1030
|
+
"--aspect-ratio <r>",
|
|
1031
|
+
"Aspect ratio (16:9, 9:16, 1:1, ...)",
|
|
1032
|
+
BYTEPLUS_DEFAULTS.videoAspectRatio
|
|
1033
|
+
).option("--audio", "Enable audio track").option("--no-audio", "Disable audio track").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--camera-fixed", "Lock camera (no motion)").option("--async", "Submit task and exit; print taskId only").option("--wait-timeout <ms>", "Poll timeout in ms", "600000").option("--output <path>", "Output MP4 path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
1034
|
+
async (opts) => {
|
|
1035
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1036
|
+
const client = createBytePlusClient();
|
|
1037
|
+
const body = buildVideoTaskBody({
|
|
1038
|
+
model: opts.model,
|
|
1039
|
+
prompt: opts.prompt,
|
|
1040
|
+
resolution: opts.resolution,
|
|
1041
|
+
duration: Number.parseInt(opts.duration, 10),
|
|
1042
|
+
aspectRatio: opts.aspectRatio,
|
|
1043
|
+
audio: opts.audio,
|
|
1044
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
1045
|
+
negativePrompt: opts.negative,
|
|
1046
|
+
cameraFixed: opts.cameraFixed
|
|
1047
|
+
});
|
|
1048
|
+
logger.info(
|
|
1049
|
+
`Submitting Seedance task (${opts.model}, ${opts.resolution}, ${opts.duration}s)`
|
|
1050
|
+
);
|
|
1051
|
+
const submitted = await submitVideoTask(client, body, logger);
|
|
1052
|
+
const taskId = submitted.id;
|
|
1053
|
+
logger.debug(`taskId: ${taskId}`);
|
|
1054
|
+
if (opts.async) {
|
|
1055
|
+
console.log(taskId);
|
|
1056
|
+
console.error(`Poll with: multix byteplus status ${taskId}`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
try {
|
|
1060
|
+
const final = await waitForVideoTask(client, taskId, {
|
|
1061
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
1062
|
+
logger
|
|
1063
|
+
});
|
|
1064
|
+
const url = final.content?.video_url;
|
|
1065
|
+
if (!url) {
|
|
1066
|
+
logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
|
|
1067
|
+
process.exit(1);
|
|
1068
|
+
}
|
|
1069
|
+
const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1070
|
+
await downloadVideo(url, outPath, logger);
|
|
1071
|
+
await maybeDownloadThumb(final, outPath, {
|
|
1072
|
+
skip: opts.thumb === false,
|
|
1073
|
+
copyTo: opts.output,
|
|
1074
|
+
logger
|
|
1075
|
+
});
|
|
1076
|
+
console.log(`
|
|
1077
|
+
Task: ${taskId}`);
|
|
1078
|
+
console.log(`Saved: ${outPath}`);
|
|
1079
|
+
} catch (e) {
|
|
1080
|
+
if (e instanceof PollTimeoutError) {
|
|
1081
|
+
logger.error(
|
|
1082
|
+
`Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
|
|
1083
|
+
);
|
|
1084
|
+
process.exit(1);
|
|
1085
|
+
}
|
|
1086
|
+
if (e instanceof PollFailedError) {
|
|
1087
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1088
|
+
process.exit(1);
|
|
1089
|
+
}
|
|
1090
|
+
throw e;
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// src/providers/byteplus/commands/index.ts
|
|
1097
|
+
function registerBytePlusCommands(program2) {
|
|
1098
|
+
const byteplus = program2.command("byteplus").description("BytePlus ModelArk: Seedream image generation, Seedance video generation");
|
|
1099
|
+
registerBytePlusGenerateCommand(byteplus);
|
|
1100
|
+
registerBytePlusImageToImageCommand(byteplus);
|
|
1101
|
+
registerBytePlusVideoCommand(byteplus);
|
|
1102
|
+
registerBytePlusImageToVideoCommand(byteplus);
|
|
1103
|
+
registerBytePlusReferenceToVideoCommand(byteplus);
|
|
1104
|
+
registerBytePlusStatusCommand(byteplus);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// src/providers/gemini/commands/analyze.ts
|
|
1108
|
+
init_errors();
|
|
1109
|
+
import fs9 from "fs";
|
|
1110
|
+
|
|
1111
|
+
// src/providers/gemini/client.ts
|
|
1112
|
+
import fs7 from "fs";
|
|
1113
|
+
import path12 from "path";
|
|
1114
|
+
init_errors();
|
|
1115
|
+
init_http_client();
|
|
1116
|
+
|
|
1117
|
+
// src/providers/gemini/task-resolver.ts
|
|
1118
|
+
import path11 from "path";
|
|
1119
|
+
var MIME_MAP = {
|
|
1120
|
+
// Audio
|
|
1121
|
+
".mp3": "audio/mp3",
|
|
1122
|
+
".wav": "audio/wav",
|
|
1123
|
+
".aac": "audio/aac",
|
|
1124
|
+
".flac": "audio/flac",
|
|
1125
|
+
".ogg": "audio/ogg",
|
|
1126
|
+
".aiff": "audio/aiff",
|
|
1127
|
+
".m4a": "audio/mp4",
|
|
1128
|
+
// Image
|
|
1129
|
+
".jpg": "image/jpeg",
|
|
1130
|
+
".jpeg": "image/jpeg",
|
|
1131
|
+
".png": "image/png",
|
|
1132
|
+
".webp": "image/webp",
|
|
1133
|
+
".heic": "image/heic",
|
|
1134
|
+
".heif": "image/heif",
|
|
1135
|
+
".gif": "image/gif",
|
|
1136
|
+
".bmp": "image/bmp",
|
|
1137
|
+
// Video
|
|
1138
|
+
".mp4": "video/mp4",
|
|
1139
|
+
".mpeg": "video/mpeg",
|
|
1140
|
+
".mov": "video/quicktime",
|
|
1141
|
+
".avi": "video/x-msvideo",
|
|
1142
|
+
".flv": "video/x-flv",
|
|
1143
|
+
".mpg": "video/mpeg",
|
|
1144
|
+
".webm": "video/webm",
|
|
1145
|
+
".wmv": "video/x-ms-wmv",
|
|
1146
|
+
".3gpp": "video/3gpp",
|
|
1147
|
+
// Document
|
|
1148
|
+
".pdf": "application/pdf",
|
|
1149
|
+
".txt": "text/plain",
|
|
1150
|
+
".html": "text/html",
|
|
1151
|
+
".htm": "text/html",
|
|
1152
|
+
".md": "text/markdown",
|
|
1153
|
+
".csv": "text/csv",
|
|
1154
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1155
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1156
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
1157
|
+
};
|
|
1158
|
+
function getMimeType(filePath) {
|
|
1159
|
+
const ext = path11.extname(filePath).toLowerCase();
|
|
1160
|
+
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
1161
|
+
}
|
|
1162
|
+
function requiresProcessingWait(mimeType) {
|
|
1163
|
+
return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// src/providers/gemini/client.ts
|
|
1167
|
+
var BASE = "https://generativelanguage.googleapis.com";
|
|
1168
|
+
var FILES_BASE = `${BASE}/upload/v1beta/files`;
|
|
1169
|
+
var GENERATE_BASE = `${BASE}/v1beta/models`;
|
|
1170
|
+
function requireApiKey() {
|
|
1171
|
+
const key = resolveKey("GEMINI_API_KEY");
|
|
1172
|
+
if (!key)
|
|
1173
|
+
throw new ConfigError(
|
|
1174
|
+
"GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
|
|
1175
|
+
);
|
|
1176
|
+
return key;
|
|
1177
|
+
}
|
|
1178
|
+
function authHeaders(apiKey) {
|
|
1179
|
+
return { "x-goog-api-key": apiKey };
|
|
1180
|
+
}
|
|
1181
|
+
async function uploadFile(filePath, opts = {}) {
|
|
1182
|
+
const apiKey = requireApiKey();
|
|
1183
|
+
const { timeoutMs = 3e5, logger } = opts;
|
|
1184
|
+
const mimeType = getMimeType(filePath);
|
|
1185
|
+
const fileBytes = fs7.readFileSync(filePath);
|
|
1186
|
+
const displayName = path12.basename(filePath);
|
|
1187
|
+
const boundary = `multix_${Date.now()}`;
|
|
1188
|
+
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
1189
|
+
const body = Buffer.concat([
|
|
1190
|
+
Buffer.from(
|
|
1191
|
+
`--${boundary}\r
|
|
1192
|
+
Content-Type: application/json; charset=UTF-8\r
|
|
1193
|
+
\r
|
|
1194
|
+
${metaJson}\r
|
|
1195
|
+
--${boundary}\r
|
|
1196
|
+
Content-Type: ${mimeType}\r
|
|
1197
|
+
\r
|
|
1198
|
+
`
|
|
1199
|
+
),
|
|
1200
|
+
fileBytes,
|
|
1201
|
+
Buffer.from(`\r
|
|
1202
|
+
--${boundary}--`)
|
|
1203
|
+
]);
|
|
1204
|
+
logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
1205
|
+
const resp = await globalThis.fetch(FILES_BASE, {
|
|
1206
|
+
method: "POST",
|
|
1207
|
+
headers: {
|
|
1208
|
+
"x-goog-api-key": apiKey,
|
|
1209
|
+
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
1210
|
+
"Content-Length": String(body.length),
|
|
1211
|
+
"X-Goog-Upload-Protocol": "multipart"
|
|
1212
|
+
},
|
|
1213
|
+
body,
|
|
1214
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1215
|
+
});
|
|
1216
|
+
if (!resp.ok) {
|
|
1217
|
+
const txt = await resp.text();
|
|
1218
|
+
throw new ProviderError(
|
|
1219
|
+
`File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
|
|
1220
|
+
"gemini"
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
const data = await resp.json();
|
|
1224
|
+
let fileRef = data.file;
|
|
1225
|
+
if (requiresProcessingWait(mimeType)) {
|
|
1226
|
+
const deadline = Date.now() + timeoutMs;
|
|
1227
|
+
while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
|
|
1228
|
+
await sleep(2e3);
|
|
1229
|
+
fileRef = await httpJson({
|
|
1230
|
+
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
1231
|
+
headers: authHeaders(apiKey)
|
|
1232
|
+
}).then((r) => r.file);
|
|
1233
|
+
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
1234
|
+
}
|
|
1235
|
+
if (fileRef.state === "FAILED")
|
|
1236
|
+
throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
|
|
1237
|
+
if (fileRef.state === "PROCESSING")
|
|
1238
|
+
throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
|
|
1239
|
+
}
|
|
1240
|
+
logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
|
|
1241
|
+
return fileRef;
|
|
1242
|
+
}
|
|
1243
|
+
async function generateContent(req) {
|
|
1244
|
+
const apiKey = requireApiKey();
|
|
1245
|
+
return httpJson({
|
|
1246
|
+
url: `${GENERATE_BASE}/${req.model}:generateContent`,
|
|
1247
|
+
method: "POST",
|
|
1248
|
+
headers: authHeaders(apiKey),
|
|
1249
|
+
body: {
|
|
1250
|
+
contents: req.contents,
|
|
1251
|
+
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
function extractText(resp) {
|
|
1256
|
+
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
|
1257
|
+
return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
|
|
1258
|
+
}
|
|
1259
|
+
function extractAudio(resp) {
|
|
1260
|
+
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
|
1261
|
+
for (const p of parts) {
|
|
1262
|
+
if (p.inlineData?.mimeType.startsWith("audio/")) {
|
|
1263
|
+
return { mimeType: p.inlineData.mimeType, data: p.inlineData.data };
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return null;
|
|
1267
|
+
}
|
|
1268
|
+
function extractImages(resp) {
|
|
1269
|
+
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
|
1270
|
+
return parts.filter(
|
|
1271
|
+
(p) => p.inlineData !== void 0
|
|
1272
|
+
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
1273
|
+
}
|
|
1274
|
+
function inlinePart(filePath) {
|
|
1275
|
+
const data = fs7.readFileSync(filePath).toString("base64");
|
|
1276
|
+
const mimeType = getMimeType(filePath);
|
|
1277
|
+
return { inlineData: { mimeType, data } };
|
|
1278
|
+
}
|
|
1279
|
+
function shouldUseFileApi(filePath) {
|
|
1280
|
+
const stat = fs7.statSync(filePath);
|
|
1281
|
+
return stat.size > 20 * 1024 * 1024;
|
|
1282
|
+
}
|
|
1283
|
+
function sleep(ms) {
|
|
1284
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// src/providers/gemini/models.ts
|
|
1288
|
+
var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
|
|
1289
|
+
var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
|
|
1290
|
+
var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
|
|
1291
|
+
var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
|
|
1292
|
+
var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
|
|
1293
|
+
var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
|
|
1294
|
+
var IMAGEN_MODELS = /* @__PURE__ */ new Set([
|
|
1295
|
+
"imagen-4.0-generate-001",
|
|
1296
|
+
"imagen-4.0-ultra-generate-001",
|
|
1297
|
+
"imagen-4.0-fast-generate-001"
|
|
1298
|
+
]);
|
|
1299
|
+
var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
1300
|
+
"gemini-3.1-flash-image-preview",
|
|
1301
|
+
"gemini-3-pro-image-preview",
|
|
1302
|
+
"gemini-2.5-flash-image",
|
|
1303
|
+
"gemini-2.5-flash-image-preview",
|
|
1304
|
+
...IMAGEN_MODELS
|
|
1305
|
+
]);
|
|
1306
|
+
var ASPECT_RATIOS = [
|
|
1307
|
+
"1:1",
|
|
1308
|
+
"2:3",
|
|
1309
|
+
"3:2",
|
|
1310
|
+
"3:4",
|
|
1311
|
+
"4:3",
|
|
1312
|
+
"4:5",
|
|
1313
|
+
"5:4",
|
|
1314
|
+
"9:16",
|
|
1315
|
+
"16:9",
|
|
1316
|
+
"21:9"
|
|
1317
|
+
];
|
|
1318
|
+
var IMAGE_SIZES = ["1K", "2K", "4K"];
|
|
1319
|
+
function getDefaultModel(task) {
|
|
1320
|
+
if (task === "generate") {
|
|
1321
|
+
return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
|
|
1322
|
+
}
|
|
1323
|
+
if (task === "generate-video") {
|
|
1324
|
+
return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
|
|
1325
|
+
}
|
|
1326
|
+
if (task === "generate-speech") {
|
|
1327
|
+
return process.env.GEMINI_TTS_MODEL ?? process.env.TTS_MODEL ?? TTS_MODEL_DEFAULT;
|
|
1328
|
+
}
|
|
1329
|
+
return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
// src/providers/gemini/result-formatter.ts
|
|
1333
|
+
import fs8 from "fs";
|
|
1334
|
+
import path13 from "path";
|
|
1335
|
+
function formatResults(results, format, outputPath) {
|
|
1336
|
+
const content = renderFormat(results, format);
|
|
1337
|
+
if (outputPath) {
|
|
1338
|
+
fs8.mkdirSync(path13.dirname(path13.resolve(outputPath)), { recursive: true });
|
|
1339
|
+
fs8.writeFileSync(outputPath, content, "utf8");
|
|
1340
|
+
} else {
|
|
1341
|
+
process.stdout.write(content);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
function renderFormat(results, format) {
|
|
1345
|
+
switch (format) {
|
|
1346
|
+
case "json":
|
|
1347
|
+
return `${JSON.stringify(results, null, 2)}
|
|
1348
|
+
`;
|
|
1349
|
+
case "csv":
|
|
1350
|
+
return renderCsv(results);
|
|
1351
|
+
case "markdown":
|
|
1352
|
+
return renderMarkdown(results);
|
|
1353
|
+
default:
|
|
1354
|
+
return renderText(results);
|
|
467
1355
|
}
|
|
468
1356
|
}
|
|
469
1357
|
function renderText(results) {
|
|
@@ -481,46 +1369,727 @@ Error: ${r.error}
|
|
|
481
1369
|
`;
|
|
482
1370
|
}).join("\n");
|
|
483
1371
|
}
|
|
484
|
-
function renderCsv(results) {
|
|
485
|
-
const header = "file,status,response,error\n";
|
|
486
|
-
const rows = results.map((r) => {
|
|
487
|
-
const csvCell = (v) => `"${(v ?? "").replace(/"/g, '""')}"`;
|
|
488
|
-
return [csvCell(r.file), csvCell(r.status), csvCell(r.response), csvCell(r.error)].join(",");
|
|
489
|
-
});
|
|
490
|
-
return `${header + rows.join("\n")}
|
|
491
|
-
`;
|
|
1372
|
+
function renderCsv(results) {
|
|
1373
|
+
const header = "file,status,response,error\n";
|
|
1374
|
+
const rows = results.map((r) => {
|
|
1375
|
+
const csvCell = (v) => `"${(v ?? "").replace(/"/g, '""')}"`;
|
|
1376
|
+
return [csvCell(r.file), csvCell(r.status), csvCell(r.response), csvCell(r.error)].join(",");
|
|
1377
|
+
});
|
|
1378
|
+
return `${header + rows.join("\n")}
|
|
1379
|
+
`;
|
|
1380
|
+
}
|
|
1381
|
+
function renderMarkdown(results) {
|
|
1382
|
+
const lines = ["# Results\n"];
|
|
1383
|
+
for (const r of results) {
|
|
1384
|
+
lines.push(`## ${r.file}
|
|
1385
|
+
`);
|
|
1386
|
+
lines.push(`**Status**: ${r.status}
|
|
1387
|
+
`);
|
|
1388
|
+
if (r.status === "success" && r.response) lines.push(`**Response**:
|
|
1389
|
+
|
|
1390
|
+
${r.response}
|
|
1391
|
+
`);
|
|
1392
|
+
if (r.status === "error") lines.push(`**Error**: ${r.error}
|
|
1393
|
+
`);
|
|
1394
|
+
lines.push("---\n");
|
|
1395
|
+
}
|
|
1396
|
+
return lines.join("\n");
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/providers/gemini/commands/analyze.ts
|
|
1400
|
+
function registerAnalyzeCommand(parent) {
|
|
1401
|
+
parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
|
|
1402
|
+
async (opts) => {
|
|
1403
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1404
|
+
const model = opts.model ?? getDefaultModel("analyze");
|
|
1405
|
+
for (const f of opts.files) {
|
|
1406
|
+
if (!fs9.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1407
|
+
}
|
|
1408
|
+
const results = [];
|
|
1409
|
+
for (let i = 0; i < opts.files.length; i++) {
|
|
1410
|
+
const filePath = opts.files[i];
|
|
1411
|
+
logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
|
|
1412
|
+
try {
|
|
1413
|
+
const useApi = shouldUseFileApi(filePath);
|
|
1414
|
+
let contentParts;
|
|
1415
|
+
if (useApi) {
|
|
1416
|
+
logger.debug("File >20MB \u2014 using Files API");
|
|
1417
|
+
const ref = await uploadFile(filePath, { logger });
|
|
1418
|
+
contentParts = [
|
|
1419
|
+
{ text: opts.prompt },
|
|
1420
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
1421
|
+
];
|
|
1422
|
+
} else {
|
|
1423
|
+
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
1424
|
+
}
|
|
1425
|
+
const resp = await generateContent({
|
|
1426
|
+
model,
|
|
1427
|
+
contents: [{ parts: contentParts }]
|
|
1428
|
+
});
|
|
1429
|
+
results.push({
|
|
1430
|
+
file: filePath,
|
|
1431
|
+
status: "success",
|
|
1432
|
+
response: extractText(resp)
|
|
1433
|
+
});
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1436
|
+
logger.error(`Failed: ${filePath} \u2014 ${msg}`);
|
|
1437
|
+
results.push({ file: filePath, status: "error", error: msg });
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
formatResults(results, opts.format, opts.output);
|
|
1441
|
+
logger.debug(
|
|
1442
|
+
`Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
|
|
1443
|
+
);
|
|
1444
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
1445
|
+
if (failed > 0) process.exit(1);
|
|
1446
|
+
}
|
|
1447
|
+
);
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
// src/providers/gemini/commands/extract.ts
|
|
1451
|
+
init_errors();
|
|
1452
|
+
import fs10 from "fs";
|
|
1453
|
+
function registerExtractCommand(parent) {
|
|
1454
|
+
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(
|
|
1455
|
+
async (opts) => {
|
|
1456
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1457
|
+
const model = opts.model ?? getDefaultModel("extract");
|
|
1458
|
+
for (const f of opts.files) {
|
|
1459
|
+
if (!fs10.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1460
|
+
}
|
|
1461
|
+
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
1462
|
+
const results = [];
|
|
1463
|
+
for (let i = 0; i < opts.files.length; i++) {
|
|
1464
|
+
const filePath = opts.files[i];
|
|
1465
|
+
logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
|
|
1466
|
+
try {
|
|
1467
|
+
const useApi = shouldUseFileApi(filePath);
|
|
1468
|
+
let contentParts;
|
|
1469
|
+
if (useApi) {
|
|
1470
|
+
const ref = await uploadFile(filePath, { logger });
|
|
1471
|
+
contentParts = [
|
|
1472
|
+
{ text: opts.prompt },
|
|
1473
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
1474
|
+
];
|
|
1475
|
+
} else {
|
|
1476
|
+
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
1477
|
+
}
|
|
1478
|
+
const resp = await generateContent({
|
|
1479
|
+
model,
|
|
1480
|
+
contents: [{ parts: contentParts }],
|
|
1481
|
+
generationConfig
|
|
1482
|
+
});
|
|
1483
|
+
results.push({ file: filePath, status: "success", response: extractText(resp) });
|
|
1484
|
+
} catch (e) {
|
|
1485
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1486
|
+
logger.error(`Failed: ${filePath} \u2014 ${msg}`);
|
|
1487
|
+
results.push({ file: filePath, status: "error", error: msg });
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
formatResults(results, opts.format, opts.output);
|
|
1491
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
1492
|
+
if (failed > 0) process.exit(1);
|
|
1493
|
+
}
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// src/providers/gemini/commands/generate-speech.ts
|
|
1498
|
+
init_errors();
|
|
1499
|
+
|
|
1500
|
+
// src/providers/gemini/generators/speech.ts
|
|
1501
|
+
import fs11 from "fs";
|
|
1502
|
+
import path14 from "path";
|
|
1503
|
+
|
|
1504
|
+
// src/providers/gemini/voices.ts
|
|
1505
|
+
var GEMINI_TTS_VOICES = [
|
|
1506
|
+
"Zephyr",
|
|
1507
|
+
"Puck",
|
|
1508
|
+
"Charon",
|
|
1509
|
+
"Kore",
|
|
1510
|
+
"Fenrir",
|
|
1511
|
+
"Leda",
|
|
1512
|
+
"Orus",
|
|
1513
|
+
"Aoede",
|
|
1514
|
+
"Callirrhoe",
|
|
1515
|
+
"Autonoe",
|
|
1516
|
+
"Enceladus",
|
|
1517
|
+
"Iapetus",
|
|
1518
|
+
"Umbriel",
|
|
1519
|
+
"Algieba",
|
|
1520
|
+
"Despina",
|
|
1521
|
+
"Erinome",
|
|
1522
|
+
"Algenib",
|
|
1523
|
+
"Rasalgethi",
|
|
1524
|
+
"Laomedeia",
|
|
1525
|
+
"Achernar",
|
|
1526
|
+
"Alnilam",
|
|
1527
|
+
"Schedar",
|
|
1528
|
+
"Gacrux",
|
|
1529
|
+
"Pulcherrima",
|
|
1530
|
+
"Achird",
|
|
1531
|
+
"Zubenelgenubi",
|
|
1532
|
+
"Vindemiatrix",
|
|
1533
|
+
"Sadachbia",
|
|
1534
|
+
"Sadaltager",
|
|
1535
|
+
"Sulafat"
|
|
1536
|
+
];
|
|
1537
|
+
var GEMINI_TTS_MODELS = /* @__PURE__ */ new Set([
|
|
1538
|
+
"gemini-3.1-flash-tts-preview",
|
|
1539
|
+
"gemini-2.5-flash-preview-tts",
|
|
1540
|
+
"gemini-2.5-pro-preview-tts"
|
|
1541
|
+
]);
|
|
1542
|
+
var TTS_VOICE_DEFAULT = "Kore";
|
|
1543
|
+
var TTS_PCM_SAMPLE_RATE = 24e3;
|
|
1544
|
+
var TTS_PCM_CHANNELS = 1;
|
|
1545
|
+
var TTS_PCM_BITS_PER_SAMPLE = 16;
|
|
1546
|
+
var TTS_OUTPUT_FORMATS = ["wav", "pcm"];
|
|
1547
|
+
function isValidGeminiVoice(name) {
|
|
1548
|
+
return GEMINI_TTS_VOICES.includes(name);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// src/providers/gemini/generators/speech.ts
|
|
1552
|
+
async function generateGeminiSpeech(opts) {
|
|
1553
|
+
const { text, model, voice, speakers, outputFormat, output, logger } = opts;
|
|
1554
|
+
const speechConfig = speakers && speakers.length > 0 ? {
|
|
1555
|
+
multiSpeakerVoiceConfig: {
|
|
1556
|
+
speakerVoiceConfigs: speakers.map((s) => ({
|
|
1557
|
+
speaker: s.speaker,
|
|
1558
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: s.voice } }
|
|
1559
|
+
}))
|
|
1560
|
+
}
|
|
1561
|
+
} : {
|
|
1562
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } }
|
|
1563
|
+
};
|
|
1564
|
+
const generationConfig = {
|
|
1565
|
+
responseModalities: ["AUDIO"],
|
|
1566
|
+
speechConfig
|
|
1567
|
+
};
|
|
1568
|
+
logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
|
|
1569
|
+
let resp;
|
|
1570
|
+
try {
|
|
1571
|
+
resp = await generateContent({
|
|
1572
|
+
model,
|
|
1573
|
+
contents: [{ parts: [{ text }] }],
|
|
1574
|
+
generationConfig
|
|
1575
|
+
});
|
|
1576
|
+
} catch (e) {
|
|
1577
|
+
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
1578
|
+
}
|
|
1579
|
+
const audio = extractAudio(resp);
|
|
1580
|
+
if (!audio) return { status: "error", error: "No audio in response" };
|
|
1581
|
+
const pcmBytes = Buffer.from(audio.data, "base64");
|
|
1582
|
+
const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
|
|
1583
|
+
const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
|
|
1584
|
+
const outDir = getOutputDir();
|
|
1585
|
+
const dest = path14.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
1586
|
+
fs11.writeFileSync(dest, outBytes);
|
|
1587
|
+
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
1588
|
+
if (output) {
|
|
1589
|
+
fs11.mkdirSync(path14.dirname(path14.resolve(output)), { recursive: true });
|
|
1590
|
+
fs11.copyFileSync(dest, output);
|
|
1591
|
+
logger?.success(`Copied to: ${output}`);
|
|
1592
|
+
}
|
|
1593
|
+
return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
|
|
1594
|
+
}
|
|
1595
|
+
function parseSampleRate(mimeType) {
|
|
1596
|
+
const m = mimeType.match(/rate=(\d+)/i);
|
|
1597
|
+
return m ? Number.parseInt(m[1], 10) : null;
|
|
1598
|
+
}
|
|
1599
|
+
function wrapPcmInWav(pcm, sampleRate, channels, bitsPerSample) {
|
|
1600
|
+
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
1601
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
1602
|
+
const dataSize = pcm.length;
|
|
1603
|
+
const header = Buffer.alloc(44);
|
|
1604
|
+
header.write("RIFF", 0, "ascii");
|
|
1605
|
+
header.writeUInt32LE(36 + dataSize, 4);
|
|
1606
|
+
header.write("WAVE", 8, "ascii");
|
|
1607
|
+
header.write("fmt ", 12, "ascii");
|
|
1608
|
+
header.writeUInt32LE(16, 16);
|
|
1609
|
+
header.writeUInt16LE(1, 20);
|
|
1610
|
+
header.writeUInt16LE(channels, 22);
|
|
1611
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
1612
|
+
header.writeUInt32LE(byteRate, 28);
|
|
1613
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
1614
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
1615
|
+
header.write("data", 36, "ascii");
|
|
1616
|
+
header.writeUInt32LE(dataSize, 40);
|
|
1617
|
+
return Buffer.concat([header, pcm]);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/providers/gemini/commands/generate-speech.ts
|
|
1621
|
+
function registerGeminiGenerateSpeechCommand(parent) {
|
|
1622
|
+
parent.command("generate-speech").description("Generate speech (TTS) with Gemini Flash TTS").option("--text <str>", "Text to speak (use --text or --prompt)").option("--prompt <str>", "Alias for --text").option("--model <id>", `TTS model (${[...GEMINI_TTS_MODELS].join("|")})`).option("--voice <name>", "Prebuilt voice for single-speaker mode", TTS_VOICE_DEFAULT).option(
|
|
1623
|
+
"--speaker <name:voice>",
|
|
1624
|
+
"Speaker mapping (repeatable, max 2 \u2014 switches to multi-speaker mode)",
|
|
1625
|
+
collectSpeakers,
|
|
1626
|
+
[]
|
|
1627
|
+
).option("--output-format <fmt>", `Container (${TTS_OUTPUT_FORMATS.join("|")})`, "wav").option("--output <path>", "Copy output audio to this path").option("-v, --verbose", "Verbose logging").action(
|
|
1628
|
+
async (opts) => {
|
|
1629
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1630
|
+
const text = opts.text ?? opts.prompt;
|
|
1631
|
+
if (!text) throw new ValidationError("Either --text or --prompt is required");
|
|
1632
|
+
const model = opts.model ?? getDefaultModel("generate-speech");
|
|
1633
|
+
if (!GEMINI_TTS_MODELS.has(model)) {
|
|
1634
|
+
throw new ValidationError(`--model must be one of: ${[...GEMINI_TTS_MODELS].join(", ")}`);
|
|
1635
|
+
}
|
|
1636
|
+
if (!TTS_OUTPUT_FORMATS.includes(opts.outputFormat)) {
|
|
1637
|
+
throw new ValidationError(
|
|
1638
|
+
`--output-format must be one of: ${TTS_OUTPUT_FORMATS.join(", ")}`
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
const speakers = opts.speaker;
|
|
1642
|
+
if (speakers.length > 2) {
|
|
1643
|
+
throw new ValidationError("Gemini TTS supports max 2 speakers in multi-speaker mode");
|
|
1644
|
+
}
|
|
1645
|
+
if (speakers.length === 0 && !isValidGeminiVoice(opts.voice)) {
|
|
1646
|
+
throw new ValidationError(
|
|
1647
|
+
`Invalid --voice "${opts.voice}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
|
|
1648
|
+
);
|
|
1649
|
+
}
|
|
1650
|
+
for (const sv of speakers) {
|
|
1651
|
+
if (!isValidGeminiVoice(sv.voice)) {
|
|
1652
|
+
throw new ValidationError(
|
|
1653
|
+
`Invalid voice "${sv.voice}" for speaker "${sv.speaker}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
|
|
1654
|
+
);
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
const result = await generateGeminiSpeech({
|
|
1658
|
+
text,
|
|
1659
|
+
model,
|
|
1660
|
+
voice: speakers.length === 0 ? opts.voice : void 0,
|
|
1661
|
+
speakers: speakers.length > 0 ? speakers : void 0,
|
|
1662
|
+
outputFormat: opts.outputFormat,
|
|
1663
|
+
output: opts.output,
|
|
1664
|
+
logger
|
|
1665
|
+
});
|
|
1666
|
+
if (result.status === "error") {
|
|
1667
|
+
logger.error(result.error ?? "Unknown error");
|
|
1668
|
+
process.exit(1);
|
|
1669
|
+
}
|
|
1670
|
+
console.log(`
|
|
1671
|
+
Generated audio: ${result.generatedAudio}`);
|
|
1672
|
+
}
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
function collectSpeakers(value, prev) {
|
|
1676
|
+
const idx = value.indexOf(":");
|
|
1677
|
+
if (idx <= 0 || idx === value.length - 1) {
|
|
1678
|
+
throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
|
|
1679
|
+
}
|
|
1680
|
+
const speaker = value.slice(0, idx).trim();
|
|
1681
|
+
const voice = value.slice(idx + 1).trim();
|
|
1682
|
+
if (!speaker || !voice) {
|
|
1683
|
+
throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
|
|
1684
|
+
}
|
|
1685
|
+
return [...prev, { speaker, voice }];
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// src/providers/gemini/commands/generate-video.ts
|
|
1689
|
+
import fs12 from "fs";
|
|
1690
|
+
import path15 from "path";
|
|
1691
|
+
init_errors();
|
|
1692
|
+
init_http_client();
|
|
1693
|
+
var BASE2 = "https://generativelanguage.googleapis.com";
|
|
1694
|
+
function registerGenerateVideoCommand(parent) {
|
|
1695
|
+
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(
|
|
1696
|
+
"--reference-images <paths...>",
|
|
1697
|
+
"Reference images (first=opening frame, second=closing)"
|
|
1698
|
+
).option("--upload-timeout <ms>", "Upload timeout in ms", "120000").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
1699
|
+
async (opts) => {
|
|
1700
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1701
|
+
const apiKey = resolveKey("GEMINI_API_KEY");
|
|
1702
|
+
if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
|
|
1703
|
+
const model = opts.model ?? getDefaultModel("generate-video");
|
|
1704
|
+
if (!model.startsWith("veo-")) {
|
|
1705
|
+
throw new ValidationError(
|
|
1706
|
+
`Video generation requires a Veo model (got '${model}'). Use: veo-3.1-generate-preview, veo-3.0-generate-001, etc.`
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
if (!["720p", "1080p"].includes(opts.resolution)) {
|
|
1710
|
+
throw new ValidationError("--resolution must be 720p or 1080p");
|
|
1711
|
+
}
|
|
1712
|
+
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
1713
|
+
throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
|
|
1714
|
+
}
|
|
1715
|
+
logger.debug(`Submitting video generation with ${model}...`);
|
|
1716
|
+
logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
|
|
1717
|
+
const config = {
|
|
1718
|
+
aspectRatio: opts.aspectRatio,
|
|
1719
|
+
resolution: opts.resolution
|
|
1720
|
+
};
|
|
1721
|
+
const contentParts = [{ text: opts.prompt }];
|
|
1722
|
+
if (opts.referenceImages?.length) {
|
|
1723
|
+
for (const imgPath of opts.referenceImages.slice(0, 2)) {
|
|
1724
|
+
if (!fs12.existsSync(imgPath))
|
|
1725
|
+
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
1726
|
+
const data = fs12.readFileSync(imgPath).toString("base64");
|
|
1727
|
+
const ext = path15.extname(imgPath).toLowerCase();
|
|
1728
|
+
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
1729
|
+
contentParts.push({ inlineData: { mimeType, data } });
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
|
|
1733
|
+
let operation;
|
|
1734
|
+
try {
|
|
1735
|
+
operation = await httpJson({
|
|
1736
|
+
url: `${BASE2}/v1beta/models/${model}:generateVideos`,
|
|
1737
|
+
method: "POST",
|
|
1738
|
+
headers,
|
|
1739
|
+
body: {
|
|
1740
|
+
contents: [{ parts: contentParts }],
|
|
1741
|
+
generationConfig: config
|
|
1742
|
+
}
|
|
1743
|
+
});
|
|
1744
|
+
} catch (e) {
|
|
1745
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1746
|
+
throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
|
|
1747
|
+
}
|
|
1748
|
+
const operationName = operation.name;
|
|
1749
|
+
logger.debug(`Operation: ${operationName}`);
|
|
1750
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
1751
|
+
let done = false;
|
|
1752
|
+
let finalOp = operation;
|
|
1753
|
+
while (!done && Date.now() < deadline) {
|
|
1754
|
+
await sleep2(1e4);
|
|
1755
|
+
finalOp = await httpJson({
|
|
1756
|
+
url: `${BASE2}/v1beta/${operationName}`,
|
|
1757
|
+
headers
|
|
1758
|
+
});
|
|
1759
|
+
done = finalOp.done === true;
|
|
1760
|
+
if (!done)
|
|
1761
|
+
logger.debug(
|
|
1762
|
+
`Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
|
|
1766
|
+
if (finalOp.error)
|
|
1767
|
+
throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
|
|
1768
|
+
const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
|
|
1769
|
+
if (!videoUri) {
|
|
1770
|
+
throw new ProviderError(
|
|
1771
|
+
`Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
|
|
1772
|
+
"gemini"
|
|
1773
|
+
);
|
|
1774
|
+
}
|
|
1775
|
+
const outDir = getOutputDir();
|
|
1776
|
+
const dest = path15.join(outDir, `veo_generated_${Date.now()}.mp4`);
|
|
1777
|
+
const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
|
|
1778
|
+
await downloadFile2(videoUri, dest);
|
|
1779
|
+
logger.success(`Saved video: ${dest}`);
|
|
1780
|
+
if (opts.output) {
|
|
1781
|
+
fs12.mkdirSync(path15.dirname(path15.resolve(opts.output)), { recursive: true });
|
|
1782
|
+
fs12.copyFileSync(dest, opts.output);
|
|
1783
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
1784
|
+
}
|
|
1785
|
+
await maybeDownloadThumb(finalOp.response, dest, {
|
|
1786
|
+
skip: opts.thumb === false,
|
|
1787
|
+
copyTo: opts.output,
|
|
1788
|
+
logger
|
|
1789
|
+
});
|
|
1790
|
+
console.log(`
|
|
1791
|
+
Generated video: ${dest}`);
|
|
1792
|
+
}
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
function sleep2(ms) {
|
|
1796
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
// src/providers/gemini/commands/generate.ts
|
|
1800
|
+
init_errors();
|
|
1801
|
+
import fs13 from "fs";
|
|
1802
|
+
import path16 from "path";
|
|
1803
|
+
function registerGenerateCommand(parent) {
|
|
1804
|
+
parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
1805
|
+
async (opts) => {
|
|
1806
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1807
|
+
const model = opts.model ?? getDefaultModel("generate");
|
|
1808
|
+
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 4);
|
|
1809
|
+
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
1810
|
+
throw new ValidationError(
|
|
1811
|
+
`Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
|
|
1815
|
+
throw new ValidationError(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
|
|
1816
|
+
}
|
|
1817
|
+
logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
|
|
1818
|
+
logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
|
|
1819
|
+
const imageConfig = { aspectRatio: opts.aspectRatio };
|
|
1820
|
+
if (opts.size) imageConfig.imageSize = opts.size;
|
|
1821
|
+
const generationConfig = {
|
|
1822
|
+
responseModalities: ["IMAGE"],
|
|
1823
|
+
imageConfig
|
|
1824
|
+
};
|
|
1825
|
+
const savedFiles = [];
|
|
1826
|
+
for (let i = 0; i < numImages; i++) {
|
|
1827
|
+
try {
|
|
1828
|
+
const resp = await generateContent({
|
|
1829
|
+
model,
|
|
1830
|
+
contents: [{ parts: [{ text: opts.prompt }] }],
|
|
1831
|
+
generationConfig
|
|
1832
|
+
});
|
|
1833
|
+
const images = extractImages(resp);
|
|
1834
|
+
if (images.length === 0) {
|
|
1835
|
+
logger.warn(
|
|
1836
|
+
`Request ${i + 1}: no images in response \u2014 model may not support image generation`
|
|
1837
|
+
);
|
|
1838
|
+
continue;
|
|
1839
|
+
}
|
|
1840
|
+
for (const img of images) {
|
|
1841
|
+
const outDir = getOutputDir();
|
|
1842
|
+
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
1843
|
+
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
1844
|
+
const dest = path16.join(outDir, fname);
|
|
1845
|
+
fs13.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
1846
|
+
savedFiles.push(dest);
|
|
1847
|
+
logger.success(`Saved: ${dest}`);
|
|
1848
|
+
}
|
|
1849
|
+
} catch (e) {
|
|
1850
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1851
|
+
logger.error(`Generation failed: ${msg}`);
|
|
1852
|
+
process.exit(1);
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
if (savedFiles.length === 0) {
|
|
1856
|
+
logger.error("No images were generated");
|
|
1857
|
+
process.exit(1);
|
|
1858
|
+
}
|
|
1859
|
+
if (opts.output && savedFiles[0]) {
|
|
1860
|
+
fs13.mkdirSync(path16.dirname(path16.resolve(opts.output)), { recursive: true });
|
|
1861
|
+
fs13.copyFileSync(savedFiles[0], opts.output);
|
|
1862
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
1863
|
+
}
|
|
1864
|
+
console.log(`
|
|
1865
|
+
Generated ${savedFiles.length} image(s):`);
|
|
1866
|
+
for (const f of savedFiles) console.log(` ${f}`);
|
|
1867
|
+
}
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
// src/providers/gemini/commands/image-to-image.ts
|
|
1872
|
+
import fs14 from "fs";
|
|
1873
|
+
import path17 from "path";
|
|
1874
|
+
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
1875
|
+
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
1876
|
+
function registerGeminiImageToImageCommand(parent) {
|
|
1877
|
+
parent.command("image-to-image").alias("i2i").description(
|
|
1878
|
+
"Edit/compose images with Gemini 2.5 Flash Image (Nano Banana) using one or more reference images"
|
|
1879
|
+
).requiredOption("--prompt <text>", "Text prompt describing the edit/transformation").requiredOption(
|
|
1880
|
+
"--ref <path>",
|
|
1881
|
+
"Reference image (URL or local path); repeatable for multi-ref",
|
|
1882
|
+
collect4,
|
|
1883
|
+
[]
|
|
1884
|
+
).option("-m, --model <id>", `Gemini image model (default ${IMAGE_MODEL_FALLBACK})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
1885
|
+
async (opts) => {
|
|
1886
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
1887
|
+
throw new Error("At least one --ref is required for image-to-image.");
|
|
1888
|
+
}
|
|
1889
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1890
|
+
const model = opts.model ?? IMAGE_MODEL_FALLBACK;
|
|
1891
|
+
const parts = [];
|
|
1892
|
+
for (const r of opts.ref) {
|
|
1893
|
+
const resolved = await resolveImageInput(r, {
|
|
1894
|
+
logger,
|
|
1895
|
+
softLimitBytes: SOFT_LIMIT_BYTES,
|
|
1896
|
+
hardLimitBytes: HARD_LIMIT_BYTES
|
|
1897
|
+
});
|
|
1898
|
+
if (resolved.kind === "url") {
|
|
1899
|
+
const buf = await fetchUrlAsBuffer(resolved.url);
|
|
1900
|
+
const mime = sniffImageMime(buf, resolved.url);
|
|
1901
|
+
if (buf.byteLength > HARD_LIMIT_BYTES) {
|
|
1902
|
+
throw new Error(
|
|
1903
|
+
`Reference ${resolved.url} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds Gemini ${HARD_LIMIT_BYTES / 1024 / 1024}MB inline cap.`
|
|
1904
|
+
);
|
|
1905
|
+
}
|
|
1906
|
+
parts.push({ inlineData: { mimeType: mime, data: buf.toString("base64") } });
|
|
1907
|
+
} else {
|
|
1908
|
+
const data = resolved.dataUrl.split(",", 2)[1] ?? "";
|
|
1909
|
+
parts.push({ inlineData: { mimeType: resolved.mime, data } });
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
parts.push({ text: opts.prompt });
|
|
1913
|
+
logger.debug(`Calling ${model} with ${opts.ref.length} reference image(s)`);
|
|
1914
|
+
const req = {
|
|
1915
|
+
model,
|
|
1916
|
+
contents: [{ parts }],
|
|
1917
|
+
generationConfig: { responseModalities: ["IMAGE"] }
|
|
1918
|
+
};
|
|
1919
|
+
const resp = await generateContent(req);
|
|
1920
|
+
const images = extractImages(resp);
|
|
1921
|
+
if (images.length === 0) {
|
|
1922
|
+
logger.error(
|
|
1923
|
+
"No images in response \u2014 model may have refused or doesn't support image output."
|
|
1924
|
+
);
|
|
1925
|
+
process.exit(1);
|
|
1926
|
+
}
|
|
1927
|
+
const outDir = getOutputDir();
|
|
1928
|
+
const ts = Date.now();
|
|
1929
|
+
const saved = [];
|
|
1930
|
+
for (let i = 0; i < images.length; i++) {
|
|
1931
|
+
const img = images[i];
|
|
1932
|
+
if (!img) continue;
|
|
1933
|
+
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
1934
|
+
const dest = path17.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
1935
|
+
fs14.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
1936
|
+
saved.push(dest);
|
|
1937
|
+
logger.success(`Saved: ${dest}`);
|
|
1938
|
+
}
|
|
1939
|
+
if (opts.output && saved[0]) {
|
|
1940
|
+
fs14.mkdirSync(path17.dirname(path17.resolve(opts.output)), { recursive: true });
|
|
1941
|
+
fs14.copyFileSync(saved[0], opts.output);
|
|
1942
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
1943
|
+
}
|
|
1944
|
+
console.log(`
|
|
1945
|
+
Generated ${saved.length} image(s):`);
|
|
1946
|
+
for (const f of saved) console.log(` ${f}`);
|
|
1947
|
+
}
|
|
1948
|
+
);
|
|
1949
|
+
}
|
|
1950
|
+
function collect4(value, prev) {
|
|
1951
|
+
return [...prev, value];
|
|
1952
|
+
}
|
|
1953
|
+
async function fetchUrlAsBuffer(url) {
|
|
1954
|
+
const res = await fetch(url);
|
|
1955
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
1956
|
+
const arr = await res.arrayBuffer();
|
|
1957
|
+
return Buffer.from(arr);
|
|
1958
|
+
}
|
|
1959
|
+
function sniffImageMime(buf, url) {
|
|
1960
|
+
if (buf.length >= 4) {
|
|
1961
|
+
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
1962
|
+
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71)
|
|
1963
|
+
return "image/png";
|
|
1964
|
+
if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
|
|
1965
|
+
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
1966
|
+
return "image/webp";
|
|
1967
|
+
}
|
|
1968
|
+
const ext = path17.extname(new URL(url).pathname).toLowerCase();
|
|
1969
|
+
if (ext === ".png") return "image/png";
|
|
1970
|
+
if (ext === ".webp") return "image/webp";
|
|
1971
|
+
if (ext === ".gif") return "image/gif";
|
|
1972
|
+
return "image/jpeg";
|
|
492
1973
|
}
|
|
493
|
-
function renderMarkdown(results) {
|
|
494
|
-
const lines = ["# Results\n"];
|
|
495
|
-
for (const r of results) {
|
|
496
|
-
lines.push(`## ${r.file}
|
|
497
|
-
`);
|
|
498
|
-
lines.push(`**Status**: ${r.status}
|
|
499
|
-
`);
|
|
500
|
-
if (r.status === "success" && r.response) lines.push(`**Response**:
|
|
501
1974
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
1975
|
+
// src/providers/gemini/commands/image-to-video.ts
|
|
1976
|
+
import fs15 from "fs";
|
|
1977
|
+
import path18 from "path";
|
|
1978
|
+
init_errors();
|
|
1979
|
+
init_http_client();
|
|
1980
|
+
var BASE3 = "https://generativelanguage.googleapis.com";
|
|
1981
|
+
function imagePart(imgPath) {
|
|
1982
|
+
if (!fs15.existsSync(imgPath)) {
|
|
1983
|
+
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
507
1984
|
}
|
|
508
|
-
|
|
1985
|
+
const data = fs15.readFileSync(imgPath).toString("base64");
|
|
1986
|
+
const ext = path18.extname(imgPath).toLowerCase();
|
|
1987
|
+
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
1988
|
+
return { inlineData: { mimeType, data } };
|
|
1989
|
+
}
|
|
1990
|
+
function sleep3(ms) {
|
|
1991
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1992
|
+
}
|
|
1993
|
+
function registerGeminiImageToVideoCommand(parent) {
|
|
1994
|
+
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(
|
|
1995
|
+
async (imagePath, opts) => {
|
|
1996
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1997
|
+
const apiKey = resolveKey("GEMINI_API_KEY");
|
|
1998
|
+
if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
|
|
1999
|
+
const model = opts.model ?? getDefaultModel("generate-video");
|
|
2000
|
+
if (!model.startsWith("veo-")) {
|
|
2001
|
+
throw new ValidationError(
|
|
2002
|
+
`Image-to-video requires a Veo model (got '${model}'). Try veo-3.1-generate-preview.`
|
|
2003
|
+
);
|
|
2004
|
+
}
|
|
2005
|
+
if (!["720p", "1080p"].includes(opts.resolution)) {
|
|
2006
|
+
throw new ValidationError("--resolution must be 720p or 1080p");
|
|
2007
|
+
}
|
|
2008
|
+
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
2009
|
+
throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
|
|
2010
|
+
}
|
|
2011
|
+
const contentParts = [
|
|
2012
|
+
{ text: opts.prompt },
|
|
2013
|
+
imagePart(imagePath)
|
|
2014
|
+
];
|
|
2015
|
+
if (opts.lastFrame) contentParts.push(imagePart(opts.lastFrame));
|
|
2016
|
+
const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
|
|
2017
|
+
logger.info(
|
|
2018
|
+
"Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
|
|
2019
|
+
);
|
|
2020
|
+
let operation;
|
|
2021
|
+
try {
|
|
2022
|
+
operation = await httpJson({
|
|
2023
|
+
url: `${BASE3}/v1beta/models/${model}:generateVideos`,
|
|
2024
|
+
method: "POST",
|
|
2025
|
+
headers,
|
|
2026
|
+
body: {
|
|
2027
|
+
contents: [{ parts: contentParts }],
|
|
2028
|
+
generationConfig: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
|
|
2029
|
+
}
|
|
2030
|
+
});
|
|
2031
|
+
} catch (e) {
|
|
2032
|
+
throw new ProviderError(
|
|
2033
|
+
`Submit failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2034
|
+
"gemini"
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
const operationName = operation.name;
|
|
2038
|
+
logger.debug(`Operation: ${operationName}`);
|
|
2039
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
2040
|
+
let finalOp = operation;
|
|
2041
|
+
while (!finalOp.done && Date.now() < deadline) {
|
|
2042
|
+
await sleep3(1e4);
|
|
2043
|
+
finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
|
|
2044
|
+
if (!finalOp.done) {
|
|
2045
|
+
logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
if (!finalOp.done) throw new ProviderError("Veo timed out after 10 minutes", "gemini");
|
|
2049
|
+
if (finalOp.error) {
|
|
2050
|
+
throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
|
|
2051
|
+
}
|
|
2052
|
+
const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
|
|
2053
|
+
if (!videoUri) {
|
|
2054
|
+
throw new ProviderError(
|
|
2055
|
+
`Unexpected Veo response: ${JSON.stringify(finalOp.response)}`,
|
|
2056
|
+
"gemini"
|
|
2057
|
+
);
|
|
2058
|
+
}
|
|
2059
|
+
const dest = path18.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
2060
|
+
await downloadFile(videoUri, dest);
|
|
2061
|
+
logger.success(`Saved video: ${dest}`);
|
|
2062
|
+
if (opts.output) {
|
|
2063
|
+
fs15.mkdirSync(path18.dirname(path18.resolve(opts.output)), { recursive: true });
|
|
2064
|
+
fs15.copyFileSync(dest, opts.output);
|
|
2065
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
2066
|
+
}
|
|
2067
|
+
await maybeDownloadThumb(finalOp.response, dest, {
|
|
2068
|
+
skip: opts.thumb === false,
|
|
2069
|
+
copyTo: opts.output,
|
|
2070
|
+
logger
|
|
2071
|
+
});
|
|
2072
|
+
console.log(`
|
|
2073
|
+
Generated video: ${dest}`);
|
|
2074
|
+
}
|
|
2075
|
+
);
|
|
509
2076
|
}
|
|
510
2077
|
|
|
511
|
-
// src/providers/gemini/commands/
|
|
512
|
-
|
|
513
|
-
|
|
2078
|
+
// src/providers/gemini/commands/transcribe.ts
|
|
2079
|
+
init_errors();
|
|
2080
|
+
import fs16 from "fs";
|
|
2081
|
+
function registerTranscribeCommand(parent) {
|
|
2082
|
+
parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
|
|
514
2083
|
async (opts) => {
|
|
515
2084
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
516
|
-
const model = opts.model ?? getDefaultModel("
|
|
2085
|
+
const model = opts.model ?? getDefaultModel("transcribe");
|
|
517
2086
|
for (const f of opts.files) {
|
|
518
|
-
if (!
|
|
2087
|
+
if (!fs16.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
519
2088
|
}
|
|
520
2089
|
const results = [];
|
|
521
2090
|
for (let i = 0; i < opts.files.length; i++) {
|
|
522
2091
|
const filePath = opts.files[i];
|
|
523
|
-
logger.debug(`[${i + 1}/${opts.files.length}]
|
|
2092
|
+
logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
|
|
524
2093
|
try {
|
|
525
2094
|
const useApi = shouldUseFileApi(filePath);
|
|
526
2095
|
let contentParts;
|
|
@@ -550,312 +2119,710 @@ function registerAnalyzeCommand(parent) {
|
|
|
550
2119
|
}
|
|
551
2120
|
}
|
|
552
2121
|
formatResults(results, opts.format, opts.output);
|
|
553
|
-
logger.debug(
|
|
554
|
-
`Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
|
|
555
|
-
);
|
|
556
2122
|
const failed = results.filter((r) => r.status === "error").length;
|
|
557
2123
|
if (failed > 0) process.exit(1);
|
|
558
2124
|
}
|
|
559
2125
|
);
|
|
560
2126
|
}
|
|
561
2127
|
|
|
562
|
-
// src/providers/gemini/commands/
|
|
2128
|
+
// src/providers/gemini/commands/index.ts
|
|
2129
|
+
function registerGeminiCommands(program2) {
|
|
2130
|
+
const gemini = program2.command("gemini").description(
|
|
2131
|
+
"Gemini AI operations: analyze, transcribe, extract, generate images/video/speech"
|
|
2132
|
+
);
|
|
2133
|
+
registerAnalyzeCommand(gemini);
|
|
2134
|
+
registerTranscribeCommand(gemini);
|
|
2135
|
+
registerExtractCommand(gemini);
|
|
2136
|
+
registerGenerateCommand(gemini);
|
|
2137
|
+
registerGeminiImageToImageCommand(gemini);
|
|
2138
|
+
registerGenerateVideoCommand(gemini);
|
|
2139
|
+
registerGeminiImageToVideoCommand(gemini);
|
|
2140
|
+
registerGeminiGenerateSpeechCommand(gemini);
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// src/providers/leonardo/commands/generate.ts
|
|
2144
|
+
init_http_client();
|
|
2145
|
+
import fs17 from "fs";
|
|
2146
|
+
import path19 from "path";
|
|
2147
|
+
|
|
2148
|
+
// src/providers/leonardo/client.ts
|
|
563
2149
|
init_errors();
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
2150
|
+
init_http_client();
|
|
2151
|
+
|
|
2152
|
+
// src/providers/leonardo/models.ts
|
|
2153
|
+
var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
|
|
2154
|
+
var DEFAULT_LEONARDO_MODEL_ID = "7b592283-e8a7-4c5a-9ba6-d18c31f258b9";
|
|
2155
|
+
var LEONARDO_DEFAULTS = {
|
|
2156
|
+
modelId: process.env.LEONARDO_DEFAULT_MODEL ?? DEFAULT_LEONARDO_MODEL_ID,
|
|
2157
|
+
videoModel: process.env.LEONARDO_VIDEO_MODEL ?? "MOTION2",
|
|
2158
|
+
width: 1024,
|
|
2159
|
+
height: 1024,
|
|
2160
|
+
numImages: 1
|
|
2161
|
+
};
|
|
2162
|
+
var LEONARDO_VIDEO_MODELS = [
|
|
2163
|
+
{ id: "MOTION2", name: "Motion 2.0", modes: ["t2v", "i2v"] },
|
|
2164
|
+
{ id: "MOTION2FAST", name: "Motion 2.0 Fast", modes: ["t2v", "i2v"] },
|
|
2165
|
+
{ id: "VEO3", name: "Veo 3.0", modes: ["t2v", "i2v"] },
|
|
2166
|
+
{ id: "VEO3FAST", name: "Veo 3.0 Fast", modes: ["t2v"] },
|
|
2167
|
+
{ id: "VEO3_1", name: "Veo 3.1", modes: ["t2v", "i2v"] },
|
|
2168
|
+
{ id: "KLING2_1", name: "Kling 2.1 Pro", modes: ["t2v", "i2v"] },
|
|
2169
|
+
{ id: "Kling2_5", name: "Kling 2.5 Turbo", modes: ["t2v", "i2v"] },
|
|
2170
|
+
{ id: "kling-2.6", name: "Kling 2.6", modes: ["t2v", "i2v"] },
|
|
2171
|
+
{ id: "kling-3.0", name: "Kling 3.0", modes: ["t2v"] },
|
|
2172
|
+
{ id: "kling-video-o-1", name: "Kling O1", modes: ["t2v", "i2v"] },
|
|
2173
|
+
{ id: "kling-video-o-3", name: "Kling O3", modes: ["t2v", "i2v"] },
|
|
2174
|
+
{ id: "hailuo-2_3", name: "Hailuo 2.3", modes: ["t2v", "i2v"] },
|
|
2175
|
+
{ id: "ltxv-2.0-pro", name: "LTX 2.0 Pro", modes: ["t2v", "i2v"] },
|
|
2176
|
+
{ id: "ltxv-2.3-pro", name: "LTX 2.3 Pro", modes: ["t2v", "i2v"] },
|
|
2177
|
+
{ id: "seedance-2.0", name: "Seedance 2.0", modes: ["t2v", "i2v"] },
|
|
2178
|
+
{ id: "seedance-2.0-fast", name: "Seedance 2.0 Fast", modes: ["t2v", "i2v"] }
|
|
2179
|
+
];
|
|
2180
|
+
var LEONARDO_VIDEO_RESOLUTIONS = [
|
|
2181
|
+
"RESOLUTION_480",
|
|
2182
|
+
"RESOLUTION_720",
|
|
2183
|
+
"RESOLUTION_1080"
|
|
2184
|
+
];
|
|
2185
|
+
var LEONARDO_V2_VIDEO_MODELS = /* @__PURE__ */ new Set([
|
|
2186
|
+
"kling-2.6",
|
|
2187
|
+
"kling-3.0",
|
|
2188
|
+
"kling-video-o-1",
|
|
2189
|
+
"kling-video-o-3",
|
|
2190
|
+
"hailuo-2_3",
|
|
2191
|
+
"ltxv-2.0-pro",
|
|
2192
|
+
"ltxv-2.3-pro",
|
|
2193
|
+
"seedance-2.0",
|
|
2194
|
+
"seedance-2.0-fast"
|
|
2195
|
+
]);
|
|
2196
|
+
function isGptImageModel(modelId) {
|
|
2197
|
+
return typeof modelId === "string" && modelId.startsWith("gpt-image-");
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
// src/providers/leonardo/client.ts
|
|
2201
|
+
function requireLeonardoKey() {
|
|
2202
|
+
const key = resolveKey("LEONARDO_API_KEY");
|
|
2203
|
+
if (!key) {
|
|
2204
|
+
throw new ConfigError(
|
|
2205
|
+
"LEONARDO_API_KEY is not set. Get one at https://app.leonardo.ai/settings/api-keys"
|
|
2206
|
+
);
|
|
2207
|
+
}
|
|
2208
|
+
return key;
|
|
2209
|
+
}
|
|
2210
|
+
function leonardoBaseUrl() {
|
|
2211
|
+
return resolveKey("LEONARDO_BASE_URL") ?? DEFAULT_LEONARDO_BASE_URL;
|
|
2212
|
+
}
|
|
2213
|
+
var LeonardoClient = class {
|
|
2214
|
+
constructor(apiKey, baseUrl = leonardoBaseUrl()) {
|
|
2215
|
+
this.apiKey = apiKey;
|
|
2216
|
+
this.baseUrl = baseUrl;
|
|
2217
|
+
}
|
|
2218
|
+
apiKey;
|
|
2219
|
+
baseUrl;
|
|
2220
|
+
async request(path35, opts = {}) {
|
|
2221
|
+
const base = this.baseUrl.replace(/\/$/, "");
|
|
2222
|
+
const v2Override = path35.startsWith("v2:");
|
|
2223
|
+
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
2224
|
+
const finalPath = v2Override ? path35.slice(3) : path35;
|
|
2225
|
+
const url = new URL(finalBase + finalPath);
|
|
2226
|
+
if (opts.query) {
|
|
2227
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
2228
|
+
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
572
2229
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
2230
|
+
}
|
|
2231
|
+
opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
|
|
2232
|
+
return httpJson({
|
|
2233
|
+
url: url.toString(),
|
|
2234
|
+
method: opts.method ?? "GET",
|
|
2235
|
+
headers: {
|
|
2236
|
+
accept: "application/json",
|
|
2237
|
+
authorization: `Bearer ${this.apiKey}`
|
|
2238
|
+
},
|
|
2239
|
+
body: opts.body,
|
|
2240
|
+
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2241
|
+
});
|
|
2242
|
+
}
|
|
2243
|
+
get(path35, query, logger) {
|
|
2244
|
+
return this.request(path35, { method: "GET", query, logger });
|
|
2245
|
+
}
|
|
2246
|
+
post(path35, body, logger) {
|
|
2247
|
+
return this.request(path35, { method: "POST", body, logger });
|
|
2248
|
+
}
|
|
2249
|
+
delete(path35, logger) {
|
|
2250
|
+
return this.request(path35, { method: "DELETE", logger });
|
|
2251
|
+
}
|
|
2252
|
+
};
|
|
2253
|
+
function createLeonardoClient() {
|
|
2254
|
+
return new LeonardoClient(requireLeonardoKey(), leonardoBaseUrl());
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// src/providers/leonardo/commands/generate.ts
|
|
2258
|
+
function extFromUrl(url, fallback = ".png") {
|
|
2259
|
+
try {
|
|
2260
|
+
const u = new URL(url);
|
|
2261
|
+
const e = path19.extname(u.pathname);
|
|
2262
|
+
return e.length > 0 ? e : fallback;
|
|
2263
|
+
} catch {
|
|
2264
|
+
return fallback;
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
function autoFilename(generationId, index, ext) {
|
|
2268
|
+
const e = ext.startsWith(".") ? ext : `.${ext}`;
|
|
2269
|
+
return `leonardo-${generationId.slice(0, 8)}-${String(index).padStart(2, "0")}${e}`;
|
|
2270
|
+
}
|
|
2271
|
+
function registerLeonardoGenerateCommand(parent) {
|
|
2272
|
+
parent.command("generate <prompt>").alias("gen").description("Generate images from a text prompt via Leonardo").option("-m, --model <id>", "Model UUID or GPT-image id").option("-w, --width <n>", "Image width").option("-h, --height <n>", "Image height").option("-n, --num <n>", "Number of images").option("--alchemy", "Enable Alchemy").option("--ultra", "Enable Ultra mode").option("--contrast <n>", "Contrast").option("--style <uuid>", "Style UUID").option("--seed <n>", "Seed").option("--negative <text>", "Negative prompt").option("--enhance", "Enhance prompt").option("--quality <level>", "GPT Image quality: LOW|MEDIUM|HIGH", "HIGH").option("--output <path>", "Copy first image to this path (single-image only)").option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
|
|
2273
|
+
async (prompt, opts) => {
|
|
2274
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2275
|
+
const client = createLeonardoClient();
|
|
2276
|
+
const input = {
|
|
2277
|
+
prompt,
|
|
2278
|
+
modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
|
|
2279
|
+
width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
|
|
2280
|
+
height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
|
|
2281
|
+
num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
|
|
2282
|
+
alchemy: opts.alchemy,
|
|
2283
|
+
ultra: opts.ultra,
|
|
2284
|
+
contrast: opts.contrast ? Number.parseFloat(opts.contrast) : void 0,
|
|
2285
|
+
styleUUID: opts.style,
|
|
2286
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
2287
|
+
negative_prompt: opts.negative,
|
|
2288
|
+
enhancePrompt: opts.enhance
|
|
2289
|
+
};
|
|
2290
|
+
logger.info(
|
|
2291
|
+
`Submitting generation (${input.width}x${input.height}, n=${input.num_images})`
|
|
2292
|
+
);
|
|
2293
|
+
let generationId;
|
|
2294
|
+
if (isGptImageModel(input.modelId)) {
|
|
2295
|
+
const v2Body = {
|
|
2296
|
+
public: false,
|
|
2297
|
+
model: input.modelId,
|
|
2298
|
+
parameters: {
|
|
2299
|
+
prompt: input.prompt,
|
|
2300
|
+
quantity: input.num_images ?? 1,
|
|
2301
|
+
width: input.width,
|
|
2302
|
+
height: input.height,
|
|
2303
|
+
quality: opts.quality,
|
|
2304
|
+
prompt_enhance: input.enhancePrompt ? "ON" : "OFF",
|
|
2305
|
+
...input.seed !== void 0 ? { seed: input.seed } : {}
|
|
589
2306
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
2307
|
+
};
|
|
2308
|
+
const v2Res = await client.post(
|
|
2309
|
+
"v2:/generations",
|
|
2310
|
+
v2Body,
|
|
2311
|
+
logger
|
|
2312
|
+
);
|
|
2313
|
+
generationId = v2Res.generate.generationId;
|
|
2314
|
+
} else {
|
|
2315
|
+
const createRes = await client.post(
|
|
2316
|
+
"/generations",
|
|
2317
|
+
input,
|
|
2318
|
+
logger
|
|
2319
|
+
);
|
|
2320
|
+
generationId = createRes.sdGenerationJob.generationId;
|
|
2321
|
+
}
|
|
2322
|
+
logger.debug(`generationId: ${generationId}`);
|
|
2323
|
+
logger.info("Polling for completion...");
|
|
2324
|
+
const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
|
|
2325
|
+
const intervalMs = 4e3;
|
|
2326
|
+
const final = await poll({
|
|
2327
|
+
fetch: () => client.get(`/generations/${generationId}`),
|
|
2328
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
|
|
2329
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2330
|
+
intervalMs,
|
|
2331
|
+
maxAttempts: Math.ceil(waitTimeout / intervalMs),
|
|
2332
|
+
onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
|
|
2333
|
+
});
|
|
2334
|
+
const images = final.generations_by_pk?.generated_images ?? [];
|
|
2335
|
+
if (opts.download === false) {
|
|
2336
|
+
for (const img of images) console.log(img.url);
|
|
2337
|
+
return;
|
|
2338
|
+
}
|
|
2339
|
+
const outDir = getOutputDir();
|
|
2340
|
+
const saved = [];
|
|
2341
|
+
for (let i = 0; i < images.length; i++) {
|
|
2342
|
+
const img = images[i];
|
|
2343
|
+
if (!img) continue;
|
|
2344
|
+
const ext = extFromUrl(img.url);
|
|
2345
|
+
const filename = autoFilename(generationId, i + 1, ext);
|
|
2346
|
+
const filepath = path19.join(outDir, filename);
|
|
2347
|
+
try {
|
|
2348
|
+
await downloadFile(img.url, filepath);
|
|
2349
|
+
saved.push(filepath);
|
|
2350
|
+
logger.success(`Saved ${filepath}`);
|
|
596
2351
|
} catch (e) {
|
|
597
|
-
|
|
598
|
-
logger.error(`Failed: ${filePath} \u2014 ${msg}`);
|
|
599
|
-
results.push({ file: filePath, status: "error", error: msg });
|
|
2352
|
+
logger.warn(`Download failed for ${img.id}: ${e instanceof Error ? e.message : e}`);
|
|
600
2353
|
}
|
|
601
2354
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
2355
|
+
if (opts.output && saved[0]) {
|
|
2356
|
+
const dest = path19.resolve(opts.output);
|
|
2357
|
+
fs17.mkdirSync(path19.dirname(dest), { recursive: true });
|
|
2358
|
+
fs17.copyFileSync(saved[0], dest);
|
|
2359
|
+
logger.success(`Copied to ${dest}`);
|
|
2360
|
+
}
|
|
2361
|
+
console.log(`
|
|
2362
|
+
Generated ${saved.length} image(s):`);
|
|
2363
|
+
for (const f of saved) console.log(` ${f}`);
|
|
605
2364
|
}
|
|
606
2365
|
);
|
|
607
2366
|
}
|
|
608
2367
|
|
|
609
|
-
// src/providers/
|
|
610
|
-
import fs8 from "fs";
|
|
611
|
-
import path7 from "path";
|
|
612
|
-
init_errors();
|
|
2368
|
+
// src/providers/leonardo/commands/image-to-image.ts
|
|
613
2369
|
init_http_client();
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
fs7.mkdirSync(dir, { recursive: true });
|
|
623
|
-
cachedOutputDir = dir;
|
|
624
|
-
return dir;
|
|
2370
|
+
import fs18 from "fs";
|
|
2371
|
+
import path20 from "path";
|
|
2372
|
+
function extFromUrl2(url, fallback = ".png") {
|
|
2373
|
+
try {
|
|
2374
|
+
return path20.extname(new URL(url).pathname) || fallback;
|
|
2375
|
+
} catch {
|
|
2376
|
+
return fallback;
|
|
2377
|
+
}
|
|
625
2378
|
}
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
var BASE2 = "https://generativelanguage.googleapis.com";
|
|
629
|
-
function registerGenerateVideoCommand(parent) {
|
|
630
|
-
parent.command("generate-video").description("Generate video with Veo via Gemini API [EXPERIMENTAL]").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", "Veo model to use").option("--resolution <res>", "Video resolution: 720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option(
|
|
631
|
-
"--reference-images <paths...>",
|
|
632
|
-
"Reference images (first=opening frame, second=closing)"
|
|
633
|
-
).option("--upload-timeout <ms>", "Upload timeout in ms", "120000").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").action(
|
|
2379
|
+
function registerLeonardoImageToImageCommand(parent) {
|
|
2380
|
+
parent.command("image-to-image").alias("i2i").description("Generate an image conditioned on an existing Leonardo image id (init image)").requiredOption("--prompt <text>", "Text prompt describing the desired output").requiredOption("--ref <imageId>", "Existing Leonardo image id to use as init image").option("--image-type <type>", "GENERATED | UPLOADED", "GENERATED").option("--init-strength <n>", "Init image strength (0.0-0.9)", "0.5").option("-m, --model <id>", "Model UUID").option("-w, --width <n>", "Image width").option("-h, --height <n>", "Image height").option("-n, --num <n>", "Number of images").option("--seed <n>", "Seed").option("--negative <text>", "Negative prompt").option("--output <path>", "Copy first image to this path (single-image only)").option("--no-download", "Skip downloading; print URLs only").option("--wait-timeout <ms>", "Polling timeout in milliseconds", "480000").option("-v, --verbose", "Verbose logging").action(
|
|
634
2381
|
async (opts) => {
|
|
635
2382
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
636
|
-
const
|
|
637
|
-
if (
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
throw new ValidationError(
|
|
641
|
-
`Video generation requires a Veo model (got '${model}'). Use: veo-3.1-generate-preview, veo-3.0-generate-001, etc.`
|
|
642
|
-
);
|
|
643
|
-
}
|
|
644
|
-
if (!["720p", "1080p"].includes(opts.resolution)) {
|
|
645
|
-
throw new ValidationError("--resolution must be 720p or 1080p");
|
|
2383
|
+
const imageType = opts.imageType.toUpperCase();
|
|
2384
|
+
if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
|
|
2385
|
+
logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
|
|
2386
|
+
process.exit(2);
|
|
646
2387
|
}
|
|
647
|
-
|
|
648
|
-
|
|
2388
|
+
const initStrength = Number.parseFloat(opts.initStrength);
|
|
2389
|
+
if (Number.isNaN(initStrength) || initStrength < 0 || initStrength > 0.9) {
|
|
2390
|
+
logger.error("--init-strength must be a number in [0.0, 0.9]");
|
|
2391
|
+
process.exit(2);
|
|
649
2392
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
2393
|
+
const client = createLeonardoClient();
|
|
2394
|
+
const body = {
|
|
2395
|
+
prompt: opts.prompt,
|
|
2396
|
+
modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
|
|
2397
|
+
width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
|
|
2398
|
+
height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
|
|
2399
|
+
num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
|
|
2400
|
+
init_strength: initStrength
|
|
655
2401
|
};
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
661
|
-
const data = fs8.readFileSync(imgPath).toString("base64");
|
|
662
|
-
const ext = path7.extname(imgPath).toLowerCase();
|
|
663
|
-
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
664
|
-
contentParts.push({ inlineData: { mimeType, data } });
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
|
|
668
|
-
let operation;
|
|
669
|
-
try {
|
|
670
|
-
operation = await httpJson({
|
|
671
|
-
url: `${BASE2}/v1beta/models/${model}:generateVideos`,
|
|
672
|
-
method: "POST",
|
|
673
|
-
headers,
|
|
674
|
-
body: {
|
|
675
|
-
contents: [{ parts: contentParts }],
|
|
676
|
-
generationConfig: config
|
|
677
|
-
}
|
|
678
|
-
});
|
|
679
|
-
} catch (e) {
|
|
680
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
681
|
-
throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
|
|
682
|
-
}
|
|
683
|
-
const operationName = operation.name;
|
|
684
|
-
logger.debug(`Operation: ${operationName}`);
|
|
685
|
-
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
686
|
-
let done = false;
|
|
687
|
-
let finalOp = operation;
|
|
688
|
-
while (!done && Date.now() < deadline) {
|
|
689
|
-
await sleep2(1e4);
|
|
690
|
-
finalOp = await httpJson({
|
|
691
|
-
url: `${BASE2}/v1beta/${operationName}`,
|
|
692
|
-
headers
|
|
693
|
-
});
|
|
694
|
-
done = finalOp.done === true;
|
|
695
|
-
if (!done)
|
|
696
|
-
logger.debug(
|
|
697
|
-
`Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
|
|
698
|
-
);
|
|
2402
|
+
if (imageType === "GENERATED") {
|
|
2403
|
+
body.init_generation_image_id = opts.ref;
|
|
2404
|
+
} else {
|
|
2405
|
+
body.init_image_id = opts.ref;
|
|
699
2406
|
}
|
|
700
|
-
if (
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
2407
|
+
if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
|
|
2408
|
+
if (opts.negative) body.negative_prompt = opts.negative;
|
|
2409
|
+
logger.info(`Submitting image-to-image (init=${opts.ref}, type=${imageType})`);
|
|
2410
|
+
const createRes = await client.post("/generations", body, logger);
|
|
2411
|
+
const generationId = createRes.sdGenerationJob.generationId;
|
|
2412
|
+
logger.debug(`generationId: ${generationId}`);
|
|
2413
|
+
logger.info("Polling for completion...");
|
|
2414
|
+
const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
|
|
2415
|
+
const intervalMs = 4e3;
|
|
2416
|
+
const final = await poll({
|
|
2417
|
+
fetch: () => client.get(`/generations/${generationId}`),
|
|
2418
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
|
|
2419
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2420
|
+
intervalMs,
|
|
2421
|
+
maxAttempts: Math.ceil(waitTimeout / intervalMs),
|
|
2422
|
+
onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
|
|
2423
|
+
});
|
|
2424
|
+
const images = final.generations_by_pk?.generated_images ?? [];
|
|
2425
|
+
if (opts.download === false) {
|
|
2426
|
+
for (const img of images) console.log(img.url);
|
|
2427
|
+
return;
|
|
709
2428
|
}
|
|
710
2429
|
const outDir = getOutputDir();
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
2430
|
+
const saved = [];
|
|
2431
|
+
for (let i = 0; i < images.length; i++) {
|
|
2432
|
+
const img = images[i];
|
|
2433
|
+
if (!img) continue;
|
|
2434
|
+
const ext = extFromUrl2(img.url);
|
|
2435
|
+
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
2436
|
+
const filepath = path20.join(outDir, filename);
|
|
2437
|
+
try {
|
|
2438
|
+
await downloadFile(img.url, filepath);
|
|
2439
|
+
saved.push(filepath);
|
|
2440
|
+
logger.success(`Saved ${filepath}`);
|
|
2441
|
+
} catch (e) {
|
|
2442
|
+
logger.warn(`Download failed: ${e instanceof Error ? e.message : e}`);
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (opts.output && saved[0]) {
|
|
2446
|
+
const dest = path20.resolve(opts.output);
|
|
2447
|
+
fs18.mkdirSync(path20.dirname(dest), { recursive: true });
|
|
2448
|
+
fs18.copyFileSync(saved[0], dest);
|
|
2449
|
+
logger.success(`Copied to ${dest}`);
|
|
719
2450
|
}
|
|
720
2451
|
console.log(`
|
|
721
|
-
Generated
|
|
2452
|
+
Generated ${saved.length} image(s):`);
|
|
2453
|
+
for (const f of saved) console.log(` ${f}`);
|
|
722
2454
|
}
|
|
723
2455
|
);
|
|
724
2456
|
}
|
|
725
|
-
function sleep2(ms) {
|
|
726
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
727
|
-
}
|
|
728
2457
|
|
|
729
|
-
// src/providers/
|
|
730
|
-
|
|
731
|
-
import
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
2458
|
+
// src/providers/leonardo/commands/video.ts
|
|
2459
|
+
init_http_client();
|
|
2460
|
+
import path21 from "path";
|
|
2461
|
+
function v2DimsFor(resolution) {
|
|
2462
|
+
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
2463
|
+
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
2464
|
+
return { width: 1280, height: 720 };
|
|
2465
|
+
}
|
|
2466
|
+
function registerLeonardoVideoCommand(parent) {
|
|
2467
|
+
parent.command("video <prompt>").description("Generate a video from a text prompt via Leonardo (text-to-video)").option("--model <model>", "Video model (e.g. MOTION2, VEO3, kling-2.6)").option(
|
|
2468
|
+
"--resolution <r>",
|
|
2469
|
+
"Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
|
|
2470
|
+
"RESOLUTION_720"
|
|
2471
|
+
).option("--enhance", "Enable prompt enhance").option("--frame-interpolation", "Enable frame interpolation (v1 only)").option("--wait", "Poll until COMPLETE/FAILED instead of returning the job id").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download the generated video on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
2472
|
+
async (prompt, opts) => {
|
|
736
2473
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
737
|
-
const model = opts.model ??
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
2474
|
+
const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
|
|
2475
|
+
const client = createLeonardoClient();
|
|
2476
|
+
logger.info(`Submitting text-to-video (${model})`);
|
|
2477
|
+
let jobId;
|
|
2478
|
+
if (LEONARDO_V2_VIDEO_MODELS.has(model)) {
|
|
2479
|
+
const { width, height } = v2DimsFor(opts.resolution);
|
|
2480
|
+
const v2Res = await client.post(
|
|
2481
|
+
"v2:/generations",
|
|
2482
|
+
{
|
|
2483
|
+
public: false,
|
|
2484
|
+
model,
|
|
2485
|
+
parameters: {
|
|
2486
|
+
prompt,
|
|
2487
|
+
duration: 8,
|
|
2488
|
+
mode: opts.resolution,
|
|
2489
|
+
prompt_enhance: opts.enhance ? "ON" : "OFF",
|
|
2490
|
+
width,
|
|
2491
|
+
height
|
|
2492
|
+
}
|
|
2493
|
+
},
|
|
2494
|
+
logger
|
|
2495
|
+
);
|
|
2496
|
+
jobId = v2Res.generate.generationId;
|
|
2497
|
+
} else {
|
|
2498
|
+
const res = await client.post(
|
|
2499
|
+
"/generations-text-to-video",
|
|
2500
|
+
{
|
|
2501
|
+
prompt,
|
|
2502
|
+
model,
|
|
2503
|
+
resolution: opts.resolution,
|
|
2504
|
+
promptEnhance: !!opts.enhance,
|
|
2505
|
+
frameInterpolation: !!opts.frameInterpolation
|
|
2506
|
+
},
|
|
2507
|
+
logger
|
|
742
2508
|
);
|
|
2509
|
+
jobId = res.motionVideoGenerationJob?.generationId;
|
|
743
2510
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
2511
|
+
if (!jobId) {
|
|
2512
|
+
logger.error("No generationId returned by Leonardo");
|
|
2513
|
+
process.exit(1);
|
|
746
2514
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
2515
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
2516
|
+
if (!shouldWait) {
|
|
2517
|
+
console.log(jobId);
|
|
2518
|
+
console.error(`Poll with: multix leonardo status ${jobId}`);
|
|
2519
|
+
return;
|
|
2520
|
+
}
|
|
2521
|
+
await pollAndDownload(client, jobId, {
|
|
2522
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
2523
|
+
download: !!opts.download,
|
|
2524
|
+
output: opts.output,
|
|
2525
|
+
thumb: opts.thumb !== false,
|
|
2526
|
+
logger,
|
|
2527
|
+
basename: "leonardo-video"
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
async function pollAndDownload(client, jobId, opts) {
|
|
2533
|
+
const { logger } = opts;
|
|
2534
|
+
let res;
|
|
2535
|
+
try {
|
|
2536
|
+
res = await poll({
|
|
2537
|
+
fetch: () => client.get(`/generations/${jobId}`, void 0, logger),
|
|
2538
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE",
|
|
2539
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2540
|
+
intervalMs: 5e3,
|
|
2541
|
+
maxAttempts: Math.max(1, Math.ceil(opts.waitTimeoutMs / 5e3)),
|
|
2542
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
2543
|
+
});
|
|
2544
|
+
} catch (e) {
|
|
2545
|
+
if (e instanceof PollTimeoutError) {
|
|
2546
|
+
logger.error(`Timed out \u2014 multix leonardo status ${jobId}`);
|
|
2547
|
+
process.exit(1);
|
|
2548
|
+
}
|
|
2549
|
+
if (e instanceof PollFailedError) {
|
|
2550
|
+
logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
|
|
2551
|
+
process.exit(1);
|
|
2552
|
+
}
|
|
2553
|
+
throw e;
|
|
2554
|
+
}
|
|
2555
|
+
const gen = res.generations_by_pk;
|
|
2556
|
+
if (!gen) {
|
|
2557
|
+
logger.error("No generation in response");
|
|
2558
|
+
process.exit(1);
|
|
2559
|
+
}
|
|
2560
|
+
const videoUrl = gen.generated_images?.find(
|
|
2561
|
+
(img) => typeof img.url === "string" && /\.(mp4|webm|mov)(\?|$)/i.test(img.url)
|
|
2562
|
+
)?.url ?? gen.generated_images?.[0]?.motionMP4URL;
|
|
2563
|
+
if (!opts.download) {
|
|
2564
|
+
if (typeof videoUrl === "string") console.log(videoUrl);
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
if (typeof videoUrl !== "string") {
|
|
2568
|
+
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
2569
|
+
process.exit(1);
|
|
2570
|
+
}
|
|
2571
|
+
const outPath = opts.output ?? path21.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
2572
|
+
await downloadFile(videoUrl, outPath);
|
|
2573
|
+
logger.success(`Saved ${outPath}`);
|
|
2574
|
+
await maybeDownloadThumb(gen, outPath, {
|
|
2575
|
+
skip: !opts.thumb,
|
|
2576
|
+
copyTo: opts.output,
|
|
2577
|
+
logger
|
|
2578
|
+
});
|
|
2579
|
+
console.log(`
|
|
2580
|
+
Job: ${jobId}`);
|
|
2581
|
+
console.log(`Saved: ${outPath}`);
|
|
2582
|
+
}
|
|
2583
|
+
|
|
2584
|
+
// src/providers/leonardo/commands/image-to-video.ts
|
|
2585
|
+
function registerLeonardoImageToVideoCommand(parent) {
|
|
2586
|
+
parent.command("image-to-video <imageId>").alias("i2v").description("Generate a video from an existing Leonardo image (image-to-video)").requiredOption("--prompt <text>", "Motion / scene prompt for the video").option("--image-type <type>", "GENERATED | UPLOADED", "GENERATED").option("--model <model>", "Video model (e.g. MOTION2, VEO3, KLING2_5)").option(
|
|
2587
|
+
"--resolution <r>",
|
|
2588
|
+
"Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
|
|
2589
|
+
"RESOLUTION_720"
|
|
2590
|
+
).option("--duration <n>", "Duration seconds (model-dependent: 4|5|6|8|10)").option("--seed <n>", "Fixed seed").option("--negative <text>", "Negative prompt").option("--enhance", "Enable prompt enhance").option("--frame-interpolation", "Enable frame interpolation").option("--wait", "Poll until COMPLETE/FAILED instead of returning the job id").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download the generated video on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
2591
|
+
async (imageId, opts) => {
|
|
2592
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2593
|
+
const imageType = opts.imageType.toUpperCase();
|
|
2594
|
+
if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
|
|
2595
|
+
logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
|
|
2596
|
+
process.exit(2);
|
|
2597
|
+
}
|
|
2598
|
+
const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
|
|
2599
|
+
const client = createLeonardoClient();
|
|
2600
|
+
logger.info(`Submitting image-to-video (${model}, image=${imageId}, ${imageType})`);
|
|
2601
|
+
const body = {
|
|
2602
|
+
prompt: opts.prompt,
|
|
2603
|
+
imageId,
|
|
2604
|
+
imageType,
|
|
2605
|
+
model,
|
|
2606
|
+
resolution: opts.resolution,
|
|
2607
|
+
promptEnhance: !!opts.enhance,
|
|
2608
|
+
frameInterpolation: !!opts.frameInterpolation
|
|
754
2609
|
};
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
2610
|
+
if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
|
|
2611
|
+
if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
|
|
2612
|
+
if (opts.negative) body.negativePrompt = opts.negative;
|
|
2613
|
+
const res = await client.post(
|
|
2614
|
+
"/generations-image-to-video",
|
|
2615
|
+
body,
|
|
2616
|
+
logger
|
|
2617
|
+
);
|
|
2618
|
+
const jobId = res.motionVideoGenerationJob?.generationId;
|
|
2619
|
+
if (!jobId) {
|
|
2620
|
+
logger.error(`No generationId returned by Leonardo: ${JSON.stringify(res)}`);
|
|
2621
|
+
process.exit(1);
|
|
2622
|
+
}
|
|
2623
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
2624
|
+
if (!shouldWait) {
|
|
2625
|
+
console.log(jobId);
|
|
2626
|
+
console.error(`Poll with: multix leonardo status ${jobId}`);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
await pollAndDownload(client, jobId, {
|
|
2630
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
2631
|
+
download: !!opts.download,
|
|
2632
|
+
output: opts.output,
|
|
2633
|
+
thumb: opts.thumb !== false,
|
|
2634
|
+
logger,
|
|
2635
|
+
basename: "leonardo-i2v"
|
|
2636
|
+
});
|
|
2637
|
+
}
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// src/providers/leonardo/commands/me.ts
|
|
2642
|
+
function registerLeonardoMeCommand(parent) {
|
|
2643
|
+
parent.command("me").description("Show authenticated Leonardo user info and remaining API credits").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
2644
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2645
|
+
const client = createLeonardoClient();
|
|
2646
|
+
const info = await client.get("/me", void 0, logger);
|
|
2647
|
+
const u = info.user_details?.[0];
|
|
2648
|
+
if (!u) {
|
|
2649
|
+
logger.warn("No user info returned.");
|
|
2650
|
+
return;
|
|
2651
|
+
}
|
|
2652
|
+
logger.success(`User: ${u.user.username} (${u.user.id})`);
|
|
2653
|
+
console.log(`Tokens: ${u.subscriptionTokens} | API credit: ${u.apiCredit ?? "n/a"}`);
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/providers/leonardo/commands/models.ts
|
|
2658
|
+
function registerLeonardoModelsCommand(parent) {
|
|
2659
|
+
parent.command("models").description("List Leonardo platform image models").option("--limit <n>", "Limit number of results").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
2660
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2661
|
+
const client = createLeonardoClient();
|
|
2662
|
+
const res = await client.get(
|
|
2663
|
+
"/platformModels",
|
|
2664
|
+
void 0,
|
|
2665
|
+
logger
|
|
2666
|
+
);
|
|
2667
|
+
const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
|
|
2668
|
+
const models = limit ? res.custom_models.slice(0, limit) : res.custom_models;
|
|
2669
|
+
for (const m of models) {
|
|
2670
|
+
console.log(`${m.id} ${m.name}`);
|
|
2671
|
+
}
|
|
2672
|
+
logger.debug(`${models.length} model(s)`);
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
// src/providers/leonardo/commands/status.ts
|
|
2677
|
+
init_http_client();
|
|
2678
|
+
import path22 from "path";
|
|
2679
|
+
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
2680
|
+
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
2681
|
+
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
2682
|
+
function collectAssetUrls(gen) {
|
|
2683
|
+
const videos = [];
|
|
2684
|
+
const images = [];
|
|
2685
|
+
for (const img of gen.generated_images ?? []) {
|
|
2686
|
+
if (!img.url) continue;
|
|
2687
|
+
if (VIDEO_EXT_RE.test(img.url)) videos.push(img.url);
|
|
2688
|
+
else if (IMAGE_EXT_RE2.test(img.url)) images.push(img.url);
|
|
2689
|
+
else images.push(img.url);
|
|
2690
|
+
for (const k of VIDEO_KEYS) {
|
|
2691
|
+
const extra = img[k];
|
|
2692
|
+
if (typeof extra === "string" && VIDEO_EXT_RE.test(extra)) videos.push(extra);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
return { videos, images };
|
|
2696
|
+
}
|
|
2697
|
+
function registerLeonardoStatusCommand(parent) {
|
|
2698
|
+
parent.command("status <generationId>").description("Get a Leonardo generation's status and asset URLs").option("--wait", "Poll until COMPLETE or FAILED").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download assets on success (implies --wait)").option("--output <path>", "Output directory or single-file path (with --download)").option("--no-thumb", "Skip downloading thumbnails if the API returns them").option("-v, --verbose", "Verbose logging").action(
|
|
2699
|
+
async (generationId, opts) => {
|
|
2700
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2701
|
+
const client = createLeonardoClient();
|
|
2702
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
2703
|
+
const fetchOnce = () => client.get(`/generations/${generationId}`, void 0, logger);
|
|
2704
|
+
let res;
|
|
2705
|
+
try {
|
|
2706
|
+
res = shouldWait ? await poll({
|
|
2707
|
+
fetch: fetchOnce,
|
|
2708
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE",
|
|
2709
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2710
|
+
intervalMs: 5e3,
|
|
2711
|
+
maxAttempts: Math.max(
|
|
2712
|
+
1,
|
|
2713
|
+
Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
|
|
2714
|
+
),
|
|
2715
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
2716
|
+
}) : await fetchOnce();
|
|
2717
|
+
} catch (e) {
|
|
2718
|
+
if (e instanceof PollTimeoutError) {
|
|
2719
|
+
logger.error(`Timed out waiting for generation ${generationId}`);
|
|
2720
|
+
process.exit(1);
|
|
2721
|
+
}
|
|
2722
|
+
if (e instanceof PollFailedError) {
|
|
2723
|
+
logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
|
|
782
2724
|
process.exit(1);
|
|
783
2725
|
}
|
|
2726
|
+
throw e;
|
|
784
2727
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
2728
|
+
const gen = res.generations_by_pk;
|
|
2729
|
+
if (!gen) {
|
|
2730
|
+
logger.warn("No generation found.");
|
|
2731
|
+
return;
|
|
788
2732
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
2733
|
+
console.error(`status: ${gen.status}`);
|
|
2734
|
+
const { videos, images } = collectAssetUrls(gen);
|
|
2735
|
+
for (const u of [...videos, ...images]) console.log(u);
|
|
2736
|
+
if (!opts.download || gen.status !== "COMPLETE") {
|
|
2737
|
+
if (opts.download) logger.warn(`Cannot download \u2014 status is '${gen.status}'`);
|
|
2738
|
+
return;
|
|
2739
|
+
}
|
|
2740
|
+
const outDir = getOutputDir();
|
|
2741
|
+
let idx = 0;
|
|
2742
|
+
for (const url of videos) {
|
|
2743
|
+
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path22.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
|
|
2744
|
+
await downloadFile(url, dest);
|
|
2745
|
+
logger.success(`Saved ${dest}`);
|
|
2746
|
+
await maybeDownloadThumb(gen, dest, {
|
|
2747
|
+
skip: opts.thumb === false,
|
|
2748
|
+
copyTo: opts.output,
|
|
2749
|
+
logger
|
|
2750
|
+
});
|
|
2751
|
+
idx++;
|
|
2752
|
+
}
|
|
2753
|
+
for (const url of images) {
|
|
2754
|
+
const m = url.match(IMAGE_EXT_RE2);
|
|
2755
|
+
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
2756
|
+
const dest = path22.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
2757
|
+
await downloadFile(url, dest);
|
|
2758
|
+
logger.success(`Saved ${dest}`);
|
|
2759
|
+
idx++;
|
|
793
2760
|
}
|
|
794
|
-
console.log(`
|
|
795
|
-
Generated ${savedFiles.length} image(s):`);
|
|
796
|
-
for (const f of savedFiles) console.log(` ${f}`);
|
|
797
2761
|
}
|
|
798
2762
|
);
|
|
799
2763
|
}
|
|
800
2764
|
|
|
801
|
-
// src/providers/
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
|
|
806
|
-
async (opts) => {
|
|
2765
|
+
// src/providers/leonardo/commands/upscale.ts
|
|
2766
|
+
function registerLeonardoUpscaleCommands(parent) {
|
|
2767
|
+
parent.command("upscale <generatedImageId>").description("Upscale a generated image via Leonardo Universal Upscaler").option("--style <style>", "Upscaler style (GENERAL, CINEMATIC, ...)", "GENERAL").option("--strength <n>", "Creativity strength (0-1)", "0.35").option("--multiplier <n>", "Upscale multiplier (1-2)", "1.5").option("-v, --verbose", "Verbose logging").action(
|
|
2768
|
+
async (generatedImageId, opts) => {
|
|
807
2769
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
808
|
-
const
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
825
|
-
];
|
|
826
|
-
} else {
|
|
827
|
-
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
828
|
-
}
|
|
829
|
-
const resp = await generateContent({
|
|
830
|
-
model,
|
|
831
|
-
contents: [{ parts: contentParts }]
|
|
832
|
-
});
|
|
833
|
-
results.push({
|
|
834
|
-
file: filePath,
|
|
835
|
-
status: "success",
|
|
836
|
-
response: extractText(resp)
|
|
837
|
-
});
|
|
838
|
-
} catch (e) {
|
|
839
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
840
|
-
logger.error(`Failed: ${filePath} \u2014 ${msg}`);
|
|
841
|
-
results.push({ file: filePath, status: "error", error: msg });
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
formatResults(results, opts.format, opts.output);
|
|
845
|
-
const failed = results.filter((r) => r.status === "error").length;
|
|
846
|
-
if (failed > 0) process.exit(1);
|
|
2770
|
+
const client = createLeonardoClient();
|
|
2771
|
+
logger.info(`Submitting upscale for image ${generatedImageId}`);
|
|
2772
|
+
const res = await client.post(
|
|
2773
|
+
"/variations/universal-upscaler",
|
|
2774
|
+
{
|
|
2775
|
+
generatedImageId,
|
|
2776
|
+
upscalerStyle: opts.style,
|
|
2777
|
+
creativityStrength: Number.parseFloat(opts.strength),
|
|
2778
|
+
upscaleMultiplier: Number.parseFloat(opts.multiplier)
|
|
2779
|
+
},
|
|
2780
|
+
logger
|
|
2781
|
+
);
|
|
2782
|
+
const variationId = res.universalUpscaler.id;
|
|
2783
|
+
logger.success(`variationId: ${variationId}`);
|
|
2784
|
+
console.log(variationId);
|
|
2785
|
+
console.error(`Use: multix leonardo variation ${variationId}`);
|
|
847
2786
|
}
|
|
848
2787
|
);
|
|
2788
|
+
parent.command("variation <variationId>").description("Fetch a Leonardo variation/upscale result by ID").option("-v, --verbose", "Verbose logging").action(async (variationId, opts) => {
|
|
2789
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2790
|
+
const client = createLeonardoClient();
|
|
2791
|
+
const res = await client.get(
|
|
2792
|
+
`/variations/${variationId}`,
|
|
2793
|
+
void 0,
|
|
2794
|
+
logger
|
|
2795
|
+
);
|
|
2796
|
+
for (const v of res.generated_image_variation_generic ?? []) {
|
|
2797
|
+
console.log(`${v.status} ${v.url ?? ""}`);
|
|
2798
|
+
}
|
|
2799
|
+
});
|
|
849
2800
|
}
|
|
850
2801
|
|
|
851
|
-
// src/providers/
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
2802
|
+
// src/providers/leonardo/commands/video-models.ts
|
|
2803
|
+
function registerLeonardoVideoModelsCommand(parent) {
|
|
2804
|
+
parent.command("video-models").description("List supported Leonardo video models (static enum from Leonardo docs)").action(() => {
|
|
2805
|
+
for (const m of LEONARDO_VIDEO_MODELS) {
|
|
2806
|
+
console.log(`${m.id} ${m.name} [${m.modes.join(",")}]`);
|
|
2807
|
+
}
|
|
2808
|
+
console.error(`
|
|
2809
|
+
Resolutions: ${LEONARDO_VIDEO_RESOLUTIONS.join(", ")}`);
|
|
2810
|
+
console.error("Source: https://docs.leonardo.ai/llms.txt");
|
|
2811
|
+
});
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
// src/providers/leonardo/commands/index.ts
|
|
2815
|
+
function registerLeonardoCommands(program2) {
|
|
2816
|
+
const leonardo = program2.command("leonardo").description("Leonardo.Ai: image/video generation, upscaling, account info");
|
|
2817
|
+
registerLeonardoGenerateCommand(leonardo);
|
|
2818
|
+
registerLeonardoImageToImageCommand(leonardo);
|
|
2819
|
+
registerLeonardoVideoCommand(leonardo);
|
|
2820
|
+
registerLeonardoImageToVideoCommand(leonardo);
|
|
2821
|
+
registerLeonardoVideoModelsCommand(leonardo);
|
|
2822
|
+
registerLeonardoUpscaleCommands(leonardo);
|
|
2823
|
+
registerLeonardoStatusCommand(leonardo);
|
|
2824
|
+
registerLeonardoModelsCommand(leonardo);
|
|
2825
|
+
registerLeonardoMeCommand(leonardo);
|
|
859
2826
|
}
|
|
860
2827
|
|
|
861
2828
|
// src/providers/minimax/commands/generate-music.ts
|
|
@@ -918,7 +2885,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
|
918
2885
|
if (status === "Failed" || status === "Error") {
|
|
919
2886
|
throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
|
|
920
2887
|
}
|
|
921
|
-
await
|
|
2888
|
+
await sleep4(intervalMs);
|
|
922
2889
|
elapsed += intervalMs;
|
|
923
2890
|
}
|
|
924
2891
|
throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
|
|
@@ -937,14 +2904,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
|
|
|
937
2904
|
await downloadFile(downloadUrl, dest);
|
|
938
2905
|
return dest;
|
|
939
2906
|
}
|
|
940
|
-
function
|
|
2907
|
+
function sleep4(ms) {
|
|
941
2908
|
return new Promise((r) => setTimeout(r, ms));
|
|
942
2909
|
}
|
|
943
2910
|
|
|
944
2911
|
// src/providers/minimax/generators/music.ts
|
|
945
2912
|
init_http_client();
|
|
946
|
-
import
|
|
947
|
-
import
|
|
2913
|
+
import fs19 from "fs";
|
|
2914
|
+
import path23 from "path";
|
|
948
2915
|
async function generateMinimaxMusic(opts) {
|
|
949
2916
|
const {
|
|
950
2917
|
apiKey,
|
|
@@ -989,14 +2956,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
989
2956
|
}
|
|
990
2957
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
991
2958
|
const outDir = getOutputDir();
|
|
992
|
-
const dest =
|
|
2959
|
+
const dest = path23.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
993
2960
|
try {
|
|
994
2961
|
if (audioData.startsWith("http")) {
|
|
995
2962
|
const bytes = await fetchBytes(audioData);
|
|
996
|
-
|
|
2963
|
+
fs19.writeFileSync(dest, bytes);
|
|
997
2964
|
} else {
|
|
998
2965
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
999
|
-
|
|
2966
|
+
fs19.writeFileSync(dest, audioBytes);
|
|
1000
2967
|
}
|
|
1001
2968
|
} catch (e) {
|
|
1002
2969
|
return {
|
|
@@ -1007,8 +2974,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
1007
2974
|
const durSec = durationMs / 1e3;
|
|
1008
2975
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
1009
2976
|
if (output) {
|
|
1010
|
-
|
|
1011
|
-
|
|
2977
|
+
fs19.mkdirSync(path23.dirname(path23.resolve(output)), { recursive: true });
|
|
2978
|
+
fs19.copyFileSync(dest, output);
|
|
1012
2979
|
}
|
|
1013
2980
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
1014
2981
|
}
|
|
@@ -1082,8 +3049,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
1082
3049
|
init_errors();
|
|
1083
3050
|
|
|
1084
3051
|
// src/providers/minimax/generators/speech.ts
|
|
1085
|
-
import
|
|
1086
|
-
import
|
|
3052
|
+
import fs20 from "fs";
|
|
3053
|
+
import path24 from "path";
|
|
1087
3054
|
async function generateMinimaxSpeech(opts) {
|
|
1088
3055
|
const {
|
|
1089
3056
|
apiKey,
|
|
@@ -1130,12 +3097,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
1130
3097
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
1131
3098
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
1132
3099
|
const outDir = getOutputDir();
|
|
1133
|
-
const dest =
|
|
1134
|
-
|
|
3100
|
+
const dest = path24.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
3101
|
+
fs20.writeFileSync(dest, audioBytes);
|
|
1135
3102
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
1136
3103
|
if (output) {
|
|
1137
|
-
|
|
1138
|
-
|
|
3104
|
+
fs20.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
|
|
3105
|
+
fs20.copyFileSync(dest, output);
|
|
1139
3106
|
}
|
|
1140
3107
|
return { status: "success", generatedAudio: dest, model };
|
|
1141
3108
|
}
|
|
@@ -1182,8 +3149,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
1182
3149
|
init_errors();
|
|
1183
3150
|
|
|
1184
3151
|
// src/providers/minimax/generators/video.ts
|
|
1185
|
-
import
|
|
1186
|
-
import
|
|
3152
|
+
import fs21 from "fs";
|
|
3153
|
+
import path25 from "path";
|
|
1187
3154
|
async function generateMinimaxVideo(opts) {
|
|
1188
3155
|
const {
|
|
1189
3156
|
apiKey,
|
|
@@ -1193,6 +3160,7 @@ async function generateMinimaxVideo(opts) {
|
|
|
1193
3160
|
resolution = "1080P",
|
|
1194
3161
|
firstFrame,
|
|
1195
3162
|
output,
|
|
3163
|
+
thumb,
|
|
1196
3164
|
logger
|
|
1197
3165
|
} = opts;
|
|
1198
3166
|
const payload = { prompt, model, duration, resolution };
|
|
@@ -1228,19 +3196,24 @@ async function generateMinimaxVideo(opts) {
|
|
|
1228
3196
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
1229
3197
|
}
|
|
1230
3198
|
const outDir = getOutputDir();
|
|
1231
|
-
const dest =
|
|
3199
|
+
const dest = path25.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
1232
3200
|
try {
|
|
1233
3201
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
1234
3202
|
} catch (e) {
|
|
1235
3203
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
1236
3204
|
}
|
|
1237
3205
|
const elapsed = (Date.now() - start) / 1e3;
|
|
1238
|
-
const stat =
|
|
3206
|
+
const stat = fs21.statSync(dest);
|
|
1239
3207
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
1240
3208
|
if (output) {
|
|
1241
|
-
|
|
1242
|
-
|
|
3209
|
+
fs21.mkdirSync(path25.dirname(path25.resolve(output)), { recursive: true });
|
|
3210
|
+
fs21.copyFileSync(dest, output);
|
|
1243
3211
|
}
|
|
3212
|
+
await maybeDownloadThumb(pollResult, dest, {
|
|
3213
|
+
skip: thumb === false,
|
|
3214
|
+
copyTo: output,
|
|
3215
|
+
logger
|
|
3216
|
+
});
|
|
1244
3217
|
logger?.debug(`Generated in ${elapsed.toFixed(1)}s, size: ${fileSizeMb.toFixed(2)} MB`);
|
|
1245
3218
|
return {
|
|
1246
3219
|
status: "success",
|
|
@@ -1253,7 +3226,7 @@ async function generateMinimaxVideo(opts) {
|
|
|
1253
3226
|
|
|
1254
3227
|
// src/providers/minimax/commands/generate-video.ts
|
|
1255
3228
|
function registerMinimaxGenerateVideoCommand(parent) {
|
|
1256
|
-
parent.command("generate-video").description("Generate video with MiniMax Hailuo models (async)").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", `Model (${[...MINIMAX_VIDEO_MODELS].join("|")})`).option("--duration <n>", `Duration in seconds (${VIDEO_DURATIONS.join("|")})`, "6").option("--resolution <res>", `Resolution (${VIDEO_RESOLUTIONS.join("|")})`, "1080P").option("--first-frame <url>", "First frame image URL").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").action(
|
|
3229
|
+
parent.command("generate-video").description("Generate video with MiniMax Hailuo models (async)").requiredOption("--prompt <text>", "Video generation prompt").option("--model <id>", `Model (${[...MINIMAX_VIDEO_MODELS].join("|")})`).option("--duration <n>", `Duration in seconds (${VIDEO_DURATIONS.join("|")})`, "6").option("--resolution <res>", `Resolution (${VIDEO_RESOLUTIONS.join("|")})`, "1080P").option("--first-frame <url>", "First frame image URL").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
1257
3230
|
async (opts) => {
|
|
1258
3231
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1259
3232
|
const apiKey = requireMinimaxKey();
|
|
@@ -1278,6 +3251,7 @@ function registerMinimaxGenerateVideoCommand(parent) {
|
|
|
1278
3251
|
resolution,
|
|
1279
3252
|
firstFrame: opts.firstFrame,
|
|
1280
3253
|
output: opts.output,
|
|
3254
|
+
thumb: opts.thumb,
|
|
1281
3255
|
logger
|
|
1282
3256
|
});
|
|
1283
3257
|
if (result.status === "error") {
|
|
@@ -1296,8 +3270,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
1296
3270
|
// src/providers/minimax/generators/image.ts
|
|
1297
3271
|
init_errors();
|
|
1298
3272
|
init_http_client();
|
|
1299
|
-
import
|
|
1300
|
-
import
|
|
3273
|
+
import fs22 from "fs";
|
|
3274
|
+
import path26 from "path";
|
|
1301
3275
|
async function generateMinimaxImage(opts) {
|
|
1302
3276
|
const {
|
|
1303
3277
|
apiKey,
|
|
@@ -1335,8 +3309,8 @@ async function generateMinimaxImage(opts) {
|
|
|
1335
3309
|
try {
|
|
1336
3310
|
const bytes = await fetchBytes(url);
|
|
1337
3311
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
1338
|
-
const dest =
|
|
1339
|
-
|
|
3312
|
+
const dest = path26.join(outDir, fname);
|
|
3313
|
+
fs22.writeFileSync(dest, bytes);
|
|
1340
3314
|
savedFiles.push(dest);
|
|
1341
3315
|
logger?.success(`Saved: ${dest}`);
|
|
1342
3316
|
} catch (e) {
|
|
@@ -1347,8 +3321,8 @@ async function generateMinimaxImage(opts) {
|
|
|
1347
3321
|
}
|
|
1348
3322
|
}
|
|
1349
3323
|
if (output && savedFiles[0]) {
|
|
1350
|
-
|
|
1351
|
-
|
|
3324
|
+
fs22.mkdirSync(path26.dirname(path26.resolve(output)), { recursive: true });
|
|
3325
|
+
fs22.copyFileSync(savedFiles[0], output);
|
|
1352
3326
|
}
|
|
1353
3327
|
return { status: "success", generatedImages: savedFiles, model };
|
|
1354
3328
|
}
|
|
@@ -1381,18 +3355,101 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
1381
3355
|
);
|
|
1382
3356
|
}
|
|
1383
3357
|
|
|
3358
|
+
// src/providers/minimax/commands/image-to-image.ts
|
|
3359
|
+
init_errors();
|
|
3360
|
+
init_http_client();
|
|
3361
|
+
import fs23 from "fs";
|
|
3362
|
+
import path27 from "path";
|
|
3363
|
+
function registerMinimaxImageToImageCommand(parent) {
|
|
3364
|
+
parent.command("image-to-image").alias("i2i").description(
|
|
3365
|
+
"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."
|
|
3366
|
+
).requiredOption("--prompt <text>", "Prompt describing the desired NEW scene/composition").requiredOption(
|
|
3367
|
+
"--ref <path>",
|
|
3368
|
+
"Subject reference image (URL or local path) \u2014 character/face reference only, single image"
|
|
3369
|
+
).option("--model <id>", "Model id (default image-01)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9)", "1:1").option("--num-images <n>", "Number of images (1-9)", "1").option("--output <path>", "Copy first image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
3370
|
+
async (opts) => {
|
|
3371
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3372
|
+
const apiKey = requireMinimaxKey();
|
|
3373
|
+
const model = opts.model ?? TASK_DEFAULTS.image;
|
|
3374
|
+
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
|
|
3375
|
+
logger.warn(
|
|
3376
|
+
"MiniMax i2i preserves a CHARACTER reference only. It does not edit the input image. If you need free-form edits, use byteplus, gemini, openrouter, or leonardo image-to-image."
|
|
3377
|
+
);
|
|
3378
|
+
const resolved = await resolveImageInput(opts.ref, { logger });
|
|
3379
|
+
const imageFile = refUrl(resolved);
|
|
3380
|
+
const payload = {
|
|
3381
|
+
model,
|
|
3382
|
+
prompt: opts.prompt,
|
|
3383
|
+
aspect_ratio: opts.aspectRatio,
|
|
3384
|
+
n: numImages,
|
|
3385
|
+
response_format: "url",
|
|
3386
|
+
prompt_optimizer: true,
|
|
3387
|
+
subject_reference: [
|
|
3388
|
+
{
|
|
3389
|
+
type: "character",
|
|
3390
|
+
image_file: imageFile
|
|
3391
|
+
}
|
|
3392
|
+
]
|
|
3393
|
+
};
|
|
3394
|
+
logger.debug(`MiniMax i2i: model=${model}, ref=${opts.ref}, n=${numImages}`);
|
|
3395
|
+
let resp;
|
|
3396
|
+
try {
|
|
3397
|
+
resp = await apiPost("image_generation", payload, apiKey, {
|
|
3398
|
+
logger
|
|
3399
|
+
});
|
|
3400
|
+
} catch (e) {
|
|
3401
|
+
logger.error(e instanceof Error ? e.message : String(e));
|
|
3402
|
+
process.exit(1);
|
|
3403
|
+
}
|
|
3404
|
+
const imageUrls = resp.data?.image_urls ?? [];
|
|
3405
|
+
if (imageUrls.length === 0) {
|
|
3406
|
+
logger.error("No images in response");
|
|
3407
|
+
process.exit(1);
|
|
3408
|
+
}
|
|
3409
|
+
const outDir = getOutputDir();
|
|
3410
|
+
const ts = Date.now();
|
|
3411
|
+
const saved = [];
|
|
3412
|
+
for (let i = 0; i < imageUrls.length; i++) {
|
|
3413
|
+
const url = imageUrls[i];
|
|
3414
|
+
if (!url) continue;
|
|
3415
|
+
try {
|
|
3416
|
+
const bytes = await fetchBytes(url);
|
|
3417
|
+
const dest = path27.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
3418
|
+
fs23.writeFileSync(dest, bytes);
|
|
3419
|
+
saved.push(dest);
|
|
3420
|
+
logger.success(`Saved: ${dest}`);
|
|
3421
|
+
} catch (e) {
|
|
3422
|
+
throw new ProviderError(
|
|
3423
|
+
`Failed to download image: ${e instanceof Error ? e.message : String(e)}`,
|
|
3424
|
+
"minimax"
|
|
3425
|
+
);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
if (opts.output && saved[0]) {
|
|
3429
|
+
fs23.mkdirSync(path27.dirname(path27.resolve(opts.output)), { recursive: true });
|
|
3430
|
+
fs23.copyFileSync(saved[0], opts.output);
|
|
3431
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
3432
|
+
}
|
|
3433
|
+
console.log(`
|
|
3434
|
+
Generated ${saved.length} image(s):`);
|
|
3435
|
+
for (const f of saved) console.log(` ${f}`);
|
|
3436
|
+
}
|
|
3437
|
+
);
|
|
3438
|
+
}
|
|
3439
|
+
|
|
1384
3440
|
// src/providers/minimax/commands/index.ts
|
|
1385
3441
|
function registerMinimaxCommands(program2) {
|
|
1386
3442
|
const minimax = program2.command("minimax").description("MiniMax generation: images, video, speech, and music");
|
|
1387
3443
|
registerMinimaxGenerateCommand(minimax);
|
|
3444
|
+
registerMinimaxImageToImageCommand(minimax);
|
|
1388
3445
|
registerMinimaxGenerateVideoCommand(minimax);
|
|
1389
3446
|
registerMinimaxGenerateSpeechCommand(minimax);
|
|
1390
3447
|
registerMinimaxGenerateMusicCommand(minimax);
|
|
1391
3448
|
}
|
|
1392
3449
|
|
|
1393
3450
|
// src/providers/openrouter/client.ts
|
|
1394
|
-
import
|
|
1395
|
-
import
|
|
3451
|
+
import fs24 from "fs";
|
|
3452
|
+
import path28 from "path";
|
|
1396
3453
|
init_errors();
|
|
1397
3454
|
init_http_client();
|
|
1398
3455
|
var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
@@ -1481,8 +3538,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
1481
3538
|
if (!url) continue;
|
|
1482
3539
|
try {
|
|
1483
3540
|
const bytes = await fetchBytes(url);
|
|
1484
|
-
const dest =
|
|
1485
|
-
|
|
3541
|
+
const dest = path28.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
3542
|
+
fs24.writeFileSync(dest, bytes);
|
|
1486
3543
|
savedFiles.push(dest);
|
|
1487
3544
|
logger?.success(`Saved: ${dest}`);
|
|
1488
3545
|
} catch (e) {
|
|
@@ -1493,8 +3550,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
1493
3550
|
}
|
|
1494
3551
|
}
|
|
1495
3552
|
if (output && savedFiles[0]) {
|
|
1496
|
-
|
|
1497
|
-
|
|
3553
|
+
fs24.mkdirSync(path28.dirname(path28.resolve(output)), { recursive: true });
|
|
3554
|
+
fs24.writeFileSync(output, fs24.readFileSync(savedFiles[0]));
|
|
1498
3555
|
}
|
|
1499
3556
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
1500
3557
|
}
|
|
@@ -1528,10 +3585,328 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
1528
3585
|
);
|
|
1529
3586
|
}
|
|
1530
3587
|
|
|
3588
|
+
// src/providers/openrouter/commands/image-to-image.ts
|
|
3589
|
+
import fs25 from "fs";
|
|
3590
|
+
import path29 from "path";
|
|
3591
|
+
init_errors();
|
|
3592
|
+
init_http_client();
|
|
3593
|
+
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
3594
|
+
var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
|
|
3595
|
+
var HARD_LIMIT_BYTES2 = 8 * 1024 * 1024;
|
|
3596
|
+
function registerOpenRouterImageToImageCommand(parent) {
|
|
3597
|
+
parent.command("image-to-image").alias("i2i").description("Edit/transform images via OpenRouter (default: google/gemini-2.5-flash-image)").requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
3598
|
+
"--ref <path>",
|
|
3599
|
+
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
3600
|
+
collect5,
|
|
3601
|
+
[]
|
|
3602
|
+
).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
3603
|
+
async (opts) => {
|
|
3604
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
3605
|
+
throw new Error("At least one --ref is required for image-to-image.");
|
|
3606
|
+
}
|
|
3607
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3608
|
+
const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_MODEL;
|
|
3609
|
+
const resolvedRefs = [];
|
|
3610
|
+
for (const r of opts.ref) {
|
|
3611
|
+
const resolved = await resolveImageInput(r, {
|
|
3612
|
+
logger,
|
|
3613
|
+
softLimitBytes: SOFT_LIMIT_BYTES2,
|
|
3614
|
+
hardLimitBytes: HARD_LIMIT_BYTES2
|
|
3615
|
+
});
|
|
3616
|
+
resolvedRefs.push(refUrl(resolved));
|
|
3617
|
+
}
|
|
3618
|
+
logger.debug(`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}`);
|
|
3619
|
+
const saved = await runImageToImage({
|
|
3620
|
+
prompt: opts.prompt,
|
|
3621
|
+
model,
|
|
3622
|
+
refs: resolvedRefs,
|
|
3623
|
+
output: opts.output,
|
|
3624
|
+
logger
|
|
3625
|
+
});
|
|
3626
|
+
console.log(`
|
|
3627
|
+
Generated ${saved.length} image(s):`);
|
|
3628
|
+
for (const f of saved) console.log(` ${f}`);
|
|
3629
|
+
console.log(`Model used: ${model}`);
|
|
3630
|
+
}
|
|
3631
|
+
);
|
|
3632
|
+
}
|
|
3633
|
+
function collect5(value, prev) {
|
|
3634
|
+
return [...prev, value];
|
|
3635
|
+
}
|
|
3636
|
+
async function runImageToImage(opts) {
|
|
3637
|
+
const apiKey = requireOpenRouterKey();
|
|
3638
|
+
const headers = {
|
|
3639
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3640
|
+
"Content-Type": "application/json"
|
|
3641
|
+
};
|
|
3642
|
+
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
3643
|
+
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
3644
|
+
if (referer) headers["HTTP-Referer"] = referer;
|
|
3645
|
+
if (title) headers["X-Title"] = title;
|
|
3646
|
+
const content = opts.refs.map((url) => ({
|
|
3647
|
+
type: "image_url",
|
|
3648
|
+
image_url: { url }
|
|
3649
|
+
}));
|
|
3650
|
+
content.push({ type: "text", text: opts.prompt });
|
|
3651
|
+
const payload = {
|
|
3652
|
+
model: opts.model,
|
|
3653
|
+
messages: [{ role: "user", content }],
|
|
3654
|
+
modalities: opts.model.includes("gemini") ? ["image", "text"] : ["image"]
|
|
3655
|
+
};
|
|
3656
|
+
const data = await httpJson({
|
|
3657
|
+
url: OPENROUTER_API_URL,
|
|
3658
|
+
method: "POST",
|
|
3659
|
+
headers,
|
|
3660
|
+
body: payload,
|
|
3661
|
+
timeoutMs: 24e4
|
|
3662
|
+
});
|
|
3663
|
+
const messageImages = data.choices?.[0]?.message?.images ?? [];
|
|
3664
|
+
if (messageImages.length === 0) {
|
|
3665
|
+
throw new ProviderError(
|
|
3666
|
+
`No images in response \u2014 model '${opts.model}' may not support image-to-image`,
|
|
3667
|
+
"openrouter"
|
|
3668
|
+
);
|
|
3669
|
+
}
|
|
3670
|
+
const outDir = getOutputDir();
|
|
3671
|
+
const ts = Date.now();
|
|
3672
|
+
const saved = [];
|
|
3673
|
+
for (let i = 0; i < messageImages.length; i++) {
|
|
3674
|
+
const url = messageImages[i]?.image_url?.url;
|
|
3675
|
+
if (!url) continue;
|
|
3676
|
+
const bytes = await fetchBytes(url);
|
|
3677
|
+
const dest = path29.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
3678
|
+
fs25.writeFileSync(dest, bytes);
|
|
3679
|
+
saved.push(dest);
|
|
3680
|
+
opts.logger.success(`Saved: ${dest}`);
|
|
3681
|
+
}
|
|
3682
|
+
if (opts.output && saved[0]) {
|
|
3683
|
+
fs25.mkdirSync(path29.dirname(path29.resolve(opts.output)), { recursive: true });
|
|
3684
|
+
fs25.copyFileSync(saved[0], opts.output);
|
|
3685
|
+
opts.logger.success(`Copied to: ${opts.output}`);
|
|
3686
|
+
}
|
|
3687
|
+
return saved;
|
|
3688
|
+
}
|
|
3689
|
+
|
|
3690
|
+
// src/providers/openrouter/video-client.ts
|
|
3691
|
+
init_http_client();
|
|
3692
|
+
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
3693
|
+
function authHeaders3(apiKey) {
|
|
3694
|
+
const headers = {
|
|
3695
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3696
|
+
"Content-Type": "application/json"
|
|
3697
|
+
};
|
|
3698
|
+
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
3699
|
+
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
3700
|
+
if (referer) headers["HTTP-Referer"] = referer;
|
|
3701
|
+
if (title) headers["X-Title"] = title;
|
|
3702
|
+
return headers;
|
|
3703
|
+
}
|
|
3704
|
+
async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
3705
|
+
return httpJson({
|
|
3706
|
+
url: OPENROUTER_VIDEOS_URL,
|
|
3707
|
+
method: "POST",
|
|
3708
|
+
headers: authHeaders3(apiKey),
|
|
3709
|
+
body,
|
|
3710
|
+
timeoutMs: 12e4
|
|
3711
|
+
});
|
|
3712
|
+
}
|
|
3713
|
+
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
3714
|
+
return httpJson({
|
|
3715
|
+
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
3716
|
+
headers: authHeaders3(apiKey)
|
|
3717
|
+
});
|
|
3718
|
+
}
|
|
3719
|
+
function videoContentUrl(jobId) {
|
|
3720
|
+
return `${OPENROUTER_VIDEOS_URL}/${jobId}/content`;
|
|
3721
|
+
}
|
|
3722
|
+
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
3723
|
+
return httpJson({
|
|
3724
|
+
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
3725
|
+
headers: authHeaders3(apiKey)
|
|
3726
|
+
});
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
// src/providers/openrouter/commands/video-status.ts
|
|
3730
|
+
import fs26 from "fs";
|
|
3731
|
+
import path30 from "path";
|
|
3732
|
+
async function downloadOpenRouterVideo(opts) {
|
|
3733
|
+
const { jobId, logger, source } = opts;
|
|
3734
|
+
const apiKey = requireOpenRouterKey();
|
|
3735
|
+
const url = videoContentUrl(jobId);
|
|
3736
|
+
const dest = path30.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
3737
|
+
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
3738
|
+
if (!r.ok) {
|
|
3739
|
+
logger.error(`Download HTTP ${r.status}`);
|
|
3740
|
+
process.exit(1);
|
|
3741
|
+
}
|
|
3742
|
+
if (!r.body) {
|
|
3743
|
+
logger.error("Empty response body");
|
|
3744
|
+
process.exit(1);
|
|
3745
|
+
}
|
|
3746
|
+
fs26.mkdirSync(path30.dirname(dest), { recursive: true });
|
|
3747
|
+
const ws = fs26.createWriteStream(dest);
|
|
3748
|
+
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
3749
|
+
await pipeline2(r.body, ws);
|
|
3750
|
+
logger.success(`Saved ${dest}`);
|
|
3751
|
+
if (opts.output) {
|
|
3752
|
+
fs26.mkdirSync(path30.dirname(path30.resolve(opts.output)), { recursive: true });
|
|
3753
|
+
fs26.copyFileSync(dest, opts.output);
|
|
3754
|
+
logger.success(`Copied to ${opts.output}`);
|
|
3755
|
+
}
|
|
3756
|
+
await maybeDownloadThumb(source, dest, {
|
|
3757
|
+
skip: opts.thumb === false,
|
|
3758
|
+
copyTo: opts.output,
|
|
3759
|
+
logger
|
|
3760
|
+
});
|
|
3761
|
+
return dest;
|
|
3762
|
+
}
|
|
3763
|
+
function registerOpenRouterVideoStatusCommand(parent) {
|
|
3764
|
+
parent.command("video-status <jobId>").description("Poll an OpenRouter video generation job").option("--wait", "Poll until status is 'completed' or 'failed'").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "If completed, download the video (implies --wait)").option("--output <path>", "Copy downloaded video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
3765
|
+
async (jobId, opts) => {
|
|
3766
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3767
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
3768
|
+
let res;
|
|
3769
|
+
try {
|
|
3770
|
+
res = shouldWait ? await poll({
|
|
3771
|
+
fetch: () => pollVideoJob(jobId),
|
|
3772
|
+
done: (v) => v.status === "completed",
|
|
3773
|
+
failed: (v) => v.status === "failed",
|
|
3774
|
+
intervalMs: 5e3,
|
|
3775
|
+
maxAttempts: Math.max(
|
|
3776
|
+
1,
|
|
3777
|
+
Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
|
|
3778
|
+
),
|
|
3779
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
3780
|
+
}) : await pollVideoJob(jobId);
|
|
3781
|
+
} catch (e) {
|
|
3782
|
+
if (e instanceof PollTimeoutError) {
|
|
3783
|
+
logger.error(`Timed out waiting for job ${jobId}`);
|
|
3784
|
+
process.exit(1);
|
|
3785
|
+
}
|
|
3786
|
+
if (e instanceof PollFailedError) {
|
|
3787
|
+
logger.error(`Job failed: ${JSON.stringify(e.value)}`);
|
|
3788
|
+
process.exit(1);
|
|
3789
|
+
}
|
|
3790
|
+
throw e;
|
|
3791
|
+
}
|
|
3792
|
+
console.error(`status: ${res.status}`);
|
|
3793
|
+
for (const u of res.unsigned_urls ?? []) console.log(u);
|
|
3794
|
+
if (opts.download && res.status === "completed") {
|
|
3795
|
+
await downloadOpenRouterVideo({
|
|
3796
|
+
jobId,
|
|
3797
|
+
output: opts.output,
|
|
3798
|
+
thumb: opts.thumb,
|
|
3799
|
+
logger,
|
|
3800
|
+
source: res
|
|
3801
|
+
});
|
|
3802
|
+
} else if (opts.download) {
|
|
3803
|
+
logger.warn(`Cannot download \u2014 status is '${res.status}', not 'completed'`);
|
|
3804
|
+
}
|
|
3805
|
+
if (res.usage?.cost !== void 0) {
|
|
3806
|
+
logger.debug(`cost: ${res.usage.cost}${res.usage.is_byok ? " (BYOK)" : ""}`);
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
);
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3812
|
+
// src/providers/openrouter/commands/image-to-video.ts
|
|
3813
|
+
function registerOpenRouterImageToVideoCommand(parent) {
|
|
3814
|
+
parent.command("image-to-video").alias("i2v").description("Submit an image-to-video job to OpenRouter (async, returns job id)").requiredOption("--prompt <text>", "Motion/scene prompt").requiredOption("--image-url <url>", "First-frame image URL (https)").option("--last-frame-url <url>", "Optional last-frame image URL").option("--model <id>", "OpenRouter video model id (e.g. google/veo-3.1)").option("--resolution <r>", "Resolution hint (e.g. 720p, 1080p)").option("--aspect-ratio <r>", "Aspect ratio hint (e.g. 16:9, 9:16)").option("--duration <n>", "Duration seconds").option("--seed <n>", "Fixed seed").option("--wait", "Poll until status is 'completed' or 'failed'").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download the generated video on success (implies --wait)").option("--output <path>", "Copy downloaded video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
3815
|
+
async (opts) => {
|
|
3816
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3817
|
+
const model = opts.model ?? resolveKey("OPENROUTER_VIDEO_MODEL") ?? "google/veo-3.1";
|
|
3818
|
+
const frame_images = [
|
|
3819
|
+
{
|
|
3820
|
+
type: "image_url",
|
|
3821
|
+
image_url: { url: opts.imageUrl },
|
|
3822
|
+
frame_type: "first_frame"
|
|
3823
|
+
}
|
|
3824
|
+
];
|
|
3825
|
+
if (opts.lastFrameUrl) {
|
|
3826
|
+
frame_images.push({
|
|
3827
|
+
type: "image_url",
|
|
3828
|
+
image_url: { url: opts.lastFrameUrl },
|
|
3829
|
+
frame_type: "last_frame"
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
const body = {
|
|
3833
|
+
model,
|
|
3834
|
+
prompt: opts.prompt,
|
|
3835
|
+
frame_images
|
|
3836
|
+
};
|
|
3837
|
+
if (opts.resolution) body.resolution = opts.resolution;
|
|
3838
|
+
if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
|
|
3839
|
+
if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
|
|
3840
|
+
if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
|
|
3841
|
+
logger.info(`Submitting image-to-video job (model=${model})`);
|
|
3842
|
+
const res = await submitVideoJob(body);
|
|
3843
|
+
logger.success(`Job ${res.id} (status: ${res.status})`);
|
|
3844
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
3845
|
+
if (!shouldWait) {
|
|
3846
|
+
console.log(res.id);
|
|
3847
|
+
console.error(`Poll with: multix openrouter video-status ${res.id}`);
|
|
3848
|
+
return;
|
|
3849
|
+
}
|
|
3850
|
+
let final;
|
|
3851
|
+
try {
|
|
3852
|
+
final = await poll({
|
|
3853
|
+
fetch: () => pollVideoJob(res.id),
|
|
3854
|
+
done: (v) => v.status === "completed",
|
|
3855
|
+
failed: (v) => v.status === "failed",
|
|
3856
|
+
intervalMs: 5e3,
|
|
3857
|
+
maxAttempts: Math.max(
|
|
3858
|
+
1,
|
|
3859
|
+
Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
|
|
3860
|
+
),
|
|
3861
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
3862
|
+
});
|
|
3863
|
+
} catch (e) {
|
|
3864
|
+
if (e instanceof PollTimeoutError) {
|
|
3865
|
+
logger.error(`Timed out \u2014 multix openrouter video-status ${res.id}`);
|
|
3866
|
+
process.exit(1);
|
|
3867
|
+
}
|
|
3868
|
+
if (e instanceof PollFailedError) {
|
|
3869
|
+
logger.error(`Job failed: ${JSON.stringify(e.value)}`);
|
|
3870
|
+
process.exit(1);
|
|
3871
|
+
}
|
|
3872
|
+
throw e;
|
|
3873
|
+
}
|
|
3874
|
+
console.error(`status: ${final.status}`);
|
|
3875
|
+
for (const u of final.unsigned_urls ?? []) console.log(u);
|
|
3876
|
+
if (opts.download) {
|
|
3877
|
+
await downloadOpenRouterVideo({
|
|
3878
|
+
jobId: res.id,
|
|
3879
|
+
output: opts.output,
|
|
3880
|
+
thumb: opts.thumb,
|
|
3881
|
+
logger,
|
|
3882
|
+
source: final
|
|
3883
|
+
});
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
);
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
// src/providers/openrouter/commands/video-models.ts
|
|
3890
|
+
function registerOpenRouterVideoModelsCommand(parent) {
|
|
3891
|
+
parent.command("video-models").description("List available OpenRouter video models").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
3892
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3893
|
+
const res = await listVideoModels();
|
|
3894
|
+
const list = res.data ?? res.models ?? [];
|
|
3895
|
+
for (const m of list) {
|
|
3896
|
+
console.log(`${m.id} ${m.name ?? ""}`);
|
|
3897
|
+
}
|
|
3898
|
+
logger.debug(`${list.length} model(s)`);
|
|
3899
|
+
});
|
|
3900
|
+
}
|
|
3901
|
+
|
|
1531
3902
|
// src/providers/openrouter/commands/index.ts
|
|
1532
3903
|
function registerOpenRouterCommands(program2) {
|
|
1533
|
-
const openrouter = program2.command("openrouter").description("OpenRouter image
|
|
3904
|
+
const openrouter = program2.command("openrouter").description("OpenRouter image + video generation");
|
|
1534
3905
|
registerOpenRouterGenerateCommand(openrouter);
|
|
3906
|
+
registerOpenRouterImageToImageCommand(openrouter);
|
|
3907
|
+
registerOpenRouterImageToVideoCommand(openrouter);
|
|
3908
|
+
registerOpenRouterVideoStatusCommand(openrouter);
|
|
3909
|
+
registerOpenRouterVideoModelsCommand(openrouter);
|
|
1535
3910
|
}
|
|
1536
3911
|
|
|
1537
3912
|
// src/commands/check-helpers/binary-check.ts
|
|
@@ -1585,6 +3960,8 @@ To configure multix, set at least one provider API key:
|
|
|
1585
3960
|
Gemini / AI Studio : https://aistudio.google.com/apikey
|
|
1586
3961
|
OpenRouter : https://openrouter.ai/settings/keys
|
|
1587
3962
|
MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
|
|
3963
|
+
Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
|
|
3964
|
+
BytePlus : https://console.byteplus.com/auth/api-keys
|
|
1588
3965
|
|
|
1589
3966
|
Option A \u2014 user global config (recommended):
|
|
1590
3967
|
echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
|
|
@@ -1604,6 +3981,48 @@ var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands u
|
|
|
1604
3981
|
var MAGICK_HINT = "ImageMagick (magick) not found \u2014 image optimization unavailable.\n Linux: sudo apt-get install imagemagick\n macOS: brew install imagemagick\n Windows: https://imagemagick.org/script/download.php";
|
|
1605
3982
|
|
|
1606
3983
|
// src/commands/check.ts
|
|
3984
|
+
var PROVIDERS = [
|
|
3985
|
+
{
|
|
3986
|
+
name: "Gemini",
|
|
3987
|
+
envPrimary: "GEMINI_API_KEY",
|
|
3988
|
+
optional: false,
|
|
3989
|
+
link: "https://aistudio.google.com/apikey"
|
|
3990
|
+
},
|
|
3991
|
+
{
|
|
3992
|
+
name: "OpenRouter",
|
|
3993
|
+
envPrimary: "OPENROUTER_API_KEY",
|
|
3994
|
+
optional: true,
|
|
3995
|
+
link: "https://openrouter.ai/settings/keys"
|
|
3996
|
+
},
|
|
3997
|
+
{
|
|
3998
|
+
name: "MiniMax",
|
|
3999
|
+
envPrimary: "MINIMAX_API_KEY",
|
|
4000
|
+
optional: true,
|
|
4001
|
+
link: "https://platform.minimax.io/user-center/basic-information/interface-key"
|
|
4002
|
+
},
|
|
4003
|
+
{
|
|
4004
|
+
name: "Leonardo.Ai",
|
|
4005
|
+
envPrimary: "LEONARDO_API_KEY",
|
|
4006
|
+
optional: true,
|
|
4007
|
+
link: "https://app.leonardo.ai/settings/api-keys"
|
|
4008
|
+
},
|
|
4009
|
+
{
|
|
4010
|
+
name: "BytePlus",
|
|
4011
|
+
envPrimary: "BYTEPLUS_API_KEY",
|
|
4012
|
+
envFallback: "ARK_API_KEY",
|
|
4013
|
+
optional: true,
|
|
4014
|
+
link: "https://console.byteplus.com/auth/api-keys"
|
|
4015
|
+
}
|
|
4016
|
+
];
|
|
4017
|
+
function resolveProviderKey(p) {
|
|
4018
|
+
const primary = resolveKey(p.envPrimary);
|
|
4019
|
+
if (primary) return { key: primary, envUsed: p.envPrimary };
|
|
4020
|
+
if (p.envFallback) {
|
|
4021
|
+
const fb = resolveKey(p.envFallback);
|
|
4022
|
+
if (fb) return { key: fb, envUsed: p.envFallback };
|
|
4023
|
+
}
|
|
4024
|
+
return {};
|
|
4025
|
+
}
|
|
1607
4026
|
function registerCheckCommand(program2) {
|
|
1608
4027
|
program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
|
|
1609
4028
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -1624,29 +4043,28 @@ function registerCheckCommand(program2) {
|
|
|
1624
4043
|
if (opts.verbose) console.log(` ${MAGICK_HINT}`);
|
|
1625
4044
|
}
|
|
1626
4045
|
logger.header("API Keys");
|
|
1627
|
-
|
|
1628
|
-
const
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
} else {
|
|
1643
|
-
logger.info("MINIMAX_API_KEY not set (optional)");
|
|
4046
|
+
let anyKey = false;
|
|
4047
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4048
|
+
for (const p of PROVIDERS) {
|
|
4049
|
+
const r = resolveProviderKey(p);
|
|
4050
|
+
resolved.set(p.name, r);
|
|
4051
|
+
if (r.key) anyKey = true;
|
|
4052
|
+
const envLabel = r.envUsed ?? p.envPrimary;
|
|
4053
|
+
if (r.key) {
|
|
4054
|
+
logger.success(`${envLabel} found \u2014 ${redact(r.key)}`);
|
|
4055
|
+
} else if (p.optional) {
|
|
4056
|
+
const fbNote = p.envFallback ? ` (or ${p.envFallback})` : "";
|
|
4057
|
+
logger.info(`${p.envPrimary}${fbNote} not set (optional)`);
|
|
4058
|
+
} else {
|
|
4059
|
+
logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
|
|
4060
|
+
}
|
|
1644
4061
|
}
|
|
1645
|
-
if (!
|
|
4062
|
+
if (!anyKey) {
|
|
1646
4063
|
logger.header("No Provider Keys Configured");
|
|
1647
4064
|
console.log(SETUP_HINTS);
|
|
1648
4065
|
process.exit(1);
|
|
1649
4066
|
}
|
|
4067
|
+
const geminiKey = resolved.get("Gemini")?.key;
|
|
1650
4068
|
if (geminiKey) {
|
|
1651
4069
|
logger.header("Gemini Connectivity");
|
|
1652
4070
|
logger.info("Pinging Gemini models endpoint...");
|
|
@@ -1668,7 +4086,7 @@ function registerCheckCommand(program2) {
|
|
|
1668
4086
|
" Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
|
|
1669
4087
|
);
|
|
1670
4088
|
} else {
|
|
1671
|
-
console.log(" Image generation via OpenRouter/MiniMax available");
|
|
4089
|
+
console.log(" Image generation via OpenRouter/MiniMax/Leonardo/BytePlus available");
|
|
1672
4090
|
console.log(" Add GEMINI_API_KEY for analysis, transcription, and doc conversion");
|
|
1673
4091
|
}
|
|
1674
4092
|
} else {
|
|
@@ -1680,8 +4098,8 @@ function registerCheckCommand(program2) {
|
|
|
1680
4098
|
|
|
1681
4099
|
// src/commands/doc/convert.ts
|
|
1682
4100
|
init_errors();
|
|
1683
|
-
import
|
|
1684
|
-
import
|
|
4101
|
+
import fs27 from "fs";
|
|
4102
|
+
import path31 from "path";
|
|
1685
4103
|
|
|
1686
4104
|
// src/commands/doc/default-prompt.ts
|
|
1687
4105
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -1702,18 +4120,18 @@ async function convertDocuments(opts) {
|
|
|
1702
4120
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
1703
4121
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
1704
4122
|
for (const f of inputs) {
|
|
1705
|
-
if (!
|
|
4123
|
+
if (!fs27.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
1706
4124
|
}
|
|
1707
4125
|
let outputPath;
|
|
1708
4126
|
if (output) {
|
|
1709
|
-
outputPath =
|
|
4127
|
+
outputPath = path31.resolve(output);
|
|
1710
4128
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
1711
|
-
const base =
|
|
1712
|
-
outputPath =
|
|
4129
|
+
const base = path31.basename(inputs[0], path31.extname(inputs[0]));
|
|
4130
|
+
outputPath = path31.join(getOutputDir(), `${base}-extraction.md`);
|
|
1713
4131
|
} else {
|
|
1714
|
-
outputPath =
|
|
4132
|
+
outputPath = path31.join(getOutputDir(), "document-extraction.md");
|
|
1715
4133
|
}
|
|
1716
|
-
|
|
4134
|
+
fs27.mkdirSync(path31.dirname(outputPath), { recursive: true });
|
|
1717
4135
|
const results = [];
|
|
1718
4136
|
for (let i = 0; i < inputs.length; i++) {
|
|
1719
4137
|
const filePath = inputs[i];
|
|
@@ -1745,7 +4163,7 @@ async function convertDocuments(opts) {
|
|
|
1745
4163
|
if (attempt < 2) {
|
|
1746
4164
|
const wait = 2 ** attempt * 1e3;
|
|
1747
4165
|
logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
|
|
1748
|
-
await
|
|
4166
|
+
await sleep5(wait);
|
|
1749
4167
|
}
|
|
1750
4168
|
}
|
|
1751
4169
|
}
|
|
@@ -1764,7 +4182,7 @@ async function convertDocuments(opts) {
|
|
|
1764
4182
|
`);
|
|
1765
4183
|
for (const r of results) {
|
|
1766
4184
|
lines.push(`
|
|
1767
|
-
## ${
|
|
4185
|
+
## ${path31.basename(r.file)}
|
|
1768
4186
|
`);
|
|
1769
4187
|
if (r.status === "success" && r.markdown) {
|
|
1770
4188
|
lines.push(`${r.markdown}
|
|
@@ -1777,7 +4195,7 @@ async function convertDocuments(opts) {
|
|
|
1777
4195
|
}
|
|
1778
4196
|
lines.push("\n---\n");
|
|
1779
4197
|
}
|
|
1780
|
-
|
|
4198
|
+
fs27.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
1781
4199
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
1782
4200
|
console.log(`
|
|
1783
4201
|
${"=".repeat(50)}`);
|
|
@@ -1793,7 +4211,7 @@ ${"=".repeat(50)}`);
|
|
|
1793
4211
|
const failed = results.filter((r) => r.status === "error").length;
|
|
1794
4212
|
if (failed > 0) process.exit(1);
|
|
1795
4213
|
}
|
|
1796
|
-
function
|
|
4214
|
+
function sleep5(ms) {
|
|
1797
4215
|
return new Promise((r) => setTimeout(r, ms));
|
|
1798
4216
|
}
|
|
1799
4217
|
|
|
@@ -1823,12 +4241,12 @@ function registerDocCommands(program2) {
|
|
|
1823
4241
|
|
|
1824
4242
|
// src/commands/media/index.ts
|
|
1825
4243
|
init_errors();
|
|
1826
|
-
import
|
|
1827
|
-
import
|
|
4244
|
+
import fs30 from "fs";
|
|
4245
|
+
import path34 from "path";
|
|
1828
4246
|
|
|
1829
4247
|
// src/commands/media/batch.ts
|
|
1830
|
-
import
|
|
1831
|
-
import
|
|
4248
|
+
import fs28 from "fs";
|
|
4249
|
+
import path32 from "path";
|
|
1832
4250
|
|
|
1833
4251
|
// src/commands/media/detect-type.ts
|
|
1834
4252
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -2024,10 +4442,10 @@ async function batchOptimize(opts) {
|
|
|
2024
4442
|
verbose,
|
|
2025
4443
|
logger
|
|
2026
4444
|
} = opts;
|
|
2027
|
-
|
|
2028
|
-
const entries =
|
|
4445
|
+
fs28.mkdirSync(outputDir, { recursive: true });
|
|
4446
|
+
const entries = fs28.readdirSync(inputDir);
|
|
2029
4447
|
const mediaFiles = entries.filter((f) => {
|
|
2030
|
-
const ext =
|
|
4448
|
+
const ext = path32.extname(f).toLowerCase();
|
|
2031
4449
|
return ALL_EXTS.has(ext);
|
|
2032
4450
|
});
|
|
2033
4451
|
if (mediaFiles.length === 0) {
|
|
@@ -2038,9 +4456,9 @@ async function batchOptimize(opts) {
|
|
|
2038
4456
|
let succeeded = 0;
|
|
2039
4457
|
const errors = [];
|
|
2040
4458
|
for (const file of mediaFiles) {
|
|
2041
|
-
const inputPath =
|
|
2042
|
-
const outputPath =
|
|
2043
|
-
const ext =
|
|
4459
|
+
const inputPath = path32.join(inputDir, file);
|
|
4460
|
+
const outputPath = path32.join(outputDir, file);
|
|
4461
|
+
const ext = path32.extname(file).toLowerCase();
|
|
2044
4462
|
const kind = extToKind(ext);
|
|
2045
4463
|
logger?.debug(`Processing: ${file}`);
|
|
2046
4464
|
try {
|
|
@@ -2077,8 +4495,8 @@ async function batchOptimize(opts) {
|
|
|
2077
4495
|
|
|
2078
4496
|
// src/commands/media/split-video.ts
|
|
2079
4497
|
init_errors();
|
|
2080
|
-
import
|
|
2081
|
-
import
|
|
4498
|
+
import fs29 from "fs";
|
|
4499
|
+
import path33 from "path";
|
|
2082
4500
|
async function splitVideo(opts) {
|
|
2083
4501
|
await assertFfmpeg();
|
|
2084
4502
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -2089,10 +4507,10 @@ async function splitVideo(opts) {
|
|
|
2089
4507
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
2090
4508
|
return [input];
|
|
2091
4509
|
}
|
|
2092
|
-
|
|
2093
|
-
const ext =
|
|
2094
|
-
const baseName =
|
|
2095
|
-
const outputPattern =
|
|
4510
|
+
fs29.mkdirSync(outputDir, { recursive: true });
|
|
4511
|
+
const ext = path33.extname(input) || ".mp4";
|
|
4512
|
+
const baseName = path33.basename(input, ext);
|
|
4513
|
+
const outputPattern = path33.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
2096
4514
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
2097
4515
|
const args = [
|
|
2098
4516
|
"-i",
|
|
@@ -2111,8 +4529,8 @@ async function splitVideo(opts) {
|
|
|
2111
4529
|
await runFfmpeg(args, { verbose, logger });
|
|
2112
4530
|
const files = [];
|
|
2113
4531
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
2114
|
-
const fname =
|
|
2115
|
-
if (
|
|
4532
|
+
const fname = path33.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
4533
|
+
if (fs29.existsSync(fname)) files.push(fname);
|
|
2116
4534
|
}
|
|
2117
4535
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
2118
4536
|
return files;
|
|
@@ -2126,9 +4544,9 @@ function registerMediaCommands(program2) {
|
|
|
2126
4544
|
media.command("optimize").description("Optimize a single media file (video, audio, or image)").requiredOption("--input <file>", "Input media file").requiredOption("--output <file>", "Output file path").option("--target-size <MB>", "Target file size in MB").option("--quality <n>", "Quality: video CRF (0-51) or image (1-100)", "85").option("--max-width <px>", "Max image/video width in pixels", "1920").option("--bitrate <rate>", "Audio bitrate (e.g. 64k)", "64k").option("--resolution <WxH>", "Force video resolution (e.g. 1920x1080)").option("-v, --verbose", "Verbose logging").action(
|
|
2127
4545
|
async (opts) => {
|
|
2128
4546
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2129
|
-
if (!
|
|
2130
|
-
|
|
2131
|
-
const ext =
|
|
4547
|
+
if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
4548
|
+
fs30.mkdirSync(path34.dirname(path34.resolve(opts.output)), { recursive: true });
|
|
4549
|
+
const ext = path34.extname(opts.input).toLowerCase();
|
|
2132
4550
|
const kind = extToKind(ext);
|
|
2133
4551
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
2134
4552
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -2168,7 +4586,7 @@ function registerMediaCommands(program2) {
|
|
|
2168
4586
|
media.command("split").description("Split a long video into fixed-duration chunks").requiredOption("--input <file>", "Input video file").option("--output-dir <dir>", "Output directory for chunks", "./chunks").option("--chunk-duration <sec>", "Chunk duration in seconds", "3600").option("-v, --verbose", "Verbose logging").action(
|
|
2169
4587
|
async (opts) => {
|
|
2170
4588
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2171
|
-
if (!
|
|
4589
|
+
if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
2172
4590
|
const chunks = await splitVideo({
|
|
2173
4591
|
input: opts.input,
|
|
2174
4592
|
outputDir: opts.outputDir,
|
|
@@ -2184,7 +4602,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
2184
4602
|
media.command("batch").description("Batch optimize all media files in a directory").requiredOption("--input-dir <dir>", "Input directory").requiredOption("--output-dir <dir>", "Output directory").option("--quality <n>", "Quality setting", "85").option("--max-width <px>", "Max image/video width", "1920").option("--bitrate <rate>", "Audio bitrate", "64k").option("-v, --verbose", "Verbose logging").action(
|
|
2185
4603
|
async (opts) => {
|
|
2186
4604
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2187
|
-
if (!
|
|
4605
|
+
if (!fs30.existsSync(opts.inputDir))
|
|
2188
4606
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
2189
4607
|
const result = await batchOptimize({
|
|
2190
4608
|
inputDir: opts.inputDir,
|
|
@@ -2211,6 +4629,8 @@ function registerCommands(program2) {
|
|
|
2211
4629
|
registerGeminiCommands(program2);
|
|
2212
4630
|
registerMinimaxCommands(program2);
|
|
2213
4631
|
registerOpenRouterCommands(program2);
|
|
4632
|
+
registerLeonardoCommands(program2);
|
|
4633
|
+
registerBytePlusCommands(program2);
|
|
2214
4634
|
registerMediaCommands(program2);
|
|
2215
4635
|
registerDocCommands(program2);
|
|
2216
4636
|
}
|