@mrgoonie/multix 0.0.2 → 0.0.6
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 +29 -0
- package/README.md +154 -2
- package/dist/cli.js +3161 -602
- package/package.json +11 -3
package/dist/cli.js
CHANGED
|
@@ -67,8 +67,8 @@ __export(http_client_exports, {
|
|
|
67
67
|
fetchBytes: () => fetchBytes,
|
|
68
68
|
httpJson: () => httpJson
|
|
69
69
|
});
|
|
70
|
-
import
|
|
71
|
-
import
|
|
70
|
+
import fs4 from "fs";
|
|
71
|
+
import path4 from "path";
|
|
72
72
|
import { pipeline } from "stream/promises";
|
|
73
73
|
async function httpJson(opts) {
|
|
74
74
|
const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
@@ -103,7 +103,7 @@ async function httpJson(opts) {
|
|
|
103
103
|
return response.json();
|
|
104
104
|
}
|
|
105
105
|
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
106
|
-
|
|
106
|
+
fs4.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
107
107
|
let response;
|
|
108
108
|
try {
|
|
109
109
|
response = await globalThis.fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -117,7 +117,7 @@ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
117
117
|
if (!response.body) {
|
|
118
118
|
throw new HttpError(0, "Response body is null", url);
|
|
119
119
|
}
|
|
120
|
-
const fileStream =
|
|
120
|
+
const fileStream = fs4.createWriteStream(dest);
|
|
121
121
|
await pipeline(response.body, fileStream);
|
|
122
122
|
}
|
|
123
123
|
async function fetchBytes(urlOrDataUri) {
|
|
@@ -155,9 +155,89 @@ var init_http_client = __esm({
|
|
|
155
155
|
import { createRequire } from "module";
|
|
156
156
|
import { Command } from "commander";
|
|
157
157
|
|
|
158
|
-
// src/providers/
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
// src/providers/byteplus/commands/generate-3d.ts
|
|
159
|
+
import path5 from "path";
|
|
160
|
+
|
|
161
|
+
// src/core/image-input.ts
|
|
162
|
+
import fs from "fs/promises";
|
|
163
|
+
import path from "path";
|
|
164
|
+
var IMAGE_MIME_BY_EXT = {
|
|
165
|
+
".jpg": "image/jpeg",
|
|
166
|
+
".jpeg": "image/jpeg",
|
|
167
|
+
".png": "image/png",
|
|
168
|
+
".webp": "image/webp",
|
|
169
|
+
".gif": "image/gif"
|
|
170
|
+
};
|
|
171
|
+
var VIDEO_MIME_BY_EXT = {
|
|
172
|
+
".mp4": "video/mp4",
|
|
173
|
+
".webm": "video/webm",
|
|
174
|
+
".mov": "video/quicktime",
|
|
175
|
+
".m4v": "video/x-m4v"
|
|
176
|
+
};
|
|
177
|
+
var AUDIO_MIME_BY_EXT = {
|
|
178
|
+
".mp3": "audio/mpeg",
|
|
179
|
+
".wav": "audio/wav",
|
|
180
|
+
".m4a": "audio/mp4",
|
|
181
|
+
".flac": "audio/flac",
|
|
182
|
+
".ogg": "audio/ogg"
|
|
183
|
+
};
|
|
184
|
+
var DEFAULT_SOFT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
185
|
+
var DEFAULT_HARD_LIMIT_BYTES = 25 * 1024 * 1024;
|
|
186
|
+
function isHttpUrl(input) {
|
|
187
|
+
return /^https?:\/\//i.test(input);
|
|
188
|
+
}
|
|
189
|
+
async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
|
|
190
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
191
|
+
const mime = mimeTable[ext];
|
|
192
|
+
if (!mime) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`Unsupported ${kindLabel} extension '${ext}'. Allowed: ${Object.keys(mimeTable).join(", ")}`
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
|
|
198
|
+
const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
|
|
199
|
+
const buf = await fs.readFile(filePath);
|
|
200
|
+
if (buf.byteLength > hard) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`${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.`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (buf.byteLength > soft) {
|
|
206
|
+
opts.logger?.warn(
|
|
207
|
+
`${kindLabel} payload ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB is large; consider URL input.`
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const dataUrl = `data:${mime};base64,${buf.toString("base64")}`;
|
|
211
|
+
return { dataUrl, mime, bytes: buf.byteLength };
|
|
212
|
+
}
|
|
213
|
+
async function resolveImageInput(input, opts = {}) {
|
|
214
|
+
const o = normalizeOpts(opts);
|
|
215
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
216
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, IMAGE_MIME_BY_EXT, "image", o);
|
|
217
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
218
|
+
}
|
|
219
|
+
async function resolveVideoInput(input, opts = {}) {
|
|
220
|
+
const o = normalizeOpts(opts);
|
|
221
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
222
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, VIDEO_MIME_BY_EXT, "video", o);
|
|
223
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
224
|
+
}
|
|
225
|
+
async function resolveAudioInput(input, opts = {}) {
|
|
226
|
+
const o = normalizeOpts(opts);
|
|
227
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
228
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, AUDIO_MIME_BY_EXT, "audio", o);
|
|
229
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
230
|
+
}
|
|
231
|
+
function refUrl(r) {
|
|
232
|
+
return r.kind === "url" ? r.url : r.dataUrl;
|
|
233
|
+
}
|
|
234
|
+
function normalizeOpts(opts) {
|
|
235
|
+
if (!opts) return {};
|
|
236
|
+
if (typeof opts.info === "function" && typeof opts.warn === "function") {
|
|
237
|
+
return { logger: opts };
|
|
238
|
+
}
|
|
239
|
+
return opts;
|
|
240
|
+
}
|
|
161
241
|
|
|
162
242
|
// src/core/logger.ts
|
|
163
243
|
var ANSI = {
|
|
@@ -204,26 +284,69 @@ ${line}
|
|
|
204
284
|
}
|
|
205
285
|
var defaultLogger = createLogger({ verbose: false });
|
|
206
286
|
|
|
207
|
-
// src/
|
|
208
|
-
import
|
|
209
|
-
import
|
|
287
|
+
// src/core/output-dir.ts
|
|
288
|
+
import fs2 from "fs";
|
|
289
|
+
import path2 from "path";
|
|
290
|
+
var cachedOutputDir;
|
|
291
|
+
function getOutputDir() {
|
|
292
|
+
if (cachedOutputDir) return cachedOutputDir;
|
|
293
|
+
const dir = process.env.MULTIX_OUTPUT_DIR ? path2.resolve(process.env.MULTIX_OUTPUT_DIR) : path2.resolve(process.cwd(), "multix-output");
|
|
294
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
295
|
+
cachedOutputDir = dir;
|
|
296
|
+
return dir;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// src/providers/leonardo/poll.ts
|
|
300
|
+
var PollTimeoutError = class extends Error {
|
|
301
|
+
constructor() {
|
|
302
|
+
super("Polling timed out before the operation completed.");
|
|
303
|
+
this.name = "PollTimeoutError";
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
var PollFailedError = class extends Error {
|
|
307
|
+
constructor(value) {
|
|
308
|
+
super("Operation failed.");
|
|
309
|
+
this.value = value;
|
|
310
|
+
this.name = "PollFailedError";
|
|
311
|
+
}
|
|
312
|
+
value;
|
|
313
|
+
};
|
|
314
|
+
async function poll(opts) {
|
|
315
|
+
const interval = opts.intervalMs ?? 4e3;
|
|
316
|
+
const max = opts.maxAttempts ?? 120;
|
|
317
|
+
for (let attempt = 1; attempt <= max; attempt++) {
|
|
318
|
+
if (opts.signal?.aborted) throw new Error("Aborted");
|
|
319
|
+
const value = await opts.fetch();
|
|
320
|
+
opts.onTick?.(attempt, value);
|
|
321
|
+
if (opts.failed?.(value)) throw new PollFailedError(value);
|
|
322
|
+
if (opts.done(value)) return value;
|
|
323
|
+
await new Promise((resolve, reject) => {
|
|
324
|
+
const t = setTimeout(resolve, interval);
|
|
325
|
+
opts.signal?.addEventListener("abort", () => {
|
|
326
|
+
clearTimeout(t);
|
|
327
|
+
reject(new Error("Aborted"));
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
throw new PollTimeoutError();
|
|
332
|
+
}
|
|
210
333
|
|
|
211
334
|
// src/core/env-loader.ts
|
|
212
|
-
import
|
|
335
|
+
import fs3 from "fs";
|
|
213
336
|
import os from "os";
|
|
214
|
-
import
|
|
337
|
+
import path3 from "path";
|
|
215
338
|
import { config as dotenvConfig } from "dotenv";
|
|
216
339
|
var loaded = false;
|
|
217
340
|
function loadEnv() {
|
|
218
341
|
if (loaded) return;
|
|
219
342
|
loaded = true;
|
|
220
|
-
const cwdEnv =
|
|
221
|
-
if (
|
|
343
|
+
const cwdEnv = path3.resolve(process.cwd(), ".env");
|
|
344
|
+
if (fs3.existsSync(cwdEnv)) {
|
|
222
345
|
dotenvConfig({ path: cwdEnv, override: false });
|
|
223
346
|
}
|
|
224
347
|
if (!process.env.MULTIX_DISABLE_HOME_ENV) {
|
|
225
|
-
const homeEnv =
|
|
226
|
-
if (
|
|
348
|
+
const homeEnv = path3.join(os.homedir(), ".multix", ".env");
|
|
349
|
+
if (fs3.existsSync(homeEnv)) {
|
|
227
350
|
dotenvConfig({ path: homeEnv, override: false });
|
|
228
351
|
}
|
|
229
352
|
}
|
|
@@ -236,291 +359,1876 @@ function redact(key) {
|
|
|
236
359
|
return `${key.slice(0, 6)}...${key.slice(-4)}`;
|
|
237
360
|
}
|
|
238
361
|
|
|
239
|
-
// src/providers/
|
|
362
|
+
// src/providers/byteplus/client.ts
|
|
240
363
|
init_errors();
|
|
241
364
|
init_http_client();
|
|
242
365
|
|
|
243
|
-
// src/providers/
|
|
244
|
-
|
|
245
|
-
var
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
"
|
|
250
|
-
".
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
".heif": "image/heif",
|
|
261
|
-
".gif": "image/gif",
|
|
262
|
-
".bmp": "image/bmp",
|
|
263
|
-
// Video
|
|
264
|
-
".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
|
-
".webm": "video/webm",
|
|
271
|
-
".wmv": "video/x-ms-wmv",
|
|
272
|
-
".3gpp": "video/3gpp",
|
|
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"
|
|
366
|
+
// src/providers/byteplus/models.ts
|
|
367
|
+
var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
|
|
368
|
+
var DEFAULT_BYTEPLUS_IMAGE_MODEL = "seedream-4-0-250828";
|
|
369
|
+
var DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-2.0";
|
|
370
|
+
var DEFAULT_BYTEPLUS_3D_MODEL = "hyper3d-gen2-260112";
|
|
371
|
+
var BYTEPLUS_3D_MODELS = [
|
|
372
|
+
{ id: "hyper3d-gen2-260112", name: "Hyper3D Gen2", family: "hyper3d" },
|
|
373
|
+
{ id: "hitem3d-2-0-251223", name: "Hitem3d 2.0", family: "hitem3d" }
|
|
374
|
+
];
|
|
375
|
+
var BYTEPLUS_DEFAULTS = {
|
|
376
|
+
imageModel: process.env.BYTEPLUS_IMAGE_MODEL ?? DEFAULT_BYTEPLUS_IMAGE_MODEL,
|
|
377
|
+
videoModel: process.env.BYTEPLUS_VIDEO_MODEL ?? DEFAULT_BYTEPLUS_VIDEO_MODEL,
|
|
378
|
+
threeDModel: process.env.BYTEPLUS_3D_MODEL ?? DEFAULT_BYTEPLUS_3D_MODEL,
|
|
379
|
+
videoResolution: "1080p",
|
|
380
|
+
videoDuration: 8,
|
|
381
|
+
videoAspectRatio: "16:9",
|
|
382
|
+
imageSize: "2K"
|
|
283
383
|
};
|
|
284
|
-
function
|
|
285
|
-
const
|
|
286
|
-
return
|
|
287
|
-
}
|
|
288
|
-
function requiresProcessingWait(mimeType) {
|
|
289
|
-
return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
|
|
384
|
+
function bytePlusVideoParamsMode() {
|
|
385
|
+
const m = process.env.BYTEPLUS_VIDEO_PARAMS_MODE;
|
|
386
|
+
return m === "structured" ? "structured" : "flags";
|
|
290
387
|
}
|
|
291
388
|
|
|
292
|
-
// src/providers/
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
function requireApiKey() {
|
|
297
|
-
const key = resolveKey("GEMINI_API_KEY");
|
|
298
|
-
if (!key)
|
|
389
|
+
// src/providers/byteplus/client.ts
|
|
390
|
+
function requireBytePlusKey() {
|
|
391
|
+
const key = resolveKey("BYTEPLUS_API_KEY") ?? resolveKey("ARK_API_KEY");
|
|
392
|
+
if (!key) {
|
|
299
393
|
throw new ConfigError(
|
|
300
|
-
"
|
|
394
|
+
"BytePlus API key not set. Set BYTEPLUS_API_KEY or ARK_API_KEY. Get one at https://console.byteplus.com/auth/api-keys"
|
|
301
395
|
);
|
|
396
|
+
}
|
|
302
397
|
return key;
|
|
303
398
|
}
|
|
304
|
-
function
|
|
305
|
-
return
|
|
399
|
+
function bytePlusBaseUrl() {
|
|
400
|
+
return resolveKey("BYTEPLUS_BASE_URL") ?? DEFAULT_BYTEPLUS_BASE_URL;
|
|
306
401
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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"
|
|
347
|
-
);
|
|
402
|
+
var BytePlusClient = class {
|
|
403
|
+
constructor(apiKey, baseUrl = bytePlusBaseUrl()) {
|
|
404
|
+
this.apiKey = apiKey;
|
|
405
|
+
this.baseUrl = baseUrl;
|
|
348
406
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
const
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
url
|
|
357
|
-
|
|
358
|
-
}).then((r) => r.file);
|
|
359
|
-
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
407
|
+
apiKey;
|
|
408
|
+
baseUrl;
|
|
409
|
+
async request(path36, opts = {}) {
|
|
410
|
+
const base = this.baseUrl.replace(/\/$/, "");
|
|
411
|
+
const url = new URL(base + path36);
|
|
412
|
+
if (opts.query) {
|
|
413
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
414
|
+
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
415
|
+
}
|
|
360
416
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
417
|
+
opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
|
|
418
|
+
return httpJson({
|
|
419
|
+
url: url.toString(),
|
|
420
|
+
method: opts.method ?? "GET",
|
|
421
|
+
headers: {
|
|
422
|
+
accept: "application/json",
|
|
423
|
+
authorization: `Bearer ${this.apiKey}`
|
|
424
|
+
},
|
|
425
|
+
body: opts.body,
|
|
426
|
+
timeoutMs: opts.timeoutMs ?? 12e4
|
|
427
|
+
});
|
|
365
428
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
method: "
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
379
|
-
});
|
|
429
|
+
get(path36, query, logger) {
|
|
430
|
+
return this.request(path36, { method: "GET", query, logger });
|
|
431
|
+
}
|
|
432
|
+
post(path36, body, logger) {
|
|
433
|
+
return this.request(path36, { method: "POST", body, logger });
|
|
434
|
+
}
|
|
435
|
+
delete(path36, logger) {
|
|
436
|
+
return this.request(path36, { method: "DELETE", logger });
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
function createBytePlusClient() {
|
|
440
|
+
return new BytePlusClient(requireBytePlusKey(), bytePlusBaseUrl());
|
|
380
441
|
}
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
442
|
+
|
|
443
|
+
// src/providers/byteplus/generators/three-d.ts
|
|
444
|
+
init_http_client();
|
|
445
|
+
function buildThreeDTaskBody(opts) {
|
|
446
|
+
const promptText = (opts.prompt ?? "").trim();
|
|
447
|
+
const flagPart = opts.flags?.trim() ?? "";
|
|
448
|
+
const text = [promptText, flagPart].filter((s) => s.length > 0).join(" ");
|
|
449
|
+
const content = [];
|
|
450
|
+
if (text.length > 0) content.push({ type: "text", text });
|
|
451
|
+
for (const img of opts.imageInputs ?? []) {
|
|
452
|
+
content.push({ type: "image_url", image_url: { url: refUrl(img) } });
|
|
453
|
+
}
|
|
454
|
+
if (content.length === 0) {
|
|
455
|
+
throw new Error("3D task requires either a prompt or at least one image input.");
|
|
456
|
+
}
|
|
457
|
+
const body = { model: opts.model, content };
|
|
458
|
+
if (opts.seed !== void 0) body.seed = opts.seed;
|
|
459
|
+
return body;
|
|
384
460
|
}
|
|
385
|
-
function
|
|
386
|
-
|
|
387
|
-
return parts.filter(
|
|
388
|
-
(p) => p.inlineData !== void 0
|
|
389
|
-
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
461
|
+
async function submitThreeDTask(client, body, logger) {
|
|
462
|
+
return client.post("/contents/generations/tasks", body, logger);
|
|
390
463
|
}
|
|
391
|
-
function
|
|
392
|
-
|
|
393
|
-
const mimeType = getMimeType(filePath);
|
|
394
|
-
return { inlineData: { mimeType, data } };
|
|
464
|
+
async function getThreeDTaskStatus(client, id, logger) {
|
|
465
|
+
return client.get(`/contents/generations/tasks/${id}`, void 0, logger);
|
|
395
466
|
}
|
|
396
|
-
function
|
|
397
|
-
const
|
|
398
|
-
|
|
467
|
+
async function waitForThreeDTask(client, id, opts = {}) {
|
|
468
|
+
const intervalMs = opts.intervalMs ?? 4e3;
|
|
469
|
+
const maxAttempts = opts.maxAttempts ?? (opts.waitTimeoutMs ? Math.max(1, Math.ceil(opts.waitTimeoutMs / intervalMs)) : 180);
|
|
470
|
+
return poll({
|
|
471
|
+
fetch: () => getThreeDTaskStatus(client, id, opts.logger),
|
|
472
|
+
done: (v) => v.status === "succeeded",
|
|
473
|
+
failed: (v) => v.status === "failed" || v.status === "cancelled",
|
|
474
|
+
intervalMs,
|
|
475
|
+
maxAttempts,
|
|
476
|
+
onTick: (attempt, v) => opts.logger?.debug(`attempt ${attempt} \u2014 status: ${v.status}`)
|
|
477
|
+
});
|
|
399
478
|
}
|
|
400
|
-
function
|
|
401
|
-
|
|
479
|
+
function inferModelFileExt(url) {
|
|
480
|
+
const m = url.match(/\.(glb|gltf|obj|fbx|usdz|stl|ply|zip)(?:\?|#|$)/i);
|
|
481
|
+
return m?.[1] ? m[1].toLowerCase() : "glb";
|
|
402
482
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
|
|
407
|
-
var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
|
|
408
|
-
var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
|
|
409
|
-
var IMAGEN_MODELS = /* @__PURE__ */ new Set([
|
|
410
|
-
"imagen-4.0-generate-001",
|
|
411
|
-
"imagen-4.0-ultra-generate-001",
|
|
412
|
-
"imagen-4.0-fast-generate-001"
|
|
413
|
-
]);
|
|
414
|
-
var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
415
|
-
"gemini-3.1-flash-image-preview",
|
|
416
|
-
"gemini-3-pro-image-preview",
|
|
417
|
-
"gemini-2.5-flash-image",
|
|
418
|
-
"gemini-2.5-flash-image-preview",
|
|
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;
|
|
437
|
-
}
|
|
438
|
-
if (task === "generate-video") {
|
|
439
|
-
return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
|
|
440
|
-
}
|
|
441
|
-
return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
|
|
483
|
+
async function downloadModelFile(url, outputPath, logger) {
|
|
484
|
+
await downloadFile(url, outputPath);
|
|
485
|
+
logger?.success(`Saved ${outputPath}`);
|
|
442
486
|
}
|
|
443
487
|
|
|
444
|
-
// src/providers/
|
|
445
|
-
|
|
446
|
-
|
|
488
|
+
// src/providers/byteplus/commands/generate-3d.ts
|
|
489
|
+
function registerBytePlusGenerate3DCommand(parent) {
|
|
490
|
+
parent.command("generate-3d").alias("3d").description(
|
|
491
|
+
`Generate a 3D model via BytePlus (Hyper3D / Hitem3d). Models: ${BYTEPLUS_3D_MODELS.map((m) => m.id).join(", ")}`
|
|
492
|
+
).option("--prompt <text>", "Text prompt (text-to-3D or to guide image-to-3D)").option(
|
|
493
|
+
"-m, --model <id>",
|
|
494
|
+
`Model id (default ${BYTEPLUS_DEFAULTS.threeDModel})`,
|
|
495
|
+
BYTEPLUS_DEFAULTS.threeDModel
|
|
496
|
+
).option("--input-image <path|url...>", "1\u20135 reference images for image-to-3D (path or URL)").option(
|
|
497
|
+
"--flags <raw>",
|
|
498
|
+
"Raw flags appended to the prompt (e.g. '--mesh_mode Raw --hd_texture true' for Hyper3D, '--ff 2 --resolution 1536pro' for Hitem3d)"
|
|
499
|
+
).option("--seed <n>", "Fixed seed").option("--async", "Submit task and exit; print taskId only").option("--wait-timeout <ms>", "Poll timeout in ms", "900000").option("--output <path>", "Output file path (extension auto-detected from URL if omitted)").option("-v, --verbose", "Verbose logging").action(
|
|
500
|
+
async (opts) => {
|
|
501
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
502
|
+
const client = createBytePlusClient();
|
|
503
|
+
const images = opts.inputImage ?? [];
|
|
504
|
+
if (images.length > 5) {
|
|
505
|
+
logger.error("BytePlus 3D supports at most 5 reference images.");
|
|
506
|
+
process.exit(1);
|
|
507
|
+
}
|
|
508
|
+
const imageInputs = await Promise.all(images.map((src) => resolveImageInput(src, logger)));
|
|
509
|
+
if (!opts.prompt && imageInputs.length === 0) {
|
|
510
|
+
logger.error("Provide --prompt and/or --input-image to describe the 3D content.");
|
|
511
|
+
process.exit(1);
|
|
512
|
+
}
|
|
513
|
+
const body = buildThreeDTaskBody({
|
|
514
|
+
model: opts.model,
|
|
515
|
+
prompt: opts.prompt,
|
|
516
|
+
flags: opts.flags,
|
|
517
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
518
|
+
imageInputs
|
|
519
|
+
});
|
|
520
|
+
logger.info(
|
|
521
|
+
`Submitting 3D task (${opts.model}${imageInputs.length > 0 ? `, ${imageInputs.length} image ref${imageInputs.length === 1 ? "" : "s"}` : ""})`
|
|
522
|
+
);
|
|
523
|
+
const submitted = await submitThreeDTask(client, body, logger);
|
|
524
|
+
const taskId = submitted.id;
|
|
525
|
+
logger.debug(`taskId: ${taskId}`);
|
|
526
|
+
if (opts.async) {
|
|
527
|
+
console.log(taskId);
|
|
528
|
+
console.error(`Poll with: multix byteplus status ${taskId} --wait --download`);
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
try {
|
|
532
|
+
const final = await waitForThreeDTask(client, taskId, {
|
|
533
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 9e5,
|
|
534
|
+
logger
|
|
535
|
+
});
|
|
536
|
+
const url = final.content?.file_url;
|
|
537
|
+
if (!url) {
|
|
538
|
+
logger.error(`Task succeeded but no file_url in response: ${JSON.stringify(final)}`);
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
const ext = inferModelFileExt(url);
|
|
542
|
+
const outPath = opts.output ?? path5.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}-${Date.now()}.${ext}`);
|
|
543
|
+
await downloadModelFile(url, outPath, logger);
|
|
544
|
+
console.log(`
|
|
545
|
+
Task: ${taskId}`);
|
|
546
|
+
console.log(`Saved: ${outPath}`);
|
|
547
|
+
} catch (e) {
|
|
548
|
+
if (e instanceof PollTimeoutError) {
|
|
549
|
+
logger.error(
|
|
550
|
+
`Polling timed out \u2014 keep the task and check later: multix byteplus status ${taskId} --wait --download`
|
|
551
|
+
);
|
|
552
|
+
process.exit(1);
|
|
553
|
+
}
|
|
554
|
+
if (e instanceof PollFailedError) {
|
|
555
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
556
|
+
process.exit(1);
|
|
557
|
+
}
|
|
558
|
+
throw e;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/providers/byteplus/generators/image.ts
|
|
565
|
+
init_http_client();
|
|
566
|
+
import fs5 from "fs";
|
|
567
|
+
import path6 from "path";
|
|
568
|
+
var ASPECT_TO_SIZE = {
|
|
569
|
+
"1:1": "2048x2048",
|
|
570
|
+
"16:9": "2560x1440",
|
|
571
|
+
"9:16": "1440x2560",
|
|
572
|
+
"4:3": "2048x1536",
|
|
573
|
+
"3:4": "1536x2048",
|
|
574
|
+
"21:9": "2560x1080",
|
|
575
|
+
"3:2": "2048x1365",
|
|
576
|
+
"2:3": "1365x2048"
|
|
577
|
+
};
|
|
578
|
+
function resolveSize(opts) {
|
|
579
|
+
if (opts.size && opts.aspectRatio) {
|
|
580
|
+
opts.logger?.warn("Both --size and --aspect-ratio given; using --size.");
|
|
581
|
+
return opts.size;
|
|
582
|
+
}
|
|
583
|
+
if (opts.size) return opts.size;
|
|
584
|
+
if (opts.aspectRatio) {
|
|
585
|
+
const mapped = ASPECT_TO_SIZE[opts.aspectRatio];
|
|
586
|
+
if (!mapped) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
`Unsupported --aspect-ratio '${opts.aspectRatio}'. Supported: ${Object.keys(ASPECT_TO_SIZE).join(", ")}`
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
return mapped;
|
|
592
|
+
}
|
|
593
|
+
return BYTEPLUS_DEFAULTS.imageSize;
|
|
594
|
+
}
|
|
595
|
+
async function generateSeedream(client, opts) {
|
|
596
|
+
const logger = opts.logger;
|
|
597
|
+
const model = opts.model ?? BYTEPLUS_DEFAULTS.imageModel;
|
|
598
|
+
const resolvedInputs = [];
|
|
599
|
+
for (const input of opts.inputImages ?? []) {
|
|
600
|
+
const r = await resolveImageInput(input, logger);
|
|
601
|
+
resolvedInputs.push(refUrl(r));
|
|
602
|
+
}
|
|
603
|
+
const body = {
|
|
604
|
+
model,
|
|
605
|
+
prompt: opts.prompt,
|
|
606
|
+
response_format: "url",
|
|
607
|
+
n: opts.numImages ?? 1,
|
|
608
|
+
watermark: opts.watermark ?? true,
|
|
609
|
+
size: resolveSize(opts)
|
|
610
|
+
};
|
|
611
|
+
if (resolvedInputs.length > 0) body.image = resolvedInputs;
|
|
612
|
+
if (opts.seed !== void 0) body.seed = opts.seed;
|
|
613
|
+
logger?.debug(
|
|
614
|
+
`Seedream body: ${JSON.stringify({ ...body, image: body.image ? `[${body.image.length}]` : void 0 })}`
|
|
615
|
+
);
|
|
616
|
+
const res = await client.post("/images/generations", body, logger);
|
|
617
|
+
const items = res.data ?? [];
|
|
618
|
+
const ts = Date.now();
|
|
619
|
+
const outDir = getOutputDir();
|
|
620
|
+
const saved = [];
|
|
621
|
+
for (let i = 0; i < items.length; i++) {
|
|
622
|
+
const item = items[i];
|
|
623
|
+
if (!item) continue;
|
|
624
|
+
const url = item.url;
|
|
625
|
+
if (!url) {
|
|
626
|
+
logger?.warn(`Image ${i + 1}: no URL in response (b64_json fallback not yet supported).`);
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
const ext = (() => {
|
|
630
|
+
try {
|
|
631
|
+
const e = path6.extname(new URL(url).pathname);
|
|
632
|
+
return e || ".jpg";
|
|
633
|
+
} catch {
|
|
634
|
+
return ".jpg";
|
|
635
|
+
}
|
|
636
|
+
})();
|
|
637
|
+
const filename = opts.output && items.length === 1 ? path6.resolve(opts.output) : path6.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
|
|
638
|
+
fs5.mkdirSync(path6.dirname(filename), { recursive: true });
|
|
639
|
+
await downloadFile(url, filename);
|
|
640
|
+
saved.push({ path: filename, url, seed: item.seed });
|
|
641
|
+
logger?.success(`Saved ${filename}`);
|
|
642
|
+
}
|
|
643
|
+
return saved;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// src/providers/byteplus/commands/generate.ts
|
|
647
|
+
function registerBytePlusGenerateCommand(parent) {
|
|
648
|
+
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(
|
|
649
|
+
"--input-image <path>",
|
|
650
|
+
"Reference image (URL or local path); repeatable",
|
|
651
|
+
collect,
|
|
652
|
+
[]
|
|
653
|
+
).option("--output <path>", "Save single image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
654
|
+
async (opts) => {
|
|
655
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
656
|
+
const client = createBytePlusClient();
|
|
657
|
+
const saved = await generateSeedream(client, {
|
|
658
|
+
prompt: opts.prompt,
|
|
659
|
+
model: opts.model,
|
|
660
|
+
size: opts.size,
|
|
661
|
+
aspectRatio: opts.aspectRatio,
|
|
662
|
+
numImages: Number.parseInt(opts.numImages, 10) || 1,
|
|
663
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
664
|
+
watermark: opts.watermark,
|
|
665
|
+
inputImages: opts.inputImage,
|
|
666
|
+
output: opts.output,
|
|
667
|
+
logger
|
|
668
|
+
});
|
|
669
|
+
console.log(`
|
|
670
|
+
Generated ${saved.length} image(s):`);
|
|
671
|
+
for (const f of saved) console.log(` ${f.path}`);
|
|
672
|
+
}
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
function collect(value, prev) {
|
|
676
|
+
return [...prev, value];
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/providers/byteplus/commands/image-to-image.ts
|
|
680
|
+
function registerBytePlusImageToImageCommand(parent) {
|
|
681
|
+
parent.command("image-to-image").alias("i2i").description(
|
|
682
|
+
"Edit/transform image(s) with BytePlus Seedream/Seededit using one or more reference images"
|
|
683
|
+
).requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
684
|
+
"--ref <path>",
|
|
685
|
+
"Reference image (URL or local path); repeatable for multi-ref",
|
|
686
|
+
collect2,
|
|
687
|
+
[]
|
|
688
|
+
).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(
|
|
689
|
+
async (opts) => {
|
|
690
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
691
|
+
throw new Error("At least one --ref is required for image-to-image.");
|
|
692
|
+
}
|
|
693
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
694
|
+
const client = createBytePlusClient();
|
|
695
|
+
const saved = await generateSeedream(client, {
|
|
696
|
+
prompt: opts.prompt,
|
|
697
|
+
model: opts.model,
|
|
698
|
+
size: opts.size,
|
|
699
|
+
aspectRatio: opts.aspectRatio,
|
|
700
|
+
numImages: Number.parseInt(opts.numImages, 10) || 1,
|
|
701
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
702
|
+
watermark: opts.watermark,
|
|
703
|
+
inputImages: opts.ref,
|
|
704
|
+
output: opts.output,
|
|
705
|
+
logger
|
|
706
|
+
});
|
|
707
|
+
console.log(`
|
|
708
|
+
Generated ${saved.length} image(s):`);
|
|
709
|
+
for (const f of saved) console.log(` ${f.path}`);
|
|
710
|
+
}
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
function collect2(value, prev) {
|
|
714
|
+
return [...prev, value];
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// src/providers/byteplus/commands/image-to-video.ts
|
|
718
|
+
import path8 from "path";
|
|
719
|
+
|
|
720
|
+
// src/core/video-thumb.ts
|
|
721
|
+
init_http_client();
|
|
722
|
+
import fs6 from "fs";
|
|
723
|
+
import path7 from "path";
|
|
724
|
+
var THUMB_KEYS = /* @__PURE__ */ new Set([
|
|
725
|
+
"cover_image_url",
|
|
726
|
+
"coverImageUrl",
|
|
727
|
+
"cover_url",
|
|
728
|
+
"coverUrl",
|
|
729
|
+
"thumbnail_url",
|
|
730
|
+
"thumbnailUrl",
|
|
731
|
+
"thumb_url",
|
|
732
|
+
"thumbUrl",
|
|
733
|
+
"preview_url",
|
|
734
|
+
"previewUrl",
|
|
735
|
+
"first_frame_url",
|
|
736
|
+
"firstFrameUrl",
|
|
737
|
+
"poster_url",
|
|
738
|
+
"posterUrl",
|
|
739
|
+
"image_url",
|
|
740
|
+
"imageUrl"
|
|
741
|
+
]);
|
|
742
|
+
var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
|
|
743
|
+
function isImageUrl(value) {
|
|
744
|
+
if (typeof value !== "string") return false;
|
|
745
|
+
if (!/^https?:\/\//i.test(value)) return false;
|
|
746
|
+
return IMAGE_EXT_RE.test(value);
|
|
747
|
+
}
|
|
748
|
+
function detectThumbUrl(obj) {
|
|
749
|
+
if (obj === null || obj === void 0) return null;
|
|
750
|
+
if (Array.isArray(obj)) {
|
|
751
|
+
for (const item of obj) {
|
|
752
|
+
const found = detectThumbUrl(item);
|
|
753
|
+
if (found) return found;
|
|
754
|
+
}
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
if (typeof obj !== "object") return null;
|
|
758
|
+
const record = obj;
|
|
759
|
+
for (const [key, value] of Object.entries(record)) {
|
|
760
|
+
if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
|
|
761
|
+
}
|
|
762
|
+
for (const value of Object.values(record)) {
|
|
763
|
+
if (value && typeof value === "object") {
|
|
764
|
+
const found = detectThumbUrl(value);
|
|
765
|
+
if (found) return found;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return null;
|
|
769
|
+
}
|
|
770
|
+
async function downloadThumbBeside(thumbUrl, videoPath, logger) {
|
|
771
|
+
const match = thumbUrl.match(IMAGE_EXT_RE);
|
|
772
|
+
const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
|
|
773
|
+
const dir = path7.dirname(videoPath);
|
|
774
|
+
const base = path7.basename(videoPath, path7.extname(videoPath));
|
|
775
|
+
const dest = path7.join(dir, `${base}_thumb.${ext}`);
|
|
776
|
+
try {
|
|
777
|
+
await downloadFile(thumbUrl, dest);
|
|
778
|
+
logger?.success(`Saved thumb ${dest}`);
|
|
779
|
+
return dest;
|
|
780
|
+
} catch (e) {
|
|
781
|
+
logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
786
|
+
if (opts.skip) return null;
|
|
787
|
+
const url = detectThumbUrl(source);
|
|
788
|
+
if (!url) {
|
|
789
|
+
opts.logger?.debug("No thumbnail URL detected in provider response");
|
|
790
|
+
return null;
|
|
791
|
+
}
|
|
792
|
+
const saved = await downloadThumbBeside(url, videoPath, opts.logger);
|
|
793
|
+
if (saved && opts.copyTo) {
|
|
794
|
+
const copyExt = path7.extname(saved);
|
|
795
|
+
const copyBase = path7.basename(opts.copyTo, path7.extname(opts.copyTo));
|
|
796
|
+
const copyDir = path7.dirname(path7.resolve(opts.copyTo));
|
|
797
|
+
const copyDest = path7.join(copyDir, `${copyBase}_thumb${copyExt}`);
|
|
798
|
+
fs6.mkdirSync(copyDir, { recursive: true });
|
|
799
|
+
fs6.copyFileSync(saved, copyDest);
|
|
800
|
+
opts.logger?.success(`Copied thumb to ${copyDest}`);
|
|
801
|
+
}
|
|
802
|
+
return saved;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// src/providers/byteplus/generators/video.ts
|
|
806
|
+
init_http_client();
|
|
807
|
+
function buildFlagSuffix(opts) {
|
|
808
|
+
const parts = [];
|
|
809
|
+
if (opts.resolution) parts.push(`--rs ${opts.resolution}`);
|
|
810
|
+
if (opts.duration !== void 0) parts.push(`--dur ${opts.duration}`);
|
|
811
|
+
if (opts.aspectRatio) parts.push(`--rt ${opts.aspectRatio}`);
|
|
812
|
+
if (opts.cameraFixed !== void 0) parts.push(`--cf ${opts.cameraFixed}`);
|
|
813
|
+
if (opts.seed !== void 0) parts.push(`--seed ${opts.seed}`);
|
|
814
|
+
if (opts.audio !== void 0) parts.push(`--wm ${opts.audio}`);
|
|
815
|
+
return parts.length > 0 ? ` ${parts.join(" ")}` : "";
|
|
816
|
+
}
|
|
817
|
+
function buildVideoTaskBody(opts) {
|
|
818
|
+
const mode = opts.paramsMode ?? bytePlusVideoParamsMode();
|
|
819
|
+
const text = mode === "flags" ? `${opts.prompt}${buildFlagSuffix(opts)}${opts.negativePrompt ? ` negative: "${opts.negativePrompt}"` : ""}` : opts.prompt;
|
|
820
|
+
const content = [{ type: "text", text }];
|
|
821
|
+
for (const img of opts.imageInputs ?? []) {
|
|
822
|
+
content.push({ type: "image_url", image_url: { url: refUrl(img) } });
|
|
823
|
+
}
|
|
824
|
+
for (const ref of opts.references ?? []) {
|
|
825
|
+
if (ref.kind === "image") {
|
|
826
|
+
content.push({
|
|
827
|
+
type: "image_url",
|
|
828
|
+
image_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
|
|
829
|
+
});
|
|
830
|
+
} else if (ref.kind === "video") {
|
|
831
|
+
content.push({
|
|
832
|
+
type: "video_url",
|
|
833
|
+
video_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
|
|
834
|
+
});
|
|
835
|
+
} else {
|
|
836
|
+
content.push({
|
|
837
|
+
type: "audio_url",
|
|
838
|
+
audio_url: { url: ref.url, ...ref.role ? { role: ref.role } : {} }
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
const body = { model: opts.model, content };
|
|
843
|
+
if (mode === "structured") {
|
|
844
|
+
body.parameters = {
|
|
845
|
+
...opts.resolution ? { resolution: opts.resolution } : {},
|
|
846
|
+
...opts.duration !== void 0 ? { duration: opts.duration } : {},
|
|
847
|
+
...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
|
|
848
|
+
...opts.audio !== void 0 ? { audio: opts.audio } : {},
|
|
849
|
+
...opts.seed !== void 0 ? { seed: opts.seed } : {},
|
|
850
|
+
...opts.negativePrompt ? { negative_prompt: opts.negativePrompt } : {},
|
|
851
|
+
...opts.cameraFixed !== void 0 ? { camera_fixed: opts.cameraFixed } : {}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
return body;
|
|
855
|
+
}
|
|
856
|
+
async function submitVideoTask(client, body, logger) {
|
|
857
|
+
return client.post("/contents/generations/tasks", body, logger);
|
|
858
|
+
}
|
|
859
|
+
async function getVideoTaskStatus(client, id, logger) {
|
|
860
|
+
return client.get(`/contents/generations/tasks/${id}`, void 0, logger);
|
|
861
|
+
}
|
|
862
|
+
async function waitForVideoTask(client, id, opts = {}) {
|
|
863
|
+
const intervalMs = opts.intervalMs ?? 4e3;
|
|
864
|
+
const maxAttempts = opts.maxAttempts ?? (opts.waitTimeoutMs ? Math.max(1, Math.ceil(opts.waitTimeoutMs / intervalMs)) : 120);
|
|
865
|
+
return poll({
|
|
866
|
+
fetch: () => getVideoTaskStatus(client, id, opts.logger),
|
|
867
|
+
done: (v) => v.status === "succeeded",
|
|
868
|
+
failed: (v) => v.status === "failed" || v.status === "cancelled",
|
|
869
|
+
intervalMs,
|
|
870
|
+
maxAttempts,
|
|
871
|
+
onTick: (attempt, v) => opts.logger?.debug(`attempt ${attempt} \u2014 status: ${v.status}`)
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
async function downloadVideo(url, outputPath, logger) {
|
|
875
|
+
await downloadFile(url, outputPath);
|
|
876
|
+
logger?.success(`Saved ${outputPath}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/providers/byteplus/commands/image-to-video.ts
|
|
880
|
+
function registerBytePlusImageToVideoCommand(parent) {
|
|
881
|
+
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(
|
|
882
|
+
async (image, opts) => {
|
|
883
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
884
|
+
const client = createBytePlusClient();
|
|
885
|
+
const first = await resolveImageInput(image, logger);
|
|
886
|
+
const refs = [];
|
|
887
|
+
if (opts.lastFrame) {
|
|
888
|
+
const last = await resolveImageInput(opts.lastFrame, logger);
|
|
889
|
+
refs.push({
|
|
890
|
+
kind: "image",
|
|
891
|
+
url: last.kind === "url" ? last.url : last.dataUrl,
|
|
892
|
+
mime: last.kind === "data" ? last.mime : void 0,
|
|
893
|
+
role: "last_frame"
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
const body = buildVideoTaskBody({
|
|
897
|
+
model: opts.model,
|
|
898
|
+
prompt: opts.prompt,
|
|
899
|
+
resolution: opts.resolution,
|
|
900
|
+
duration: Number.parseInt(opts.duration, 10),
|
|
901
|
+
aspectRatio: opts.aspectRatio,
|
|
902
|
+
audio: opts.audio,
|
|
903
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
904
|
+
negativePrompt: opts.negative,
|
|
905
|
+
cameraFixed: opts.cameraFixed,
|
|
906
|
+
imageInputs: [first],
|
|
907
|
+
references: refs
|
|
908
|
+
});
|
|
909
|
+
logger.info(`Submitting i2v task (${opts.model})`);
|
|
910
|
+
const submitted = await submitVideoTask(client, body, logger);
|
|
911
|
+
const taskId = submitted.id;
|
|
912
|
+
if (opts.async) {
|
|
913
|
+
console.log(taskId);
|
|
914
|
+
console.error(`Poll with: multix byteplus status ${taskId}`);
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
try {
|
|
918
|
+
const final = await waitForVideoTask(client, taskId, {
|
|
919
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
920
|
+
logger
|
|
921
|
+
});
|
|
922
|
+
const url = final.content?.video_url;
|
|
923
|
+
if (!url) {
|
|
924
|
+
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
925
|
+
process.exit(1);
|
|
926
|
+
}
|
|
927
|
+
const outPath = opts.output ?? path8.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
928
|
+
await downloadVideo(url, outPath, logger);
|
|
929
|
+
await maybeDownloadThumb(final, outPath, {
|
|
930
|
+
skip: opts.thumb === false,
|
|
931
|
+
copyTo: opts.output,
|
|
932
|
+
logger
|
|
933
|
+
});
|
|
934
|
+
console.log(`
|
|
935
|
+
Task: ${taskId}`);
|
|
936
|
+
console.log(`Saved: ${outPath}`);
|
|
937
|
+
} catch (e) {
|
|
938
|
+
if (e instanceof PollTimeoutError) {
|
|
939
|
+
logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
|
|
940
|
+
process.exit(1);
|
|
941
|
+
}
|
|
942
|
+
if (e instanceof PollFailedError) {
|
|
943
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
944
|
+
process.exit(1);
|
|
945
|
+
}
|
|
946
|
+
throw e;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// src/providers/byteplus/commands/reference-to-video.ts
|
|
953
|
+
import path9 from "path";
|
|
954
|
+
|
|
955
|
+
// src/providers/byteplus/reference-input.ts
|
|
956
|
+
var MAX_TOTAL_REFS = 12;
|
|
957
|
+
var MAX_IMAGE_REFS = 9;
|
|
958
|
+
var MAX_VIDEO_REFS = 3;
|
|
959
|
+
var MAX_AUDIO_REFS = 3;
|
|
960
|
+
function parseRefSpec(input) {
|
|
961
|
+
const PLACEHOLDER = "\0";
|
|
962
|
+
const escaped = input.replace(/\\:/g, PLACEHOLDER);
|
|
963
|
+
const lastIdx = escaped.lastIndexOf(":");
|
|
964
|
+
if (lastIdx === -1) {
|
|
965
|
+
return { path: input.replace(/\\:/g, ":") };
|
|
966
|
+
}
|
|
967
|
+
const left = escaped.slice(0, lastIdx);
|
|
968
|
+
const right = escaped.slice(lastIdx + 1);
|
|
969
|
+
if (/^[a-zA-Z]$/.test(left) && /[\\/]/.test(right)) {
|
|
970
|
+
return { path: input.replace(/\\:/g, ":") };
|
|
971
|
+
}
|
|
972
|
+
if (right.length === 0) {
|
|
973
|
+
return { path: left.replace(new RegExp(PLACEHOLDER, "g"), ":") };
|
|
974
|
+
}
|
|
975
|
+
return {
|
|
976
|
+
path: left.replace(new RegExp(PLACEHOLDER, "g"), ":"),
|
|
977
|
+
role: right.replace(new RegExp(PLACEHOLDER, "g"), ":") || void 0
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function toRef(kind, resolved, role) {
|
|
981
|
+
if (resolved.kind === "url") return { kind, url: resolved.url, role };
|
|
982
|
+
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
983
|
+
}
|
|
984
|
+
async function resolveRefSpec(kind, spec, logger) {
|
|
985
|
+
const { path: path36, role } = parseRefSpec(spec);
|
|
986
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path36, logger), role);
|
|
987
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path36, logger), role);
|
|
988
|
+
return toRef(kind, await resolveAudioInput(path36, logger), role);
|
|
989
|
+
}
|
|
990
|
+
async function resolveRefSpecs(specs, logger) {
|
|
991
|
+
const out = [];
|
|
992
|
+
for (const { kind, spec } of specs) {
|
|
993
|
+
out.push(await resolveRefSpec(kind, spec, logger));
|
|
994
|
+
}
|
|
995
|
+
return out;
|
|
996
|
+
}
|
|
997
|
+
function validateRefCounts(refs) {
|
|
998
|
+
if (refs.length > MAX_TOTAL_REFS) {
|
|
999
|
+
throw new Error(`Too many references: ${refs.length} > ${MAX_TOTAL_REFS}`);
|
|
1000
|
+
}
|
|
1001
|
+
const images = refs.filter((r) => r.kind === "image").length;
|
|
1002
|
+
const videos = refs.filter((r) => r.kind === "video").length;
|
|
1003
|
+
const audios = refs.filter((r) => r.kind === "audio").length;
|
|
1004
|
+
if (images > MAX_IMAGE_REFS)
|
|
1005
|
+
throw new Error(`Too many image refs: ${images} > ${MAX_IMAGE_REFS}`);
|
|
1006
|
+
if (videos > MAX_VIDEO_REFS)
|
|
1007
|
+
throw new Error(`Too many video refs: ${videos} > ${MAX_VIDEO_REFS}`);
|
|
1008
|
+
if (audios > MAX_AUDIO_REFS)
|
|
1009
|
+
throw new Error(`Too many audio refs: ${audios} > ${MAX_AUDIO_REFS}`);
|
|
1010
|
+
}
|
|
1011
|
+
var REF_LIMITS = {
|
|
1012
|
+
total: MAX_TOTAL_REFS,
|
|
1013
|
+
image: MAX_IMAGE_REFS,
|
|
1014
|
+
video: MAX_VIDEO_REFS,
|
|
1015
|
+
audio: MAX_AUDIO_REFS
|
|
1016
|
+
};
|
|
1017
|
+
|
|
1018
|
+
// src/providers/byteplus/commands/reference-to-video.ts
|
|
1019
|
+
function collect3(value, prev) {
|
|
1020
|
+
return [...prev, value];
|
|
1021
|
+
}
|
|
1022
|
+
function registerBytePlusReferenceToVideoCommand(parent) {
|
|
1023
|
+
parent.command("reference-to-video").alias("r2v").description(
|
|
1024
|
+
`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 \\:).`
|
|
1025
|
+
).requiredOption("--prompt <text>", "Text prompt").option(
|
|
1026
|
+
"--ref-image <spec>",
|
|
1027
|
+
"Image ref (path|url[:role]); repeatable",
|
|
1028
|
+
collect3,
|
|
1029
|
+
[]
|
|
1030
|
+
).option(
|
|
1031
|
+
"--ref-video <spec>",
|
|
1032
|
+
"Video ref (path|url[:role]); repeatable",
|
|
1033
|
+
collect3,
|
|
1034
|
+
[]
|
|
1035
|
+
).option(
|
|
1036
|
+
"--ref-audio <spec>",
|
|
1037
|
+
"Audio ref (path|url[:role]); repeatable",
|
|
1038
|
+
collect3,
|
|
1039
|
+
[]
|
|
1040
|
+
).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(
|
|
1041
|
+
async (opts) => {
|
|
1042
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1043
|
+
const specs = [
|
|
1044
|
+
...opts.refImage.map((s) => ({ kind: "image", spec: s })),
|
|
1045
|
+
...opts.refVideo.map((s) => ({ kind: "video", spec: s })),
|
|
1046
|
+
...opts.refAudio.map((s) => ({ kind: "audio", spec: s }))
|
|
1047
|
+
];
|
|
1048
|
+
if (specs.length === 0) {
|
|
1049
|
+
logger.error("At least one --ref-image / --ref-video / --ref-audio is required");
|
|
1050
|
+
process.exit(2);
|
|
1051
|
+
}
|
|
1052
|
+
const refs = await resolveRefSpecs(specs, logger);
|
|
1053
|
+
validateRefCounts(refs);
|
|
1054
|
+
const client = createBytePlusClient();
|
|
1055
|
+
const body = buildVideoTaskBody({
|
|
1056
|
+
model: opts.model,
|
|
1057
|
+
prompt: opts.prompt,
|
|
1058
|
+
resolution: opts.resolution,
|
|
1059
|
+
duration: Number.parseInt(opts.duration, 10),
|
|
1060
|
+
aspectRatio: opts.aspectRatio,
|
|
1061
|
+
audio: opts.audio,
|
|
1062
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
1063
|
+
negativePrompt: opts.negative,
|
|
1064
|
+
cameraFixed: opts.cameraFixed,
|
|
1065
|
+
references: refs
|
|
1066
|
+
});
|
|
1067
|
+
logger.info(`Submitting r2v task (${opts.model}, ${refs.length} refs)`);
|
|
1068
|
+
const submitted = await submitVideoTask(client, body, logger);
|
|
1069
|
+
const taskId = submitted.id;
|
|
1070
|
+
if (opts.async) {
|
|
1071
|
+
console.log(taskId);
|
|
1072
|
+
console.error(`Poll with: multix byteplus status ${taskId}`);
|
|
1073
|
+
return;
|
|
1074
|
+
}
|
|
1075
|
+
try {
|
|
1076
|
+
const final = await waitForVideoTask(client, taskId, {
|
|
1077
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
1078
|
+
logger
|
|
1079
|
+
});
|
|
1080
|
+
const url = final.content?.video_url;
|
|
1081
|
+
if (!url) {
|
|
1082
|
+
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
1083
|
+
process.exit(1);
|
|
1084
|
+
}
|
|
1085
|
+
const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1086
|
+
await downloadVideo(url, outPath, logger);
|
|
1087
|
+
await maybeDownloadThumb(final, outPath, {
|
|
1088
|
+
skip: opts.thumb === false,
|
|
1089
|
+
copyTo: opts.output,
|
|
1090
|
+
logger
|
|
1091
|
+
});
|
|
1092
|
+
console.log(`
|
|
1093
|
+
Task: ${taskId}`);
|
|
1094
|
+
console.log(`Saved: ${outPath}`);
|
|
1095
|
+
} catch (e) {
|
|
1096
|
+
if (e instanceof PollTimeoutError) {
|
|
1097
|
+
logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
if (e instanceof PollFailedError) {
|
|
1101
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
1104
|
+
throw e;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/providers/byteplus/commands/status.ts
|
|
1111
|
+
import path10 from "path";
|
|
1112
|
+
function registerBytePlusStatusCommand(parent) {
|
|
1113
|
+
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(
|
|
1114
|
+
async (taskId, opts) => {
|
|
1115
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1116
|
+
const client = createBytePlusClient();
|
|
1117
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
1118
|
+
try {
|
|
1119
|
+
const status = shouldWait ? await waitForVideoTask(client, taskId, {
|
|
1120
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
1121
|
+
logger
|
|
1122
|
+
}) : await getVideoTaskStatus(client, taskId, logger);
|
|
1123
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1124
|
+
if (opts.download && status.status === "succeeded") {
|
|
1125
|
+
const content = status.content;
|
|
1126
|
+
const videoUrl = content?.video_url;
|
|
1127
|
+
const fileUrl = content?.file_url;
|
|
1128
|
+
if (videoUrl) {
|
|
1129
|
+
const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
|
|
1130
|
+
await downloadVideo(videoUrl, outPath, logger);
|
|
1131
|
+
await maybeDownloadThumb(status, outPath, {
|
|
1132
|
+
skip: opts.thumb === false,
|
|
1133
|
+
copyTo: opts.output,
|
|
1134
|
+
logger
|
|
1135
|
+
});
|
|
1136
|
+
console.log(`Saved: ${outPath}`);
|
|
1137
|
+
} else if (fileUrl) {
|
|
1138
|
+
const ext = inferModelFileExt(fileUrl);
|
|
1139
|
+
const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
|
|
1140
|
+
await downloadModelFile(fileUrl, outPath, logger);
|
|
1141
|
+
console.log(`Saved: ${outPath}`);
|
|
1142
|
+
} else {
|
|
1143
|
+
logger.error("Task succeeded but no video_url or file_url present");
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
} catch (e) {
|
|
1148
|
+
if (e instanceof PollTimeoutError) {
|
|
1149
|
+
logger.error(`Timed out waiting for task ${taskId}`);
|
|
1150
|
+
process.exit(1);
|
|
1151
|
+
}
|
|
1152
|
+
if (e instanceof PollFailedError) {
|
|
1153
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1154
|
+
process.exit(1);
|
|
1155
|
+
}
|
|
1156
|
+
throw e;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// src/providers/byteplus/commands/video.ts
|
|
1163
|
+
import path11 from "path";
|
|
1164
|
+
function registerBytePlusVideoCommand(parent) {
|
|
1165
|
+
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(
|
|
1166
|
+
"--aspect-ratio <r>",
|
|
1167
|
+
"Aspect ratio (16:9, 9:16, 1:1, ...)",
|
|
1168
|
+
BYTEPLUS_DEFAULTS.videoAspectRatio
|
|
1169
|
+
).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(
|
|
1170
|
+
async (opts) => {
|
|
1171
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1172
|
+
const client = createBytePlusClient();
|
|
1173
|
+
const body = buildVideoTaskBody({
|
|
1174
|
+
model: opts.model,
|
|
1175
|
+
prompt: opts.prompt,
|
|
1176
|
+
resolution: opts.resolution,
|
|
1177
|
+
duration: Number.parseInt(opts.duration, 10),
|
|
1178
|
+
aspectRatio: opts.aspectRatio,
|
|
1179
|
+
audio: opts.audio,
|
|
1180
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
1181
|
+
negativePrompt: opts.negative,
|
|
1182
|
+
cameraFixed: opts.cameraFixed
|
|
1183
|
+
});
|
|
1184
|
+
logger.info(
|
|
1185
|
+
`Submitting Seedance task (${opts.model}, ${opts.resolution}, ${opts.duration}s)`
|
|
1186
|
+
);
|
|
1187
|
+
const submitted = await submitVideoTask(client, body, logger);
|
|
1188
|
+
const taskId = submitted.id;
|
|
1189
|
+
logger.debug(`taskId: ${taskId}`);
|
|
1190
|
+
if (opts.async) {
|
|
1191
|
+
console.log(taskId);
|
|
1192
|
+
console.error(`Poll with: multix byteplus status ${taskId}`);
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
try {
|
|
1196
|
+
const final = await waitForVideoTask(client, taskId, {
|
|
1197
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
1198
|
+
logger
|
|
1199
|
+
});
|
|
1200
|
+
const url = final.content?.video_url;
|
|
1201
|
+
if (!url) {
|
|
1202
|
+
logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
|
|
1203
|
+
process.exit(1);
|
|
1204
|
+
}
|
|
1205
|
+
const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1206
|
+
await downloadVideo(url, outPath, logger);
|
|
1207
|
+
await maybeDownloadThumb(final, outPath, {
|
|
1208
|
+
skip: opts.thumb === false,
|
|
1209
|
+
copyTo: opts.output,
|
|
1210
|
+
logger
|
|
1211
|
+
});
|
|
1212
|
+
console.log(`
|
|
1213
|
+
Task: ${taskId}`);
|
|
1214
|
+
console.log(`Saved: ${outPath}`);
|
|
1215
|
+
} catch (e) {
|
|
1216
|
+
if (e instanceof PollTimeoutError) {
|
|
1217
|
+
logger.error(
|
|
1218
|
+
`Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
|
|
1219
|
+
);
|
|
1220
|
+
process.exit(1);
|
|
1221
|
+
}
|
|
1222
|
+
if (e instanceof PollFailedError) {
|
|
1223
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
throw e;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// src/providers/byteplus/commands/index.ts
|
|
1233
|
+
function registerBytePlusCommands(program2) {
|
|
1234
|
+
const byteplus = program2.command("byteplus").description(
|
|
1235
|
+
"BytePlus ModelArk: Seedream image, Seedance video, Hyper3D / Hitem3d 3D generation"
|
|
1236
|
+
);
|
|
1237
|
+
registerBytePlusGenerateCommand(byteplus);
|
|
1238
|
+
registerBytePlusImageToImageCommand(byteplus);
|
|
1239
|
+
registerBytePlusVideoCommand(byteplus);
|
|
1240
|
+
registerBytePlusImageToVideoCommand(byteplus);
|
|
1241
|
+
registerBytePlusReferenceToVideoCommand(byteplus);
|
|
1242
|
+
registerBytePlusGenerate3DCommand(byteplus);
|
|
1243
|
+
registerBytePlusStatusCommand(byteplus);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// src/providers/gemini/commands/analyze.ts
|
|
1247
|
+
init_errors();
|
|
1248
|
+
import fs9 from "fs";
|
|
1249
|
+
|
|
1250
|
+
// src/providers/gemini/client.ts
|
|
1251
|
+
import fs7 from "fs";
|
|
1252
|
+
import path13 from "path";
|
|
1253
|
+
init_errors();
|
|
1254
|
+
init_http_client();
|
|
1255
|
+
|
|
1256
|
+
// src/providers/gemini/task-resolver.ts
|
|
1257
|
+
import path12 from "path";
|
|
1258
|
+
var MIME_MAP = {
|
|
1259
|
+
// Audio
|
|
1260
|
+
".mp3": "audio/mp3",
|
|
1261
|
+
".wav": "audio/wav",
|
|
1262
|
+
".aac": "audio/aac",
|
|
1263
|
+
".flac": "audio/flac",
|
|
1264
|
+
".ogg": "audio/ogg",
|
|
1265
|
+
".aiff": "audio/aiff",
|
|
1266
|
+
".m4a": "audio/mp4",
|
|
1267
|
+
// Image
|
|
1268
|
+
".jpg": "image/jpeg",
|
|
1269
|
+
".jpeg": "image/jpeg",
|
|
1270
|
+
".png": "image/png",
|
|
1271
|
+
".webp": "image/webp",
|
|
1272
|
+
".heic": "image/heic",
|
|
1273
|
+
".heif": "image/heif",
|
|
1274
|
+
".gif": "image/gif",
|
|
1275
|
+
".bmp": "image/bmp",
|
|
1276
|
+
// Video
|
|
1277
|
+
".mp4": "video/mp4",
|
|
1278
|
+
".mpeg": "video/mpeg",
|
|
1279
|
+
".mov": "video/quicktime",
|
|
1280
|
+
".avi": "video/x-msvideo",
|
|
1281
|
+
".flv": "video/x-flv",
|
|
1282
|
+
".mpg": "video/mpeg",
|
|
1283
|
+
".webm": "video/webm",
|
|
1284
|
+
".wmv": "video/x-ms-wmv",
|
|
1285
|
+
".3gpp": "video/3gpp",
|
|
1286
|
+
// Document
|
|
1287
|
+
".pdf": "application/pdf",
|
|
1288
|
+
".txt": "text/plain",
|
|
1289
|
+
".html": "text/html",
|
|
1290
|
+
".htm": "text/html",
|
|
1291
|
+
".md": "text/markdown",
|
|
1292
|
+
".csv": "text/csv",
|
|
1293
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1294
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1295
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
1296
|
+
};
|
|
1297
|
+
function getMimeType(filePath) {
|
|
1298
|
+
const ext = path12.extname(filePath).toLowerCase();
|
|
1299
|
+
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
1300
|
+
}
|
|
1301
|
+
function requiresProcessingWait(mimeType) {
|
|
1302
|
+
return mimeType.startsWith("audio/") || mimeType.startsWith("video/");
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// src/providers/gemini/client.ts
|
|
1306
|
+
var BASE = "https://generativelanguage.googleapis.com";
|
|
1307
|
+
var FILES_BASE = `${BASE}/upload/v1beta/files`;
|
|
1308
|
+
var GENERATE_BASE = `${BASE}/v1beta/models`;
|
|
1309
|
+
function requireApiKey() {
|
|
1310
|
+
const key = resolveKey("GEMINI_API_KEY");
|
|
1311
|
+
if (!key)
|
|
1312
|
+
throw new ConfigError(
|
|
1313
|
+
"GEMINI_API_KEY is not set. Get one at https://aistudio.google.com/apikey"
|
|
1314
|
+
);
|
|
1315
|
+
return key;
|
|
1316
|
+
}
|
|
1317
|
+
function authHeaders(apiKey) {
|
|
1318
|
+
return { "x-goog-api-key": apiKey };
|
|
1319
|
+
}
|
|
1320
|
+
async function uploadFile(filePath, opts = {}) {
|
|
1321
|
+
const apiKey = requireApiKey();
|
|
1322
|
+
const { timeoutMs = 3e5, logger } = opts;
|
|
1323
|
+
const mimeType = getMimeType(filePath);
|
|
1324
|
+
const fileBytes = fs7.readFileSync(filePath);
|
|
1325
|
+
const displayName = path13.basename(filePath);
|
|
1326
|
+
const boundary = `multix_${Date.now()}`;
|
|
1327
|
+
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
1328
|
+
const body = Buffer.concat([
|
|
1329
|
+
Buffer.from(
|
|
1330
|
+
`--${boundary}\r
|
|
1331
|
+
Content-Type: application/json; charset=UTF-8\r
|
|
1332
|
+
\r
|
|
1333
|
+
${metaJson}\r
|
|
1334
|
+
--${boundary}\r
|
|
1335
|
+
Content-Type: ${mimeType}\r
|
|
1336
|
+
\r
|
|
1337
|
+
`
|
|
1338
|
+
),
|
|
1339
|
+
fileBytes,
|
|
1340
|
+
Buffer.from(`\r
|
|
1341
|
+
--${boundary}--`)
|
|
1342
|
+
]);
|
|
1343
|
+
logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
1344
|
+
const resp = await globalThis.fetch(FILES_BASE, {
|
|
1345
|
+
method: "POST",
|
|
1346
|
+
headers: {
|
|
1347
|
+
"x-goog-api-key": apiKey,
|
|
1348
|
+
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
1349
|
+
"Content-Length": String(body.length),
|
|
1350
|
+
"X-Goog-Upload-Protocol": "multipart"
|
|
1351
|
+
},
|
|
1352
|
+
body,
|
|
1353
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1354
|
+
});
|
|
1355
|
+
if (!resp.ok) {
|
|
1356
|
+
const txt = await resp.text();
|
|
1357
|
+
throw new ProviderError(
|
|
1358
|
+
`File upload failed (HTTP ${resp.status}): ${txt.slice(0, 500)}`,
|
|
1359
|
+
"gemini"
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
const data = await resp.json();
|
|
1363
|
+
let fileRef = data.file;
|
|
1364
|
+
if (requiresProcessingWait(mimeType)) {
|
|
1365
|
+
const deadline = Date.now() + timeoutMs;
|
|
1366
|
+
while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
|
|
1367
|
+
await sleep(2e3);
|
|
1368
|
+
fileRef = await httpJson({
|
|
1369
|
+
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
1370
|
+
headers: authHeaders(apiKey)
|
|
1371
|
+
}).then((r) => r.file);
|
|
1372
|
+
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
1373
|
+
}
|
|
1374
|
+
if (fileRef.state === "FAILED")
|
|
1375
|
+
throw new ProviderError(`File processing failed: ${filePath}`, "gemini");
|
|
1376
|
+
if (fileRef.state === "PROCESSING")
|
|
1377
|
+
throw new ProviderError(`Processing timeout: ${filePath}`, "gemini");
|
|
1378
|
+
}
|
|
1379
|
+
logger?.debug(`Uploaded: ${fileRef.name} (${fileRef.uri})`);
|
|
1380
|
+
return fileRef;
|
|
1381
|
+
}
|
|
1382
|
+
async function generateContent(req) {
|
|
1383
|
+
const apiKey = requireApiKey();
|
|
1384
|
+
return httpJson({
|
|
1385
|
+
url: `${GENERATE_BASE}/${req.model}:generateContent`,
|
|
1386
|
+
method: "POST",
|
|
1387
|
+
headers: authHeaders(apiKey),
|
|
1388
|
+
body: {
|
|
1389
|
+
contents: req.contents,
|
|
1390
|
+
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
function extractText(resp) {
|
|
1395
|
+
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
|
1396
|
+
return parts.filter((p) => p.text !== void 0).map((p) => p.text ?? "").join("");
|
|
1397
|
+
}
|
|
1398
|
+
function extractAudio(resp) {
|
|
1399
|
+
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
|
1400
|
+
for (const p of parts) {
|
|
1401
|
+
if (p.inlineData?.mimeType.startsWith("audio/")) {
|
|
1402
|
+
return { mimeType: p.inlineData.mimeType, data: p.inlineData.data };
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return null;
|
|
1406
|
+
}
|
|
1407
|
+
function extractImages(resp) {
|
|
1408
|
+
const parts = resp.candidates?.[0]?.content?.parts ?? [];
|
|
1409
|
+
return parts.filter(
|
|
1410
|
+
(p) => p.inlineData !== void 0
|
|
1411
|
+
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
1412
|
+
}
|
|
1413
|
+
function inlinePart(filePath) {
|
|
1414
|
+
const data = fs7.readFileSync(filePath).toString("base64");
|
|
1415
|
+
const mimeType = getMimeType(filePath);
|
|
1416
|
+
return { inlineData: { mimeType, data } };
|
|
1417
|
+
}
|
|
1418
|
+
function shouldUseFileApi(filePath) {
|
|
1419
|
+
const stat = fs7.statSync(filePath);
|
|
1420
|
+
return stat.size > 20 * 1024 * 1024;
|
|
1421
|
+
}
|
|
1422
|
+
function sleep(ms) {
|
|
1423
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
// src/providers/gemini/models.ts
|
|
1427
|
+
var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
|
|
1428
|
+
var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
|
|
1429
|
+
var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
|
|
1430
|
+
var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
|
|
1431
|
+
var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
|
|
1432
|
+
var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
|
|
1433
|
+
var IMAGEN_MODELS = /* @__PURE__ */ new Set([
|
|
1434
|
+
"imagen-4.0-generate-001",
|
|
1435
|
+
"imagen-4.0-ultra-generate-001",
|
|
1436
|
+
"imagen-4.0-fast-generate-001"
|
|
1437
|
+
]);
|
|
1438
|
+
var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
1439
|
+
"gemini-3.1-flash-image-preview",
|
|
1440
|
+
"gemini-3-pro-image-preview",
|
|
1441
|
+
"gemini-2.5-flash-image",
|
|
1442
|
+
"gemini-2.5-flash-image-preview",
|
|
1443
|
+
...IMAGEN_MODELS
|
|
1444
|
+
]);
|
|
1445
|
+
var ASPECT_RATIOS = [
|
|
1446
|
+
"1:1",
|
|
1447
|
+
"2:3",
|
|
1448
|
+
"3:2",
|
|
1449
|
+
"3:4",
|
|
1450
|
+
"4:3",
|
|
1451
|
+
"4:5",
|
|
1452
|
+
"5:4",
|
|
1453
|
+
"9:16",
|
|
1454
|
+
"16:9",
|
|
1455
|
+
"21:9"
|
|
1456
|
+
];
|
|
1457
|
+
var IMAGE_SIZES = ["1K", "2K", "4K"];
|
|
1458
|
+
function getDefaultModel(task) {
|
|
1459
|
+
if (task === "generate") {
|
|
1460
|
+
return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
|
|
1461
|
+
}
|
|
1462
|
+
if (task === "generate-video") {
|
|
1463
|
+
return process.env.VIDEO_GEN_MODEL ?? VIDEO_MODEL_DEFAULT;
|
|
1464
|
+
}
|
|
1465
|
+
if (task === "generate-speech") {
|
|
1466
|
+
return process.env.GEMINI_TTS_MODEL ?? process.env.TTS_MODEL ?? TTS_MODEL_DEFAULT;
|
|
1467
|
+
}
|
|
1468
|
+
return process.env.MULTIMODAL_MODEL ?? process.env.GEMINI_MODEL ?? ANALYSIS_MODEL_DEFAULT;
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
// src/providers/gemini/result-formatter.ts
|
|
1472
|
+
import fs8 from "fs";
|
|
1473
|
+
import path14 from "path";
|
|
447
1474
|
function formatResults(results, format, outputPath) {
|
|
448
1475
|
const content = renderFormat(results, format);
|
|
449
1476
|
if (outputPath) {
|
|
450
|
-
|
|
451
|
-
|
|
1477
|
+
fs8.mkdirSync(path14.dirname(path14.resolve(outputPath)), { recursive: true });
|
|
1478
|
+
fs8.writeFileSync(outputPath, content, "utf8");
|
|
452
1479
|
} else {
|
|
453
1480
|
process.stdout.write(content);
|
|
454
1481
|
}
|
|
455
1482
|
}
|
|
456
|
-
function renderFormat(results, format) {
|
|
457
|
-
switch (format) {
|
|
458
|
-
case "json":
|
|
459
|
-
return `${JSON.stringify(results, null, 2)}
|
|
460
|
-
`;
|
|
461
|
-
case "csv":
|
|
462
|
-
return renderCsv(results);
|
|
463
|
-
case "markdown":
|
|
464
|
-
return renderMarkdown(results);
|
|
465
|
-
default:
|
|
466
|
-
return renderText(results);
|
|
467
|
-
}
|
|
1483
|
+
function renderFormat(results, format) {
|
|
1484
|
+
switch (format) {
|
|
1485
|
+
case "json":
|
|
1486
|
+
return `${JSON.stringify(results, null, 2)}
|
|
1487
|
+
`;
|
|
1488
|
+
case "csv":
|
|
1489
|
+
return renderCsv(results);
|
|
1490
|
+
case "markdown":
|
|
1491
|
+
return renderMarkdown(results);
|
|
1492
|
+
default:
|
|
1493
|
+
return renderText(results);
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
function renderText(results) {
|
|
1497
|
+
return results.map((r) => {
|
|
1498
|
+
const header = `[${r.file}]
|
|
1499
|
+
Status: ${r.status}`;
|
|
1500
|
+
if (r.status === "success" && r.response) return `${header}
|
|
1501
|
+
Result:
|
|
1502
|
+
${r.response}
|
|
1503
|
+
`;
|
|
1504
|
+
if (r.status === "error") return `${header}
|
|
1505
|
+
Error: ${r.error}
|
|
1506
|
+
`;
|
|
1507
|
+
return `${header}
|
|
1508
|
+
`;
|
|
1509
|
+
}).join("\n");
|
|
1510
|
+
}
|
|
1511
|
+
function renderCsv(results) {
|
|
1512
|
+
const header = "file,status,response,error\n";
|
|
1513
|
+
const rows = results.map((r) => {
|
|
1514
|
+
const csvCell = (v) => `"${(v ?? "").replace(/"/g, '""')}"`;
|
|
1515
|
+
return [csvCell(r.file), csvCell(r.status), csvCell(r.response), csvCell(r.error)].join(",");
|
|
1516
|
+
});
|
|
1517
|
+
return `${header + rows.join("\n")}
|
|
1518
|
+
`;
|
|
1519
|
+
}
|
|
1520
|
+
function renderMarkdown(results) {
|
|
1521
|
+
const lines = ["# Results\n"];
|
|
1522
|
+
for (const r of results) {
|
|
1523
|
+
lines.push(`## ${r.file}
|
|
1524
|
+
`);
|
|
1525
|
+
lines.push(`**Status**: ${r.status}
|
|
1526
|
+
`);
|
|
1527
|
+
if (r.status === "success" && r.response) lines.push(`**Response**:
|
|
1528
|
+
|
|
1529
|
+
${r.response}
|
|
1530
|
+
`);
|
|
1531
|
+
if (r.status === "error") lines.push(`**Error**: ${r.error}
|
|
1532
|
+
`);
|
|
1533
|
+
lines.push("---\n");
|
|
1534
|
+
}
|
|
1535
|
+
return lines.join("\n");
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// src/providers/gemini/commands/analyze.ts
|
|
1539
|
+
function registerAnalyzeCommand(parent) {
|
|
1540
|
+
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(
|
|
1541
|
+
async (opts) => {
|
|
1542
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1543
|
+
const model = opts.model ?? getDefaultModel("analyze");
|
|
1544
|
+
for (const f of opts.files) {
|
|
1545
|
+
if (!fs9.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1546
|
+
}
|
|
1547
|
+
const results = [];
|
|
1548
|
+
for (let i = 0; i < opts.files.length; i++) {
|
|
1549
|
+
const filePath = opts.files[i];
|
|
1550
|
+
logger.debug(`[${i + 1}/${opts.files.length}] Processing: ${filePath}`);
|
|
1551
|
+
try {
|
|
1552
|
+
const useApi = shouldUseFileApi(filePath);
|
|
1553
|
+
let contentParts;
|
|
1554
|
+
if (useApi) {
|
|
1555
|
+
logger.debug("File >20MB \u2014 using Files API");
|
|
1556
|
+
const ref = await uploadFile(filePath, { logger });
|
|
1557
|
+
contentParts = [
|
|
1558
|
+
{ text: opts.prompt },
|
|
1559
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
1560
|
+
];
|
|
1561
|
+
} else {
|
|
1562
|
+
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
1563
|
+
}
|
|
1564
|
+
const resp = await generateContent({
|
|
1565
|
+
model,
|
|
1566
|
+
contents: [{ parts: contentParts }]
|
|
1567
|
+
});
|
|
1568
|
+
results.push({
|
|
1569
|
+
file: filePath,
|
|
1570
|
+
status: "success",
|
|
1571
|
+
response: extractText(resp)
|
|
1572
|
+
});
|
|
1573
|
+
} catch (e) {
|
|
1574
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1575
|
+
logger.error(`Failed: ${filePath} \u2014 ${msg}`);
|
|
1576
|
+
results.push({ file: filePath, status: "error", error: msg });
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
formatResults(results, opts.format, opts.output);
|
|
1580
|
+
logger.debug(
|
|
1581
|
+
`Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
|
|
1582
|
+
);
|
|
1583
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
1584
|
+
if (failed > 0) process.exit(1);
|
|
1585
|
+
}
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/providers/gemini/commands/extract.ts
|
|
1590
|
+
init_errors();
|
|
1591
|
+
import fs10 from "fs";
|
|
1592
|
+
function registerExtractCommand(parent) {
|
|
1593
|
+
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(
|
|
1594
|
+
async (opts) => {
|
|
1595
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1596
|
+
const model = opts.model ?? getDefaultModel("extract");
|
|
1597
|
+
for (const f of opts.files) {
|
|
1598
|
+
if (!fs10.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1599
|
+
}
|
|
1600
|
+
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
1601
|
+
const results = [];
|
|
1602
|
+
for (let i = 0; i < opts.files.length; i++) {
|
|
1603
|
+
const filePath = opts.files[i];
|
|
1604
|
+
logger.debug(`[${i + 1}/${opts.files.length}] Extracting: ${filePath}`);
|
|
1605
|
+
try {
|
|
1606
|
+
const useApi = shouldUseFileApi(filePath);
|
|
1607
|
+
let contentParts;
|
|
1608
|
+
if (useApi) {
|
|
1609
|
+
const ref = await uploadFile(filePath, { logger });
|
|
1610
|
+
contentParts = [
|
|
1611
|
+
{ text: opts.prompt },
|
|
1612
|
+
{ fileData: { mimeType: ref.mimeType, fileUri: ref.uri } }
|
|
1613
|
+
];
|
|
1614
|
+
} else {
|
|
1615
|
+
contentParts = [{ text: opts.prompt }, inlinePart(filePath)];
|
|
1616
|
+
}
|
|
1617
|
+
const resp = await generateContent({
|
|
1618
|
+
model,
|
|
1619
|
+
contents: [{ parts: contentParts }],
|
|
1620
|
+
generationConfig
|
|
1621
|
+
});
|
|
1622
|
+
results.push({ file: filePath, status: "success", response: extractText(resp) });
|
|
1623
|
+
} catch (e) {
|
|
1624
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1625
|
+
logger.error(`Failed: ${filePath} \u2014 ${msg}`);
|
|
1626
|
+
results.push({ file: filePath, status: "error", error: msg });
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
formatResults(results, opts.format, opts.output);
|
|
1630
|
+
const failed = results.filter((r) => r.status === "error").length;
|
|
1631
|
+
if (failed > 0) process.exit(1);
|
|
1632
|
+
}
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// src/providers/gemini/commands/generate-speech.ts
|
|
1637
|
+
init_errors();
|
|
1638
|
+
|
|
1639
|
+
// src/providers/gemini/generators/speech.ts
|
|
1640
|
+
import fs11 from "fs";
|
|
1641
|
+
import path15 from "path";
|
|
1642
|
+
|
|
1643
|
+
// src/providers/gemini/voices.ts
|
|
1644
|
+
var GEMINI_TTS_VOICES = [
|
|
1645
|
+
"Zephyr",
|
|
1646
|
+
"Puck",
|
|
1647
|
+
"Charon",
|
|
1648
|
+
"Kore",
|
|
1649
|
+
"Fenrir",
|
|
1650
|
+
"Leda",
|
|
1651
|
+
"Orus",
|
|
1652
|
+
"Aoede",
|
|
1653
|
+
"Callirrhoe",
|
|
1654
|
+
"Autonoe",
|
|
1655
|
+
"Enceladus",
|
|
1656
|
+
"Iapetus",
|
|
1657
|
+
"Umbriel",
|
|
1658
|
+
"Algieba",
|
|
1659
|
+
"Despina",
|
|
1660
|
+
"Erinome",
|
|
1661
|
+
"Algenib",
|
|
1662
|
+
"Rasalgethi",
|
|
1663
|
+
"Laomedeia",
|
|
1664
|
+
"Achernar",
|
|
1665
|
+
"Alnilam",
|
|
1666
|
+
"Schedar",
|
|
1667
|
+
"Gacrux",
|
|
1668
|
+
"Pulcherrima",
|
|
1669
|
+
"Achird",
|
|
1670
|
+
"Zubenelgenubi",
|
|
1671
|
+
"Vindemiatrix",
|
|
1672
|
+
"Sadachbia",
|
|
1673
|
+
"Sadaltager",
|
|
1674
|
+
"Sulafat"
|
|
1675
|
+
];
|
|
1676
|
+
var GEMINI_TTS_MODELS = /* @__PURE__ */ new Set([
|
|
1677
|
+
"gemini-3.1-flash-tts-preview",
|
|
1678
|
+
"gemini-2.5-flash-preview-tts",
|
|
1679
|
+
"gemini-2.5-pro-preview-tts"
|
|
1680
|
+
]);
|
|
1681
|
+
var TTS_VOICE_DEFAULT = "Kore";
|
|
1682
|
+
var TTS_PCM_SAMPLE_RATE = 24e3;
|
|
1683
|
+
var TTS_PCM_CHANNELS = 1;
|
|
1684
|
+
var TTS_PCM_BITS_PER_SAMPLE = 16;
|
|
1685
|
+
var TTS_OUTPUT_FORMATS = ["wav", "pcm"];
|
|
1686
|
+
function isValidGeminiVoice(name) {
|
|
1687
|
+
return GEMINI_TTS_VOICES.includes(name);
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
// src/providers/gemini/generators/speech.ts
|
|
1691
|
+
async function generateGeminiSpeech(opts) {
|
|
1692
|
+
const { text, model, voice, speakers, outputFormat, output, logger } = opts;
|
|
1693
|
+
const speechConfig = speakers && speakers.length > 0 ? {
|
|
1694
|
+
multiSpeakerVoiceConfig: {
|
|
1695
|
+
speakerVoiceConfigs: speakers.map((s) => ({
|
|
1696
|
+
speaker: s.speaker,
|
|
1697
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: s.voice } }
|
|
1698
|
+
}))
|
|
1699
|
+
}
|
|
1700
|
+
} : {
|
|
1701
|
+
voiceConfig: { prebuiltVoiceConfig: { voiceName: voice } }
|
|
1702
|
+
};
|
|
1703
|
+
const generationConfig = {
|
|
1704
|
+
responseModalities: ["AUDIO"],
|
|
1705
|
+
speechConfig
|
|
1706
|
+
};
|
|
1707
|
+
logger?.debug(`Gemini TTS model=${model} mode=${speakers?.length ? "multi" : "single"}`);
|
|
1708
|
+
let resp;
|
|
1709
|
+
try {
|
|
1710
|
+
resp = await generateContent({
|
|
1711
|
+
model,
|
|
1712
|
+
contents: [{ parts: [{ text }] }],
|
|
1713
|
+
generationConfig
|
|
1714
|
+
});
|
|
1715
|
+
} catch (e) {
|
|
1716
|
+
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
1717
|
+
}
|
|
1718
|
+
const audio = extractAudio(resp);
|
|
1719
|
+
if (!audio) return { status: "error", error: "No audio in response" };
|
|
1720
|
+
const pcmBytes = Buffer.from(audio.data, "base64");
|
|
1721
|
+
const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
|
|
1722
|
+
const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
|
|
1723
|
+
const outDir = getOutputDir();
|
|
1724
|
+
const dest = path15.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
1725
|
+
fs11.writeFileSync(dest, outBytes);
|
|
1726
|
+
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
1727
|
+
if (output) {
|
|
1728
|
+
fs11.mkdirSync(path15.dirname(path15.resolve(output)), { recursive: true });
|
|
1729
|
+
fs11.copyFileSync(dest, output);
|
|
1730
|
+
logger?.success(`Copied to: ${output}`);
|
|
1731
|
+
}
|
|
1732
|
+
return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
|
|
1733
|
+
}
|
|
1734
|
+
function parseSampleRate(mimeType) {
|
|
1735
|
+
const m = mimeType.match(/rate=(\d+)/i);
|
|
1736
|
+
return m ? Number.parseInt(m[1], 10) : null;
|
|
1737
|
+
}
|
|
1738
|
+
function wrapPcmInWav(pcm, sampleRate, channels, bitsPerSample) {
|
|
1739
|
+
const byteRate = sampleRate * channels * bitsPerSample / 8;
|
|
1740
|
+
const blockAlign = channels * bitsPerSample / 8;
|
|
1741
|
+
const dataSize = pcm.length;
|
|
1742
|
+
const header = Buffer.alloc(44);
|
|
1743
|
+
header.write("RIFF", 0, "ascii");
|
|
1744
|
+
header.writeUInt32LE(36 + dataSize, 4);
|
|
1745
|
+
header.write("WAVE", 8, "ascii");
|
|
1746
|
+
header.write("fmt ", 12, "ascii");
|
|
1747
|
+
header.writeUInt32LE(16, 16);
|
|
1748
|
+
header.writeUInt16LE(1, 20);
|
|
1749
|
+
header.writeUInt16LE(channels, 22);
|
|
1750
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
1751
|
+
header.writeUInt32LE(byteRate, 28);
|
|
1752
|
+
header.writeUInt16LE(blockAlign, 32);
|
|
1753
|
+
header.writeUInt16LE(bitsPerSample, 34);
|
|
1754
|
+
header.write("data", 36, "ascii");
|
|
1755
|
+
header.writeUInt32LE(dataSize, 40);
|
|
1756
|
+
return Buffer.concat([header, pcm]);
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
// src/providers/gemini/commands/generate-speech.ts
|
|
1760
|
+
function registerGeminiGenerateSpeechCommand(parent) {
|
|
1761
|
+
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(
|
|
1762
|
+
"--speaker <name:voice>",
|
|
1763
|
+
"Speaker mapping (repeatable, max 2 \u2014 switches to multi-speaker mode)",
|
|
1764
|
+
collectSpeakers,
|
|
1765
|
+
[]
|
|
1766
|
+
).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(
|
|
1767
|
+
async (opts) => {
|
|
1768
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1769
|
+
const text = opts.text ?? opts.prompt;
|
|
1770
|
+
if (!text) throw new ValidationError("Either --text or --prompt is required");
|
|
1771
|
+
const model = opts.model ?? getDefaultModel("generate-speech");
|
|
1772
|
+
if (!GEMINI_TTS_MODELS.has(model)) {
|
|
1773
|
+
throw new ValidationError(`--model must be one of: ${[...GEMINI_TTS_MODELS].join(", ")}`);
|
|
1774
|
+
}
|
|
1775
|
+
if (!TTS_OUTPUT_FORMATS.includes(opts.outputFormat)) {
|
|
1776
|
+
throw new ValidationError(
|
|
1777
|
+
`--output-format must be one of: ${TTS_OUTPUT_FORMATS.join(", ")}`
|
|
1778
|
+
);
|
|
1779
|
+
}
|
|
1780
|
+
const speakers = opts.speaker;
|
|
1781
|
+
if (speakers.length > 2) {
|
|
1782
|
+
throw new ValidationError("Gemini TTS supports max 2 speakers in multi-speaker mode");
|
|
1783
|
+
}
|
|
1784
|
+
if (speakers.length === 0 && !isValidGeminiVoice(opts.voice)) {
|
|
1785
|
+
throw new ValidationError(
|
|
1786
|
+
`Invalid --voice "${opts.voice}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
for (const sv of speakers) {
|
|
1790
|
+
if (!isValidGeminiVoice(sv.voice)) {
|
|
1791
|
+
throw new ValidationError(
|
|
1792
|
+
`Invalid voice "${sv.voice}" for speaker "${sv.speaker}". Valid: ${GEMINI_TTS_VOICES.join(", ")}`
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
const result = await generateGeminiSpeech({
|
|
1797
|
+
text,
|
|
1798
|
+
model,
|
|
1799
|
+
voice: speakers.length === 0 ? opts.voice : void 0,
|
|
1800
|
+
speakers: speakers.length > 0 ? speakers : void 0,
|
|
1801
|
+
outputFormat: opts.outputFormat,
|
|
1802
|
+
output: opts.output,
|
|
1803
|
+
logger
|
|
1804
|
+
});
|
|
1805
|
+
if (result.status === "error") {
|
|
1806
|
+
logger.error(result.error ?? "Unknown error");
|
|
1807
|
+
process.exit(1);
|
|
1808
|
+
}
|
|
1809
|
+
console.log(`
|
|
1810
|
+
Generated audio: ${result.generatedAudio}`);
|
|
1811
|
+
}
|
|
1812
|
+
);
|
|
1813
|
+
}
|
|
1814
|
+
function collectSpeakers(value, prev) {
|
|
1815
|
+
const idx = value.indexOf(":");
|
|
1816
|
+
if (idx <= 0 || idx === value.length - 1) {
|
|
1817
|
+
throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
|
|
1818
|
+
}
|
|
1819
|
+
const speaker = value.slice(0, idx).trim();
|
|
1820
|
+
const voice = value.slice(idx + 1).trim();
|
|
1821
|
+
if (!speaker || !voice) {
|
|
1822
|
+
throw new ValidationError(`--speaker must be "name:voice" (got "${value}")`);
|
|
1823
|
+
}
|
|
1824
|
+
return [...prev, { speaker, voice }];
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
// src/providers/gemini/commands/generate-video.ts
|
|
1828
|
+
import fs12 from "fs";
|
|
1829
|
+
import path16 from "path";
|
|
1830
|
+
init_errors();
|
|
1831
|
+
init_http_client();
|
|
1832
|
+
var BASE2 = "https://generativelanguage.googleapis.com";
|
|
1833
|
+
function registerGenerateVideoCommand(parent) {
|
|
1834
|
+
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(
|
|
1835
|
+
"--reference-images <paths...>",
|
|
1836
|
+
"Reference images (first=opening frame, second=closing)"
|
|
1837
|
+
).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(
|
|
1838
|
+
async (opts) => {
|
|
1839
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1840
|
+
const apiKey = resolveKey("GEMINI_API_KEY");
|
|
1841
|
+
if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
|
|
1842
|
+
const model = opts.model ?? getDefaultModel("generate-video");
|
|
1843
|
+
if (!model.startsWith("veo-")) {
|
|
1844
|
+
throw new ValidationError(
|
|
1845
|
+
`Video generation requires a Veo model (got '${model}'). Use: veo-3.1-generate-preview, veo-3.0-generate-001, etc.`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
if (!["720p", "1080p"].includes(opts.resolution)) {
|
|
1849
|
+
throw new ValidationError("--resolution must be 720p or 1080p");
|
|
1850
|
+
}
|
|
1851
|
+
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
1852
|
+
throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
|
|
1853
|
+
}
|
|
1854
|
+
logger.debug(`Submitting video generation with ${model}...`);
|
|
1855
|
+
logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
|
|
1856
|
+
const config = {
|
|
1857
|
+
aspectRatio: opts.aspectRatio,
|
|
1858
|
+
resolution: opts.resolution
|
|
1859
|
+
};
|
|
1860
|
+
const contentParts = [{ text: opts.prompt }];
|
|
1861
|
+
if (opts.referenceImages?.length) {
|
|
1862
|
+
for (const imgPath of opts.referenceImages.slice(0, 2)) {
|
|
1863
|
+
if (!fs12.existsSync(imgPath))
|
|
1864
|
+
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
1865
|
+
const data = fs12.readFileSync(imgPath).toString("base64");
|
|
1866
|
+
const ext = path16.extname(imgPath).toLowerCase();
|
|
1867
|
+
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
1868
|
+
contentParts.push({ inlineData: { mimeType, data } });
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
|
|
1872
|
+
let operation;
|
|
1873
|
+
try {
|
|
1874
|
+
operation = await httpJson({
|
|
1875
|
+
url: `${BASE2}/v1beta/models/${model}:generateVideos`,
|
|
1876
|
+
method: "POST",
|
|
1877
|
+
headers,
|
|
1878
|
+
body: {
|
|
1879
|
+
contents: [{ parts: contentParts }],
|
|
1880
|
+
generationConfig: config
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
} catch (e) {
|
|
1884
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1885
|
+
throw new ProviderError(`Video generation submit failed: ${msg}`, "gemini");
|
|
1886
|
+
}
|
|
1887
|
+
const operationName = operation.name;
|
|
1888
|
+
logger.debug(`Operation: ${operationName}`);
|
|
1889
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
1890
|
+
let done = false;
|
|
1891
|
+
let finalOp = operation;
|
|
1892
|
+
while (!done && Date.now() < deadline) {
|
|
1893
|
+
await sleep2(1e4);
|
|
1894
|
+
finalOp = await httpJson({
|
|
1895
|
+
url: `${BASE2}/v1beta/${operationName}`,
|
|
1896
|
+
headers
|
|
1897
|
+
});
|
|
1898
|
+
done = finalOp.done === true;
|
|
1899
|
+
if (!done)
|
|
1900
|
+
logger.debug(
|
|
1901
|
+
`Still generating... (${Math.round((deadline - Date.now()) / 1e3)}s left)`
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
if (!done) throw new ProviderError("Video generation timed out after 10 minutes", "gemini");
|
|
1905
|
+
if (finalOp.error)
|
|
1906
|
+
throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
|
|
1907
|
+
const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
|
|
1908
|
+
if (!videoUri) {
|
|
1909
|
+
throw new ProviderError(
|
|
1910
|
+
`Unexpected Veo response structure: ${JSON.stringify(finalOp.response)}`,
|
|
1911
|
+
"gemini"
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
const outDir = getOutputDir();
|
|
1915
|
+
const dest = path16.join(outDir, `veo_generated_${Date.now()}.mp4`);
|
|
1916
|
+
const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
|
|
1917
|
+
await downloadFile2(videoUri, dest);
|
|
1918
|
+
logger.success(`Saved video: ${dest}`);
|
|
1919
|
+
if (opts.output) {
|
|
1920
|
+
fs12.mkdirSync(path16.dirname(path16.resolve(opts.output)), { recursive: true });
|
|
1921
|
+
fs12.copyFileSync(dest, opts.output);
|
|
1922
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
1923
|
+
}
|
|
1924
|
+
await maybeDownloadThumb(finalOp.response, dest, {
|
|
1925
|
+
skip: opts.thumb === false,
|
|
1926
|
+
copyTo: opts.output,
|
|
1927
|
+
logger
|
|
1928
|
+
});
|
|
1929
|
+
console.log(`
|
|
1930
|
+
Generated video: ${dest}`);
|
|
1931
|
+
}
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1934
|
+
function sleep2(ms) {
|
|
1935
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// src/providers/gemini/commands/generate.ts
|
|
1939
|
+
init_errors();
|
|
1940
|
+
import fs13 from "fs";
|
|
1941
|
+
import path17 from "path";
|
|
1942
|
+
function registerGenerateCommand(parent) {
|
|
1943
|
+
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(
|
|
1944
|
+
async (opts) => {
|
|
1945
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1946
|
+
const model = opts.model ?? getDefaultModel("generate");
|
|
1947
|
+
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 4);
|
|
1948
|
+
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
1949
|
+
throw new ValidationError(
|
|
1950
|
+
`Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
|
|
1951
|
+
);
|
|
1952
|
+
}
|
|
1953
|
+
if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
|
|
1954
|
+
throw new ValidationError(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
|
|
1955
|
+
}
|
|
1956
|
+
logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
|
|
1957
|
+
logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
|
|
1958
|
+
const imageConfig = { aspectRatio: opts.aspectRatio };
|
|
1959
|
+
if (opts.size) imageConfig.imageSize = opts.size;
|
|
1960
|
+
const generationConfig = {
|
|
1961
|
+
responseModalities: ["IMAGE"],
|
|
1962
|
+
imageConfig
|
|
1963
|
+
};
|
|
1964
|
+
const savedFiles = [];
|
|
1965
|
+
for (let i = 0; i < numImages; i++) {
|
|
1966
|
+
try {
|
|
1967
|
+
const resp = await generateContent({
|
|
1968
|
+
model,
|
|
1969
|
+
contents: [{ parts: [{ text: opts.prompt }] }],
|
|
1970
|
+
generationConfig
|
|
1971
|
+
});
|
|
1972
|
+
const images = extractImages(resp);
|
|
1973
|
+
if (images.length === 0) {
|
|
1974
|
+
logger.warn(
|
|
1975
|
+
`Request ${i + 1}: no images in response \u2014 model may not support image generation`
|
|
1976
|
+
);
|
|
1977
|
+
continue;
|
|
1978
|
+
}
|
|
1979
|
+
for (const img of images) {
|
|
1980
|
+
const outDir = getOutputDir();
|
|
1981
|
+
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
1982
|
+
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
1983
|
+
const dest = path17.join(outDir, fname);
|
|
1984
|
+
fs13.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
1985
|
+
savedFiles.push(dest);
|
|
1986
|
+
logger.success(`Saved: ${dest}`);
|
|
1987
|
+
}
|
|
1988
|
+
} catch (e) {
|
|
1989
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1990
|
+
logger.error(`Generation failed: ${msg}`);
|
|
1991
|
+
process.exit(1);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (savedFiles.length === 0) {
|
|
1995
|
+
logger.error("No images were generated");
|
|
1996
|
+
process.exit(1);
|
|
1997
|
+
}
|
|
1998
|
+
if (opts.output && savedFiles[0]) {
|
|
1999
|
+
fs13.mkdirSync(path17.dirname(path17.resolve(opts.output)), { recursive: true });
|
|
2000
|
+
fs13.copyFileSync(savedFiles[0], opts.output);
|
|
2001
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
2002
|
+
}
|
|
2003
|
+
console.log(`
|
|
2004
|
+
Generated ${savedFiles.length} image(s):`);
|
|
2005
|
+
for (const f of savedFiles) console.log(` ${f}`);
|
|
2006
|
+
}
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
|
|
2010
|
+
// src/providers/gemini/commands/image-to-image.ts
|
|
2011
|
+
import fs14 from "fs";
|
|
2012
|
+
import path18 from "path";
|
|
2013
|
+
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
2014
|
+
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
2015
|
+
function registerGeminiImageToImageCommand(parent) {
|
|
2016
|
+
parent.command("image-to-image").alias("i2i").description(
|
|
2017
|
+
"Edit/compose images with Gemini 2.5 Flash Image (Nano Banana) using one or more reference images"
|
|
2018
|
+
).requiredOption("--prompt <text>", "Text prompt describing the edit/transformation").requiredOption(
|
|
2019
|
+
"--ref <path>",
|
|
2020
|
+
"Reference image (URL or local path); repeatable for multi-ref",
|
|
2021
|
+
collect4,
|
|
2022
|
+
[]
|
|
2023
|
+
).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(
|
|
2024
|
+
async (opts) => {
|
|
2025
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
2026
|
+
throw new Error("At least one --ref is required for image-to-image.");
|
|
2027
|
+
}
|
|
2028
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2029
|
+
const model = opts.model ?? IMAGE_MODEL_FALLBACK;
|
|
2030
|
+
const parts = [];
|
|
2031
|
+
for (const r of opts.ref) {
|
|
2032
|
+
const resolved = await resolveImageInput(r, {
|
|
2033
|
+
logger,
|
|
2034
|
+
softLimitBytes: SOFT_LIMIT_BYTES,
|
|
2035
|
+
hardLimitBytes: HARD_LIMIT_BYTES
|
|
2036
|
+
});
|
|
2037
|
+
if (resolved.kind === "url") {
|
|
2038
|
+
const buf = await fetchUrlAsBuffer(resolved.url);
|
|
2039
|
+
const mime = sniffImageMime(buf, resolved.url);
|
|
2040
|
+
if (buf.byteLength > HARD_LIMIT_BYTES) {
|
|
2041
|
+
throw new Error(
|
|
2042
|
+
`Reference ${resolved.url} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds Gemini ${HARD_LIMIT_BYTES / 1024 / 1024}MB inline cap.`
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
parts.push({ inlineData: { mimeType: mime, data: buf.toString("base64") } });
|
|
2046
|
+
} else {
|
|
2047
|
+
const data = resolved.dataUrl.split(",", 2)[1] ?? "";
|
|
2048
|
+
parts.push({ inlineData: { mimeType: resolved.mime, data } });
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
parts.push({ text: opts.prompt });
|
|
2052
|
+
logger.debug(`Calling ${model} with ${opts.ref.length} reference image(s)`);
|
|
2053
|
+
const req = {
|
|
2054
|
+
model,
|
|
2055
|
+
contents: [{ parts }],
|
|
2056
|
+
generationConfig: { responseModalities: ["IMAGE"] }
|
|
2057
|
+
};
|
|
2058
|
+
const resp = await generateContent(req);
|
|
2059
|
+
const images = extractImages(resp);
|
|
2060
|
+
if (images.length === 0) {
|
|
2061
|
+
logger.error(
|
|
2062
|
+
"No images in response \u2014 model may have refused or doesn't support image output."
|
|
2063
|
+
);
|
|
2064
|
+
process.exit(1);
|
|
2065
|
+
}
|
|
2066
|
+
const outDir = getOutputDir();
|
|
2067
|
+
const ts = Date.now();
|
|
2068
|
+
const saved = [];
|
|
2069
|
+
for (let i = 0; i < images.length; i++) {
|
|
2070
|
+
const img = images[i];
|
|
2071
|
+
if (!img) continue;
|
|
2072
|
+
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
2073
|
+
const dest = path18.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
2074
|
+
fs14.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
2075
|
+
saved.push(dest);
|
|
2076
|
+
logger.success(`Saved: ${dest}`);
|
|
2077
|
+
}
|
|
2078
|
+
if (opts.output && saved[0]) {
|
|
2079
|
+
fs14.mkdirSync(path18.dirname(path18.resolve(opts.output)), { recursive: true });
|
|
2080
|
+
fs14.copyFileSync(saved[0], opts.output);
|
|
2081
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
2082
|
+
}
|
|
2083
|
+
console.log(`
|
|
2084
|
+
Generated ${saved.length} image(s):`);
|
|
2085
|
+
for (const f of saved) console.log(` ${f}`);
|
|
2086
|
+
}
|
|
2087
|
+
);
|
|
468
2088
|
}
|
|
469
|
-
function
|
|
470
|
-
return
|
|
471
|
-
const header = `[${r.file}]
|
|
472
|
-
Status: ${r.status}`;
|
|
473
|
-
if (r.status === "success" && r.response) return `${header}
|
|
474
|
-
Result:
|
|
475
|
-
${r.response}
|
|
476
|
-
`;
|
|
477
|
-
if (r.status === "error") return `${header}
|
|
478
|
-
Error: ${r.error}
|
|
479
|
-
`;
|
|
480
|
-
return `${header}
|
|
481
|
-
`;
|
|
482
|
-
}).join("\n");
|
|
2089
|
+
function collect4(value, prev) {
|
|
2090
|
+
return [...prev, value];
|
|
483
2091
|
}
|
|
484
|
-
function
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
2092
|
+
async function fetchUrlAsBuffer(url) {
|
|
2093
|
+
const res = await fetch(url);
|
|
2094
|
+
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status} ${res.statusText}`);
|
|
2095
|
+
const arr = await res.arrayBuffer();
|
|
2096
|
+
return Buffer.from(arr);
|
|
2097
|
+
}
|
|
2098
|
+
function sniffImageMime(buf, url) {
|
|
2099
|
+
if (buf.length >= 4) {
|
|
2100
|
+
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
2101
|
+
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71)
|
|
2102
|
+
return "image/png";
|
|
2103
|
+
if (buf[0] === 71 && buf[1] === 73 && buf[2] === 70) return "image/gif";
|
|
2104
|
+
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
2105
|
+
return "image/webp";
|
|
2106
|
+
}
|
|
2107
|
+
const ext = path18.extname(new URL(url).pathname).toLowerCase();
|
|
2108
|
+
if (ext === ".png") return "image/png";
|
|
2109
|
+
if (ext === ".webp") return "image/webp";
|
|
2110
|
+
if (ext === ".gif") return "image/gif";
|
|
2111
|
+
return "image/jpeg";
|
|
492
2112
|
}
|
|
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
2113
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
2114
|
+
// src/providers/gemini/commands/image-to-video.ts
|
|
2115
|
+
import fs15 from "fs";
|
|
2116
|
+
import path19 from "path";
|
|
2117
|
+
init_errors();
|
|
2118
|
+
init_http_client();
|
|
2119
|
+
var BASE3 = "https://generativelanguage.googleapis.com";
|
|
2120
|
+
function imagePart(imgPath) {
|
|
2121
|
+
if (!fs15.existsSync(imgPath)) {
|
|
2122
|
+
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
507
2123
|
}
|
|
508
|
-
|
|
2124
|
+
const data = fs15.readFileSync(imgPath).toString("base64");
|
|
2125
|
+
const ext = path19.extname(imgPath).toLowerCase();
|
|
2126
|
+
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
2127
|
+
return { inlineData: { mimeType, data } };
|
|
2128
|
+
}
|
|
2129
|
+
function sleep3(ms) {
|
|
2130
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
2131
|
+
}
|
|
2132
|
+
function registerGeminiImageToVideoCommand(parent) {
|
|
2133
|
+
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(
|
|
2134
|
+
async (imagePath, opts) => {
|
|
2135
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2136
|
+
const apiKey = resolveKey("GEMINI_API_KEY");
|
|
2137
|
+
if (!apiKey) throw new ValidationError("GEMINI_API_KEY is not set");
|
|
2138
|
+
const model = opts.model ?? getDefaultModel("generate-video");
|
|
2139
|
+
if (!model.startsWith("veo-")) {
|
|
2140
|
+
throw new ValidationError(
|
|
2141
|
+
`Image-to-video requires a Veo model (got '${model}'). Try veo-3.1-generate-preview.`
|
|
2142
|
+
);
|
|
2143
|
+
}
|
|
2144
|
+
if (!["720p", "1080p"].includes(opts.resolution)) {
|
|
2145
|
+
throw new ValidationError("--resolution must be 720p or 1080p");
|
|
2146
|
+
}
|
|
2147
|
+
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
2148
|
+
throw new ValidationError(`Invalid aspect ratio: ${opts.aspectRatio}`);
|
|
2149
|
+
}
|
|
2150
|
+
const contentParts = [
|
|
2151
|
+
{ text: opts.prompt },
|
|
2152
|
+
imagePart(imagePath)
|
|
2153
|
+
];
|
|
2154
|
+
if (opts.lastFrame) contentParts.push(imagePart(opts.lastFrame));
|
|
2155
|
+
const headers = { "x-goog-api-key": apiKey, "Content-Type": "application/json" };
|
|
2156
|
+
logger.info(
|
|
2157
|
+
"Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
|
|
2158
|
+
);
|
|
2159
|
+
let operation;
|
|
2160
|
+
try {
|
|
2161
|
+
operation = await httpJson({
|
|
2162
|
+
url: `${BASE3}/v1beta/models/${model}:generateVideos`,
|
|
2163
|
+
method: "POST",
|
|
2164
|
+
headers,
|
|
2165
|
+
body: {
|
|
2166
|
+
contents: [{ parts: contentParts }],
|
|
2167
|
+
generationConfig: { aspectRatio: opts.aspectRatio, resolution: opts.resolution }
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
} catch (e) {
|
|
2171
|
+
throw new ProviderError(
|
|
2172
|
+
`Submit failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2173
|
+
"gemini"
|
|
2174
|
+
);
|
|
2175
|
+
}
|
|
2176
|
+
const operationName = operation.name;
|
|
2177
|
+
logger.debug(`Operation: ${operationName}`);
|
|
2178
|
+
const deadline = Date.now() + 10 * 60 * 1e3;
|
|
2179
|
+
let finalOp = operation;
|
|
2180
|
+
while (!finalOp.done && Date.now() < deadline) {
|
|
2181
|
+
await sleep3(1e4);
|
|
2182
|
+
finalOp = await httpJson({ url: `${BASE3}/v1beta/${operationName}`, headers });
|
|
2183
|
+
if (!finalOp.done) {
|
|
2184
|
+
logger.debug(`Still generating... ${Math.round((deadline - Date.now()) / 1e3)}s left`);
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
if (!finalOp.done) throw new ProviderError("Veo timed out after 10 minutes", "gemini");
|
|
2188
|
+
if (finalOp.error) {
|
|
2189
|
+
throw new ProviderError(`Veo error: ${JSON.stringify(finalOp.error)}`, "gemini");
|
|
2190
|
+
}
|
|
2191
|
+
const videoUri = finalOp.response?.generatedSamples?.[0]?.video?.uri ?? finalOp.response?.generatedVideos?.[0]?.video?.uri;
|
|
2192
|
+
if (!videoUri) {
|
|
2193
|
+
throw new ProviderError(
|
|
2194
|
+
`Unexpected Veo response: ${JSON.stringify(finalOp.response)}`,
|
|
2195
|
+
"gemini"
|
|
2196
|
+
);
|
|
2197
|
+
}
|
|
2198
|
+
const dest = path19.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
2199
|
+
await downloadFile(videoUri, dest);
|
|
2200
|
+
logger.success(`Saved video: ${dest}`);
|
|
2201
|
+
if (opts.output) {
|
|
2202
|
+
fs15.mkdirSync(path19.dirname(path19.resolve(opts.output)), { recursive: true });
|
|
2203
|
+
fs15.copyFileSync(dest, opts.output);
|
|
2204
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
2205
|
+
}
|
|
2206
|
+
await maybeDownloadThumb(finalOp.response, dest, {
|
|
2207
|
+
skip: opts.thumb === false,
|
|
2208
|
+
copyTo: opts.output,
|
|
2209
|
+
logger
|
|
2210
|
+
});
|
|
2211
|
+
console.log(`
|
|
2212
|
+
Generated video: ${dest}`);
|
|
2213
|
+
}
|
|
2214
|
+
);
|
|
509
2215
|
}
|
|
510
2216
|
|
|
511
|
-
// src/providers/gemini/commands/
|
|
512
|
-
|
|
513
|
-
|
|
2217
|
+
// src/providers/gemini/commands/transcribe.ts
|
|
2218
|
+
init_errors();
|
|
2219
|
+
import fs16 from "fs";
|
|
2220
|
+
function registerTranscribeCommand(parent) {
|
|
2221
|
+
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
2222
|
async (opts) => {
|
|
515
2223
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
516
|
-
const model = opts.model ?? getDefaultModel("
|
|
2224
|
+
const model = opts.model ?? getDefaultModel("transcribe");
|
|
517
2225
|
for (const f of opts.files) {
|
|
518
|
-
if (!
|
|
2226
|
+
if (!fs16.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
519
2227
|
}
|
|
520
2228
|
const results = [];
|
|
521
2229
|
for (let i = 0; i < opts.files.length; i++) {
|
|
522
2230
|
const filePath = opts.files[i];
|
|
523
|
-
logger.debug(`[${i + 1}/${opts.files.length}]
|
|
2231
|
+
logger.debug(`[${i + 1}/${opts.files.length}] Transcribing: ${filePath}`);
|
|
524
2232
|
try {
|
|
525
2233
|
const useApi = shouldUseFileApi(filePath);
|
|
526
2234
|
let contentParts;
|
|
@@ -550,312 +2258,710 @@ function registerAnalyzeCommand(parent) {
|
|
|
550
2258
|
}
|
|
551
2259
|
}
|
|
552
2260
|
formatResults(results, opts.format, opts.output);
|
|
553
|
-
logger.debug(
|
|
554
|
-
`Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
|
|
555
|
-
);
|
|
556
2261
|
const failed = results.filter((r) => r.status === "error").length;
|
|
557
2262
|
if (failed > 0) process.exit(1);
|
|
558
2263
|
}
|
|
559
2264
|
);
|
|
560
2265
|
}
|
|
561
2266
|
|
|
562
|
-
// src/providers/gemini/commands/
|
|
2267
|
+
// src/providers/gemini/commands/index.ts
|
|
2268
|
+
function registerGeminiCommands(program2) {
|
|
2269
|
+
const gemini = program2.command("gemini").description(
|
|
2270
|
+
"Gemini AI operations: analyze, transcribe, extract, generate images/video/speech"
|
|
2271
|
+
);
|
|
2272
|
+
registerAnalyzeCommand(gemini);
|
|
2273
|
+
registerTranscribeCommand(gemini);
|
|
2274
|
+
registerExtractCommand(gemini);
|
|
2275
|
+
registerGenerateCommand(gemini);
|
|
2276
|
+
registerGeminiImageToImageCommand(gemini);
|
|
2277
|
+
registerGenerateVideoCommand(gemini);
|
|
2278
|
+
registerGeminiImageToVideoCommand(gemini);
|
|
2279
|
+
registerGeminiGenerateSpeechCommand(gemini);
|
|
2280
|
+
}
|
|
2281
|
+
|
|
2282
|
+
// src/providers/leonardo/commands/generate.ts
|
|
2283
|
+
init_http_client();
|
|
2284
|
+
import fs17 from "fs";
|
|
2285
|
+
import path20 from "path";
|
|
2286
|
+
|
|
2287
|
+
// src/providers/leonardo/client.ts
|
|
563
2288
|
init_errors();
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
2289
|
+
init_http_client();
|
|
2290
|
+
|
|
2291
|
+
// src/providers/leonardo/models.ts
|
|
2292
|
+
var DEFAULT_LEONARDO_BASE_URL = "https://cloud.leonardo.ai/api/rest/v1";
|
|
2293
|
+
var DEFAULT_LEONARDO_MODEL_ID = "7b592283-e8a7-4c5a-9ba6-d18c31f258b9";
|
|
2294
|
+
var LEONARDO_DEFAULTS = {
|
|
2295
|
+
modelId: process.env.LEONARDO_DEFAULT_MODEL ?? DEFAULT_LEONARDO_MODEL_ID,
|
|
2296
|
+
videoModel: process.env.LEONARDO_VIDEO_MODEL ?? "MOTION2",
|
|
2297
|
+
width: 1024,
|
|
2298
|
+
height: 1024,
|
|
2299
|
+
numImages: 1
|
|
2300
|
+
};
|
|
2301
|
+
var LEONARDO_VIDEO_MODELS = [
|
|
2302
|
+
{ id: "MOTION2", name: "Motion 2.0", modes: ["t2v", "i2v"] },
|
|
2303
|
+
{ id: "MOTION2FAST", name: "Motion 2.0 Fast", modes: ["t2v", "i2v"] },
|
|
2304
|
+
{ id: "VEO3", name: "Veo 3.0", modes: ["t2v", "i2v"] },
|
|
2305
|
+
{ id: "VEO3FAST", name: "Veo 3.0 Fast", modes: ["t2v"] },
|
|
2306
|
+
{ id: "VEO3_1", name: "Veo 3.1", modes: ["t2v", "i2v"] },
|
|
2307
|
+
{ id: "KLING2_1", name: "Kling 2.1 Pro", modes: ["t2v", "i2v"] },
|
|
2308
|
+
{ id: "Kling2_5", name: "Kling 2.5 Turbo", modes: ["t2v", "i2v"] },
|
|
2309
|
+
{ id: "kling-2.6", name: "Kling 2.6", modes: ["t2v", "i2v"] },
|
|
2310
|
+
{ id: "kling-3.0", name: "Kling 3.0", modes: ["t2v"] },
|
|
2311
|
+
{ id: "kling-video-o-1", name: "Kling O1", modes: ["t2v", "i2v"] },
|
|
2312
|
+
{ id: "kling-video-o-3", name: "Kling O3", modes: ["t2v", "i2v"] },
|
|
2313
|
+
{ id: "hailuo-2_3", name: "Hailuo 2.3", modes: ["t2v", "i2v"] },
|
|
2314
|
+
{ id: "ltxv-2.0-pro", name: "LTX 2.0 Pro", modes: ["t2v", "i2v"] },
|
|
2315
|
+
{ id: "ltxv-2.3-pro", name: "LTX 2.3 Pro", modes: ["t2v", "i2v"] },
|
|
2316
|
+
{ id: "seedance-2.0", name: "Seedance 2.0", modes: ["t2v", "i2v"] },
|
|
2317
|
+
{ id: "seedance-2.0-fast", name: "Seedance 2.0 Fast", modes: ["t2v", "i2v"] }
|
|
2318
|
+
];
|
|
2319
|
+
var LEONARDO_VIDEO_RESOLUTIONS = [
|
|
2320
|
+
"RESOLUTION_480",
|
|
2321
|
+
"RESOLUTION_720",
|
|
2322
|
+
"RESOLUTION_1080"
|
|
2323
|
+
];
|
|
2324
|
+
var LEONARDO_V2_VIDEO_MODELS = /* @__PURE__ */ new Set([
|
|
2325
|
+
"kling-2.6",
|
|
2326
|
+
"kling-3.0",
|
|
2327
|
+
"kling-video-o-1",
|
|
2328
|
+
"kling-video-o-3",
|
|
2329
|
+
"hailuo-2_3",
|
|
2330
|
+
"ltxv-2.0-pro",
|
|
2331
|
+
"ltxv-2.3-pro",
|
|
2332
|
+
"seedance-2.0",
|
|
2333
|
+
"seedance-2.0-fast"
|
|
2334
|
+
]);
|
|
2335
|
+
function isGptImageModel(modelId) {
|
|
2336
|
+
return typeof modelId === "string" && modelId.startsWith("gpt-image-");
|
|
2337
|
+
}
|
|
2338
|
+
|
|
2339
|
+
// src/providers/leonardo/client.ts
|
|
2340
|
+
function requireLeonardoKey() {
|
|
2341
|
+
const key = resolveKey("LEONARDO_API_KEY");
|
|
2342
|
+
if (!key) {
|
|
2343
|
+
throw new ConfigError(
|
|
2344
|
+
"LEONARDO_API_KEY is not set. Get one at https://app.leonardo.ai/settings/api-keys"
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
return key;
|
|
2348
|
+
}
|
|
2349
|
+
function leonardoBaseUrl() {
|
|
2350
|
+
return resolveKey("LEONARDO_BASE_URL") ?? DEFAULT_LEONARDO_BASE_URL;
|
|
2351
|
+
}
|
|
2352
|
+
var LeonardoClient = class {
|
|
2353
|
+
constructor(apiKey, baseUrl = leonardoBaseUrl()) {
|
|
2354
|
+
this.apiKey = apiKey;
|
|
2355
|
+
this.baseUrl = baseUrl;
|
|
2356
|
+
}
|
|
2357
|
+
apiKey;
|
|
2358
|
+
baseUrl;
|
|
2359
|
+
async request(path36, opts = {}) {
|
|
2360
|
+
const base = this.baseUrl.replace(/\/$/, "");
|
|
2361
|
+
const v2Override = path36.startsWith("v2:");
|
|
2362
|
+
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
2363
|
+
const finalPath = v2Override ? path36.slice(3) : path36;
|
|
2364
|
+
const url = new URL(finalBase + finalPath);
|
|
2365
|
+
if (opts.query) {
|
|
2366
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
2367
|
+
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
572
2368
|
}
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
2369
|
+
}
|
|
2370
|
+
opts.logger?.debug(`${opts.method ?? "GET"} ${url.toString()}`);
|
|
2371
|
+
return httpJson({
|
|
2372
|
+
url: url.toString(),
|
|
2373
|
+
method: opts.method ?? "GET",
|
|
2374
|
+
headers: {
|
|
2375
|
+
accept: "application/json",
|
|
2376
|
+
authorization: `Bearer ${this.apiKey}`
|
|
2377
|
+
},
|
|
2378
|
+
body: opts.body,
|
|
2379
|
+
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
get(path36, query, logger) {
|
|
2383
|
+
return this.request(path36, { method: "GET", query, logger });
|
|
2384
|
+
}
|
|
2385
|
+
post(path36, body, logger) {
|
|
2386
|
+
return this.request(path36, { method: "POST", body, logger });
|
|
2387
|
+
}
|
|
2388
|
+
delete(path36, logger) {
|
|
2389
|
+
return this.request(path36, { method: "DELETE", logger });
|
|
2390
|
+
}
|
|
2391
|
+
};
|
|
2392
|
+
function createLeonardoClient() {
|
|
2393
|
+
return new LeonardoClient(requireLeonardoKey(), leonardoBaseUrl());
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
// src/providers/leonardo/commands/generate.ts
|
|
2397
|
+
function extFromUrl(url, fallback = ".png") {
|
|
2398
|
+
try {
|
|
2399
|
+
const u = new URL(url);
|
|
2400
|
+
const e = path20.extname(u.pathname);
|
|
2401
|
+
return e.length > 0 ? e : fallback;
|
|
2402
|
+
} catch {
|
|
2403
|
+
return fallback;
|
|
2404
|
+
}
|
|
2405
|
+
}
|
|
2406
|
+
function autoFilename(generationId, index, ext) {
|
|
2407
|
+
const e = ext.startsWith(".") ? ext : `.${ext}`;
|
|
2408
|
+
return `leonardo-${generationId.slice(0, 8)}-${String(index).padStart(2, "0")}${e}`;
|
|
2409
|
+
}
|
|
2410
|
+
function registerLeonardoGenerateCommand(parent) {
|
|
2411
|
+
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(
|
|
2412
|
+
async (prompt, opts) => {
|
|
2413
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2414
|
+
const client = createLeonardoClient();
|
|
2415
|
+
const input = {
|
|
2416
|
+
prompt,
|
|
2417
|
+
modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
|
|
2418
|
+
width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
|
|
2419
|
+
height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
|
|
2420
|
+
num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
|
|
2421
|
+
alchemy: opts.alchemy,
|
|
2422
|
+
ultra: opts.ultra,
|
|
2423
|
+
contrast: opts.contrast ? Number.parseFloat(opts.contrast) : void 0,
|
|
2424
|
+
styleUUID: opts.style,
|
|
2425
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
2426
|
+
negative_prompt: opts.negative,
|
|
2427
|
+
enhancePrompt: opts.enhance
|
|
2428
|
+
};
|
|
2429
|
+
logger.info(
|
|
2430
|
+
`Submitting generation (${input.width}x${input.height}, n=${input.num_images})`
|
|
2431
|
+
);
|
|
2432
|
+
let generationId;
|
|
2433
|
+
if (isGptImageModel(input.modelId)) {
|
|
2434
|
+
const v2Body = {
|
|
2435
|
+
public: false,
|
|
2436
|
+
model: input.modelId,
|
|
2437
|
+
parameters: {
|
|
2438
|
+
prompt: input.prompt,
|
|
2439
|
+
quantity: input.num_images ?? 1,
|
|
2440
|
+
width: input.width,
|
|
2441
|
+
height: input.height,
|
|
2442
|
+
quality: opts.quality,
|
|
2443
|
+
prompt_enhance: input.enhancePrompt ? "ON" : "OFF",
|
|
2444
|
+
...input.seed !== void 0 ? { seed: input.seed } : {}
|
|
589
2445
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
2446
|
+
};
|
|
2447
|
+
const v2Res = await client.post(
|
|
2448
|
+
"v2:/generations",
|
|
2449
|
+
v2Body,
|
|
2450
|
+
logger
|
|
2451
|
+
);
|
|
2452
|
+
generationId = v2Res.generate.generationId;
|
|
2453
|
+
} else {
|
|
2454
|
+
const createRes = await client.post(
|
|
2455
|
+
"/generations",
|
|
2456
|
+
input,
|
|
2457
|
+
logger
|
|
2458
|
+
);
|
|
2459
|
+
generationId = createRes.sdGenerationJob.generationId;
|
|
2460
|
+
}
|
|
2461
|
+
logger.debug(`generationId: ${generationId}`);
|
|
2462
|
+
logger.info("Polling for completion...");
|
|
2463
|
+
const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
|
|
2464
|
+
const intervalMs = 4e3;
|
|
2465
|
+
const final = await poll({
|
|
2466
|
+
fetch: () => client.get(`/generations/${generationId}`),
|
|
2467
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
|
|
2468
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2469
|
+
intervalMs,
|
|
2470
|
+
maxAttempts: Math.ceil(waitTimeout / intervalMs),
|
|
2471
|
+
onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
|
|
2472
|
+
});
|
|
2473
|
+
const images = final.generations_by_pk?.generated_images ?? [];
|
|
2474
|
+
if (opts.download === false) {
|
|
2475
|
+
for (const img of images) console.log(img.url);
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
const outDir = getOutputDir();
|
|
2479
|
+
const saved = [];
|
|
2480
|
+
for (let i = 0; i < images.length; i++) {
|
|
2481
|
+
const img = images[i];
|
|
2482
|
+
if (!img) continue;
|
|
2483
|
+
const ext = extFromUrl(img.url);
|
|
2484
|
+
const filename = autoFilename(generationId, i + 1, ext);
|
|
2485
|
+
const filepath = path20.join(outDir, filename);
|
|
2486
|
+
try {
|
|
2487
|
+
await downloadFile(img.url, filepath);
|
|
2488
|
+
saved.push(filepath);
|
|
2489
|
+
logger.success(`Saved ${filepath}`);
|
|
596
2490
|
} catch (e) {
|
|
597
|
-
|
|
598
|
-
logger.error(`Failed: ${filePath} \u2014 ${msg}`);
|
|
599
|
-
results.push({ file: filePath, status: "error", error: msg });
|
|
2491
|
+
logger.warn(`Download failed for ${img.id}: ${e instanceof Error ? e.message : e}`);
|
|
600
2492
|
}
|
|
601
2493
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
2494
|
+
if (opts.output && saved[0]) {
|
|
2495
|
+
const dest = path20.resolve(opts.output);
|
|
2496
|
+
fs17.mkdirSync(path20.dirname(dest), { recursive: true });
|
|
2497
|
+
fs17.copyFileSync(saved[0], dest);
|
|
2498
|
+
logger.success(`Copied to ${dest}`);
|
|
2499
|
+
}
|
|
2500
|
+
console.log(`
|
|
2501
|
+
Generated ${saved.length} image(s):`);
|
|
2502
|
+
for (const f of saved) console.log(` ${f}`);
|
|
605
2503
|
}
|
|
606
2504
|
);
|
|
607
2505
|
}
|
|
608
2506
|
|
|
609
|
-
// src/providers/
|
|
610
|
-
import fs8 from "fs";
|
|
611
|
-
import path7 from "path";
|
|
612
|
-
init_errors();
|
|
2507
|
+
// src/providers/leonardo/commands/image-to-image.ts
|
|
613
2508
|
init_http_client();
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
fs7.mkdirSync(dir, { recursive: true });
|
|
623
|
-
cachedOutputDir = dir;
|
|
624
|
-
return dir;
|
|
2509
|
+
import fs18 from "fs";
|
|
2510
|
+
import path21 from "path";
|
|
2511
|
+
function extFromUrl2(url, fallback = ".png") {
|
|
2512
|
+
try {
|
|
2513
|
+
return path21.extname(new URL(url).pathname) || fallback;
|
|
2514
|
+
} catch {
|
|
2515
|
+
return fallback;
|
|
2516
|
+
}
|
|
625
2517
|
}
|
|
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(
|
|
2518
|
+
function registerLeonardoImageToImageCommand(parent) {
|
|
2519
|
+
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
2520
|
async (opts) => {
|
|
635
2521
|
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");
|
|
2522
|
+
const imageType = opts.imageType.toUpperCase();
|
|
2523
|
+
if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
|
|
2524
|
+
logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
|
|
2525
|
+
process.exit(2);
|
|
646
2526
|
}
|
|
647
|
-
|
|
648
|
-
|
|
2527
|
+
const initStrength = Number.parseFloat(opts.initStrength);
|
|
2528
|
+
if (Number.isNaN(initStrength) || initStrength < 0 || initStrength > 0.9) {
|
|
2529
|
+
logger.error("--init-strength must be a number in [0.0, 0.9]");
|
|
2530
|
+
process.exit(2);
|
|
649
2531
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
2532
|
+
const client = createLeonardoClient();
|
|
2533
|
+
const body = {
|
|
2534
|
+
prompt: opts.prompt,
|
|
2535
|
+
modelId: opts.model ?? LEONARDO_DEFAULTS.modelId,
|
|
2536
|
+
width: opts.width ? Number.parseInt(opts.width, 10) : LEONARDO_DEFAULTS.width,
|
|
2537
|
+
height: opts.height ? Number.parseInt(opts.height, 10) : LEONARDO_DEFAULTS.height,
|
|
2538
|
+
num_images: opts.num ? Number.parseInt(opts.num, 10) : LEONARDO_DEFAULTS.numImages,
|
|
2539
|
+
init_strength: initStrength
|
|
655
2540
|
};
|
|
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
|
-
);
|
|
2541
|
+
if (imageType === "GENERATED") {
|
|
2542
|
+
body.init_generation_image_id = opts.ref;
|
|
2543
|
+
} else {
|
|
2544
|
+
body.init_image_id = opts.ref;
|
|
699
2545
|
}
|
|
700
|
-
if (
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
const
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
2546
|
+
if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
|
|
2547
|
+
if (opts.negative) body.negative_prompt = opts.negative;
|
|
2548
|
+
logger.info(`Submitting image-to-image (init=${opts.ref}, type=${imageType})`);
|
|
2549
|
+
const createRes = await client.post("/generations", body, logger);
|
|
2550
|
+
const generationId = createRes.sdGenerationJob.generationId;
|
|
2551
|
+
logger.debug(`generationId: ${generationId}`);
|
|
2552
|
+
logger.info("Polling for completion...");
|
|
2553
|
+
const waitTimeout = Number.parseInt(opts.waitTimeout, 10) || 48e4;
|
|
2554
|
+
const intervalMs = 4e3;
|
|
2555
|
+
const final = await poll({
|
|
2556
|
+
fetch: () => client.get(`/generations/${generationId}`),
|
|
2557
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE" && (v.generations_by_pk?.generated_images?.length ?? 0) > 0,
|
|
2558
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2559
|
+
intervalMs,
|
|
2560
|
+
maxAttempts: Math.ceil(waitTimeout / intervalMs),
|
|
2561
|
+
onTick: (attempt, v) => logger.debug(`attempt ${attempt} \u2014 status: ${v.generations_by_pk?.status ?? "?"}`)
|
|
2562
|
+
});
|
|
2563
|
+
const images = final.generations_by_pk?.generated_images ?? [];
|
|
2564
|
+
if (opts.download === false) {
|
|
2565
|
+
for (const img of images) console.log(img.url);
|
|
2566
|
+
return;
|
|
709
2567
|
}
|
|
710
2568
|
const outDir = getOutputDir();
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
2569
|
+
const saved = [];
|
|
2570
|
+
for (let i = 0; i < images.length; i++) {
|
|
2571
|
+
const img = images[i];
|
|
2572
|
+
if (!img) continue;
|
|
2573
|
+
const ext = extFromUrl2(img.url);
|
|
2574
|
+
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
2575
|
+
const filepath = path21.join(outDir, filename);
|
|
2576
|
+
try {
|
|
2577
|
+
await downloadFile(img.url, filepath);
|
|
2578
|
+
saved.push(filepath);
|
|
2579
|
+
logger.success(`Saved ${filepath}`);
|
|
2580
|
+
} catch (e) {
|
|
2581
|
+
logger.warn(`Download failed: ${e instanceof Error ? e.message : e}`);
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
if (opts.output && saved[0]) {
|
|
2585
|
+
const dest = path21.resolve(opts.output);
|
|
2586
|
+
fs18.mkdirSync(path21.dirname(dest), { recursive: true });
|
|
2587
|
+
fs18.copyFileSync(saved[0], dest);
|
|
2588
|
+
logger.success(`Copied to ${dest}`);
|
|
719
2589
|
}
|
|
720
2590
|
console.log(`
|
|
721
|
-
Generated
|
|
2591
|
+
Generated ${saved.length} image(s):`);
|
|
2592
|
+
for (const f of saved) console.log(` ${f}`);
|
|
722
2593
|
}
|
|
723
2594
|
);
|
|
724
2595
|
}
|
|
725
|
-
function sleep2(ms) {
|
|
726
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
727
|
-
}
|
|
728
2596
|
|
|
729
|
-
// src/providers/
|
|
730
|
-
|
|
731
|
-
import
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
2597
|
+
// src/providers/leonardo/commands/video.ts
|
|
2598
|
+
init_http_client();
|
|
2599
|
+
import path22 from "path";
|
|
2600
|
+
function v2DimsFor(resolution) {
|
|
2601
|
+
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
2602
|
+
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
2603
|
+
return { width: 1280, height: 720 };
|
|
2604
|
+
}
|
|
2605
|
+
function registerLeonardoVideoCommand(parent) {
|
|
2606
|
+
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(
|
|
2607
|
+
"--resolution <r>",
|
|
2608
|
+
"Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
|
|
2609
|
+
"RESOLUTION_720"
|
|
2610
|
+
).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(
|
|
2611
|
+
async (prompt, opts) => {
|
|
736
2612
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
737
|
-
const model = opts.model ??
|
|
738
|
-
const
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
2613
|
+
const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
|
|
2614
|
+
const client = createLeonardoClient();
|
|
2615
|
+
logger.info(`Submitting text-to-video (${model})`);
|
|
2616
|
+
let jobId;
|
|
2617
|
+
if (LEONARDO_V2_VIDEO_MODELS.has(model)) {
|
|
2618
|
+
const { width, height } = v2DimsFor(opts.resolution);
|
|
2619
|
+
const v2Res = await client.post(
|
|
2620
|
+
"v2:/generations",
|
|
2621
|
+
{
|
|
2622
|
+
public: false,
|
|
2623
|
+
model,
|
|
2624
|
+
parameters: {
|
|
2625
|
+
prompt,
|
|
2626
|
+
duration: 8,
|
|
2627
|
+
mode: opts.resolution,
|
|
2628
|
+
prompt_enhance: opts.enhance ? "ON" : "OFF",
|
|
2629
|
+
width,
|
|
2630
|
+
height
|
|
2631
|
+
}
|
|
2632
|
+
},
|
|
2633
|
+
logger
|
|
2634
|
+
);
|
|
2635
|
+
jobId = v2Res.generate.generationId;
|
|
2636
|
+
} else {
|
|
2637
|
+
const res = await client.post(
|
|
2638
|
+
"/generations-text-to-video",
|
|
2639
|
+
{
|
|
2640
|
+
prompt,
|
|
2641
|
+
model,
|
|
2642
|
+
resolution: opts.resolution,
|
|
2643
|
+
promptEnhance: !!opts.enhance,
|
|
2644
|
+
frameInterpolation: !!opts.frameInterpolation
|
|
2645
|
+
},
|
|
2646
|
+
logger
|
|
742
2647
|
);
|
|
2648
|
+
jobId = res.motionVideoGenerationJob?.generationId;
|
|
743
2649
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
2650
|
+
if (!jobId) {
|
|
2651
|
+
logger.error("No generationId returned by Leonardo");
|
|
2652
|
+
process.exit(1);
|
|
746
2653
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
2654
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
2655
|
+
if (!shouldWait) {
|
|
2656
|
+
console.log(jobId);
|
|
2657
|
+
console.error(`Poll with: multix leonardo status ${jobId}`);
|
|
2658
|
+
return;
|
|
2659
|
+
}
|
|
2660
|
+
await pollAndDownload(client, jobId, {
|
|
2661
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
2662
|
+
download: !!opts.download,
|
|
2663
|
+
output: opts.output,
|
|
2664
|
+
thumb: opts.thumb !== false,
|
|
2665
|
+
logger,
|
|
2666
|
+
basename: "leonardo-video"
|
|
2667
|
+
});
|
|
2668
|
+
}
|
|
2669
|
+
);
|
|
2670
|
+
}
|
|
2671
|
+
async function pollAndDownload(client, jobId, opts) {
|
|
2672
|
+
const { logger } = opts;
|
|
2673
|
+
let res;
|
|
2674
|
+
try {
|
|
2675
|
+
res = await poll({
|
|
2676
|
+
fetch: () => client.get(`/generations/${jobId}`, void 0, logger),
|
|
2677
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE",
|
|
2678
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2679
|
+
intervalMs: 5e3,
|
|
2680
|
+
maxAttempts: Math.max(1, Math.ceil(opts.waitTimeoutMs / 5e3)),
|
|
2681
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
2682
|
+
});
|
|
2683
|
+
} catch (e) {
|
|
2684
|
+
if (e instanceof PollTimeoutError) {
|
|
2685
|
+
logger.error(`Timed out \u2014 multix leonardo status ${jobId}`);
|
|
2686
|
+
process.exit(1);
|
|
2687
|
+
}
|
|
2688
|
+
if (e instanceof PollFailedError) {
|
|
2689
|
+
logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
|
|
2690
|
+
process.exit(1);
|
|
2691
|
+
}
|
|
2692
|
+
throw e;
|
|
2693
|
+
}
|
|
2694
|
+
const gen = res.generations_by_pk;
|
|
2695
|
+
if (!gen) {
|
|
2696
|
+
logger.error("No generation in response");
|
|
2697
|
+
process.exit(1);
|
|
2698
|
+
}
|
|
2699
|
+
const videoUrl = gen.generated_images?.find(
|
|
2700
|
+
(img) => typeof img.url === "string" && /\.(mp4|webm|mov)(\?|$)/i.test(img.url)
|
|
2701
|
+
)?.url ?? gen.generated_images?.[0]?.motionMP4URL;
|
|
2702
|
+
if (!opts.download) {
|
|
2703
|
+
if (typeof videoUrl === "string") console.log(videoUrl);
|
|
2704
|
+
return;
|
|
2705
|
+
}
|
|
2706
|
+
if (typeof videoUrl !== "string") {
|
|
2707
|
+
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
2708
|
+
process.exit(1);
|
|
2709
|
+
}
|
|
2710
|
+
const outPath = opts.output ?? path22.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
2711
|
+
await downloadFile(videoUrl, outPath);
|
|
2712
|
+
logger.success(`Saved ${outPath}`);
|
|
2713
|
+
await maybeDownloadThumb(gen, outPath, {
|
|
2714
|
+
skip: !opts.thumb,
|
|
2715
|
+
copyTo: opts.output,
|
|
2716
|
+
logger
|
|
2717
|
+
});
|
|
2718
|
+
console.log(`
|
|
2719
|
+
Job: ${jobId}`);
|
|
2720
|
+
console.log(`Saved: ${outPath}`);
|
|
2721
|
+
}
|
|
2722
|
+
|
|
2723
|
+
// src/providers/leonardo/commands/image-to-video.ts
|
|
2724
|
+
function registerLeonardoImageToVideoCommand(parent) {
|
|
2725
|
+
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(
|
|
2726
|
+
"--resolution <r>",
|
|
2727
|
+
"Resolution (RESOLUTION_480|RESOLUTION_720|RESOLUTION_1080)",
|
|
2728
|
+
"RESOLUTION_720"
|
|
2729
|
+
).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(
|
|
2730
|
+
async (imageId, opts) => {
|
|
2731
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2732
|
+
const imageType = opts.imageType.toUpperCase();
|
|
2733
|
+
if (imageType !== "GENERATED" && imageType !== "UPLOADED") {
|
|
2734
|
+
logger.error(`--image-type must be GENERATED or UPLOADED, got '${opts.imageType}'`);
|
|
2735
|
+
process.exit(2);
|
|
2736
|
+
}
|
|
2737
|
+
const model = opts.model ?? LEONARDO_DEFAULTS.videoModel;
|
|
2738
|
+
const client = createLeonardoClient();
|
|
2739
|
+
logger.info(`Submitting image-to-video (${model}, image=${imageId}, ${imageType})`);
|
|
2740
|
+
const body = {
|
|
2741
|
+
prompt: opts.prompt,
|
|
2742
|
+
imageId,
|
|
2743
|
+
imageType,
|
|
2744
|
+
model,
|
|
2745
|
+
resolution: opts.resolution,
|
|
2746
|
+
promptEnhance: !!opts.enhance,
|
|
2747
|
+
frameInterpolation: !!opts.frameInterpolation
|
|
754
2748
|
};
|
|
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
|
-
|
|
2749
|
+
if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
|
|
2750
|
+
if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
|
|
2751
|
+
if (opts.negative) body.negativePrompt = opts.negative;
|
|
2752
|
+
const res = await client.post(
|
|
2753
|
+
"/generations-image-to-video",
|
|
2754
|
+
body,
|
|
2755
|
+
logger
|
|
2756
|
+
);
|
|
2757
|
+
const jobId = res.motionVideoGenerationJob?.generationId;
|
|
2758
|
+
if (!jobId) {
|
|
2759
|
+
logger.error(`No generationId returned by Leonardo: ${JSON.stringify(res)}`);
|
|
2760
|
+
process.exit(1);
|
|
2761
|
+
}
|
|
2762
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
2763
|
+
if (!shouldWait) {
|
|
2764
|
+
console.log(jobId);
|
|
2765
|
+
console.error(`Poll with: multix leonardo status ${jobId}`);
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
await pollAndDownload(client, jobId, {
|
|
2769
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
2770
|
+
download: !!opts.download,
|
|
2771
|
+
output: opts.output,
|
|
2772
|
+
thumb: opts.thumb !== false,
|
|
2773
|
+
logger,
|
|
2774
|
+
basename: "leonardo-i2v"
|
|
2775
|
+
});
|
|
2776
|
+
}
|
|
2777
|
+
);
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// src/providers/leonardo/commands/me.ts
|
|
2781
|
+
function registerLeonardoMeCommand(parent) {
|
|
2782
|
+
parent.command("me").description("Show authenticated Leonardo user info and remaining API credits").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
2783
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2784
|
+
const client = createLeonardoClient();
|
|
2785
|
+
const info = await client.get("/me", void 0, logger);
|
|
2786
|
+
const u = info.user_details?.[0];
|
|
2787
|
+
if (!u) {
|
|
2788
|
+
logger.warn("No user info returned.");
|
|
2789
|
+
return;
|
|
2790
|
+
}
|
|
2791
|
+
logger.success(`User: ${u.user.username} (${u.user.id})`);
|
|
2792
|
+
console.log(`Tokens: ${u.subscriptionTokens} | API credit: ${u.apiCredit ?? "n/a"}`);
|
|
2793
|
+
});
|
|
2794
|
+
}
|
|
2795
|
+
|
|
2796
|
+
// src/providers/leonardo/commands/models.ts
|
|
2797
|
+
function registerLeonardoModelsCommand(parent) {
|
|
2798
|
+
parent.command("models").description("List Leonardo platform image models").option("--limit <n>", "Limit number of results").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
2799
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2800
|
+
const client = createLeonardoClient();
|
|
2801
|
+
const res = await client.get(
|
|
2802
|
+
"/platformModels",
|
|
2803
|
+
void 0,
|
|
2804
|
+
logger
|
|
2805
|
+
);
|
|
2806
|
+
const limit = opts.limit ? Number.parseInt(opts.limit, 10) : void 0;
|
|
2807
|
+
const models = limit ? res.custom_models.slice(0, limit) : res.custom_models;
|
|
2808
|
+
for (const m of models) {
|
|
2809
|
+
console.log(`${m.id} ${m.name}`);
|
|
2810
|
+
}
|
|
2811
|
+
logger.debug(`${models.length} model(s)`);
|
|
2812
|
+
});
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/providers/leonardo/commands/status.ts
|
|
2816
|
+
init_http_client();
|
|
2817
|
+
import path23 from "path";
|
|
2818
|
+
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
2819
|
+
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
2820
|
+
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
2821
|
+
function collectAssetUrls(gen) {
|
|
2822
|
+
const videos = [];
|
|
2823
|
+
const images = [];
|
|
2824
|
+
for (const img of gen.generated_images ?? []) {
|
|
2825
|
+
if (!img.url) continue;
|
|
2826
|
+
if (VIDEO_EXT_RE.test(img.url)) videos.push(img.url);
|
|
2827
|
+
else if (IMAGE_EXT_RE2.test(img.url)) images.push(img.url);
|
|
2828
|
+
else images.push(img.url);
|
|
2829
|
+
for (const k of VIDEO_KEYS) {
|
|
2830
|
+
const extra = img[k];
|
|
2831
|
+
if (typeof extra === "string" && VIDEO_EXT_RE.test(extra)) videos.push(extra);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
return { videos, images };
|
|
2835
|
+
}
|
|
2836
|
+
function registerLeonardoStatusCommand(parent) {
|
|
2837
|
+
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(
|
|
2838
|
+
async (generationId, opts) => {
|
|
2839
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2840
|
+
const client = createLeonardoClient();
|
|
2841
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
2842
|
+
const fetchOnce = () => client.get(`/generations/${generationId}`, void 0, logger);
|
|
2843
|
+
let res;
|
|
2844
|
+
try {
|
|
2845
|
+
res = shouldWait ? await poll({
|
|
2846
|
+
fetch: fetchOnce,
|
|
2847
|
+
done: (v) => v.generations_by_pk?.status === "COMPLETE",
|
|
2848
|
+
failed: (v) => v.generations_by_pk?.status === "FAILED",
|
|
2849
|
+
intervalMs: 5e3,
|
|
2850
|
+
maxAttempts: Math.max(
|
|
2851
|
+
1,
|
|
2852
|
+
Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
|
|
2853
|
+
),
|
|
2854
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
2855
|
+
}) : await fetchOnce();
|
|
2856
|
+
} catch (e) {
|
|
2857
|
+
if (e instanceof PollTimeoutError) {
|
|
2858
|
+
logger.error(`Timed out waiting for generation ${generationId}`);
|
|
2859
|
+
process.exit(1);
|
|
2860
|
+
}
|
|
2861
|
+
if (e instanceof PollFailedError) {
|
|
2862
|
+
logger.error(`Generation failed: ${JSON.stringify(e.value)}`);
|
|
782
2863
|
process.exit(1);
|
|
783
2864
|
}
|
|
2865
|
+
throw e;
|
|
784
2866
|
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
2867
|
+
const gen = res.generations_by_pk;
|
|
2868
|
+
if (!gen) {
|
|
2869
|
+
logger.warn("No generation found.");
|
|
2870
|
+
return;
|
|
788
2871
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
2872
|
+
console.error(`status: ${gen.status}`);
|
|
2873
|
+
const { videos, images } = collectAssetUrls(gen);
|
|
2874
|
+
for (const u of [...videos, ...images]) console.log(u);
|
|
2875
|
+
if (!opts.download || gen.status !== "COMPLETE") {
|
|
2876
|
+
if (opts.download) logger.warn(`Cannot download \u2014 status is '${gen.status}'`);
|
|
2877
|
+
return;
|
|
2878
|
+
}
|
|
2879
|
+
const outDir = getOutputDir();
|
|
2880
|
+
let idx = 0;
|
|
2881
|
+
for (const url of videos) {
|
|
2882
|
+
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path23.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
|
|
2883
|
+
await downloadFile(url, dest);
|
|
2884
|
+
logger.success(`Saved ${dest}`);
|
|
2885
|
+
await maybeDownloadThumb(gen, dest, {
|
|
2886
|
+
skip: opts.thumb === false,
|
|
2887
|
+
copyTo: opts.output,
|
|
2888
|
+
logger
|
|
2889
|
+
});
|
|
2890
|
+
idx++;
|
|
2891
|
+
}
|
|
2892
|
+
for (const url of images) {
|
|
2893
|
+
const m = url.match(IMAGE_EXT_RE2);
|
|
2894
|
+
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
2895
|
+
const dest = path23.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
2896
|
+
await downloadFile(url, dest);
|
|
2897
|
+
logger.success(`Saved ${dest}`);
|
|
2898
|
+
idx++;
|
|
793
2899
|
}
|
|
794
|
-
console.log(`
|
|
795
|
-
Generated ${savedFiles.length} image(s):`);
|
|
796
|
-
for (const f of savedFiles) console.log(` ${f}`);
|
|
797
2900
|
}
|
|
798
2901
|
);
|
|
799
2902
|
}
|
|
800
2903
|
|
|
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) => {
|
|
2904
|
+
// src/providers/leonardo/commands/upscale.ts
|
|
2905
|
+
function registerLeonardoUpscaleCommands(parent) {
|
|
2906
|
+
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(
|
|
2907
|
+
async (generatedImageId, opts) => {
|
|
807
2908
|
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);
|
|
2909
|
+
const client = createLeonardoClient();
|
|
2910
|
+
logger.info(`Submitting upscale for image ${generatedImageId}`);
|
|
2911
|
+
const res = await client.post(
|
|
2912
|
+
"/variations/universal-upscaler",
|
|
2913
|
+
{
|
|
2914
|
+
generatedImageId,
|
|
2915
|
+
upscalerStyle: opts.style,
|
|
2916
|
+
creativityStrength: Number.parseFloat(opts.strength),
|
|
2917
|
+
upscaleMultiplier: Number.parseFloat(opts.multiplier)
|
|
2918
|
+
},
|
|
2919
|
+
logger
|
|
2920
|
+
);
|
|
2921
|
+
const variationId = res.universalUpscaler.id;
|
|
2922
|
+
logger.success(`variationId: ${variationId}`);
|
|
2923
|
+
console.log(variationId);
|
|
2924
|
+
console.error(`Use: multix leonardo variation ${variationId}`);
|
|
847
2925
|
}
|
|
848
2926
|
);
|
|
2927
|
+
parent.command("variation <variationId>").description("Fetch a Leonardo variation/upscale result by ID").option("-v, --verbose", "Verbose logging").action(async (variationId, opts) => {
|
|
2928
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2929
|
+
const client = createLeonardoClient();
|
|
2930
|
+
const res = await client.get(
|
|
2931
|
+
`/variations/${variationId}`,
|
|
2932
|
+
void 0,
|
|
2933
|
+
logger
|
|
2934
|
+
);
|
|
2935
|
+
for (const v of res.generated_image_variation_generic ?? []) {
|
|
2936
|
+
console.log(`${v.status} ${v.url ?? ""}`);
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
849
2939
|
}
|
|
850
2940
|
|
|
851
|
-
// src/providers/
|
|
852
|
-
function
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
2941
|
+
// src/providers/leonardo/commands/video-models.ts
|
|
2942
|
+
function registerLeonardoVideoModelsCommand(parent) {
|
|
2943
|
+
parent.command("video-models").description("List supported Leonardo video models (static enum from Leonardo docs)").action(() => {
|
|
2944
|
+
for (const m of LEONARDO_VIDEO_MODELS) {
|
|
2945
|
+
console.log(`${m.id} ${m.name} [${m.modes.join(",")}]`);
|
|
2946
|
+
}
|
|
2947
|
+
console.error(`
|
|
2948
|
+
Resolutions: ${LEONARDO_VIDEO_RESOLUTIONS.join(", ")}`);
|
|
2949
|
+
console.error("Source: https://docs.leonardo.ai/llms.txt");
|
|
2950
|
+
});
|
|
2951
|
+
}
|
|
2952
|
+
|
|
2953
|
+
// src/providers/leonardo/commands/index.ts
|
|
2954
|
+
function registerLeonardoCommands(program2) {
|
|
2955
|
+
const leonardo = program2.command("leonardo").description("Leonardo.Ai: image/video generation, upscaling, account info");
|
|
2956
|
+
registerLeonardoGenerateCommand(leonardo);
|
|
2957
|
+
registerLeonardoImageToImageCommand(leonardo);
|
|
2958
|
+
registerLeonardoVideoCommand(leonardo);
|
|
2959
|
+
registerLeonardoImageToVideoCommand(leonardo);
|
|
2960
|
+
registerLeonardoVideoModelsCommand(leonardo);
|
|
2961
|
+
registerLeonardoUpscaleCommands(leonardo);
|
|
2962
|
+
registerLeonardoStatusCommand(leonardo);
|
|
2963
|
+
registerLeonardoModelsCommand(leonardo);
|
|
2964
|
+
registerLeonardoMeCommand(leonardo);
|
|
859
2965
|
}
|
|
860
2966
|
|
|
861
2967
|
// src/providers/minimax/commands/generate-music.ts
|
|
@@ -918,7 +3024,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
|
918
3024
|
if (status === "Failed" || status === "Error") {
|
|
919
3025
|
throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
|
|
920
3026
|
}
|
|
921
|
-
await
|
|
3027
|
+
await sleep4(intervalMs);
|
|
922
3028
|
elapsed += intervalMs;
|
|
923
3029
|
}
|
|
924
3030
|
throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
|
|
@@ -937,14 +3043,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
|
|
|
937
3043
|
await downloadFile(downloadUrl, dest);
|
|
938
3044
|
return dest;
|
|
939
3045
|
}
|
|
940
|
-
function
|
|
3046
|
+
function sleep4(ms) {
|
|
941
3047
|
return new Promise((r) => setTimeout(r, ms));
|
|
942
3048
|
}
|
|
943
3049
|
|
|
944
3050
|
// src/providers/minimax/generators/music.ts
|
|
945
3051
|
init_http_client();
|
|
946
|
-
import
|
|
947
|
-
import
|
|
3052
|
+
import fs19 from "fs";
|
|
3053
|
+
import path24 from "path";
|
|
948
3054
|
async function generateMinimaxMusic(opts) {
|
|
949
3055
|
const {
|
|
950
3056
|
apiKey,
|
|
@@ -989,14 +3095,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
989
3095
|
}
|
|
990
3096
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
991
3097
|
const outDir = getOutputDir();
|
|
992
|
-
const dest =
|
|
3098
|
+
const dest = path24.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
993
3099
|
try {
|
|
994
3100
|
if (audioData.startsWith("http")) {
|
|
995
3101
|
const bytes = await fetchBytes(audioData);
|
|
996
|
-
|
|
3102
|
+
fs19.writeFileSync(dest, bytes);
|
|
997
3103
|
} else {
|
|
998
3104
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
999
|
-
|
|
3105
|
+
fs19.writeFileSync(dest, audioBytes);
|
|
1000
3106
|
}
|
|
1001
3107
|
} catch (e) {
|
|
1002
3108
|
return {
|
|
@@ -1007,8 +3113,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
1007
3113
|
const durSec = durationMs / 1e3;
|
|
1008
3114
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
1009
3115
|
if (output) {
|
|
1010
|
-
|
|
1011
|
-
|
|
3116
|
+
fs19.mkdirSync(path24.dirname(path24.resolve(output)), { recursive: true });
|
|
3117
|
+
fs19.copyFileSync(dest, output);
|
|
1012
3118
|
}
|
|
1013
3119
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
1014
3120
|
}
|
|
@@ -1082,8 +3188,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
1082
3188
|
init_errors();
|
|
1083
3189
|
|
|
1084
3190
|
// src/providers/minimax/generators/speech.ts
|
|
1085
|
-
import
|
|
1086
|
-
import
|
|
3191
|
+
import fs20 from "fs";
|
|
3192
|
+
import path25 from "path";
|
|
1087
3193
|
async function generateMinimaxSpeech(opts) {
|
|
1088
3194
|
const {
|
|
1089
3195
|
apiKey,
|
|
@@ -1130,12 +3236,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
1130
3236
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
1131
3237
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
1132
3238
|
const outDir = getOutputDir();
|
|
1133
|
-
const dest =
|
|
1134
|
-
|
|
3239
|
+
const dest = path25.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
3240
|
+
fs20.writeFileSync(dest, audioBytes);
|
|
1135
3241
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
1136
3242
|
if (output) {
|
|
1137
|
-
|
|
1138
|
-
|
|
3243
|
+
fs20.mkdirSync(path25.dirname(path25.resolve(output)), { recursive: true });
|
|
3244
|
+
fs20.copyFileSync(dest, output);
|
|
1139
3245
|
}
|
|
1140
3246
|
return { status: "success", generatedAudio: dest, model };
|
|
1141
3247
|
}
|
|
@@ -1182,8 +3288,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
1182
3288
|
init_errors();
|
|
1183
3289
|
|
|
1184
3290
|
// src/providers/minimax/generators/video.ts
|
|
1185
|
-
import
|
|
1186
|
-
import
|
|
3291
|
+
import fs21 from "fs";
|
|
3292
|
+
import path26 from "path";
|
|
1187
3293
|
async function generateMinimaxVideo(opts) {
|
|
1188
3294
|
const {
|
|
1189
3295
|
apiKey,
|
|
@@ -1193,6 +3299,7 @@ async function generateMinimaxVideo(opts) {
|
|
|
1193
3299
|
resolution = "1080P",
|
|
1194
3300
|
firstFrame,
|
|
1195
3301
|
output,
|
|
3302
|
+
thumb,
|
|
1196
3303
|
logger
|
|
1197
3304
|
} = opts;
|
|
1198
3305
|
const payload = { prompt, model, duration, resolution };
|
|
@@ -1228,19 +3335,24 @@ async function generateMinimaxVideo(opts) {
|
|
|
1228
3335
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
1229
3336
|
}
|
|
1230
3337
|
const outDir = getOutputDir();
|
|
1231
|
-
const dest =
|
|
3338
|
+
const dest = path26.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
1232
3339
|
try {
|
|
1233
3340
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
1234
3341
|
} catch (e) {
|
|
1235
3342
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
1236
3343
|
}
|
|
1237
3344
|
const elapsed = (Date.now() - start) / 1e3;
|
|
1238
|
-
const stat =
|
|
3345
|
+
const stat = fs21.statSync(dest);
|
|
1239
3346
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
1240
3347
|
if (output) {
|
|
1241
|
-
|
|
1242
|
-
|
|
3348
|
+
fs21.mkdirSync(path26.dirname(path26.resolve(output)), { recursive: true });
|
|
3349
|
+
fs21.copyFileSync(dest, output);
|
|
1243
3350
|
}
|
|
3351
|
+
await maybeDownloadThumb(pollResult, dest, {
|
|
3352
|
+
skip: thumb === false,
|
|
3353
|
+
copyTo: output,
|
|
3354
|
+
logger
|
|
3355
|
+
});
|
|
1244
3356
|
logger?.debug(`Generated in ${elapsed.toFixed(1)}s, size: ${fileSizeMb.toFixed(2)} MB`);
|
|
1245
3357
|
return {
|
|
1246
3358
|
status: "success",
|
|
@@ -1253,7 +3365,7 @@ async function generateMinimaxVideo(opts) {
|
|
|
1253
3365
|
|
|
1254
3366
|
// src/providers/minimax/commands/generate-video.ts
|
|
1255
3367
|
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(
|
|
3368
|
+
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
3369
|
async (opts) => {
|
|
1258
3370
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1259
3371
|
const apiKey = requireMinimaxKey();
|
|
@@ -1278,6 +3390,7 @@ function registerMinimaxGenerateVideoCommand(parent) {
|
|
|
1278
3390
|
resolution,
|
|
1279
3391
|
firstFrame: opts.firstFrame,
|
|
1280
3392
|
output: opts.output,
|
|
3393
|
+
thumb: opts.thumb,
|
|
1281
3394
|
logger
|
|
1282
3395
|
});
|
|
1283
3396
|
if (result.status === "error") {
|
|
@@ -1296,8 +3409,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
1296
3409
|
// src/providers/minimax/generators/image.ts
|
|
1297
3410
|
init_errors();
|
|
1298
3411
|
init_http_client();
|
|
1299
|
-
import
|
|
1300
|
-
import
|
|
3412
|
+
import fs22 from "fs";
|
|
3413
|
+
import path27 from "path";
|
|
1301
3414
|
async function generateMinimaxImage(opts) {
|
|
1302
3415
|
const {
|
|
1303
3416
|
apiKey,
|
|
@@ -1335,8 +3448,8 @@ async function generateMinimaxImage(opts) {
|
|
|
1335
3448
|
try {
|
|
1336
3449
|
const bytes = await fetchBytes(url);
|
|
1337
3450
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
1338
|
-
const dest =
|
|
1339
|
-
|
|
3451
|
+
const dest = path27.join(outDir, fname);
|
|
3452
|
+
fs22.writeFileSync(dest, bytes);
|
|
1340
3453
|
savedFiles.push(dest);
|
|
1341
3454
|
logger?.success(`Saved: ${dest}`);
|
|
1342
3455
|
} catch (e) {
|
|
@@ -1347,8 +3460,8 @@ async function generateMinimaxImage(opts) {
|
|
|
1347
3460
|
}
|
|
1348
3461
|
}
|
|
1349
3462
|
if (output && savedFiles[0]) {
|
|
1350
|
-
|
|
1351
|
-
|
|
3463
|
+
fs22.mkdirSync(path27.dirname(path27.resolve(output)), { recursive: true });
|
|
3464
|
+
fs22.copyFileSync(savedFiles[0], output);
|
|
1352
3465
|
}
|
|
1353
3466
|
return { status: "success", generatedImages: savedFiles, model };
|
|
1354
3467
|
}
|
|
@@ -1381,18 +3494,101 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
1381
3494
|
);
|
|
1382
3495
|
}
|
|
1383
3496
|
|
|
3497
|
+
// src/providers/minimax/commands/image-to-image.ts
|
|
3498
|
+
init_errors();
|
|
3499
|
+
init_http_client();
|
|
3500
|
+
import fs23 from "fs";
|
|
3501
|
+
import path28 from "path";
|
|
3502
|
+
function registerMinimaxImageToImageCommand(parent) {
|
|
3503
|
+
parent.command("image-to-image").alias("i2i").description(
|
|
3504
|
+
"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."
|
|
3505
|
+
).requiredOption("--prompt <text>", "Prompt describing the desired NEW scene/composition").requiredOption(
|
|
3506
|
+
"--ref <path>",
|
|
3507
|
+
"Subject reference image (URL or local path) \u2014 character/face reference only, single image"
|
|
3508
|
+
).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(
|
|
3509
|
+
async (opts) => {
|
|
3510
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3511
|
+
const apiKey = requireMinimaxKey();
|
|
3512
|
+
const model = opts.model ?? TASK_DEFAULTS.image;
|
|
3513
|
+
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
|
|
3514
|
+
logger.warn(
|
|
3515
|
+
"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."
|
|
3516
|
+
);
|
|
3517
|
+
const resolved = await resolveImageInput(opts.ref, { logger });
|
|
3518
|
+
const imageFile = refUrl(resolved);
|
|
3519
|
+
const payload = {
|
|
3520
|
+
model,
|
|
3521
|
+
prompt: opts.prompt,
|
|
3522
|
+
aspect_ratio: opts.aspectRatio,
|
|
3523
|
+
n: numImages,
|
|
3524
|
+
response_format: "url",
|
|
3525
|
+
prompt_optimizer: true,
|
|
3526
|
+
subject_reference: [
|
|
3527
|
+
{
|
|
3528
|
+
type: "character",
|
|
3529
|
+
image_file: imageFile
|
|
3530
|
+
}
|
|
3531
|
+
]
|
|
3532
|
+
};
|
|
3533
|
+
logger.debug(`MiniMax i2i: model=${model}, ref=${opts.ref}, n=${numImages}`);
|
|
3534
|
+
let resp;
|
|
3535
|
+
try {
|
|
3536
|
+
resp = await apiPost("image_generation", payload, apiKey, {
|
|
3537
|
+
logger
|
|
3538
|
+
});
|
|
3539
|
+
} catch (e) {
|
|
3540
|
+
logger.error(e instanceof Error ? e.message : String(e));
|
|
3541
|
+
process.exit(1);
|
|
3542
|
+
}
|
|
3543
|
+
const imageUrls = resp.data?.image_urls ?? [];
|
|
3544
|
+
if (imageUrls.length === 0) {
|
|
3545
|
+
logger.error("No images in response");
|
|
3546
|
+
process.exit(1);
|
|
3547
|
+
}
|
|
3548
|
+
const outDir = getOutputDir();
|
|
3549
|
+
const ts = Date.now();
|
|
3550
|
+
const saved = [];
|
|
3551
|
+
for (let i = 0; i < imageUrls.length; i++) {
|
|
3552
|
+
const url = imageUrls[i];
|
|
3553
|
+
if (!url) continue;
|
|
3554
|
+
try {
|
|
3555
|
+
const bytes = await fetchBytes(url);
|
|
3556
|
+
const dest = path28.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
3557
|
+
fs23.writeFileSync(dest, bytes);
|
|
3558
|
+
saved.push(dest);
|
|
3559
|
+
logger.success(`Saved: ${dest}`);
|
|
3560
|
+
} catch (e) {
|
|
3561
|
+
throw new ProviderError(
|
|
3562
|
+
`Failed to download image: ${e instanceof Error ? e.message : String(e)}`,
|
|
3563
|
+
"minimax"
|
|
3564
|
+
);
|
|
3565
|
+
}
|
|
3566
|
+
}
|
|
3567
|
+
if (opts.output && saved[0]) {
|
|
3568
|
+
fs23.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
|
|
3569
|
+
fs23.copyFileSync(saved[0], opts.output);
|
|
3570
|
+
logger.success(`Copied to: ${opts.output}`);
|
|
3571
|
+
}
|
|
3572
|
+
console.log(`
|
|
3573
|
+
Generated ${saved.length} image(s):`);
|
|
3574
|
+
for (const f of saved) console.log(` ${f}`);
|
|
3575
|
+
}
|
|
3576
|
+
);
|
|
3577
|
+
}
|
|
3578
|
+
|
|
1384
3579
|
// src/providers/minimax/commands/index.ts
|
|
1385
3580
|
function registerMinimaxCommands(program2) {
|
|
1386
3581
|
const minimax = program2.command("minimax").description("MiniMax generation: images, video, speech, and music");
|
|
1387
3582
|
registerMinimaxGenerateCommand(minimax);
|
|
3583
|
+
registerMinimaxImageToImageCommand(minimax);
|
|
1388
3584
|
registerMinimaxGenerateVideoCommand(minimax);
|
|
1389
3585
|
registerMinimaxGenerateSpeechCommand(minimax);
|
|
1390
3586
|
registerMinimaxGenerateMusicCommand(minimax);
|
|
1391
3587
|
}
|
|
1392
3588
|
|
|
1393
3589
|
// src/providers/openrouter/client.ts
|
|
1394
|
-
import
|
|
1395
|
-
import
|
|
3590
|
+
import fs24 from "fs";
|
|
3591
|
+
import path29 from "path";
|
|
1396
3592
|
init_errors();
|
|
1397
3593
|
init_http_client();
|
|
1398
3594
|
var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
@@ -1481,8 +3677,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
1481
3677
|
if (!url) continue;
|
|
1482
3678
|
try {
|
|
1483
3679
|
const bytes = await fetchBytes(url);
|
|
1484
|
-
const dest =
|
|
1485
|
-
|
|
3680
|
+
const dest = path29.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
3681
|
+
fs24.writeFileSync(dest, bytes);
|
|
1486
3682
|
savedFiles.push(dest);
|
|
1487
3683
|
logger?.success(`Saved: ${dest}`);
|
|
1488
3684
|
} catch (e) {
|
|
@@ -1493,8 +3689,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
1493
3689
|
}
|
|
1494
3690
|
}
|
|
1495
3691
|
if (output && savedFiles[0]) {
|
|
1496
|
-
|
|
1497
|
-
|
|
3692
|
+
fs24.mkdirSync(path29.dirname(path29.resolve(output)), { recursive: true });
|
|
3693
|
+
fs24.writeFileSync(output, fs24.readFileSync(savedFiles[0]));
|
|
1498
3694
|
}
|
|
1499
3695
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
1500
3696
|
}
|
|
@@ -1528,10 +3724,328 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
1528
3724
|
);
|
|
1529
3725
|
}
|
|
1530
3726
|
|
|
3727
|
+
// src/providers/openrouter/commands/image-to-image.ts
|
|
3728
|
+
import fs25 from "fs";
|
|
3729
|
+
import path30 from "path";
|
|
3730
|
+
init_errors();
|
|
3731
|
+
init_http_client();
|
|
3732
|
+
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
3733
|
+
var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
|
|
3734
|
+
var HARD_LIMIT_BYTES2 = 8 * 1024 * 1024;
|
|
3735
|
+
function registerOpenRouterImageToImageCommand(parent) {
|
|
3736
|
+
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(
|
|
3737
|
+
"--ref <path>",
|
|
3738
|
+
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
3739
|
+
collect5,
|
|
3740
|
+
[]
|
|
3741
|
+
).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(
|
|
3742
|
+
async (opts) => {
|
|
3743
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
3744
|
+
throw new Error("At least one --ref is required for image-to-image.");
|
|
3745
|
+
}
|
|
3746
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3747
|
+
const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_MODEL;
|
|
3748
|
+
const resolvedRefs = [];
|
|
3749
|
+
for (const r of opts.ref) {
|
|
3750
|
+
const resolved = await resolveImageInput(r, {
|
|
3751
|
+
logger,
|
|
3752
|
+
softLimitBytes: SOFT_LIMIT_BYTES2,
|
|
3753
|
+
hardLimitBytes: HARD_LIMIT_BYTES2
|
|
3754
|
+
});
|
|
3755
|
+
resolvedRefs.push(refUrl(resolved));
|
|
3756
|
+
}
|
|
3757
|
+
logger.debug(`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}`);
|
|
3758
|
+
const saved = await runImageToImage({
|
|
3759
|
+
prompt: opts.prompt,
|
|
3760
|
+
model,
|
|
3761
|
+
refs: resolvedRefs,
|
|
3762
|
+
output: opts.output,
|
|
3763
|
+
logger
|
|
3764
|
+
});
|
|
3765
|
+
console.log(`
|
|
3766
|
+
Generated ${saved.length} image(s):`);
|
|
3767
|
+
for (const f of saved) console.log(` ${f}`);
|
|
3768
|
+
console.log(`Model used: ${model}`);
|
|
3769
|
+
}
|
|
3770
|
+
);
|
|
3771
|
+
}
|
|
3772
|
+
function collect5(value, prev) {
|
|
3773
|
+
return [...prev, value];
|
|
3774
|
+
}
|
|
3775
|
+
async function runImageToImage(opts) {
|
|
3776
|
+
const apiKey = requireOpenRouterKey();
|
|
3777
|
+
const headers = {
|
|
3778
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3779
|
+
"Content-Type": "application/json"
|
|
3780
|
+
};
|
|
3781
|
+
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
3782
|
+
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
3783
|
+
if (referer) headers["HTTP-Referer"] = referer;
|
|
3784
|
+
if (title) headers["X-Title"] = title;
|
|
3785
|
+
const content = opts.refs.map((url) => ({
|
|
3786
|
+
type: "image_url",
|
|
3787
|
+
image_url: { url }
|
|
3788
|
+
}));
|
|
3789
|
+
content.push({ type: "text", text: opts.prompt });
|
|
3790
|
+
const payload = {
|
|
3791
|
+
model: opts.model,
|
|
3792
|
+
messages: [{ role: "user", content }],
|
|
3793
|
+
modalities: opts.model.includes("gemini") ? ["image", "text"] : ["image"]
|
|
3794
|
+
};
|
|
3795
|
+
const data = await httpJson({
|
|
3796
|
+
url: OPENROUTER_API_URL,
|
|
3797
|
+
method: "POST",
|
|
3798
|
+
headers,
|
|
3799
|
+
body: payload,
|
|
3800
|
+
timeoutMs: 24e4
|
|
3801
|
+
});
|
|
3802
|
+
const messageImages = data.choices?.[0]?.message?.images ?? [];
|
|
3803
|
+
if (messageImages.length === 0) {
|
|
3804
|
+
throw new ProviderError(
|
|
3805
|
+
`No images in response \u2014 model '${opts.model}' may not support image-to-image`,
|
|
3806
|
+
"openrouter"
|
|
3807
|
+
);
|
|
3808
|
+
}
|
|
3809
|
+
const outDir = getOutputDir();
|
|
3810
|
+
const ts = Date.now();
|
|
3811
|
+
const saved = [];
|
|
3812
|
+
for (let i = 0; i < messageImages.length; i++) {
|
|
3813
|
+
const url = messageImages[i]?.image_url?.url;
|
|
3814
|
+
if (!url) continue;
|
|
3815
|
+
const bytes = await fetchBytes(url);
|
|
3816
|
+
const dest = path30.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
3817
|
+
fs25.writeFileSync(dest, bytes);
|
|
3818
|
+
saved.push(dest);
|
|
3819
|
+
opts.logger.success(`Saved: ${dest}`);
|
|
3820
|
+
}
|
|
3821
|
+
if (opts.output && saved[0]) {
|
|
3822
|
+
fs25.mkdirSync(path30.dirname(path30.resolve(opts.output)), { recursive: true });
|
|
3823
|
+
fs25.copyFileSync(saved[0], opts.output);
|
|
3824
|
+
opts.logger.success(`Copied to: ${opts.output}`);
|
|
3825
|
+
}
|
|
3826
|
+
return saved;
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// src/providers/openrouter/video-client.ts
|
|
3830
|
+
init_http_client();
|
|
3831
|
+
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
3832
|
+
function authHeaders3(apiKey) {
|
|
3833
|
+
const headers = {
|
|
3834
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3835
|
+
"Content-Type": "application/json"
|
|
3836
|
+
};
|
|
3837
|
+
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
3838
|
+
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
3839
|
+
if (referer) headers["HTTP-Referer"] = referer;
|
|
3840
|
+
if (title) headers["X-Title"] = title;
|
|
3841
|
+
return headers;
|
|
3842
|
+
}
|
|
3843
|
+
async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
3844
|
+
return httpJson({
|
|
3845
|
+
url: OPENROUTER_VIDEOS_URL,
|
|
3846
|
+
method: "POST",
|
|
3847
|
+
headers: authHeaders3(apiKey),
|
|
3848
|
+
body,
|
|
3849
|
+
timeoutMs: 12e4
|
|
3850
|
+
});
|
|
3851
|
+
}
|
|
3852
|
+
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
3853
|
+
return httpJson({
|
|
3854
|
+
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
3855
|
+
headers: authHeaders3(apiKey)
|
|
3856
|
+
});
|
|
3857
|
+
}
|
|
3858
|
+
function videoContentUrl(jobId) {
|
|
3859
|
+
return `${OPENROUTER_VIDEOS_URL}/${jobId}/content`;
|
|
3860
|
+
}
|
|
3861
|
+
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
3862
|
+
return httpJson({
|
|
3863
|
+
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
3864
|
+
headers: authHeaders3(apiKey)
|
|
3865
|
+
});
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
// src/providers/openrouter/commands/video-status.ts
|
|
3869
|
+
import fs26 from "fs";
|
|
3870
|
+
import path31 from "path";
|
|
3871
|
+
async function downloadOpenRouterVideo(opts) {
|
|
3872
|
+
const { jobId, logger, source } = opts;
|
|
3873
|
+
const apiKey = requireOpenRouterKey();
|
|
3874
|
+
const url = videoContentUrl(jobId);
|
|
3875
|
+
const dest = path31.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
3876
|
+
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
3877
|
+
if (!r.ok) {
|
|
3878
|
+
logger.error(`Download HTTP ${r.status}`);
|
|
3879
|
+
process.exit(1);
|
|
3880
|
+
}
|
|
3881
|
+
if (!r.body) {
|
|
3882
|
+
logger.error("Empty response body");
|
|
3883
|
+
process.exit(1);
|
|
3884
|
+
}
|
|
3885
|
+
fs26.mkdirSync(path31.dirname(dest), { recursive: true });
|
|
3886
|
+
const ws = fs26.createWriteStream(dest);
|
|
3887
|
+
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
3888
|
+
await pipeline2(r.body, ws);
|
|
3889
|
+
logger.success(`Saved ${dest}`);
|
|
3890
|
+
if (opts.output) {
|
|
3891
|
+
fs26.mkdirSync(path31.dirname(path31.resolve(opts.output)), { recursive: true });
|
|
3892
|
+
fs26.copyFileSync(dest, opts.output);
|
|
3893
|
+
logger.success(`Copied to ${opts.output}`);
|
|
3894
|
+
}
|
|
3895
|
+
await maybeDownloadThumb(source, dest, {
|
|
3896
|
+
skip: opts.thumb === false,
|
|
3897
|
+
copyTo: opts.output,
|
|
3898
|
+
logger
|
|
3899
|
+
});
|
|
3900
|
+
return dest;
|
|
3901
|
+
}
|
|
3902
|
+
function registerOpenRouterVideoStatusCommand(parent) {
|
|
3903
|
+
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(
|
|
3904
|
+
async (jobId, opts) => {
|
|
3905
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3906
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
3907
|
+
let res;
|
|
3908
|
+
try {
|
|
3909
|
+
res = shouldWait ? await poll({
|
|
3910
|
+
fetch: () => pollVideoJob(jobId),
|
|
3911
|
+
done: (v) => v.status === "completed",
|
|
3912
|
+
failed: (v) => v.status === "failed",
|
|
3913
|
+
intervalMs: 5e3,
|
|
3914
|
+
maxAttempts: Math.max(
|
|
3915
|
+
1,
|
|
3916
|
+
Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
|
|
3917
|
+
),
|
|
3918
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
3919
|
+
}) : await pollVideoJob(jobId);
|
|
3920
|
+
} catch (e) {
|
|
3921
|
+
if (e instanceof PollTimeoutError) {
|
|
3922
|
+
logger.error(`Timed out waiting for job ${jobId}`);
|
|
3923
|
+
process.exit(1);
|
|
3924
|
+
}
|
|
3925
|
+
if (e instanceof PollFailedError) {
|
|
3926
|
+
logger.error(`Job failed: ${JSON.stringify(e.value)}`);
|
|
3927
|
+
process.exit(1);
|
|
3928
|
+
}
|
|
3929
|
+
throw e;
|
|
3930
|
+
}
|
|
3931
|
+
console.error(`status: ${res.status}`);
|
|
3932
|
+
for (const u of res.unsigned_urls ?? []) console.log(u);
|
|
3933
|
+
if (opts.download && res.status === "completed") {
|
|
3934
|
+
await downloadOpenRouterVideo({
|
|
3935
|
+
jobId,
|
|
3936
|
+
output: opts.output,
|
|
3937
|
+
thumb: opts.thumb,
|
|
3938
|
+
logger,
|
|
3939
|
+
source: res
|
|
3940
|
+
});
|
|
3941
|
+
} else if (opts.download) {
|
|
3942
|
+
logger.warn(`Cannot download \u2014 status is '${res.status}', not 'completed'`);
|
|
3943
|
+
}
|
|
3944
|
+
if (res.usage?.cost !== void 0) {
|
|
3945
|
+
logger.debug(`cost: ${res.usage.cost}${res.usage.is_byok ? " (BYOK)" : ""}`);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
);
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
// src/providers/openrouter/commands/image-to-video.ts
|
|
3952
|
+
function registerOpenRouterImageToVideoCommand(parent) {
|
|
3953
|
+
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(
|
|
3954
|
+
async (opts) => {
|
|
3955
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3956
|
+
const model = opts.model ?? resolveKey("OPENROUTER_VIDEO_MODEL") ?? "google/veo-3.1";
|
|
3957
|
+
const frame_images = [
|
|
3958
|
+
{
|
|
3959
|
+
type: "image_url",
|
|
3960
|
+
image_url: { url: opts.imageUrl },
|
|
3961
|
+
frame_type: "first_frame"
|
|
3962
|
+
}
|
|
3963
|
+
];
|
|
3964
|
+
if (opts.lastFrameUrl) {
|
|
3965
|
+
frame_images.push({
|
|
3966
|
+
type: "image_url",
|
|
3967
|
+
image_url: { url: opts.lastFrameUrl },
|
|
3968
|
+
frame_type: "last_frame"
|
|
3969
|
+
});
|
|
3970
|
+
}
|
|
3971
|
+
const body = {
|
|
3972
|
+
model,
|
|
3973
|
+
prompt: opts.prompt,
|
|
3974
|
+
frame_images
|
|
3975
|
+
};
|
|
3976
|
+
if (opts.resolution) body.resolution = opts.resolution;
|
|
3977
|
+
if (opts.aspectRatio) body.aspect_ratio = opts.aspectRatio;
|
|
3978
|
+
if (opts.duration) body.duration = Number.parseInt(opts.duration, 10);
|
|
3979
|
+
if (opts.seed) body.seed = Number.parseInt(opts.seed, 10);
|
|
3980
|
+
logger.info(`Submitting image-to-video job (model=${model})`);
|
|
3981
|
+
const res = await submitVideoJob(body);
|
|
3982
|
+
logger.success(`Job ${res.id} (status: ${res.status})`);
|
|
3983
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
3984
|
+
if (!shouldWait) {
|
|
3985
|
+
console.log(res.id);
|
|
3986
|
+
console.error(`Poll with: multix openrouter video-status ${res.id}`);
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
let final;
|
|
3990
|
+
try {
|
|
3991
|
+
final = await poll({
|
|
3992
|
+
fetch: () => pollVideoJob(res.id),
|
|
3993
|
+
done: (v) => v.status === "completed",
|
|
3994
|
+
failed: (v) => v.status === "failed",
|
|
3995
|
+
intervalMs: 5e3,
|
|
3996
|
+
maxAttempts: Math.max(
|
|
3997
|
+
1,
|
|
3998
|
+
Math.ceil((Number.parseInt(opts.waitTimeout, 10) || 6e5) / 5e3)
|
|
3999
|
+
),
|
|
4000
|
+
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
4001
|
+
});
|
|
4002
|
+
} catch (e) {
|
|
4003
|
+
if (e instanceof PollTimeoutError) {
|
|
4004
|
+
logger.error(`Timed out \u2014 multix openrouter video-status ${res.id}`);
|
|
4005
|
+
process.exit(1);
|
|
4006
|
+
}
|
|
4007
|
+
if (e instanceof PollFailedError) {
|
|
4008
|
+
logger.error(`Job failed: ${JSON.stringify(e.value)}`);
|
|
4009
|
+
process.exit(1);
|
|
4010
|
+
}
|
|
4011
|
+
throw e;
|
|
4012
|
+
}
|
|
4013
|
+
console.error(`status: ${final.status}`);
|
|
4014
|
+
for (const u of final.unsigned_urls ?? []) console.log(u);
|
|
4015
|
+
if (opts.download) {
|
|
4016
|
+
await downloadOpenRouterVideo({
|
|
4017
|
+
jobId: res.id,
|
|
4018
|
+
output: opts.output,
|
|
4019
|
+
thumb: opts.thumb,
|
|
4020
|
+
logger,
|
|
4021
|
+
source: final
|
|
4022
|
+
});
|
|
4023
|
+
}
|
|
4024
|
+
}
|
|
4025
|
+
);
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
// src/providers/openrouter/commands/video-models.ts
|
|
4029
|
+
function registerOpenRouterVideoModelsCommand(parent) {
|
|
4030
|
+
parent.command("video-models").description("List available OpenRouter video models").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
4031
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4032
|
+
const res = await listVideoModels();
|
|
4033
|
+
const list = res.data ?? res.models ?? [];
|
|
4034
|
+
for (const m of list) {
|
|
4035
|
+
console.log(`${m.id} ${m.name ?? ""}`);
|
|
4036
|
+
}
|
|
4037
|
+
logger.debug(`${list.length} model(s)`);
|
|
4038
|
+
});
|
|
4039
|
+
}
|
|
4040
|
+
|
|
1531
4041
|
// src/providers/openrouter/commands/index.ts
|
|
1532
4042
|
function registerOpenRouterCommands(program2) {
|
|
1533
|
-
const openrouter = program2.command("openrouter").description("OpenRouter image
|
|
4043
|
+
const openrouter = program2.command("openrouter").description("OpenRouter image + video generation");
|
|
1534
4044
|
registerOpenRouterGenerateCommand(openrouter);
|
|
4045
|
+
registerOpenRouterImageToImageCommand(openrouter);
|
|
4046
|
+
registerOpenRouterImageToVideoCommand(openrouter);
|
|
4047
|
+
registerOpenRouterVideoStatusCommand(openrouter);
|
|
4048
|
+
registerOpenRouterVideoModelsCommand(openrouter);
|
|
1535
4049
|
}
|
|
1536
4050
|
|
|
1537
4051
|
// src/commands/check-helpers/binary-check.ts
|
|
@@ -1585,6 +4099,8 @@ To configure multix, set at least one provider API key:
|
|
|
1585
4099
|
Gemini / AI Studio : https://aistudio.google.com/apikey
|
|
1586
4100
|
OpenRouter : https://openrouter.ai/settings/keys
|
|
1587
4101
|
MiniMax : https://platform.minimax.io/user-center/basic-information/interface-key
|
|
4102
|
+
Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
|
|
4103
|
+
BytePlus : https://console.byteplus.com/auth/api-keys
|
|
1588
4104
|
|
|
1589
4105
|
Option A \u2014 user global config (recommended):
|
|
1590
4106
|
echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
|
|
@@ -1604,6 +4120,48 @@ var FFMPEG_HINT = "ffmpeg not found \u2014 media optimize/split/batch commands u
|
|
|
1604
4120
|
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
4121
|
|
|
1606
4122
|
// src/commands/check.ts
|
|
4123
|
+
var PROVIDERS = [
|
|
4124
|
+
{
|
|
4125
|
+
name: "Gemini",
|
|
4126
|
+
envPrimary: "GEMINI_API_KEY",
|
|
4127
|
+
optional: false,
|
|
4128
|
+
link: "https://aistudio.google.com/apikey"
|
|
4129
|
+
},
|
|
4130
|
+
{
|
|
4131
|
+
name: "OpenRouter",
|
|
4132
|
+
envPrimary: "OPENROUTER_API_KEY",
|
|
4133
|
+
optional: true,
|
|
4134
|
+
link: "https://openrouter.ai/settings/keys"
|
|
4135
|
+
},
|
|
4136
|
+
{
|
|
4137
|
+
name: "MiniMax",
|
|
4138
|
+
envPrimary: "MINIMAX_API_KEY",
|
|
4139
|
+
optional: true,
|
|
4140
|
+
link: "https://platform.minimax.io/user-center/basic-information/interface-key"
|
|
4141
|
+
},
|
|
4142
|
+
{
|
|
4143
|
+
name: "Leonardo.Ai",
|
|
4144
|
+
envPrimary: "LEONARDO_API_KEY",
|
|
4145
|
+
optional: true,
|
|
4146
|
+
link: "https://app.leonardo.ai/settings/api-keys"
|
|
4147
|
+
},
|
|
4148
|
+
{
|
|
4149
|
+
name: "BytePlus",
|
|
4150
|
+
envPrimary: "BYTEPLUS_API_KEY",
|
|
4151
|
+
envFallback: "ARK_API_KEY",
|
|
4152
|
+
optional: true,
|
|
4153
|
+
link: "https://console.byteplus.com/auth/api-keys"
|
|
4154
|
+
}
|
|
4155
|
+
];
|
|
4156
|
+
function resolveProviderKey(p) {
|
|
4157
|
+
const primary = resolveKey(p.envPrimary);
|
|
4158
|
+
if (primary) return { key: primary, envUsed: p.envPrimary };
|
|
4159
|
+
if (p.envFallback) {
|
|
4160
|
+
const fb = resolveKey(p.envFallback);
|
|
4161
|
+
if (fb) return { key: fb, envUsed: p.envFallback };
|
|
4162
|
+
}
|
|
4163
|
+
return {};
|
|
4164
|
+
}
|
|
1607
4165
|
function registerCheckCommand(program2) {
|
|
1608
4166
|
program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
|
|
1609
4167
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -1624,29 +4182,28 @@ function registerCheckCommand(program2) {
|
|
|
1624
4182
|
if (opts.verbose) console.log(` ${MAGICK_HINT}`);
|
|
1625
4183
|
}
|
|
1626
4184
|
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)");
|
|
4185
|
+
let anyKey = false;
|
|
4186
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
4187
|
+
for (const p of PROVIDERS) {
|
|
4188
|
+
const r = resolveProviderKey(p);
|
|
4189
|
+
resolved.set(p.name, r);
|
|
4190
|
+
if (r.key) anyKey = true;
|
|
4191
|
+
const envLabel = r.envUsed ?? p.envPrimary;
|
|
4192
|
+
if (r.key) {
|
|
4193
|
+
logger.success(`${envLabel} found \u2014 ${redact(r.key)}`);
|
|
4194
|
+
} else if (p.optional) {
|
|
4195
|
+
const fbNote = p.envFallback ? ` (or ${p.envFallback})` : "";
|
|
4196
|
+
logger.info(`${p.envPrimary}${fbNote} not set (optional)`);
|
|
4197
|
+
} else {
|
|
4198
|
+
logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
|
|
4199
|
+
}
|
|
1644
4200
|
}
|
|
1645
|
-
if (!
|
|
4201
|
+
if (!anyKey) {
|
|
1646
4202
|
logger.header("No Provider Keys Configured");
|
|
1647
4203
|
console.log(SETUP_HINTS);
|
|
1648
4204
|
process.exit(1);
|
|
1649
4205
|
}
|
|
4206
|
+
const geminiKey = resolved.get("Gemini")?.key;
|
|
1650
4207
|
if (geminiKey) {
|
|
1651
4208
|
logger.header("Gemini Connectivity");
|
|
1652
4209
|
logger.info("Pinging Gemini models endpoint...");
|
|
@@ -1668,7 +4225,7 @@ function registerCheckCommand(program2) {
|
|
|
1668
4225
|
" Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
|
|
1669
4226
|
);
|
|
1670
4227
|
} else {
|
|
1671
|
-
console.log(" Image generation via OpenRouter/MiniMax available");
|
|
4228
|
+
console.log(" Image generation via OpenRouter/MiniMax/Leonardo/BytePlus available");
|
|
1672
4229
|
console.log(" Add GEMINI_API_KEY for analysis, transcription, and doc conversion");
|
|
1673
4230
|
}
|
|
1674
4231
|
} else {
|
|
@@ -1680,8 +4237,8 @@ function registerCheckCommand(program2) {
|
|
|
1680
4237
|
|
|
1681
4238
|
// src/commands/doc/convert.ts
|
|
1682
4239
|
init_errors();
|
|
1683
|
-
import
|
|
1684
|
-
import
|
|
4240
|
+
import fs27 from "fs";
|
|
4241
|
+
import path32 from "path";
|
|
1685
4242
|
|
|
1686
4243
|
// src/commands/doc/default-prompt.ts
|
|
1687
4244
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -1702,18 +4259,18 @@ async function convertDocuments(opts) {
|
|
|
1702
4259
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
1703
4260
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
1704
4261
|
for (const f of inputs) {
|
|
1705
|
-
if (!
|
|
4262
|
+
if (!fs27.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
1706
4263
|
}
|
|
1707
4264
|
let outputPath;
|
|
1708
4265
|
if (output) {
|
|
1709
|
-
outputPath =
|
|
4266
|
+
outputPath = path32.resolve(output);
|
|
1710
4267
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
1711
|
-
const base =
|
|
1712
|
-
outputPath =
|
|
4268
|
+
const base = path32.basename(inputs[0], path32.extname(inputs[0]));
|
|
4269
|
+
outputPath = path32.join(getOutputDir(), `${base}-extraction.md`);
|
|
1713
4270
|
} else {
|
|
1714
|
-
outputPath =
|
|
4271
|
+
outputPath = path32.join(getOutputDir(), "document-extraction.md");
|
|
1715
4272
|
}
|
|
1716
|
-
|
|
4273
|
+
fs27.mkdirSync(path32.dirname(outputPath), { recursive: true });
|
|
1717
4274
|
const results = [];
|
|
1718
4275
|
for (let i = 0; i < inputs.length; i++) {
|
|
1719
4276
|
const filePath = inputs[i];
|
|
@@ -1745,7 +4302,7 @@ async function convertDocuments(opts) {
|
|
|
1745
4302
|
if (attempt < 2) {
|
|
1746
4303
|
const wait = 2 ** attempt * 1e3;
|
|
1747
4304
|
logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
|
|
1748
|
-
await
|
|
4305
|
+
await sleep5(wait);
|
|
1749
4306
|
}
|
|
1750
4307
|
}
|
|
1751
4308
|
}
|
|
@@ -1764,7 +4321,7 @@ async function convertDocuments(opts) {
|
|
|
1764
4321
|
`);
|
|
1765
4322
|
for (const r of results) {
|
|
1766
4323
|
lines.push(`
|
|
1767
|
-
## ${
|
|
4324
|
+
## ${path32.basename(r.file)}
|
|
1768
4325
|
`);
|
|
1769
4326
|
if (r.status === "success" && r.markdown) {
|
|
1770
4327
|
lines.push(`${r.markdown}
|
|
@@ -1777,7 +4334,7 @@ async function convertDocuments(opts) {
|
|
|
1777
4334
|
}
|
|
1778
4335
|
lines.push("\n---\n");
|
|
1779
4336
|
}
|
|
1780
|
-
|
|
4337
|
+
fs27.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
1781
4338
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
1782
4339
|
console.log(`
|
|
1783
4340
|
${"=".repeat(50)}`);
|
|
@@ -1793,7 +4350,7 @@ ${"=".repeat(50)}`);
|
|
|
1793
4350
|
const failed = results.filter((r) => r.status === "error").length;
|
|
1794
4351
|
if (failed > 0) process.exit(1);
|
|
1795
4352
|
}
|
|
1796
|
-
function
|
|
4353
|
+
function sleep5(ms) {
|
|
1797
4354
|
return new Promise((r) => setTimeout(r, ms));
|
|
1798
4355
|
}
|
|
1799
4356
|
|
|
@@ -1823,12 +4380,12 @@ function registerDocCommands(program2) {
|
|
|
1823
4380
|
|
|
1824
4381
|
// src/commands/media/index.ts
|
|
1825
4382
|
init_errors();
|
|
1826
|
-
import
|
|
1827
|
-
import
|
|
4383
|
+
import fs30 from "fs";
|
|
4384
|
+
import path35 from "path";
|
|
1828
4385
|
|
|
1829
4386
|
// src/commands/media/batch.ts
|
|
1830
|
-
import
|
|
1831
|
-
import
|
|
4387
|
+
import fs28 from "fs";
|
|
4388
|
+
import path33 from "path";
|
|
1832
4389
|
|
|
1833
4390
|
// src/commands/media/detect-type.ts
|
|
1834
4391
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -2024,10 +4581,10 @@ async function batchOptimize(opts) {
|
|
|
2024
4581
|
verbose,
|
|
2025
4582
|
logger
|
|
2026
4583
|
} = opts;
|
|
2027
|
-
|
|
2028
|
-
const entries =
|
|
4584
|
+
fs28.mkdirSync(outputDir, { recursive: true });
|
|
4585
|
+
const entries = fs28.readdirSync(inputDir);
|
|
2029
4586
|
const mediaFiles = entries.filter((f) => {
|
|
2030
|
-
const ext =
|
|
4587
|
+
const ext = path33.extname(f).toLowerCase();
|
|
2031
4588
|
return ALL_EXTS.has(ext);
|
|
2032
4589
|
});
|
|
2033
4590
|
if (mediaFiles.length === 0) {
|
|
@@ -2038,9 +4595,9 @@ async function batchOptimize(opts) {
|
|
|
2038
4595
|
let succeeded = 0;
|
|
2039
4596
|
const errors = [];
|
|
2040
4597
|
for (const file of mediaFiles) {
|
|
2041
|
-
const inputPath =
|
|
2042
|
-
const outputPath =
|
|
2043
|
-
const ext =
|
|
4598
|
+
const inputPath = path33.join(inputDir, file);
|
|
4599
|
+
const outputPath = path33.join(outputDir, file);
|
|
4600
|
+
const ext = path33.extname(file).toLowerCase();
|
|
2044
4601
|
const kind = extToKind(ext);
|
|
2045
4602
|
logger?.debug(`Processing: ${file}`);
|
|
2046
4603
|
try {
|
|
@@ -2077,8 +4634,8 @@ async function batchOptimize(opts) {
|
|
|
2077
4634
|
|
|
2078
4635
|
// src/commands/media/split-video.ts
|
|
2079
4636
|
init_errors();
|
|
2080
|
-
import
|
|
2081
|
-
import
|
|
4637
|
+
import fs29 from "fs";
|
|
4638
|
+
import path34 from "path";
|
|
2082
4639
|
async function splitVideo(opts) {
|
|
2083
4640
|
await assertFfmpeg();
|
|
2084
4641
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -2089,10 +4646,10 @@ async function splitVideo(opts) {
|
|
|
2089
4646
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
2090
4647
|
return [input];
|
|
2091
4648
|
}
|
|
2092
|
-
|
|
2093
|
-
const ext =
|
|
2094
|
-
const baseName =
|
|
2095
|
-
const outputPattern =
|
|
4649
|
+
fs29.mkdirSync(outputDir, { recursive: true });
|
|
4650
|
+
const ext = path34.extname(input) || ".mp4";
|
|
4651
|
+
const baseName = path34.basename(input, ext);
|
|
4652
|
+
const outputPattern = path34.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
2096
4653
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
2097
4654
|
const args = [
|
|
2098
4655
|
"-i",
|
|
@@ -2111,8 +4668,8 @@ async function splitVideo(opts) {
|
|
|
2111
4668
|
await runFfmpeg(args, { verbose, logger });
|
|
2112
4669
|
const files = [];
|
|
2113
4670
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
2114
|
-
const fname =
|
|
2115
|
-
if (
|
|
4671
|
+
const fname = path34.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
4672
|
+
if (fs29.existsSync(fname)) files.push(fname);
|
|
2116
4673
|
}
|
|
2117
4674
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
2118
4675
|
return files;
|
|
@@ -2126,9 +4683,9 @@ function registerMediaCommands(program2) {
|
|
|
2126
4683
|
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
4684
|
async (opts) => {
|
|
2128
4685
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2129
|
-
if (!
|
|
2130
|
-
|
|
2131
|
-
const ext =
|
|
4686
|
+
if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
4687
|
+
fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
|
|
4688
|
+
const ext = path35.extname(opts.input).toLowerCase();
|
|
2132
4689
|
const kind = extToKind(ext);
|
|
2133
4690
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
2134
4691
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -2168,7 +4725,7 @@ function registerMediaCommands(program2) {
|
|
|
2168
4725
|
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
4726
|
async (opts) => {
|
|
2170
4727
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2171
|
-
if (!
|
|
4728
|
+
if (!fs30.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
2172
4729
|
const chunks = await splitVideo({
|
|
2173
4730
|
input: opts.input,
|
|
2174
4731
|
outputDir: opts.outputDir,
|
|
@@ -2184,7 +4741,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
2184
4741
|
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
4742
|
async (opts) => {
|
|
2186
4743
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2187
|
-
if (!
|
|
4744
|
+
if (!fs30.existsSync(opts.inputDir))
|
|
2188
4745
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
2189
4746
|
const result = await batchOptimize({
|
|
2190
4747
|
inputDir: opts.inputDir,
|
|
@@ -2211,6 +4768,8 @@ function registerCommands(program2) {
|
|
|
2211
4768
|
registerGeminiCommands(program2);
|
|
2212
4769
|
registerMinimaxCommands(program2);
|
|
2213
4770
|
registerOpenRouterCommands(program2);
|
|
4771
|
+
registerLeonardoCommands(program2);
|
|
4772
|
+
registerBytePlusCommands(program2);
|
|
2214
4773
|
registerMediaCommands(program2);
|
|
2215
4774
|
registerDocCommands(program2);
|
|
2216
4775
|
}
|