@mrgoonie/multix 0.0.5 → 0.0.7
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 +26 -0
- package/README.md +91 -1
- package/dist/cli.js +1400 -484
- package/package.json +9 -3
package/dist/cli.js
CHANGED
|
@@ -10,6 +10,14 @@ var __export = (target, all) => {
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
// src/core/errors.ts
|
|
13
|
+
var errors_exports = {};
|
|
14
|
+
__export(errors_exports, {
|
|
15
|
+
ConfigError: () => ConfigError,
|
|
16
|
+
HttpError: () => HttpError,
|
|
17
|
+
MultixError: () => MultixError,
|
|
18
|
+
ProviderError: () => ProviderError,
|
|
19
|
+
ValidationError: () => ValidationError
|
|
20
|
+
});
|
|
13
21
|
var MultixError, ConfigError, ProviderError, ValidationError, HttpError;
|
|
14
22
|
var init_errors = __esm({
|
|
15
23
|
"src/core/errors.ts"() {
|
|
@@ -67,8 +75,8 @@ __export(http_client_exports, {
|
|
|
67
75
|
fetchBytes: () => fetchBytes,
|
|
68
76
|
httpJson: () => httpJson
|
|
69
77
|
});
|
|
70
|
-
import
|
|
71
|
-
import
|
|
78
|
+
import fs4 from "fs";
|
|
79
|
+
import path4 from "path";
|
|
72
80
|
import { pipeline } from "stream/promises";
|
|
73
81
|
async function httpJson(opts) {
|
|
74
82
|
const { url, method = "GET", headers = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
@@ -103,7 +111,7 @@ async function httpJson(opts) {
|
|
|
103
111
|
return response.json();
|
|
104
112
|
}
|
|
105
113
|
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
106
|
-
|
|
114
|
+
fs4.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
107
115
|
let response;
|
|
108
116
|
try {
|
|
109
117
|
response = await globalThis.fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
@@ -117,7 +125,7 @@ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
117
125
|
if (!response.body) {
|
|
118
126
|
throw new HttpError(0, "Response body is null", url);
|
|
119
127
|
}
|
|
120
|
-
const fileStream =
|
|
128
|
+
const fileStream = fs4.createWriteStream(dest);
|
|
121
129
|
await pipeline(response.body, fileStream);
|
|
122
130
|
}
|
|
123
131
|
async function fetchBytes(urlOrDataUri) {
|
|
@@ -155,6 +163,90 @@ var init_http_client = __esm({
|
|
|
155
163
|
import { createRequire } from "module";
|
|
156
164
|
import { Command } from "commander";
|
|
157
165
|
|
|
166
|
+
// src/providers/byteplus/commands/generate-3d.ts
|
|
167
|
+
import path5 from "path";
|
|
168
|
+
|
|
169
|
+
// src/core/image-input.ts
|
|
170
|
+
import fs from "fs/promises";
|
|
171
|
+
import path from "path";
|
|
172
|
+
var IMAGE_MIME_BY_EXT = {
|
|
173
|
+
".jpg": "image/jpeg",
|
|
174
|
+
".jpeg": "image/jpeg",
|
|
175
|
+
".png": "image/png",
|
|
176
|
+
".webp": "image/webp",
|
|
177
|
+
".gif": "image/gif"
|
|
178
|
+
};
|
|
179
|
+
var VIDEO_MIME_BY_EXT = {
|
|
180
|
+
".mp4": "video/mp4",
|
|
181
|
+
".webm": "video/webm",
|
|
182
|
+
".mov": "video/quicktime",
|
|
183
|
+
".m4v": "video/x-m4v"
|
|
184
|
+
};
|
|
185
|
+
var AUDIO_MIME_BY_EXT = {
|
|
186
|
+
".mp3": "audio/mpeg",
|
|
187
|
+
".wav": "audio/wav",
|
|
188
|
+
".m4a": "audio/mp4",
|
|
189
|
+
".flac": "audio/flac",
|
|
190
|
+
".ogg": "audio/ogg"
|
|
191
|
+
};
|
|
192
|
+
var DEFAULT_SOFT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
193
|
+
var DEFAULT_HARD_LIMIT_BYTES = 25 * 1024 * 1024;
|
|
194
|
+
function isHttpUrl(input) {
|
|
195
|
+
return /^https?:\/\//i.test(input);
|
|
196
|
+
}
|
|
197
|
+
async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
|
|
198
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
199
|
+
const mime = mimeTable[ext];
|
|
200
|
+
if (!mime) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Unsupported ${kindLabel} extension '${ext}'. Allowed: ${Object.keys(mimeTable).join(", ")}`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
|
|
206
|
+
const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
|
|
207
|
+
const buf = await fs.readFile(filePath);
|
|
208
|
+
if (buf.byteLength > hard) {
|
|
209
|
+
throw new Error(
|
|
210
|
+
`${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.`
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
if (buf.byteLength > soft) {
|
|
214
|
+
opts.logger?.warn(
|
|
215
|
+
`${kindLabel} payload ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB is large; consider URL input.`
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const dataUrl = `data:${mime};base64,${buf.toString("base64")}`;
|
|
219
|
+
return { dataUrl, mime, bytes: buf.byteLength };
|
|
220
|
+
}
|
|
221
|
+
async function resolveImageInput(input, opts = {}) {
|
|
222
|
+
const o = normalizeOpts(opts);
|
|
223
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
224
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, IMAGE_MIME_BY_EXT, "image", o);
|
|
225
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
226
|
+
}
|
|
227
|
+
async function resolveVideoInput(input, opts = {}) {
|
|
228
|
+
const o = normalizeOpts(opts);
|
|
229
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
230
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, VIDEO_MIME_BY_EXT, "video", o);
|
|
231
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
232
|
+
}
|
|
233
|
+
async function resolveAudioInput(input, opts = {}) {
|
|
234
|
+
const o = normalizeOpts(opts);
|
|
235
|
+
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
236
|
+
const { dataUrl, mime, bytes } = await readAsDataUrl(input, AUDIO_MIME_BY_EXT, "audio", o);
|
|
237
|
+
return { kind: "data", dataUrl, mime, bytes };
|
|
238
|
+
}
|
|
239
|
+
function refUrl(r) {
|
|
240
|
+
return r.kind === "url" ? r.url : r.dataUrl;
|
|
241
|
+
}
|
|
242
|
+
function normalizeOpts(opts) {
|
|
243
|
+
if (!opts) return {};
|
|
244
|
+
if (typeof opts.info === "function" && typeof opts.warn === "function") {
|
|
245
|
+
return { logger: opts };
|
|
246
|
+
}
|
|
247
|
+
return opts;
|
|
248
|
+
}
|
|
249
|
+
|
|
158
250
|
// src/core/logger.ts
|
|
159
251
|
var ANSI = {
|
|
160
252
|
reset: "\x1B[0m",
|
|
@@ -200,22 +292,69 @@ ${line}
|
|
|
200
292
|
}
|
|
201
293
|
var defaultLogger = createLogger({ verbose: false });
|
|
202
294
|
|
|
295
|
+
// src/core/output-dir.ts
|
|
296
|
+
import fs2 from "fs";
|
|
297
|
+
import path2 from "path";
|
|
298
|
+
var cachedOutputDir;
|
|
299
|
+
function getOutputDir() {
|
|
300
|
+
if (cachedOutputDir) return cachedOutputDir;
|
|
301
|
+
const dir = process.env.MULTIX_OUTPUT_DIR ? path2.resolve(process.env.MULTIX_OUTPUT_DIR) : path2.resolve(process.cwd(), "multix-output");
|
|
302
|
+
fs2.mkdirSync(dir, { recursive: true });
|
|
303
|
+
cachedOutputDir = dir;
|
|
304
|
+
return dir;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/providers/leonardo/poll.ts
|
|
308
|
+
var PollTimeoutError = class extends Error {
|
|
309
|
+
constructor() {
|
|
310
|
+
super("Polling timed out before the operation completed.");
|
|
311
|
+
this.name = "PollTimeoutError";
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
var PollFailedError = class extends Error {
|
|
315
|
+
constructor(value) {
|
|
316
|
+
super("Operation failed.");
|
|
317
|
+
this.value = value;
|
|
318
|
+
this.name = "PollFailedError";
|
|
319
|
+
}
|
|
320
|
+
value;
|
|
321
|
+
};
|
|
322
|
+
async function poll(opts) {
|
|
323
|
+
const interval = opts.intervalMs ?? 4e3;
|
|
324
|
+
const max = opts.maxAttempts ?? 120;
|
|
325
|
+
for (let attempt = 1; attempt <= max; attempt++) {
|
|
326
|
+
if (opts.signal?.aborted) throw new Error("Aborted");
|
|
327
|
+
const value = await opts.fetch();
|
|
328
|
+
opts.onTick?.(attempt, value);
|
|
329
|
+
if (opts.failed?.(value)) throw new PollFailedError(value);
|
|
330
|
+
if (opts.done(value)) return value;
|
|
331
|
+
await new Promise((resolve, reject) => {
|
|
332
|
+
const t = setTimeout(resolve, interval);
|
|
333
|
+
opts.signal?.addEventListener("abort", () => {
|
|
334
|
+
clearTimeout(t);
|
|
335
|
+
reject(new Error("Aborted"));
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
throw new PollTimeoutError();
|
|
340
|
+
}
|
|
341
|
+
|
|
203
342
|
// src/core/env-loader.ts
|
|
204
|
-
import
|
|
343
|
+
import fs3 from "fs";
|
|
205
344
|
import os from "os";
|
|
206
|
-
import
|
|
345
|
+
import path3 from "path";
|
|
207
346
|
import { config as dotenvConfig } from "dotenv";
|
|
208
347
|
var loaded = false;
|
|
209
348
|
function loadEnv() {
|
|
210
349
|
if (loaded) return;
|
|
211
350
|
loaded = true;
|
|
212
|
-
const cwdEnv =
|
|
213
|
-
if (
|
|
351
|
+
const cwdEnv = path3.resolve(process.cwd(), ".env");
|
|
352
|
+
if (fs3.existsSync(cwdEnv)) {
|
|
214
353
|
dotenvConfig({ path: cwdEnv, override: false });
|
|
215
354
|
}
|
|
216
355
|
if (!process.env.MULTIX_DISABLE_HOME_ENV) {
|
|
217
|
-
const homeEnv =
|
|
218
|
-
if (
|
|
356
|
+
const homeEnv = path3.join(os.homedir(), ".multix", ".env");
|
|
357
|
+
if (fs3.existsSync(homeEnv)) {
|
|
219
358
|
dotenvConfig({ path: homeEnv, override: false });
|
|
220
359
|
}
|
|
221
360
|
}
|
|
@@ -236,9 +375,15 @@ init_http_client();
|
|
|
236
375
|
var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
|
|
237
376
|
var DEFAULT_BYTEPLUS_IMAGE_MODEL = "seedream-4-0-250828";
|
|
238
377
|
var DEFAULT_BYTEPLUS_VIDEO_MODEL = "seedance-2.0";
|
|
378
|
+
var DEFAULT_BYTEPLUS_3D_MODEL = "hyper3d-gen2-260112";
|
|
379
|
+
var BYTEPLUS_3D_MODELS = [
|
|
380
|
+
{ id: "hyper3d-gen2-260112", name: "Hyper3D Gen2", family: "hyper3d" },
|
|
381
|
+
{ id: "hitem3d-2-0-251223", name: "Hitem3d 2.0", family: "hitem3d" }
|
|
382
|
+
];
|
|
239
383
|
var BYTEPLUS_DEFAULTS = {
|
|
240
384
|
imageModel: process.env.BYTEPLUS_IMAGE_MODEL ?? DEFAULT_BYTEPLUS_IMAGE_MODEL,
|
|
241
385
|
videoModel: process.env.BYTEPLUS_VIDEO_MODEL ?? DEFAULT_BYTEPLUS_VIDEO_MODEL,
|
|
386
|
+
threeDModel: process.env.BYTEPLUS_3D_MODEL ?? DEFAULT_BYTEPLUS_3D_MODEL,
|
|
242
387
|
videoResolution: "1080p",
|
|
243
388
|
videoDuration: 8,
|
|
244
389
|
videoAspectRatio: "16:9",
|
|
@@ -269,9 +414,9 @@ var BytePlusClient = class {
|
|
|
269
414
|
}
|
|
270
415
|
apiKey;
|
|
271
416
|
baseUrl;
|
|
272
|
-
async request(
|
|
417
|
+
async request(path41, opts = {}) {
|
|
273
418
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
274
|
-
const url = new URL(base +
|
|
419
|
+
const url = new URL(base + path41);
|
|
275
420
|
if (opts.query) {
|
|
276
421
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
277
422
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -289,141 +434,167 @@ var BytePlusClient = class {
|
|
|
289
434
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
290
435
|
});
|
|
291
436
|
}
|
|
292
|
-
get(
|
|
293
|
-
return this.request(
|
|
437
|
+
get(path41, query, logger) {
|
|
438
|
+
return this.request(path41, { method: "GET", query, logger });
|
|
294
439
|
}
|
|
295
|
-
post(
|
|
296
|
-
return this.request(
|
|
440
|
+
post(path41, body, logger) {
|
|
441
|
+
return this.request(path41, { method: "POST", body, logger });
|
|
297
442
|
}
|
|
298
|
-
delete(
|
|
299
|
-
return this.request(
|
|
443
|
+
delete(path41, logger) {
|
|
444
|
+
return this.request(path41, { method: "DELETE", logger });
|
|
300
445
|
}
|
|
301
446
|
};
|
|
302
447
|
function createBytePlusClient() {
|
|
303
448
|
return new BytePlusClient(requireBytePlusKey(), bytePlusBaseUrl());
|
|
304
449
|
}
|
|
305
450
|
|
|
306
|
-
// src/providers/byteplus/generators/
|
|
451
|
+
// src/providers/byteplus/generators/three-d.ts
|
|
307
452
|
init_http_client();
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
".jpeg": "image/jpeg",
|
|
317
|
-
".png": "image/png",
|
|
318
|
-
".webp": "image/webp",
|
|
319
|
-
".gif": "image/gif"
|
|
320
|
-
};
|
|
321
|
-
var VIDEO_MIME_BY_EXT = {
|
|
322
|
-
".mp4": "video/mp4",
|
|
323
|
-
".webm": "video/webm",
|
|
324
|
-
".mov": "video/quicktime",
|
|
325
|
-
".m4v": "video/x-m4v"
|
|
326
|
-
};
|
|
327
|
-
var AUDIO_MIME_BY_EXT = {
|
|
328
|
-
".mp3": "audio/mpeg",
|
|
329
|
-
".wav": "audio/wav",
|
|
330
|
-
".m4a": "audio/mp4",
|
|
331
|
-
".flac": "audio/flac",
|
|
332
|
-
".ogg": "audio/ogg"
|
|
333
|
-
};
|
|
334
|
-
var DEFAULT_SOFT_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
335
|
-
var DEFAULT_HARD_LIMIT_BYTES = 25 * 1024 * 1024;
|
|
336
|
-
function isHttpUrl(input) {
|
|
337
|
-
return /^https?:\/\//i.test(input);
|
|
338
|
-
}
|
|
339
|
-
async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
|
|
340
|
-
const ext = path3.extname(filePath).toLowerCase();
|
|
341
|
-
const mime = mimeTable[ext];
|
|
342
|
-
if (!mime) {
|
|
343
|
-
throw new Error(
|
|
344
|
-
`Unsupported ${kindLabel} extension '${ext}'. Allowed: ${Object.keys(mimeTable).join(", ")}`
|
|
345
|
-
);
|
|
346
|
-
}
|
|
347
|
-
const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
|
|
348
|
-
const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
|
|
349
|
-
const buf = await fs3.readFile(filePath);
|
|
350
|
-
if (buf.byteLength > hard) {
|
|
351
|
-
throw new Error(
|
|
352
|
-
`${kindLabel} file ${filePath} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds ${(hard / 1024 / 1024).toFixed(0)}MB limit. Upload to a URL instead.`
|
|
353
|
-
);
|
|
453
|
+
function buildThreeDTaskBody(opts) {
|
|
454
|
+
const promptText = (opts.prompt ?? "").trim();
|
|
455
|
+
const flagPart = opts.flags?.trim() ?? "";
|
|
456
|
+
const text = [promptText, flagPart].filter((s) => s.length > 0).join(" ");
|
|
457
|
+
const content = [];
|
|
458
|
+
if (text.length > 0) content.push({ type: "text", text });
|
|
459
|
+
for (const img of opts.imageInputs ?? []) {
|
|
460
|
+
content.push({ type: "image_url", image_url: { url: refUrl(img) } });
|
|
354
461
|
}
|
|
355
|
-
if (
|
|
356
|
-
|
|
357
|
-
`${kindLabel} payload ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB is large; consider URL input.`
|
|
358
|
-
);
|
|
462
|
+
if (content.length === 0) {
|
|
463
|
+
throw new Error("3D task requires either a prompt or at least one image input.");
|
|
359
464
|
}
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
async function resolveImageInput(input, opts = {}) {
|
|
364
|
-
const o = normalizeOpts(opts);
|
|
365
|
-
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
366
|
-
const { dataUrl, mime, bytes } = await readAsDataUrl(input, IMAGE_MIME_BY_EXT, "image", o);
|
|
367
|
-
return { kind: "data", dataUrl, mime, bytes };
|
|
465
|
+
const body = { model: opts.model, content };
|
|
466
|
+
if (opts.seed !== void 0) body.seed = opts.seed;
|
|
467
|
+
return body;
|
|
368
468
|
}
|
|
369
|
-
async function
|
|
370
|
-
|
|
371
|
-
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
372
|
-
const { dataUrl, mime, bytes } = await readAsDataUrl(input, VIDEO_MIME_BY_EXT, "video", o);
|
|
373
|
-
return { kind: "data", dataUrl, mime, bytes };
|
|
469
|
+
async function submitThreeDTask(client, body, logger) {
|
|
470
|
+
return client.post("/contents/generations/tasks", body, logger);
|
|
374
471
|
}
|
|
375
|
-
async function
|
|
376
|
-
|
|
377
|
-
if (isHttpUrl(input)) return { kind: "url", url: input };
|
|
378
|
-
const { dataUrl, mime, bytes } = await readAsDataUrl(input, AUDIO_MIME_BY_EXT, "audio", o);
|
|
379
|
-
return { kind: "data", dataUrl, mime, bytes };
|
|
472
|
+
async function getThreeDTaskStatus(client, id, logger) {
|
|
473
|
+
return client.get(`/contents/generations/tasks/${id}`, void 0, logger);
|
|
380
474
|
}
|
|
381
|
-
function
|
|
382
|
-
|
|
475
|
+
async function waitForThreeDTask(client, id, opts = {}) {
|
|
476
|
+
const intervalMs = opts.intervalMs ?? 4e3;
|
|
477
|
+
const maxAttempts = opts.maxAttempts ?? (opts.waitTimeoutMs ? Math.max(1, Math.ceil(opts.waitTimeoutMs / intervalMs)) : 180);
|
|
478
|
+
return poll({
|
|
479
|
+
fetch: () => getThreeDTaskStatus(client, id, opts.logger),
|
|
480
|
+
done: (v) => v.status === "succeeded",
|
|
481
|
+
failed: (v) => v.status === "failed" || v.status === "cancelled",
|
|
482
|
+
intervalMs,
|
|
483
|
+
maxAttempts,
|
|
484
|
+
onTick: (attempt, v) => opts.logger?.debug(`attempt ${attempt} \u2014 status: ${v.status}`)
|
|
485
|
+
});
|
|
383
486
|
}
|
|
384
|
-
function
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
return { logger: opts };
|
|
388
|
-
}
|
|
389
|
-
return opts;
|
|
487
|
+
function inferModelFileExt(url) {
|
|
488
|
+
const m = url.match(/\.(glb|gltf|obj|fbx|usdz|stl|ply|zip)(?:\?|#|$)/i);
|
|
489
|
+
return m?.[1] ? m[1].toLowerCase() : "glb";
|
|
390
490
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
import path4 from "path";
|
|
395
|
-
var cachedOutputDir;
|
|
396
|
-
function getOutputDir() {
|
|
397
|
-
if (cachedOutputDir) return cachedOutputDir;
|
|
398
|
-
const dir = process.env.MULTIX_OUTPUT_DIR ? path4.resolve(process.env.MULTIX_OUTPUT_DIR) : path4.resolve(process.cwd(), "multix-output");
|
|
399
|
-
fs4.mkdirSync(dir, { recursive: true });
|
|
400
|
-
cachedOutputDir = dir;
|
|
401
|
-
return dir;
|
|
491
|
+
async function downloadModelFile(url, outputPath, logger) {
|
|
492
|
+
await downloadFile(url, outputPath);
|
|
493
|
+
logger?.success(`Saved ${outputPath}`);
|
|
402
494
|
}
|
|
403
495
|
|
|
404
|
-
// src/providers/byteplus/
|
|
405
|
-
|
|
406
|
-
"
|
|
407
|
-
|
|
408
|
-
"
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
"
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
496
|
+
// src/providers/byteplus/commands/generate-3d.ts
|
|
497
|
+
function registerBytePlusGenerate3DCommand(parent) {
|
|
498
|
+
parent.command("generate-3d").alias("3d").description(
|
|
499
|
+
`Generate a 3D model via BytePlus (Hyper3D / Hitem3d). Models: ${BYTEPLUS_3D_MODELS.map((m) => m.id).join(", ")}`
|
|
500
|
+
).option("--prompt <text>", "Text prompt (text-to-3D or to guide image-to-3D)").option(
|
|
501
|
+
"-m, --model <id>",
|
|
502
|
+
`Model id (default ${BYTEPLUS_DEFAULTS.threeDModel})`,
|
|
503
|
+
BYTEPLUS_DEFAULTS.threeDModel
|
|
504
|
+
).option("--input-image <path|url...>", "1\u20135 reference images for image-to-3D (path or URL)").option(
|
|
505
|
+
"--flags <raw>",
|
|
506
|
+
"Raw flags appended to the prompt (e.g. '--mesh_mode Raw --hd_texture true' for Hyper3D, '--ff 2 --resolution 1536pro' for Hitem3d)"
|
|
507
|
+
).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(
|
|
508
|
+
async (opts) => {
|
|
509
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
510
|
+
const client = createBytePlusClient();
|
|
511
|
+
const images = opts.inputImage ?? [];
|
|
512
|
+
if (images.length > 5) {
|
|
513
|
+
logger.error("BytePlus 3D supports at most 5 reference images.");
|
|
514
|
+
process.exit(1);
|
|
515
|
+
}
|
|
516
|
+
const imageInputs = await Promise.all(images.map((src) => resolveImageInput(src, logger)));
|
|
517
|
+
if (!opts.prompt && imageInputs.length === 0) {
|
|
518
|
+
logger.error("Provide --prompt and/or --input-image to describe the 3D content.");
|
|
519
|
+
process.exit(1);
|
|
520
|
+
}
|
|
521
|
+
const body = buildThreeDTaskBody({
|
|
522
|
+
model: opts.model,
|
|
523
|
+
prompt: opts.prompt,
|
|
524
|
+
flags: opts.flags,
|
|
525
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
526
|
+
imageInputs
|
|
527
|
+
});
|
|
528
|
+
logger.info(
|
|
529
|
+
`Submitting 3D task (${opts.model}${imageInputs.length > 0 ? `, ${imageInputs.length} image ref${imageInputs.length === 1 ? "" : "s"}` : ""})`
|
|
530
|
+
);
|
|
531
|
+
const submitted = await submitThreeDTask(client, body, logger);
|
|
532
|
+
const taskId = submitted.id;
|
|
533
|
+
logger.debug(`taskId: ${taskId}`);
|
|
534
|
+
if (opts.async) {
|
|
535
|
+
console.log(taskId);
|
|
536
|
+
console.error(`Poll with: multix byteplus status ${taskId} --wait --download`);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
try {
|
|
540
|
+
const final = await waitForThreeDTask(client, taskId, {
|
|
541
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 9e5,
|
|
542
|
+
logger
|
|
543
|
+
});
|
|
544
|
+
const url = final.content?.file_url;
|
|
545
|
+
if (!url) {
|
|
546
|
+
logger.error(`Task succeeded but no file_url in response: ${JSON.stringify(final)}`);
|
|
547
|
+
process.exit(1);
|
|
548
|
+
}
|
|
549
|
+
const ext = inferModelFileExt(url);
|
|
550
|
+
const outPath = opts.output ?? path5.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}-${Date.now()}.${ext}`);
|
|
551
|
+
await downloadModelFile(url, outPath, logger);
|
|
552
|
+
console.log(`
|
|
553
|
+
Task: ${taskId}`);
|
|
554
|
+
console.log(`Saved: ${outPath}`);
|
|
555
|
+
} catch (e) {
|
|
556
|
+
if (e instanceof PollTimeoutError) {
|
|
557
|
+
logger.error(
|
|
558
|
+
`Polling timed out \u2014 keep the task and check later: multix byteplus status ${taskId} --wait --download`
|
|
559
|
+
);
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
if (e instanceof PollFailedError) {
|
|
563
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
throw e;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/providers/byteplus/generators/image.ts
|
|
573
|
+
init_http_client();
|
|
574
|
+
import fs5 from "fs";
|
|
575
|
+
import path6 from "path";
|
|
576
|
+
var ASPECT_TO_SIZE = {
|
|
577
|
+
"1:1": "2048x2048",
|
|
578
|
+
"16:9": "2560x1440",
|
|
579
|
+
"9:16": "1440x2560",
|
|
580
|
+
"4:3": "2048x1536",
|
|
581
|
+
"3:4": "1536x2048",
|
|
582
|
+
"21:9": "2560x1080",
|
|
583
|
+
"3:2": "2048x1365",
|
|
584
|
+
"2:3": "1365x2048"
|
|
585
|
+
};
|
|
586
|
+
function resolveSize(opts) {
|
|
587
|
+
if (opts.size && opts.aspectRatio) {
|
|
588
|
+
opts.logger?.warn("Both --size and --aspect-ratio given; using --size.");
|
|
589
|
+
return opts.size;
|
|
590
|
+
}
|
|
591
|
+
if (opts.size) return opts.size;
|
|
592
|
+
if (opts.aspectRatio) {
|
|
593
|
+
const mapped = ASPECT_TO_SIZE[opts.aspectRatio];
|
|
594
|
+
if (!mapped) {
|
|
595
|
+
throw new Error(
|
|
596
|
+
`Unsupported --aspect-ratio '${opts.aspectRatio}'. Supported: ${Object.keys(ASPECT_TO_SIZE).join(", ")}`
|
|
597
|
+
);
|
|
427
598
|
}
|
|
428
599
|
return mapped;
|
|
429
600
|
}
|
|
@@ -465,14 +636,14 @@ async function generateSeedream(client, opts) {
|
|
|
465
636
|
}
|
|
466
637
|
const ext = (() => {
|
|
467
638
|
try {
|
|
468
|
-
const e =
|
|
639
|
+
const e = path6.extname(new URL(url).pathname);
|
|
469
640
|
return e || ".jpg";
|
|
470
641
|
} catch {
|
|
471
642
|
return ".jpg";
|
|
472
643
|
}
|
|
473
644
|
})();
|
|
474
|
-
const filename = opts.output && items.length === 1 ?
|
|
475
|
-
fs5.mkdirSync(
|
|
645
|
+
const filename = opts.output && items.length === 1 ? path6.resolve(opts.output) : path6.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
|
|
646
|
+
fs5.mkdirSync(path6.dirname(filename), { recursive: true });
|
|
476
647
|
await downloadFile(url, filename);
|
|
477
648
|
saved.push({ path: filename, url, seed: item.seed });
|
|
478
649
|
logger?.success(`Saved ${filename}`);
|
|
@@ -552,12 +723,12 @@ function collect2(value, prev) {
|
|
|
552
723
|
}
|
|
553
724
|
|
|
554
725
|
// src/providers/byteplus/commands/image-to-video.ts
|
|
555
|
-
import
|
|
726
|
+
import path8 from "path";
|
|
556
727
|
|
|
557
728
|
// src/core/video-thumb.ts
|
|
558
729
|
init_http_client();
|
|
559
730
|
import fs6 from "fs";
|
|
560
|
-
import
|
|
731
|
+
import path7 from "path";
|
|
561
732
|
var THUMB_KEYS = /* @__PURE__ */ new Set([
|
|
562
733
|
"cover_image_url",
|
|
563
734
|
"coverImageUrl",
|
|
@@ -607,9 +778,9 @@ function detectThumbUrl(obj) {
|
|
|
607
778
|
async function downloadThumbBeside(thumbUrl, videoPath, logger) {
|
|
608
779
|
const match = thumbUrl.match(IMAGE_EXT_RE);
|
|
609
780
|
const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
|
|
610
|
-
const dir =
|
|
611
|
-
const base =
|
|
612
|
-
const dest =
|
|
781
|
+
const dir = path7.dirname(videoPath);
|
|
782
|
+
const base = path7.basename(videoPath, path7.extname(videoPath));
|
|
783
|
+
const dest = path7.join(dir, `${base}_thumb.${ext}`);
|
|
613
784
|
try {
|
|
614
785
|
await downloadFile(thumbUrl, dest);
|
|
615
786
|
logger?.success(`Saved thumb ${dest}`);
|
|
@@ -628,10 +799,10 @@ async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
|
628
799
|
}
|
|
629
800
|
const saved = await downloadThumbBeside(url, videoPath, opts.logger);
|
|
630
801
|
if (saved && opts.copyTo) {
|
|
631
|
-
const copyExt =
|
|
632
|
-
const copyBase =
|
|
633
|
-
const copyDir =
|
|
634
|
-
const copyDest =
|
|
802
|
+
const copyExt = path7.extname(saved);
|
|
803
|
+
const copyBase = path7.basename(opts.copyTo, path7.extname(opts.copyTo));
|
|
804
|
+
const copyDir = path7.dirname(path7.resolve(opts.copyTo));
|
|
805
|
+
const copyDest = path7.join(copyDir, `${copyBase}_thumb${copyExt}`);
|
|
635
806
|
fs6.mkdirSync(copyDir, { recursive: true });
|
|
636
807
|
fs6.copyFileSync(saved, copyDest);
|
|
637
808
|
opts.logger?.success(`Copied thumb to ${copyDest}`);
|
|
@@ -639,41 +810,6 @@ async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
|
639
810
|
return saved;
|
|
640
811
|
}
|
|
641
812
|
|
|
642
|
-
// src/providers/leonardo/poll.ts
|
|
643
|
-
var PollTimeoutError = class extends Error {
|
|
644
|
-
constructor() {
|
|
645
|
-
super("Polling timed out before the operation completed.");
|
|
646
|
-
this.name = "PollTimeoutError";
|
|
647
|
-
}
|
|
648
|
-
};
|
|
649
|
-
var PollFailedError = class extends Error {
|
|
650
|
-
constructor(value) {
|
|
651
|
-
super("Operation failed.");
|
|
652
|
-
this.value = value;
|
|
653
|
-
this.name = "PollFailedError";
|
|
654
|
-
}
|
|
655
|
-
value;
|
|
656
|
-
};
|
|
657
|
-
async function poll(opts) {
|
|
658
|
-
const interval = opts.intervalMs ?? 4e3;
|
|
659
|
-
const max = opts.maxAttempts ?? 120;
|
|
660
|
-
for (let attempt = 1; attempt <= max; attempt++) {
|
|
661
|
-
if (opts.signal?.aborted) throw new Error("Aborted");
|
|
662
|
-
const value = await opts.fetch();
|
|
663
|
-
opts.onTick?.(attempt, value);
|
|
664
|
-
if (opts.failed?.(value)) throw new PollFailedError(value);
|
|
665
|
-
if (opts.done(value)) return value;
|
|
666
|
-
await new Promise((resolve, reject) => {
|
|
667
|
-
const t = setTimeout(resolve, interval);
|
|
668
|
-
opts.signal?.addEventListener("abort", () => {
|
|
669
|
-
clearTimeout(t);
|
|
670
|
-
reject(new Error("Aborted"));
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
|
-
throw new PollTimeoutError();
|
|
675
|
-
}
|
|
676
|
-
|
|
677
813
|
// src/providers/byteplus/generators/video.ts
|
|
678
814
|
init_http_client();
|
|
679
815
|
function buildFlagSuffix(opts) {
|
|
@@ -796,7 +932,7 @@ function registerBytePlusImageToVideoCommand(parent) {
|
|
|
796
932
|
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
797
933
|
process.exit(1);
|
|
798
934
|
}
|
|
799
|
-
const outPath = opts.output ??
|
|
935
|
+
const outPath = opts.output ?? path8.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
800
936
|
await downloadVideo(url, outPath, logger);
|
|
801
937
|
await maybeDownloadThumb(final, outPath, {
|
|
802
938
|
skip: opts.thumb === false,
|
|
@@ -822,7 +958,7 @@ Task: ${taskId}`);
|
|
|
822
958
|
}
|
|
823
959
|
|
|
824
960
|
// src/providers/byteplus/commands/reference-to-video.ts
|
|
825
|
-
import
|
|
961
|
+
import path9 from "path";
|
|
826
962
|
|
|
827
963
|
// src/providers/byteplus/reference-input.ts
|
|
828
964
|
var MAX_TOTAL_REFS = 12;
|
|
@@ -854,10 +990,10 @@ function toRef(kind, resolved, role) {
|
|
|
854
990
|
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
855
991
|
}
|
|
856
992
|
async function resolveRefSpec(kind, spec, logger) {
|
|
857
|
-
const { path:
|
|
858
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
859
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
860
|
-
return toRef(kind, await resolveAudioInput(
|
|
993
|
+
const { path: path41, role } = parseRefSpec(spec);
|
|
994
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path41, logger), role);
|
|
995
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path41, logger), role);
|
|
996
|
+
return toRef(kind, await resolveAudioInput(path41, logger), role);
|
|
861
997
|
}
|
|
862
998
|
async function resolveRefSpecs(specs, logger) {
|
|
863
999
|
const out = [];
|
|
@@ -954,7 +1090,7 @@ function registerBytePlusReferenceToVideoCommand(parent) {
|
|
|
954
1090
|
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
955
1091
|
process.exit(1);
|
|
956
1092
|
}
|
|
957
|
-
const outPath = opts.output ??
|
|
1093
|
+
const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
958
1094
|
await downloadVideo(url, outPath, logger);
|
|
959
1095
|
await maybeDownloadThumb(final, outPath, {
|
|
960
1096
|
skip: opts.thumb === false,
|
|
@@ -976,146 +1112,919 @@ Task: ${taskId}`);
|
|
|
976
1112
|
throw e;
|
|
977
1113
|
}
|
|
978
1114
|
}
|
|
979
|
-
);
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// src/providers/byteplus/commands/status.ts
|
|
1119
|
+
import path10 from "path";
|
|
1120
|
+
function registerBytePlusStatusCommand(parent) {
|
|
1121
|
+
parent.command("status <taskId>").description("Get a Seedance task's status; --wait + --download to fetch the result MP4").option("--wait", "Poll until terminal status").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download MP4 on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
1122
|
+
async (taskId, opts) => {
|
|
1123
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1124
|
+
const client = createBytePlusClient();
|
|
1125
|
+
const shouldWait = !!(opts.wait || opts.download);
|
|
1126
|
+
try {
|
|
1127
|
+
const status = shouldWait ? await waitForVideoTask(client, taskId, {
|
|
1128
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
1129
|
+
logger
|
|
1130
|
+
}) : await getVideoTaskStatus(client, taskId, logger);
|
|
1131
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1132
|
+
if (opts.download && status.status === "succeeded") {
|
|
1133
|
+
const content = status.content;
|
|
1134
|
+
const videoUrl = content?.video_url;
|
|
1135
|
+
const fileUrl = content?.file_url;
|
|
1136
|
+
if (videoUrl) {
|
|
1137
|
+
const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
|
|
1138
|
+
await downloadVideo(videoUrl, outPath, logger);
|
|
1139
|
+
await maybeDownloadThumb(status, outPath, {
|
|
1140
|
+
skip: opts.thumb === false,
|
|
1141
|
+
copyTo: opts.output,
|
|
1142
|
+
logger
|
|
1143
|
+
});
|
|
1144
|
+
console.log(`Saved: ${outPath}`);
|
|
1145
|
+
} else if (fileUrl) {
|
|
1146
|
+
const ext = inferModelFileExt(fileUrl);
|
|
1147
|
+
const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
|
|
1148
|
+
await downloadModelFile(fileUrl, outPath, logger);
|
|
1149
|
+
console.log(`Saved: ${outPath}`);
|
|
1150
|
+
} else {
|
|
1151
|
+
logger.error("Task succeeded but no video_url or file_url present");
|
|
1152
|
+
process.exit(1);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
} catch (e) {
|
|
1156
|
+
if (e instanceof PollTimeoutError) {
|
|
1157
|
+
logger.error(`Timed out waiting for task ${taskId}`);
|
|
1158
|
+
process.exit(1);
|
|
1159
|
+
}
|
|
1160
|
+
if (e instanceof PollFailedError) {
|
|
1161
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1162
|
+
process.exit(1);
|
|
1163
|
+
}
|
|
1164
|
+
throw e;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// src/providers/byteplus/commands/video.ts
|
|
1171
|
+
import path11 from "path";
|
|
1172
|
+
function registerBytePlusVideoCommand(parent) {
|
|
1173
|
+
parent.command("video").description("Generate a video from a text prompt via BytePlus Seedance 2.0").requiredOption("--prompt <text>", "Text prompt").option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds (4-15)", String(BYTEPLUS_DEFAULTS.videoDuration)).option(
|
|
1174
|
+
"--aspect-ratio <r>",
|
|
1175
|
+
"Aspect ratio (16:9, 9:16, 1:1, ...)",
|
|
1176
|
+
BYTEPLUS_DEFAULTS.videoAspectRatio
|
|
1177
|
+
).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(
|
|
1178
|
+
async (opts) => {
|
|
1179
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1180
|
+
const client = createBytePlusClient();
|
|
1181
|
+
const body = buildVideoTaskBody({
|
|
1182
|
+
model: opts.model,
|
|
1183
|
+
prompt: opts.prompt,
|
|
1184
|
+
resolution: opts.resolution,
|
|
1185
|
+
duration: Number.parseInt(opts.duration, 10),
|
|
1186
|
+
aspectRatio: opts.aspectRatio,
|
|
1187
|
+
audio: opts.audio,
|
|
1188
|
+
seed: opts.seed ? Number.parseInt(opts.seed, 10) : void 0,
|
|
1189
|
+
negativePrompt: opts.negative,
|
|
1190
|
+
cameraFixed: opts.cameraFixed
|
|
1191
|
+
});
|
|
1192
|
+
logger.info(
|
|
1193
|
+
`Submitting Seedance task (${opts.model}, ${opts.resolution}, ${opts.duration}s)`
|
|
1194
|
+
);
|
|
1195
|
+
const submitted = await submitVideoTask(client, body, logger);
|
|
1196
|
+
const taskId = submitted.id;
|
|
1197
|
+
logger.debug(`taskId: ${taskId}`);
|
|
1198
|
+
if (opts.async) {
|
|
1199
|
+
console.log(taskId);
|
|
1200
|
+
console.error(`Poll with: multix byteplus status ${taskId}`);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
try {
|
|
1204
|
+
const final = await waitForVideoTask(client, taskId, {
|
|
1205
|
+
waitTimeoutMs: Number.parseInt(opts.waitTimeout, 10) || 6e5,
|
|
1206
|
+
logger
|
|
1207
|
+
});
|
|
1208
|
+
const url = final.content?.video_url;
|
|
1209
|
+
if (!url) {
|
|
1210
|
+
logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
|
|
1211
|
+
process.exit(1);
|
|
1212
|
+
}
|
|
1213
|
+
const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1214
|
+
await downloadVideo(url, outPath, logger);
|
|
1215
|
+
await maybeDownloadThumb(final, outPath, {
|
|
1216
|
+
skip: opts.thumb === false,
|
|
1217
|
+
copyTo: opts.output,
|
|
1218
|
+
logger
|
|
1219
|
+
});
|
|
1220
|
+
console.log(`
|
|
1221
|
+
Task: ${taskId}`);
|
|
1222
|
+
console.log(`Saved: ${outPath}`);
|
|
1223
|
+
} catch (e) {
|
|
1224
|
+
if (e instanceof PollTimeoutError) {
|
|
1225
|
+
logger.error(
|
|
1226
|
+
`Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
|
|
1227
|
+
);
|
|
1228
|
+
process.exit(1);
|
|
1229
|
+
}
|
|
1230
|
+
if (e instanceof PollFailedError) {
|
|
1231
|
+
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1232
|
+
process.exit(1);
|
|
1233
|
+
}
|
|
1234
|
+
throw e;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
// src/providers/byteplus/commands/index.ts
|
|
1241
|
+
function registerBytePlusCommands(program2) {
|
|
1242
|
+
const byteplus = program2.command("byteplus").description(
|
|
1243
|
+
"BytePlus ModelArk: Seedream image, Seedance video, Hyper3D / Hitem3d 3D generation"
|
|
1244
|
+
);
|
|
1245
|
+
registerBytePlusGenerateCommand(byteplus);
|
|
1246
|
+
registerBytePlusImageToImageCommand(byteplus);
|
|
1247
|
+
registerBytePlusVideoCommand(byteplus);
|
|
1248
|
+
registerBytePlusImageToVideoCommand(byteplus);
|
|
1249
|
+
registerBytePlusReferenceToVideoCommand(byteplus);
|
|
1250
|
+
registerBytePlusGenerate3DCommand(byteplus);
|
|
1251
|
+
registerBytePlusStatusCommand(byteplus);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
// src/providers/elevenlabs/client.ts
|
|
1255
|
+
import fs7 from "fs";
|
|
1256
|
+
import path12 from "path";
|
|
1257
|
+
init_errors();
|
|
1258
|
+
init_http_client();
|
|
1259
|
+
|
|
1260
|
+
// src/providers/elevenlabs/models.ts
|
|
1261
|
+
var ELEVENLABS_BASE_URL = "https://api.elevenlabs.io/v1";
|
|
1262
|
+
var ELEVENLABS_TTS_MODELS = [
|
|
1263
|
+
"eleven_v3",
|
|
1264
|
+
"eleven_multilingual_v2",
|
|
1265
|
+
"eleven_flash_v2_5",
|
|
1266
|
+
"eleven_flash_v2",
|
|
1267
|
+
"eleven_turbo_v2_5",
|
|
1268
|
+
"eleven_turbo_v2"
|
|
1269
|
+
];
|
|
1270
|
+
var ELEVENLABS_STT_MODELS = ["scribe_v1", "scribe_v1_experimental"];
|
|
1271
|
+
var ELEVENLABS_OUTPUT_FORMATS = [
|
|
1272
|
+
"mp3_22050_32",
|
|
1273
|
+
"mp3_44100_32",
|
|
1274
|
+
"mp3_44100_64",
|
|
1275
|
+
"mp3_44100_96",
|
|
1276
|
+
"mp3_44100_128",
|
|
1277
|
+
"mp3_44100_192",
|
|
1278
|
+
"pcm_8000",
|
|
1279
|
+
"pcm_16000",
|
|
1280
|
+
"pcm_22050",
|
|
1281
|
+
"pcm_24000",
|
|
1282
|
+
"pcm_44100",
|
|
1283
|
+
"ulaw_8000"
|
|
1284
|
+
];
|
|
1285
|
+
var ELEVENLABS_RECOMMENDED_VOICES = {
|
|
1286
|
+
alexandra: "kdmDKE6EkgrWrrykO9Qt",
|
|
1287
|
+
archer: "L0Dsvb3SLTyegXwtm47J",
|
|
1288
|
+
jessica_anne_bogart: "g6xIsTj2HwM6VR4iXFCw",
|
|
1289
|
+
hope: "OYTbf65OHHFELVut7v2H",
|
|
1290
|
+
eryn: "dj3G1R1ilKoFKhBnWOzG",
|
|
1291
|
+
stuart: "HDA9tsk27wYi3uq0fPcK",
|
|
1292
|
+
mark: "1SM7GgM6IMuvQlz2BwM3",
|
|
1293
|
+
angela: "PT4nqlKZfc06VW1BuClj",
|
|
1294
|
+
finn: "vBKc2FfBKJfcZNyEt1n6",
|
|
1295
|
+
cassidy: "56AoDkrOh6qfVPDXZ7Pt",
|
|
1296
|
+
grandpa_spuds_oxley: "NOpBlnGInO9m6vDvFkFC"
|
|
1297
|
+
};
|
|
1298
|
+
var TASK_DEFAULTS = {
|
|
1299
|
+
ttsModel: "eleven_multilingual_v2",
|
|
1300
|
+
ttsVoice: ELEVENLABS_RECOMMENDED_VOICES.alexandra,
|
|
1301
|
+
ttsFormat: "mp3_44100_128",
|
|
1302
|
+
sttModel: "scribe_v1",
|
|
1303
|
+
voiceChangerModel: "eleven_multilingual_sts_v2",
|
|
1304
|
+
sfxFormat: "mp3_44100_128",
|
|
1305
|
+
musicFormat: "mp3_44100_128"
|
|
1306
|
+
};
|
|
1307
|
+
function extFromFormat(fmt) {
|
|
1308
|
+
if (fmt.startsWith("mp3")) return "mp3";
|
|
1309
|
+
if (fmt.startsWith("pcm")) return "pcm";
|
|
1310
|
+
if (fmt.startsWith("ulaw")) return "ulaw";
|
|
1311
|
+
return "mp3";
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/providers/elevenlabs/client.ts
|
|
1315
|
+
var DEFAULT_TIMEOUT_MS2 = 12e4;
|
|
1316
|
+
function requireElevenLabsKey() {
|
|
1317
|
+
const key = resolveKey("ELEVENLABS_API_KEY");
|
|
1318
|
+
if (!key) {
|
|
1319
|
+
throw new ConfigError(
|
|
1320
|
+
"ELEVENLABS_API_KEY is not set. Get one at https://elevenlabs.io/app/settings/api-keys"
|
|
1321
|
+
);
|
|
1322
|
+
}
|
|
1323
|
+
return key;
|
|
1324
|
+
}
|
|
1325
|
+
function authHeaders(apiKey, extra) {
|
|
1326
|
+
return { "xi-api-key": apiKey, ...extra ?? {} };
|
|
1327
|
+
}
|
|
1328
|
+
function apiGet(endpoint, apiKey, query) {
|
|
1329
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
|
|
1330
|
+
if (query) {
|
|
1331
|
+
for (const [k, v] of Object.entries(query)) {
|
|
1332
|
+
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return httpJson({ url: url.toString(), headers: authHeaders(apiKey) });
|
|
1336
|
+
}
|
|
1337
|
+
function apiPostJson(endpoint, payload, apiKey, opts = {}) {
|
|
1338
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
|
|
1339
|
+
if (opts.query) {
|
|
1340
|
+
for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
1341
|
+
}
|
|
1342
|
+
return httpJson({
|
|
1343
|
+
url: url.toString(),
|
|
1344
|
+
method: "POST",
|
|
1345
|
+
headers: authHeaders(apiKey, { "Content-Type": "application/json" }),
|
|
1346
|
+
body: payload,
|
|
1347
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
async function apiDelete(endpoint, apiKey) {
|
|
1351
|
+
const url = `${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`;
|
|
1352
|
+
const res = await globalThis.fetch(url, {
|
|
1353
|
+
method: "DELETE",
|
|
1354
|
+
headers: authHeaders(apiKey),
|
|
1355
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS2)
|
|
1356
|
+
});
|
|
1357
|
+
if (!res.ok) {
|
|
1358
|
+
const text = await res.text().catch(() => "");
|
|
1359
|
+
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function apiPostBinary(endpoint, body, apiKey, opts = {}) {
|
|
1363
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
|
|
1364
|
+
if (opts.query) {
|
|
1365
|
+
for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
1366
|
+
}
|
|
1367
|
+
const headers = authHeaders(apiKey);
|
|
1368
|
+
let init;
|
|
1369
|
+
if (body instanceof FormData) {
|
|
1370
|
+
init = {
|
|
1371
|
+
method: "POST",
|
|
1372
|
+
headers,
|
|
1373
|
+
body,
|
|
1374
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
1375
|
+
};
|
|
1376
|
+
} else {
|
|
1377
|
+
init = {
|
|
1378
|
+
method: "POST",
|
|
1379
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
1380
|
+
body: JSON.stringify(body),
|
|
1381
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
let res;
|
|
1385
|
+
try {
|
|
1386
|
+
res = await globalThis.fetch(url.toString(), init);
|
|
1387
|
+
} catch (cause) {
|
|
1388
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
1389
|
+
throw new HttpError(0, msg, url.toString());
|
|
1390
|
+
}
|
|
1391
|
+
if (!res.ok) {
|
|
1392
|
+
const text = await res.text().catch(() => "");
|
|
1393
|
+
throw new HttpError(res.status, text.slice(0, 500), url.toString());
|
|
1394
|
+
}
|
|
1395
|
+
return Buffer.from(await res.arrayBuffer());
|
|
1396
|
+
}
|
|
1397
|
+
async function apiPostMultipart(endpoint, form, apiKey, opts = {}) {
|
|
1398
|
+
const url = `${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`;
|
|
1399
|
+
let res;
|
|
1400
|
+
try {
|
|
1401
|
+
res = await globalThis.fetch(url, {
|
|
1402
|
+
method: "POST",
|
|
1403
|
+
headers: authHeaders(apiKey),
|
|
1404
|
+
body: form,
|
|
1405
|
+
signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
|
|
1406
|
+
});
|
|
1407
|
+
} catch (cause) {
|
|
1408
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
1409
|
+
throw new HttpError(0, msg, url);
|
|
1410
|
+
}
|
|
1411
|
+
if (!res.ok) {
|
|
1412
|
+
const text = await res.text().catch(() => "");
|
|
1413
|
+
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
1414
|
+
}
|
|
1415
|
+
return res.json();
|
|
1416
|
+
}
|
|
1417
|
+
function readFileAsBlob(filePath, fieldFilename) {
|
|
1418
|
+
const abs = path12.resolve(filePath);
|
|
1419
|
+
if (!fs7.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
1420
|
+
const buf = fs7.readFileSync(abs);
|
|
1421
|
+
const name = fieldFilename ?? path12.basename(abs);
|
|
1422
|
+
return new File([new Uint8Array(buf)], name);
|
|
1423
|
+
}
|
|
1424
|
+
async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
|
|
1425
|
+
const { intervalMs = 5e3, timeoutMs = 6e5, logger, label = "task" } = opts;
|
|
1426
|
+
const deadline = Date.now() + timeoutMs;
|
|
1427
|
+
let elapsed = 0;
|
|
1428
|
+
while (Date.now() < deadline) {
|
|
1429
|
+
const cur = await fetchStatus();
|
|
1430
|
+
if (isDone(cur)) return cur;
|
|
1431
|
+
if (isFailed(cur)) {
|
|
1432
|
+
throw new ProviderError(`${label} failed: ${JSON.stringify(cur)}`, "elevenlabs");
|
|
1433
|
+
}
|
|
1434
|
+
if (elapsed > 0 && elapsed % 3e4 < intervalMs) {
|
|
1435
|
+
logger?.debug(`Polling ${label}... ${Math.round(elapsed / 1e3)}s, status=${cur.status}`);
|
|
1436
|
+
}
|
|
1437
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
1438
|
+
elapsed += intervalMs;
|
|
1439
|
+
}
|
|
1440
|
+
throw new ProviderError(`${label} timed out after ${timeoutMs / 1e3}s`, "elevenlabs");
|
|
1441
|
+
}
|
|
1442
|
+
function saveBytes(opts) {
|
|
1443
|
+
const { bytes, task, ext, outputCopy, logger } = opts;
|
|
1444
|
+
const dir = getOutputDir();
|
|
1445
|
+
const dest = path12.join(dir, `elevenlabs_${task}_${Date.now()}.${ext}`);
|
|
1446
|
+
fs7.writeFileSync(dest, bytes);
|
|
1447
|
+
logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
1448
|
+
if (outputCopy) {
|
|
1449
|
+
fs7.mkdirSync(path12.dirname(path12.resolve(outputCopy)), { recursive: true });
|
|
1450
|
+
fs7.copyFileSync(dest, outputCopy);
|
|
1451
|
+
}
|
|
1452
|
+
return dest;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// src/providers/elevenlabs/commands/account.ts
|
|
1456
|
+
function registerAccountCommands(parent) {
|
|
1457
|
+
parent.command("account").description("Show ElevenLabs account info, subscription tier, and usage").action(async () => {
|
|
1458
|
+
const apiKey = requireElevenLabsKey();
|
|
1459
|
+
const user = await apiGet("user", apiKey);
|
|
1460
|
+
const sub = user.subscription ?? {};
|
|
1461
|
+
console.log(`Tier: ${sub.tier ?? "?"}`);
|
|
1462
|
+
console.log(`Characters: ${sub.character_count ?? "?"} / ${sub.character_limit ?? "?"}`);
|
|
1463
|
+
if (sub.next_character_count_reset_unix) {
|
|
1464
|
+
console.log(
|
|
1465
|
+
`Resets: ${new Date(sub.next_character_count_reset_unix * 1e3).toISOString()}`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
console.log(
|
|
1469
|
+
`Voice slots: ${sub.voice_limit ?? "?"} (PVC: ${sub.professional_voice_limit ?? 0})`
|
|
1470
|
+
);
|
|
1471
|
+
if (user.first_name) console.log(`Name: ${user.first_name}`);
|
|
1472
|
+
});
|
|
1473
|
+
parent.command("models").description("List available ElevenLabs models").action(async () => {
|
|
1474
|
+
const apiKey = requireElevenLabsKey();
|
|
1475
|
+
const models = await apiGet("models", apiKey);
|
|
1476
|
+
for (const m of models) {
|
|
1477
|
+
const caps = [];
|
|
1478
|
+
if (m.can_do_text_to_speech) caps.push("tts");
|
|
1479
|
+
if (m.can_do_voice_conversion) caps.push("vc");
|
|
1480
|
+
const langs = m.languages?.length ? `${m.languages.length} langs` : "";
|
|
1481
|
+
console.log(`${m.model_id} ${m.name ?? ""} [${caps.join(",")}] ${langs}`);
|
|
1482
|
+
}
|
|
1483
|
+
console.log(`
|
|
1484
|
+
${models.length} models`);
|
|
1485
|
+
});
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// src/providers/elevenlabs/commands/align.ts
|
|
1489
|
+
import fs8 from "fs";
|
|
1490
|
+
import path13 from "path";
|
|
1491
|
+
function registerAlignCommand(parent) {
|
|
1492
|
+
parent.command("align").description("Forced alignment of audio to a transcript (returns word/char timestamps)").requiredOption("--input <path>", "Source audio file").requiredOption("--text <str>", "Transcript text (or pass --text-file)").option("--text-file <path>", "Read transcript from file").option("--output <path>", "Save JSON to this path (default: multix-output)").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1493
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1494
|
+
const apiKey = requireElevenLabsKey();
|
|
1495
|
+
let text = opts.text;
|
|
1496
|
+
if (opts.textFile) text = fs8.readFileSync(path13.resolve(opts.textFile), "utf8");
|
|
1497
|
+
const form = new FormData();
|
|
1498
|
+
form.append("file", readFileAsBlob(opts.input));
|
|
1499
|
+
form.append("text", text);
|
|
1500
|
+
logger.debug(`Aligning ${opts.input} (${text.length} chars)`);
|
|
1501
|
+
const data = await apiPostMultipart("forced-alignment", form, apiKey, {
|
|
1502
|
+
timeoutMs: 6e5
|
|
1503
|
+
});
|
|
1504
|
+
const dest = opts.output ? path13.resolve(opts.output) : path13.join(getOutputDir(), `elevenlabs_align_${Date.now()}.json`);
|
|
1505
|
+
fs8.mkdirSync(path13.dirname(dest), { recursive: true });
|
|
1506
|
+
fs8.writeFileSync(dest, JSON.stringify(data, null, 2));
|
|
1507
|
+
logger.success(`Saved alignment: ${dest}`);
|
|
1508
|
+
if (data.words) console.log(`Aligned ${data.words.length} words (loss=${data.loss ?? "?"})`);
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
// src/providers/elevenlabs/commands/clone.ts
|
|
1513
|
+
init_errors();
|
|
1514
|
+
function registerCloneCommand(parent) {
|
|
1515
|
+
parent.command("clone").description("Instant voice cloning from local audio samples").requiredOption("--name <n>", "Display name for the new voice").option("--files <paths...>", "One or more audio sample files (1-3 minutes total)").option("--audio <path>", "Single sample file (alias for --files)").option("--description <d>", "Voice description").option("--labels <kv>", `JSON object of labels, e.g. '{"accent":"american"}'`).option("--remove-background-noise", "Server-side noise removal", false).option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1516
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1517
|
+
const apiKey = requireElevenLabsKey();
|
|
1518
|
+
const files = opts.files ?? (opts.audio ? [opts.audio] : []);
|
|
1519
|
+
if (files.length === 0) {
|
|
1520
|
+
throw new ValidationError("Provide --files <p1> <p2>... or --audio <path>");
|
|
1521
|
+
}
|
|
1522
|
+
const form = new FormData();
|
|
1523
|
+
form.append("name", opts.name);
|
|
1524
|
+
if (opts.description) form.append("description", opts.description);
|
|
1525
|
+
if (opts.labels) {
|
|
1526
|
+
try {
|
|
1527
|
+
JSON.parse(opts.labels);
|
|
1528
|
+
form.append("labels", opts.labels);
|
|
1529
|
+
} catch {
|
|
1530
|
+
throw new ValidationError("--labels must be valid JSON");
|
|
1531
|
+
}
|
|
1532
|
+
}
|
|
1533
|
+
if (opts.removeBackgroundNoise) form.append("remove_background_noise", "true");
|
|
1534
|
+
for (const f of files) form.append("files", readFileAsBlob(f));
|
|
1535
|
+
logger.debug(`Cloning ${files.length} sample(s) as "${opts.name}"`);
|
|
1536
|
+
const data = await apiPostMultipart("voices/add", form, apiKey, {
|
|
1537
|
+
timeoutMs: 3e5
|
|
1538
|
+
});
|
|
1539
|
+
console.log(`Created voice: ${data.voice_id}`);
|
|
1540
|
+
if (data.requires_verification) {
|
|
1541
|
+
console.warn("Voice requires verification before use (PVC pipeline).");
|
|
1542
|
+
}
|
|
1543
|
+
});
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// src/providers/elevenlabs/commands/dub.ts
|
|
1547
|
+
init_errors();
|
|
1548
|
+
init_http_client();
|
|
1549
|
+
import fs9 from "fs";
|
|
1550
|
+
import path14 from "path";
|
|
1551
|
+
function registerDubCommand(parent) {
|
|
1552
|
+
parent.command("dub").description("Dub a video/audio file into target languages (async)").option("--input <path>", "Source media file (use --input or --source-url)").option("--source-url <url>", "Public URL of source media").requiredOption("--target-lang <code>", "Target language ISO code (e.g. es, fr, de)").option("--source-lang <code>", "Source language (auto if omitted)").option("--num-speakers <n>", "Speaker count (auto if omitted)").option("--watermark", "Add ElevenLabs watermark", false).option("--start-time <s>", "Start time in seconds").option("--end-time <s>", "End time in seconds").option("--highest-resolution", "Use highest resolution video", false).option("--name <n>", "Project display name").option("--async", "Return job id immediately, do not wait", false).option("--wait", "Wait until status=dubbed (implied by --download)", false).option("--download", "Download dubbed media when ready", false).option("--poll-interval <s>", "Poll interval seconds", "10").option("--wait-timeout <ms>", "Poll timeout", "1800000").option("--output <path>", "Output path for downloaded dub").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1553
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1554
|
+
const apiKey = requireElevenLabsKey();
|
|
1555
|
+
if (!opts.input && !opts.sourceUrl) {
|
|
1556
|
+
throw new ValidationError("Provide --input <path> or --source-url <url>");
|
|
1557
|
+
}
|
|
1558
|
+
const form = new FormData();
|
|
1559
|
+
if (opts.input) form.append("file", readFileAsBlob(opts.input));
|
|
1560
|
+
if (opts.sourceUrl) form.append("source_url", opts.sourceUrl);
|
|
1561
|
+
form.append("target_lang", opts.targetLang);
|
|
1562
|
+
if (opts.sourceLang) form.append("source_lang", opts.sourceLang);
|
|
1563
|
+
if (opts.numSpeakers) form.append("num_speakers", opts.numSpeakers);
|
|
1564
|
+
if (opts.watermark) form.append("watermark", "true");
|
|
1565
|
+
if (opts.startTime) form.append("start_time", opts.startTime);
|
|
1566
|
+
if (opts.endTime) form.append("end_time", opts.endTime);
|
|
1567
|
+
if (opts.highestResolution) form.append("highest_resolution", "true");
|
|
1568
|
+
if (opts.name) form.append("name", opts.name);
|
|
1569
|
+
logger.debug(`Submitting dub -> ${opts.targetLang}`);
|
|
1570
|
+
const start = await apiPostMultipart("dubbing", form, apiKey, {
|
|
1571
|
+
timeoutMs: 6e5
|
|
1572
|
+
});
|
|
1573
|
+
console.log(
|
|
1574
|
+
`Dubbing job: ${start.dubbing_id} (expected ~${start.expected_duration_sec ?? "?"}s)`
|
|
1575
|
+
);
|
|
1576
|
+
if (opts.async && !opts.wait && !opts.download) {
|
|
1577
|
+
console.log("Run `multix elevenlabs dub-status <id>` to check progress.");
|
|
1578
|
+
return;
|
|
1579
|
+
}
|
|
1580
|
+
const final = await pollUntil(
|
|
1581
|
+
() => apiGet(`dubbing/${start.dubbing_id}`, apiKey),
|
|
1582
|
+
(s) => s.status === "dubbed",
|
|
1583
|
+
(s) => s.status === "failed",
|
|
1584
|
+
{
|
|
1585
|
+
intervalMs: Number.parseInt(opts.pollInterval, 10) * 1e3,
|
|
1586
|
+
timeoutMs: Number.parseInt(opts.waitTimeout, 10),
|
|
1587
|
+
logger,
|
|
1588
|
+
label: `dub ${start.dubbing_id}`
|
|
1589
|
+
}
|
|
1590
|
+
);
|
|
1591
|
+
logger.success(`Dub complete: ${start.dubbing_id}`);
|
|
1592
|
+
if (opts.download) {
|
|
1593
|
+
const dest = opts.output ? path14.resolve(opts.output) : path14.join(getOutputDir(), `elevenlabs_dub_${start.dubbing_id}_${opts.targetLang}.mp4`);
|
|
1594
|
+
const url = `${ELEVENLABS_BASE_URL}/dubbing/${start.dubbing_id}/audio/${opts.targetLang}`;
|
|
1595
|
+
await downloadDub(url, apiKey, dest);
|
|
1596
|
+
logger.success(`Saved: ${dest}`);
|
|
1597
|
+
console.log(`
|
|
1598
|
+
Dubbed file: ${dest}`);
|
|
1599
|
+
} else {
|
|
1600
|
+
console.log(`Languages ready: ${(final.target_languages ?? []).join(", ")}`);
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
parent.command("dub-status <dubbingId>").description("Check dubbing status / list available languages").option("--download <lang>", "Download dubbed file for a language code").option("--output <path>", "Output path").action(async (dubbingId, opts) => {
|
|
1604
|
+
const apiKey = requireElevenLabsKey();
|
|
1605
|
+
const status = await apiGet(`dubbing/${dubbingId}`, apiKey);
|
|
1606
|
+
console.log(JSON.stringify(status, null, 2));
|
|
1607
|
+
if (opts.download) {
|
|
1608
|
+
const dest = opts.output ? path14.resolve(opts.output) : path14.join(getOutputDir(), `elevenlabs_dub_${dubbingId}_${opts.download}.mp4`);
|
|
1609
|
+
const url = `${ELEVENLABS_BASE_URL}/dubbing/${dubbingId}/audio/${opts.download}`;
|
|
1610
|
+
await downloadDub(url, apiKey, dest);
|
|
1611
|
+
console.log(`Saved: ${dest}`);
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
async function downloadDub(url, apiKey, dest) {
|
|
1616
|
+
fs9.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
1617
|
+
const res = await globalThis.fetch(url, {
|
|
1618
|
+
headers: { "xi-api-key": apiKey },
|
|
1619
|
+
signal: AbortSignal.timeout(6e5)
|
|
1620
|
+
});
|
|
1621
|
+
if (!res.ok || !res.body) {
|
|
1622
|
+
throw new Error(`Failed to download dub: HTTP ${res.status}`);
|
|
1623
|
+
}
|
|
1624
|
+
const arr = new Uint8Array(await res.arrayBuffer());
|
|
1625
|
+
fs9.writeFileSync(dest, arr);
|
|
1626
|
+
void downloadFile;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
// src/providers/elevenlabs/commands/isolate.ts
|
|
1630
|
+
function registerIsolateCommand(parent) {
|
|
1631
|
+
parent.command("isolate").description("Voice isolator: strip background noise from audio").requiredOption("--input <path>", "Source audio/video file").option("--output <path>", "Copy isolated audio to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1632
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1633
|
+
const apiKey = requireElevenLabsKey();
|
|
1634
|
+
const form = new FormData();
|
|
1635
|
+
form.append("audio", readFileAsBlob(opts.input));
|
|
1636
|
+
logger.debug(`Isolating ${opts.input}`);
|
|
1637
|
+
const bytes = await apiPostBinary("audio-isolation", form, apiKey, {
|
|
1638
|
+
timeoutMs: 6e5
|
|
1639
|
+
});
|
|
1640
|
+
const dest = saveBytes({
|
|
1641
|
+
bytes,
|
|
1642
|
+
task: "isolate",
|
|
1643
|
+
ext: "mp3",
|
|
1644
|
+
outputCopy: opts.output,
|
|
1645
|
+
logger
|
|
1646
|
+
});
|
|
1647
|
+
console.log(`
|
|
1648
|
+
Isolated audio: ${dest}`);
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
// src/providers/elevenlabs/commands/music.ts
|
|
1653
|
+
init_errors();
|
|
1654
|
+
import fs10 from "fs";
|
|
1655
|
+
import path15 from "path";
|
|
1656
|
+
function registerMusicCommand(parent) {
|
|
1657
|
+
parent.command("music").description("Generate music from a text prompt or composition plan").option("--prompt <str>", "Music prompt (mutually exclusive with --plan)").option("--plan <path>", "Path to composition plan JSON file").option("--music-length-ms <n>", "Total length in milliseconds (default 30000)").option("--model <id>", "Music model", "music_v1").option(
|
|
1658
|
+
"--format <fmt>",
|
|
1659
|
+
`Output format (${[...ELEVENLABS_OUTPUT_FORMATS].join("|")})`,
|
|
1660
|
+
TASK_DEFAULTS.musicFormat
|
|
1661
|
+
).option("--output <path>", "Copy to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1662
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1663
|
+
const apiKey = requireElevenLabsKey();
|
|
1664
|
+
if (!opts.prompt && !opts.plan) {
|
|
1665
|
+
throw new ValidationError("Provide --prompt or --plan");
|
|
1666
|
+
}
|
|
1667
|
+
const payload = { model_id: opts.model };
|
|
1668
|
+
if (opts.prompt) payload.prompt = opts.prompt;
|
|
1669
|
+
if (opts.plan) {
|
|
1670
|
+
const planPath = path15.resolve(opts.plan);
|
|
1671
|
+
if (!fs10.existsSync(planPath)) throw new ValidationError(`Plan file not found: ${planPath}`);
|
|
1672
|
+
const planJson = JSON.parse(fs10.readFileSync(planPath, "utf8"));
|
|
1673
|
+
payload.composition_plan = planJson;
|
|
1674
|
+
}
|
|
1675
|
+
if (opts.musicLengthMs) payload.music_length_ms = Number.parseInt(opts.musicLengthMs, 10);
|
|
1676
|
+
logger.debug(`Music gen with model ${opts.model}`);
|
|
1677
|
+
const bytes = await apiPostBinary("music", payload, apiKey, {
|
|
1678
|
+
query: { output_format: opts.format },
|
|
1679
|
+
timeoutMs: 6e5
|
|
1680
|
+
});
|
|
1681
|
+
const dest = saveBytes({
|
|
1682
|
+
bytes,
|
|
1683
|
+
task: "music",
|
|
1684
|
+
ext: extFromFormat(opts.format),
|
|
1685
|
+
outputCopy: opts.output,
|
|
1686
|
+
logger
|
|
1687
|
+
});
|
|
1688
|
+
console.log(`
|
|
1689
|
+
Generated music: ${dest}`);
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
// src/providers/elevenlabs/commands/sfx.ts
|
|
1694
|
+
function registerSfxCommand(parent) {
|
|
1695
|
+
parent.command("sfx").description("Generate sound effects from a text prompt").option("--text <str>", "Sound description (alias: --prompt)").option("--prompt <str>", "Alias for --text").option("--duration-seconds <n>", "Target duration 0.5..30 (auto if omitted)").option("--prompt-influence <n>", "0..1 (default 0.3)", "0.3").option("--loop", "Loop-friendly output", false).option(
|
|
1696
|
+
"--format <fmt>",
|
|
1697
|
+
`Output format (${[...ELEVENLABS_OUTPUT_FORMATS].join("|")})`,
|
|
1698
|
+
TASK_DEFAULTS.sfxFormat
|
|
1699
|
+
).option("--output <path>", "Copy to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1700
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1701
|
+
const apiKey = requireElevenLabsKey();
|
|
1702
|
+
const text = opts.text ?? opts.prompt;
|
|
1703
|
+
if (!text) {
|
|
1704
|
+
const { ValidationError: ValidationError2 } = await Promise.resolve().then(() => (init_errors(), errors_exports));
|
|
1705
|
+
throw new ValidationError2("--text or --prompt is required");
|
|
1706
|
+
}
|
|
1707
|
+
const payload = {
|
|
1708
|
+
text,
|
|
1709
|
+
prompt_influence: Number.parseFloat(opts.promptInfluence),
|
|
1710
|
+
loop: opts.loop
|
|
1711
|
+
};
|
|
1712
|
+
if (opts.durationSeconds) payload.duration_seconds = Number.parseFloat(opts.durationSeconds);
|
|
1713
|
+
logger.debug(`SFX: "${text.slice(0, 60)}..."`);
|
|
1714
|
+
const bytes = await apiPostBinary("sound-generation", payload, apiKey, {
|
|
1715
|
+
query: { output_format: opts.format },
|
|
1716
|
+
timeoutMs: 3e5
|
|
1717
|
+
});
|
|
1718
|
+
const dest = saveBytes({
|
|
1719
|
+
bytes,
|
|
1720
|
+
task: "sfx",
|
|
1721
|
+
ext: extFromFormat(opts.format),
|
|
1722
|
+
outputCopy: opts.output,
|
|
1723
|
+
logger
|
|
1724
|
+
});
|
|
1725
|
+
console.log(`
|
|
1726
|
+
Generated SFX: ${dest}`);
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/providers/elevenlabs/commands/transcribe.ts
|
|
1731
|
+
import fs11 from "fs";
|
|
1732
|
+
import path16 from "path";
|
|
1733
|
+
function registerTranscribeCommand(parent) {
|
|
1734
|
+
parent.command("transcribe").description("Speech-to-text transcription with Scribe").requiredOption("--input <path>", "Audio or video file to transcribe").option(
|
|
1735
|
+
"--model <id>",
|
|
1736
|
+
`STT model (${[...ELEVENLABS_STT_MODELS].join("|")})`,
|
|
1737
|
+
TASK_DEFAULTS.sttModel
|
|
1738
|
+
).option("--language <code>", "ISO language code (e.g. en); auto-detect if omitted").option("--diarize", "Speaker diarization", false).option("--num-speakers <n>", "Hint for speaker count").option("--tag-audio-events", "Tag laughter/applause/etc.", false).option("--timestamps-granularity <g>", "none|word|character", "word").option("--format <f>", "text|json|srt|vtt", "text").option("--output <path>", "Save to this path (defaults to multix-output)").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1739
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1740
|
+
const apiKey = requireElevenLabsKey();
|
|
1741
|
+
const form = new FormData();
|
|
1742
|
+
form.append("file", readFileAsBlob(opts.input));
|
|
1743
|
+
form.append("model_id", opts.model);
|
|
1744
|
+
if (opts.language) form.append("language_code", opts.language);
|
|
1745
|
+
if (opts.diarize) form.append("diarize", "true");
|
|
1746
|
+
if (opts.numSpeakers) form.append("num_speakers", opts.numSpeakers);
|
|
1747
|
+
if (opts.tagAudioEvents) form.append("tag_audio_events", "true");
|
|
1748
|
+
form.append("timestamps_granularity", opts.timestampsGranularity);
|
|
1749
|
+
logger.debug(`Transcribing ${opts.input} (model=${opts.model})`);
|
|
1750
|
+
const data = await apiPostMultipart("speech-to-text", form, apiKey, {
|
|
1751
|
+
timeoutMs: 6e5
|
|
1752
|
+
});
|
|
1753
|
+
const ext = opts.format === "json" ? "json" : opts.format === "srt" ? "srt" : opts.format === "vtt" ? "vtt" : "txt";
|
|
1754
|
+
const dir = getOutputDir();
|
|
1755
|
+
const dest = opts.output ? path16.resolve(opts.output) : path16.join(dir, `elevenlabs_transcribe_${Date.now()}.${ext}`);
|
|
1756
|
+
let body;
|
|
1757
|
+
switch (opts.format) {
|
|
1758
|
+
case "json":
|
|
1759
|
+
body = JSON.stringify(data, null, 2);
|
|
1760
|
+
break;
|
|
1761
|
+
case "srt":
|
|
1762
|
+
body = wordsToSrt(data.words ?? []);
|
|
1763
|
+
break;
|
|
1764
|
+
case "vtt":
|
|
1765
|
+
body = wordsToVtt(data.words ?? []);
|
|
1766
|
+
break;
|
|
1767
|
+
default:
|
|
1768
|
+
body = data.text ?? "";
|
|
1769
|
+
}
|
|
1770
|
+
fs11.mkdirSync(path16.dirname(dest), { recursive: true });
|
|
1771
|
+
fs11.writeFileSync(dest, body);
|
|
1772
|
+
logger.success(`Saved transcript: ${dest}`);
|
|
1773
|
+
if (data.language_code) {
|
|
1774
|
+
logger.info(
|
|
1775
|
+
`Detected language: ${data.language_code} (p=${data.language_probability ?? "?"})`
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
if (opts.format === "text") console.log(`
|
|
1779
|
+
${body.trim()}`);
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
function fmtTimeSrt(s) {
|
|
1783
|
+
const hh = Math.floor(s / 3600).toString().padStart(2, "0");
|
|
1784
|
+
const mm = Math.floor(s % 3600 / 60).toString().padStart(2, "0");
|
|
1785
|
+
const ss = Math.floor(s % 60).toString().padStart(2, "0");
|
|
1786
|
+
const ms = Math.round((s - Math.floor(s)) * 1e3).toString().padStart(3, "0");
|
|
1787
|
+
return `${hh}:${mm}:${ss},${ms}`;
|
|
1788
|
+
}
|
|
1789
|
+
function fmtTimeVtt(s) {
|
|
1790
|
+
return fmtTimeSrt(s).replace(",", ".");
|
|
1791
|
+
}
|
|
1792
|
+
function wordsToSrt(words) {
|
|
1793
|
+
const lines = [];
|
|
1794
|
+
words.forEach((w, i) => {
|
|
1795
|
+
if (w.start === void 0 || w.end === void 0) return;
|
|
1796
|
+
lines.push(`${i + 1}
|
|
1797
|
+
${fmtTimeSrt(w.start)} --> ${fmtTimeSrt(w.end)}
|
|
1798
|
+
${w.text}
|
|
1799
|
+
`);
|
|
1800
|
+
});
|
|
1801
|
+
return lines.join("\n");
|
|
1802
|
+
}
|
|
1803
|
+
function wordsToVtt(words) {
|
|
1804
|
+
const lines = ["WEBVTT", ""];
|
|
1805
|
+
for (const w of words) {
|
|
1806
|
+
if (w.start === void 0 || w.end === void 0) continue;
|
|
1807
|
+
lines.push(`${fmtTimeVtt(w.start)} --> ${fmtTimeVtt(w.end)}`);
|
|
1808
|
+
lines.push(w.text);
|
|
1809
|
+
lines.push("");
|
|
1810
|
+
}
|
|
1811
|
+
return lines.join("\n");
|
|
980
1812
|
}
|
|
981
1813
|
|
|
982
|
-
// src/providers/
|
|
983
|
-
|
|
984
|
-
function
|
|
985
|
-
parent.command("
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
logger
|
|
1008
|
-
});
|
|
1009
|
-
console.log(`Saved: ${outPath}`);
|
|
1010
|
-
}
|
|
1011
|
-
} catch (e) {
|
|
1012
|
-
if (e instanceof PollTimeoutError) {
|
|
1013
|
-
logger.error(`Timed out waiting for task ${taskId}`);
|
|
1014
|
-
process.exit(1);
|
|
1015
|
-
}
|
|
1016
|
-
if (e instanceof PollFailedError) {
|
|
1017
|
-
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1018
|
-
process.exit(1);
|
|
1019
|
-
}
|
|
1020
|
-
throw e;
|
|
1814
|
+
// src/providers/elevenlabs/commands/tts.ts
|
|
1815
|
+
init_errors();
|
|
1816
|
+
function registerTtsCommand(parent) {
|
|
1817
|
+
parent.command("tts").description("Text-to-speech (returns audio synchronously)").option("--text <str>", "Text to speak (use --text or --prompt)").option("--prompt <str>", "Alias for --text").option("--voice <id>", "Voice ID", TASK_DEFAULTS.ttsVoice).option(
|
|
1818
|
+
"--model <id>",
|
|
1819
|
+
`TTS model (${[...ELEVENLABS_TTS_MODELS].join("|")})`,
|
|
1820
|
+
TASK_DEFAULTS.ttsModel
|
|
1821
|
+
).option(
|
|
1822
|
+
"--format <fmt>",
|
|
1823
|
+
`Output format (${[...ELEVENLABS_OUTPUT_FORMATS].join("|")})`,
|
|
1824
|
+
TASK_DEFAULTS.ttsFormat
|
|
1825
|
+
).option("--stability <n>", "Voice stability 0..1", "0.5").option("--similarity-boost <n>", "Voice similarity boost 0..1", "0.75").option("--style <n>", "Style exaggeration 0..1", "0.0").option("--no-speaker-boost", "Disable speaker boost (default: enabled)").option("--language-code <code>", "ISO language code (Flash/Turbo/v3 only)").option("--seed <n>", "Determinism seed (0..4294967295)").option("--previous-text <str>", "Preceding text for context continuity").option("--next-text <str>", "Following text for context continuity").option("--output <path>", "Copy generated audio to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1826
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1827
|
+
const apiKey = requireElevenLabsKey();
|
|
1828
|
+
const text = opts.text ?? opts.prompt;
|
|
1829
|
+
if (!text) throw new ValidationError("Either --text or --prompt is required");
|
|
1830
|
+
if (!opts.voice) throw new ValidationError("--voice <id> is required");
|
|
1831
|
+
const payload = {
|
|
1832
|
+
text,
|
|
1833
|
+
model_id: opts.model,
|
|
1834
|
+
voice_settings: {
|
|
1835
|
+
stability: Number.parseFloat(opts.stability),
|
|
1836
|
+
similarity_boost: Number.parseFloat(opts.similarityBoost),
|
|
1837
|
+
style: Number.parseFloat(opts.style),
|
|
1838
|
+
use_speaker_boost: opts.speakerBoost
|
|
1021
1839
|
}
|
|
1840
|
+
};
|
|
1841
|
+
if (opts.languageCode) payload.language_code = opts.languageCode;
|
|
1842
|
+
if (opts.seed !== void 0) payload.seed = Number.parseInt(opts.seed, 10);
|
|
1843
|
+
if (opts.previousText) payload.previous_text = opts.previousText;
|
|
1844
|
+
if (opts.nextText) payload.next_text = opts.nextText;
|
|
1845
|
+
logger.debug(`TTS voice=${opts.voice} model=${opts.model} fmt=${opts.format}`);
|
|
1846
|
+
const bytes = await apiPostBinary(`text-to-speech/${opts.voice}`, payload, apiKey, {
|
|
1847
|
+
query: { output_format: opts.format }
|
|
1848
|
+
});
|
|
1849
|
+
const dest = saveBytes({
|
|
1850
|
+
bytes,
|
|
1851
|
+
task: "tts",
|
|
1852
|
+
ext: extFromFormat(opts.format),
|
|
1853
|
+
outputCopy: opts.output,
|
|
1854
|
+
logger
|
|
1855
|
+
});
|
|
1856
|
+
console.log(`
|
|
1857
|
+
Generated audio: ${dest}`);
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// src/providers/elevenlabs/commands/voice-changer.ts
|
|
1862
|
+
init_errors();
|
|
1863
|
+
function registerVoiceChangerCommand(parent) {
|
|
1864
|
+
parent.command("voice-changer").description("Speech-to-speech: transform an audio file to use a different voice").requiredOption("--input <path>", "Source audio file").requiredOption("--voice <id>", "Target voice ID").option("--model <id>", "Voice changer model", TASK_DEFAULTS.voiceChangerModel).option(
|
|
1865
|
+
"--format <fmt>",
|
|
1866
|
+
`Output format (${[...ELEVENLABS_OUTPUT_FORMATS].join("|")})`,
|
|
1867
|
+
TASK_DEFAULTS.ttsFormat
|
|
1868
|
+
).option("--remove-background-noise", "Server-side noise removal", false).option("--stability <n>", "0..1", "0.5").option("--similarity-boost <n>", "0..1", "0.75").option("--style <n>", "0..1", "0.0").option("--output <path>", "Copy output to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1869
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1870
|
+
const apiKey = requireElevenLabsKey();
|
|
1871
|
+
const form = new FormData();
|
|
1872
|
+
form.append("audio", readFileAsBlob(opts.input));
|
|
1873
|
+
form.append("model_id", opts.model);
|
|
1874
|
+
form.append(
|
|
1875
|
+
"voice_settings",
|
|
1876
|
+
JSON.stringify({
|
|
1877
|
+
stability: Number.parseFloat(opts.stability),
|
|
1878
|
+
similarity_boost: Number.parseFloat(opts.similarityBoost),
|
|
1879
|
+
style: Number.parseFloat(opts.style)
|
|
1880
|
+
})
|
|
1881
|
+
);
|
|
1882
|
+
if (opts.removeBackgroundNoise) form.append("remove_background_noise", "true");
|
|
1883
|
+
logger.debug(`Voice changer voice=${opts.voice} input=${opts.input}`);
|
|
1884
|
+
const bytes = await apiPostBinary(`speech-to-speech/${opts.voice}`, form, apiKey, {
|
|
1885
|
+
query: { output_format: opts.format },
|
|
1886
|
+
timeoutMs: 3e5
|
|
1887
|
+
});
|
|
1888
|
+
if (!bytes || bytes.length === 0) {
|
|
1889
|
+
throw new ValidationError("Empty response from voice-changer");
|
|
1022
1890
|
}
|
|
1023
|
-
|
|
1891
|
+
const dest = saveBytes({
|
|
1892
|
+
bytes,
|
|
1893
|
+
task: "voice_changer",
|
|
1894
|
+
ext: extFromFormat(opts.format),
|
|
1895
|
+
outputCopy: opts.output,
|
|
1896
|
+
logger
|
|
1897
|
+
});
|
|
1898
|
+
console.log(`
|
|
1899
|
+
Generated audio: ${dest}`);
|
|
1900
|
+
});
|
|
1024
1901
|
}
|
|
1025
1902
|
|
|
1026
|
-
// src/providers/
|
|
1027
|
-
|
|
1028
|
-
function
|
|
1029
|
-
parent.command("
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1903
|
+
// src/providers/elevenlabs/commands/voices.ts
|
|
1904
|
+
init_errors();
|
|
1905
|
+
function registerVoicesCommand(parent) {
|
|
1906
|
+
const voices = parent.command("voices").description("Voice library: list, get, search, design");
|
|
1907
|
+
voices.command("list").description("List voices in your account").option("--category <c>", "Filter category (cloned, generated, premade, professional)").option("--page-size <n>", "Voices per page", "100").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1908
|
+
const apiKey = requireElevenLabsKey();
|
|
1909
|
+
const data = await apiGet("voices", apiKey, {
|
|
1910
|
+
category: opts.category,
|
|
1911
|
+
page_size: opts.pageSize
|
|
1912
|
+
});
|
|
1913
|
+
for (const v of data.voices) {
|
|
1914
|
+
const labels = v.labels ? ` [${Object.values(v.labels).join(", ")}]` : "";
|
|
1915
|
+
console.log(`${v.voice_id} ${v.name} (${v.category ?? "?"})${labels}`);
|
|
1916
|
+
}
|
|
1917
|
+
console.log(`
|
|
1918
|
+
${data.voices.length} voices`);
|
|
1919
|
+
});
|
|
1920
|
+
voices.command("get <voiceId>").description("Get a voice's metadata").action(async (voiceId) => {
|
|
1921
|
+
const apiKey = requireElevenLabsKey();
|
|
1922
|
+
const data = await apiGet(`voices/${voiceId}`, apiKey);
|
|
1923
|
+
console.log(JSON.stringify(data, null, 2));
|
|
1924
|
+
});
|
|
1925
|
+
voices.command("search").description("Search the public Voices Library (shared voices)").option("--search <term>", "Search query").option("--language <code>", "Language filter, e.g. en, es").option("--gender <g>", "male|female|neutral").option("--age <a>", "young|middle_aged|old").option("--accent <a>", "Accent filter, e.g. american, british").option("--category <c>", "Category filter").option("--page-size <n>", "Results per page", "30").action(async (opts) => {
|
|
1926
|
+
const apiKey = requireElevenLabsKey();
|
|
1927
|
+
const data = await apiGet("shared-voices", apiKey, {
|
|
1928
|
+
search: opts.search,
|
|
1929
|
+
language: opts.language,
|
|
1930
|
+
gender: opts.gender,
|
|
1931
|
+
age: opts.age,
|
|
1932
|
+
accent: opts.accent,
|
|
1933
|
+
category: opts.category,
|
|
1934
|
+
page_size: opts.pageSize
|
|
1935
|
+
});
|
|
1936
|
+
for (const v of data.voices) {
|
|
1937
|
+
console.log(`${v.voice_id} ${v.name} ${v.description ?? ""}`);
|
|
1938
|
+
}
|
|
1939
|
+
console.log(`
|
|
1940
|
+
${data.voices.length} voices${data.has_more ? " (more available)" : ""}`);
|
|
1941
|
+
});
|
|
1942
|
+
voices.command("design").description("Design a voice from text description (returns previews)").requiredOption("--description <str>", "Voice description (e.g. 'friendly female narrator')").option("--text <str>", "Sample text to generate previews from").option("--auto-text", "Let ElevenLabs generate sample text", false).option("--model <id>", "Voice design model", "eleven_multilingual_ttv_v2").option("--output-prefix <p>", "Filename prefix (default: design)", "design").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
1943
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1944
|
+
const apiKey = requireElevenLabsKey();
|
|
1945
|
+
if (!opts.autoText && !opts.text) {
|
|
1946
|
+
throw new ValidationError("Provide --text or pass --auto-text");
|
|
1947
|
+
}
|
|
1948
|
+
const payload = {
|
|
1949
|
+
voice_description: opts.description,
|
|
1950
|
+
model_id: opts.model,
|
|
1951
|
+
auto_generate_text: opts.autoText
|
|
1952
|
+
};
|
|
1953
|
+
if (opts.text) payload.text = opts.text;
|
|
1954
|
+
const data = await apiPostJson(
|
|
1955
|
+
"text-to-voice/create-previews",
|
|
1956
|
+
payload,
|
|
1957
|
+
apiKey
|
|
1958
|
+
);
|
|
1959
|
+
logger.success(`${data.previews.length} previews generated`);
|
|
1960
|
+
data.previews.forEach((p, i) => {
|
|
1961
|
+
const bytes = Buffer.from(p.audio_base_64, "base64");
|
|
1962
|
+
const dest = saveBytes({
|
|
1963
|
+
bytes,
|
|
1964
|
+
task: `${opts.outputPrefix}_${i + 1}_${p.generated_voice_id}`,
|
|
1965
|
+
ext: extFromFormat(TASK_DEFAULTS.ttsFormat),
|
|
1966
|
+
logger
|
|
1047
1967
|
});
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
});
|
|
1064
|
-
const url = final.content?.video_url;
|
|
1065
|
-
if (!url) {
|
|
1066
|
-
logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
|
|
1067
|
-
process.exit(1);
|
|
1068
|
-
}
|
|
1069
|
-
const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1070
|
-
await downloadVideo(url, outPath, logger);
|
|
1071
|
-
await maybeDownloadThumb(final, outPath, {
|
|
1072
|
-
skip: opts.thumb === false,
|
|
1073
|
-
copyTo: opts.output,
|
|
1074
|
-
logger
|
|
1075
|
-
});
|
|
1076
|
-
console.log(`
|
|
1077
|
-
Task: ${taskId}`);
|
|
1078
|
-
console.log(`Saved: ${outPath}`);
|
|
1079
|
-
} catch (e) {
|
|
1080
|
-
if (e instanceof PollTimeoutError) {
|
|
1081
|
-
logger.error(
|
|
1082
|
-
`Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
|
|
1083
|
-
);
|
|
1084
|
-
process.exit(1);
|
|
1085
|
-
}
|
|
1086
|
-
if (e instanceof PollFailedError) {
|
|
1087
|
-
logger.error(`Task failed: ${JSON.stringify(e.value)}`);
|
|
1088
|
-
process.exit(1);
|
|
1089
|
-
}
|
|
1090
|
-
throw e;
|
|
1091
|
-
}
|
|
1968
|
+
console.log(`Preview ${i + 1}: voice_id=${p.generated_voice_id} file=${dest}`);
|
|
1969
|
+
});
|
|
1970
|
+
console.log(
|
|
1971
|
+
'\nTo finalize: multix elevenlabs voices create-from-preview --generated-voice-id <id> --name "..." --description "..."'
|
|
1972
|
+
);
|
|
1973
|
+
});
|
|
1974
|
+
voices.command("create-from-preview").description("Persist a designed voice into your library").requiredOption("--generated-voice-id <id>", "ID returned by `voices design`").requiredOption("--name <n>", "Display name").requiredOption("--description <d>", "Voice description").option("--played-not-selected-voice-ids <csv>", "Other previews you considered (CSV)").action(async (opts) => {
|
|
1975
|
+
const apiKey = requireElevenLabsKey();
|
|
1976
|
+
const payload = {
|
|
1977
|
+
generated_voice_id: opts.generatedVoiceId,
|
|
1978
|
+
voice_name: opts.name,
|
|
1979
|
+
voice_description: opts.description
|
|
1980
|
+
};
|
|
1981
|
+
if (opts.playedNotSelectedVoiceIds) {
|
|
1982
|
+
payload.played_not_selected_voice_ids = opts.playedNotSelectedVoiceIds.split(",");
|
|
1092
1983
|
}
|
|
1093
|
-
|
|
1984
|
+
const data = await apiPostJson(
|
|
1985
|
+
"text-to-voice/create-voice-from-preview",
|
|
1986
|
+
payload,
|
|
1987
|
+
apiKey
|
|
1988
|
+
);
|
|
1989
|
+
console.log(`Created voice: ${data.voice_id}`);
|
|
1990
|
+
});
|
|
1991
|
+
voices.command("delete <voiceId>").description("Delete a voice from your library").action(async (voiceId) => {
|
|
1992
|
+
const apiKey = requireElevenLabsKey();
|
|
1993
|
+
await apiDelete(`voices/${voiceId}`, apiKey);
|
|
1994
|
+
console.log(`Deleted: ${voiceId}`);
|
|
1995
|
+
});
|
|
1094
1996
|
}
|
|
1095
1997
|
|
|
1096
|
-
// src/providers/
|
|
1097
|
-
function
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1998
|
+
// src/providers/elevenlabs/commands/index.ts
|
|
1999
|
+
function registerElevenLabsCommands(program2) {
|
|
2000
|
+
const el = program2.command("elevenlabs").description(
|
|
2001
|
+
"ElevenLabs: TTS, voices, cloning, STT, voice changer, SFX, music, dubbing, isolation, alignment"
|
|
2002
|
+
);
|
|
2003
|
+
registerTtsCommand(el);
|
|
2004
|
+
registerVoicesCommand(el);
|
|
2005
|
+
registerCloneCommand(el);
|
|
2006
|
+
registerVoiceChangerCommand(el);
|
|
2007
|
+
registerTranscribeCommand(el);
|
|
2008
|
+
registerSfxCommand(el);
|
|
2009
|
+
registerMusicCommand(el);
|
|
2010
|
+
registerDubCommand(el);
|
|
2011
|
+
registerIsolateCommand(el);
|
|
2012
|
+
registerAlignCommand(el);
|
|
2013
|
+
registerAccountCommands(el);
|
|
1105
2014
|
}
|
|
1106
2015
|
|
|
1107
2016
|
// src/providers/gemini/commands/analyze.ts
|
|
1108
2017
|
init_errors();
|
|
1109
|
-
import
|
|
2018
|
+
import fs14 from "fs";
|
|
1110
2019
|
|
|
1111
2020
|
// src/providers/gemini/client.ts
|
|
1112
|
-
import
|
|
1113
|
-
import
|
|
2021
|
+
import fs12 from "fs";
|
|
2022
|
+
import path18 from "path";
|
|
1114
2023
|
init_errors();
|
|
1115
2024
|
init_http_client();
|
|
1116
2025
|
|
|
1117
2026
|
// src/providers/gemini/task-resolver.ts
|
|
1118
|
-
import
|
|
2027
|
+
import path17 from "path";
|
|
1119
2028
|
var MIME_MAP = {
|
|
1120
2029
|
// Audio
|
|
1121
2030
|
".mp3": "audio/mp3",
|
|
@@ -1156,7 +2065,7 @@ var MIME_MAP = {
|
|
|
1156
2065
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
1157
2066
|
};
|
|
1158
2067
|
function getMimeType(filePath) {
|
|
1159
|
-
const ext =
|
|
2068
|
+
const ext = path17.extname(filePath).toLowerCase();
|
|
1160
2069
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
1161
2070
|
}
|
|
1162
2071
|
function requiresProcessingWait(mimeType) {
|
|
@@ -1175,15 +2084,15 @@ function requireApiKey() {
|
|
|
1175
2084
|
);
|
|
1176
2085
|
return key;
|
|
1177
2086
|
}
|
|
1178
|
-
function
|
|
2087
|
+
function authHeaders2(apiKey) {
|
|
1179
2088
|
return { "x-goog-api-key": apiKey };
|
|
1180
2089
|
}
|
|
1181
2090
|
async function uploadFile(filePath, opts = {}) {
|
|
1182
2091
|
const apiKey = requireApiKey();
|
|
1183
2092
|
const { timeoutMs = 3e5, logger } = opts;
|
|
1184
2093
|
const mimeType = getMimeType(filePath);
|
|
1185
|
-
const fileBytes =
|
|
1186
|
-
const displayName =
|
|
2094
|
+
const fileBytes = fs12.readFileSync(filePath);
|
|
2095
|
+
const displayName = path18.basename(filePath);
|
|
1187
2096
|
const boundary = `multix_${Date.now()}`;
|
|
1188
2097
|
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
1189
2098
|
const body = Buffer.concat([
|
|
@@ -1228,7 +2137,7 @@ Content-Type: ${mimeType}\r
|
|
|
1228
2137
|
await sleep(2e3);
|
|
1229
2138
|
fileRef = await httpJson({
|
|
1230
2139
|
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
1231
|
-
headers:
|
|
2140
|
+
headers: authHeaders2(apiKey)
|
|
1232
2141
|
}).then((r) => r.file);
|
|
1233
2142
|
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
1234
2143
|
}
|
|
@@ -1245,7 +2154,7 @@ async function generateContent(req) {
|
|
|
1245
2154
|
return httpJson({
|
|
1246
2155
|
url: `${GENERATE_BASE}/${req.model}:generateContent`,
|
|
1247
2156
|
method: "POST",
|
|
1248
|
-
headers:
|
|
2157
|
+
headers: authHeaders2(apiKey),
|
|
1249
2158
|
body: {
|
|
1250
2159
|
contents: req.contents,
|
|
1251
2160
|
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
@@ -1272,12 +2181,12 @@ function extractImages(resp) {
|
|
|
1272
2181
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
1273
2182
|
}
|
|
1274
2183
|
function inlinePart(filePath) {
|
|
1275
|
-
const data =
|
|
2184
|
+
const data = fs12.readFileSync(filePath).toString("base64");
|
|
1276
2185
|
const mimeType = getMimeType(filePath);
|
|
1277
2186
|
return { inlineData: { mimeType, data } };
|
|
1278
2187
|
}
|
|
1279
2188
|
function shouldUseFileApi(filePath) {
|
|
1280
|
-
const stat =
|
|
2189
|
+
const stat = fs12.statSync(filePath);
|
|
1281
2190
|
return stat.size > 20 * 1024 * 1024;
|
|
1282
2191
|
}
|
|
1283
2192
|
function sleep(ms) {
|
|
@@ -1330,13 +2239,13 @@ function getDefaultModel(task) {
|
|
|
1330
2239
|
}
|
|
1331
2240
|
|
|
1332
2241
|
// src/providers/gemini/result-formatter.ts
|
|
1333
|
-
import
|
|
1334
|
-
import
|
|
2242
|
+
import fs13 from "fs";
|
|
2243
|
+
import path19 from "path";
|
|
1335
2244
|
function formatResults(results, format, outputPath) {
|
|
1336
2245
|
const content = renderFormat(results, format);
|
|
1337
2246
|
if (outputPath) {
|
|
1338
|
-
|
|
1339
|
-
|
|
2247
|
+
fs13.mkdirSync(path19.dirname(path19.resolve(outputPath)), { recursive: true });
|
|
2248
|
+
fs13.writeFileSync(outputPath, content, "utf8");
|
|
1340
2249
|
} else {
|
|
1341
2250
|
process.stdout.write(content);
|
|
1342
2251
|
}
|
|
@@ -1403,7 +2312,7 @@ function registerAnalyzeCommand(parent) {
|
|
|
1403
2312
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1404
2313
|
const model = opts.model ?? getDefaultModel("analyze");
|
|
1405
2314
|
for (const f of opts.files) {
|
|
1406
|
-
if (!
|
|
2315
|
+
if (!fs14.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1407
2316
|
}
|
|
1408
2317
|
const results = [];
|
|
1409
2318
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -1449,14 +2358,14 @@ function registerAnalyzeCommand(parent) {
|
|
|
1449
2358
|
|
|
1450
2359
|
// src/providers/gemini/commands/extract.ts
|
|
1451
2360
|
init_errors();
|
|
1452
|
-
import
|
|
2361
|
+
import fs15 from "fs";
|
|
1453
2362
|
function registerExtractCommand(parent) {
|
|
1454
2363
|
parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths").requiredOption("--prompt <text>", "Extraction prompt (required)").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
|
|
1455
2364
|
async (opts) => {
|
|
1456
2365
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1457
2366
|
const model = opts.model ?? getDefaultModel("extract");
|
|
1458
2367
|
for (const f of opts.files) {
|
|
1459
|
-
if (!
|
|
2368
|
+
if (!fs15.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1460
2369
|
}
|
|
1461
2370
|
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
1462
2371
|
const results = [];
|
|
@@ -1498,8 +2407,8 @@ function registerExtractCommand(parent) {
|
|
|
1498
2407
|
init_errors();
|
|
1499
2408
|
|
|
1500
2409
|
// src/providers/gemini/generators/speech.ts
|
|
1501
|
-
import
|
|
1502
|
-
import
|
|
2410
|
+
import fs16 from "fs";
|
|
2411
|
+
import path20 from "path";
|
|
1503
2412
|
|
|
1504
2413
|
// src/providers/gemini/voices.ts
|
|
1505
2414
|
var GEMINI_TTS_VOICES = [
|
|
@@ -1582,12 +2491,12 @@ async function generateGeminiSpeech(opts) {
|
|
|
1582
2491
|
const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
|
|
1583
2492
|
const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
|
|
1584
2493
|
const outDir = getOutputDir();
|
|
1585
|
-
const dest =
|
|
1586
|
-
|
|
2494
|
+
const dest = path20.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
2495
|
+
fs16.writeFileSync(dest, outBytes);
|
|
1587
2496
|
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
1588
2497
|
if (output) {
|
|
1589
|
-
|
|
1590
|
-
|
|
2498
|
+
fs16.mkdirSync(path20.dirname(path20.resolve(output)), { recursive: true });
|
|
2499
|
+
fs16.copyFileSync(dest, output);
|
|
1591
2500
|
logger?.success(`Copied to: ${output}`);
|
|
1592
2501
|
}
|
|
1593
2502
|
return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
|
|
@@ -1686,8 +2595,8 @@ function collectSpeakers(value, prev) {
|
|
|
1686
2595
|
}
|
|
1687
2596
|
|
|
1688
2597
|
// src/providers/gemini/commands/generate-video.ts
|
|
1689
|
-
import
|
|
1690
|
-
import
|
|
2598
|
+
import fs17 from "fs";
|
|
2599
|
+
import path21 from "path";
|
|
1691
2600
|
init_errors();
|
|
1692
2601
|
init_http_client();
|
|
1693
2602
|
var BASE2 = "https://generativelanguage.googleapis.com";
|
|
@@ -1721,10 +2630,10 @@ function registerGenerateVideoCommand(parent) {
|
|
|
1721
2630
|
const contentParts = [{ text: opts.prompt }];
|
|
1722
2631
|
if (opts.referenceImages?.length) {
|
|
1723
2632
|
for (const imgPath of opts.referenceImages.slice(0, 2)) {
|
|
1724
|
-
if (!
|
|
2633
|
+
if (!fs17.existsSync(imgPath))
|
|
1725
2634
|
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
1726
|
-
const data =
|
|
1727
|
-
const ext =
|
|
2635
|
+
const data = fs17.readFileSync(imgPath).toString("base64");
|
|
2636
|
+
const ext = path21.extname(imgPath).toLowerCase();
|
|
1728
2637
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
1729
2638
|
contentParts.push({ inlineData: { mimeType, data } });
|
|
1730
2639
|
}
|
|
@@ -1773,13 +2682,13 @@ function registerGenerateVideoCommand(parent) {
|
|
|
1773
2682
|
);
|
|
1774
2683
|
}
|
|
1775
2684
|
const outDir = getOutputDir();
|
|
1776
|
-
const dest =
|
|
2685
|
+
const dest = path21.join(outDir, `veo_generated_${Date.now()}.mp4`);
|
|
1777
2686
|
const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
|
|
1778
2687
|
await downloadFile2(videoUri, dest);
|
|
1779
2688
|
logger.success(`Saved video: ${dest}`);
|
|
1780
2689
|
if (opts.output) {
|
|
1781
|
-
|
|
1782
|
-
|
|
2690
|
+
fs17.mkdirSync(path21.dirname(path21.resolve(opts.output)), { recursive: true });
|
|
2691
|
+
fs17.copyFileSync(dest, opts.output);
|
|
1783
2692
|
logger.success(`Copied to: ${opts.output}`);
|
|
1784
2693
|
}
|
|
1785
2694
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -1798,8 +2707,8 @@ function sleep2(ms) {
|
|
|
1798
2707
|
|
|
1799
2708
|
// src/providers/gemini/commands/generate.ts
|
|
1800
2709
|
init_errors();
|
|
1801
|
-
import
|
|
1802
|
-
import
|
|
2710
|
+
import fs18 from "fs";
|
|
2711
|
+
import path22 from "path";
|
|
1803
2712
|
function registerGenerateCommand(parent) {
|
|
1804
2713
|
parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
1805
2714
|
async (opts) => {
|
|
@@ -1841,8 +2750,8 @@ function registerGenerateCommand(parent) {
|
|
|
1841
2750
|
const outDir = getOutputDir();
|
|
1842
2751
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
1843
2752
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
1844
|
-
const dest =
|
|
1845
|
-
|
|
2753
|
+
const dest = path22.join(outDir, fname);
|
|
2754
|
+
fs18.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
1846
2755
|
savedFiles.push(dest);
|
|
1847
2756
|
logger.success(`Saved: ${dest}`);
|
|
1848
2757
|
}
|
|
@@ -1857,8 +2766,8 @@ function registerGenerateCommand(parent) {
|
|
|
1857
2766
|
process.exit(1);
|
|
1858
2767
|
}
|
|
1859
2768
|
if (opts.output && savedFiles[0]) {
|
|
1860
|
-
|
|
1861
|
-
|
|
2769
|
+
fs18.mkdirSync(path22.dirname(path22.resolve(opts.output)), { recursive: true });
|
|
2770
|
+
fs18.copyFileSync(savedFiles[0], opts.output);
|
|
1862
2771
|
logger.success(`Copied to: ${opts.output}`);
|
|
1863
2772
|
}
|
|
1864
2773
|
console.log(`
|
|
@@ -1869,8 +2778,8 @@ Generated ${savedFiles.length} image(s):`);
|
|
|
1869
2778
|
}
|
|
1870
2779
|
|
|
1871
2780
|
// src/providers/gemini/commands/image-to-image.ts
|
|
1872
|
-
import
|
|
1873
|
-
import
|
|
2781
|
+
import fs19 from "fs";
|
|
2782
|
+
import path23 from "path";
|
|
1874
2783
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
1875
2784
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
1876
2785
|
function registerGeminiImageToImageCommand(parent) {
|
|
@@ -1931,14 +2840,14 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
1931
2840
|
const img = images[i];
|
|
1932
2841
|
if (!img) continue;
|
|
1933
2842
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
1934
|
-
const dest =
|
|
1935
|
-
|
|
2843
|
+
const dest = path23.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
2844
|
+
fs19.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
1936
2845
|
saved.push(dest);
|
|
1937
2846
|
logger.success(`Saved: ${dest}`);
|
|
1938
2847
|
}
|
|
1939
2848
|
if (opts.output && saved[0]) {
|
|
1940
|
-
|
|
1941
|
-
|
|
2849
|
+
fs19.mkdirSync(path23.dirname(path23.resolve(opts.output)), { recursive: true });
|
|
2850
|
+
fs19.copyFileSync(saved[0], opts.output);
|
|
1942
2851
|
logger.success(`Copied to: ${opts.output}`);
|
|
1943
2852
|
}
|
|
1944
2853
|
console.log(`
|
|
@@ -1965,7 +2874,7 @@ function sniffImageMime(buf, url) {
|
|
|
1965
2874
|
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
1966
2875
|
return "image/webp";
|
|
1967
2876
|
}
|
|
1968
|
-
const ext =
|
|
2877
|
+
const ext = path23.extname(new URL(url).pathname).toLowerCase();
|
|
1969
2878
|
if (ext === ".png") return "image/png";
|
|
1970
2879
|
if (ext === ".webp") return "image/webp";
|
|
1971
2880
|
if (ext === ".gif") return "image/gif";
|
|
@@ -1973,17 +2882,17 @@ function sniffImageMime(buf, url) {
|
|
|
1973
2882
|
}
|
|
1974
2883
|
|
|
1975
2884
|
// src/providers/gemini/commands/image-to-video.ts
|
|
1976
|
-
import
|
|
1977
|
-
import
|
|
2885
|
+
import fs20 from "fs";
|
|
2886
|
+
import path24 from "path";
|
|
1978
2887
|
init_errors();
|
|
1979
2888
|
init_http_client();
|
|
1980
2889
|
var BASE3 = "https://generativelanguage.googleapis.com";
|
|
1981
2890
|
function imagePart(imgPath) {
|
|
1982
|
-
if (!
|
|
2891
|
+
if (!fs20.existsSync(imgPath)) {
|
|
1983
2892
|
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
1984
2893
|
}
|
|
1985
|
-
const data =
|
|
1986
|
-
const ext =
|
|
2894
|
+
const data = fs20.readFileSync(imgPath).toString("base64");
|
|
2895
|
+
const ext = path24.extname(imgPath).toLowerCase();
|
|
1987
2896
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
1988
2897
|
return { inlineData: { mimeType, data } };
|
|
1989
2898
|
}
|
|
@@ -2056,12 +2965,12 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
2056
2965
|
"gemini"
|
|
2057
2966
|
);
|
|
2058
2967
|
}
|
|
2059
|
-
const dest =
|
|
2968
|
+
const dest = path24.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
2060
2969
|
await downloadFile(videoUri, dest);
|
|
2061
2970
|
logger.success(`Saved video: ${dest}`);
|
|
2062
2971
|
if (opts.output) {
|
|
2063
|
-
|
|
2064
|
-
|
|
2972
|
+
fs20.mkdirSync(path24.dirname(path24.resolve(opts.output)), { recursive: true });
|
|
2973
|
+
fs20.copyFileSync(dest, opts.output);
|
|
2065
2974
|
logger.success(`Copied to: ${opts.output}`);
|
|
2066
2975
|
}
|
|
2067
2976
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -2077,14 +2986,14 @@ Generated video: ${dest}`);
|
|
|
2077
2986
|
|
|
2078
2987
|
// src/providers/gemini/commands/transcribe.ts
|
|
2079
2988
|
init_errors();
|
|
2080
|
-
import
|
|
2081
|
-
function
|
|
2989
|
+
import fs21 from "fs";
|
|
2990
|
+
function registerTranscribeCommand2(parent) {
|
|
2082
2991
|
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(
|
|
2083
2992
|
async (opts) => {
|
|
2084
2993
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2085
2994
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
2086
2995
|
for (const f of opts.files) {
|
|
2087
|
-
if (!
|
|
2996
|
+
if (!fs21.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2088
2997
|
}
|
|
2089
2998
|
const results = [];
|
|
2090
2999
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -2131,7 +3040,7 @@ function registerGeminiCommands(program2) {
|
|
|
2131
3040
|
"Gemini AI operations: analyze, transcribe, extract, generate images/video/speech"
|
|
2132
3041
|
);
|
|
2133
3042
|
registerAnalyzeCommand(gemini);
|
|
2134
|
-
|
|
3043
|
+
registerTranscribeCommand2(gemini);
|
|
2135
3044
|
registerExtractCommand(gemini);
|
|
2136
3045
|
registerGenerateCommand(gemini);
|
|
2137
3046
|
registerGeminiImageToImageCommand(gemini);
|
|
@@ -2142,8 +3051,8 @@ function registerGeminiCommands(program2) {
|
|
|
2142
3051
|
|
|
2143
3052
|
// src/providers/leonardo/commands/generate.ts
|
|
2144
3053
|
init_http_client();
|
|
2145
|
-
import
|
|
2146
|
-
import
|
|
3054
|
+
import fs22 from "fs";
|
|
3055
|
+
import path25 from "path";
|
|
2147
3056
|
|
|
2148
3057
|
// src/providers/leonardo/client.ts
|
|
2149
3058
|
init_errors();
|
|
@@ -2217,11 +3126,11 @@ var LeonardoClient = class {
|
|
|
2217
3126
|
}
|
|
2218
3127
|
apiKey;
|
|
2219
3128
|
baseUrl;
|
|
2220
|
-
async request(
|
|
3129
|
+
async request(path41, opts = {}) {
|
|
2221
3130
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
2222
|
-
const v2Override =
|
|
3131
|
+
const v2Override = path41.startsWith("v2:");
|
|
2223
3132
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
2224
|
-
const finalPath = v2Override ?
|
|
3133
|
+
const finalPath = v2Override ? path41.slice(3) : path41;
|
|
2225
3134
|
const url = new URL(finalBase + finalPath);
|
|
2226
3135
|
if (opts.query) {
|
|
2227
3136
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -2240,14 +3149,14 @@ var LeonardoClient = class {
|
|
|
2240
3149
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2241
3150
|
});
|
|
2242
3151
|
}
|
|
2243
|
-
get(
|
|
2244
|
-
return this.request(
|
|
3152
|
+
get(path41, query, logger) {
|
|
3153
|
+
return this.request(path41, { method: "GET", query, logger });
|
|
2245
3154
|
}
|
|
2246
|
-
post(
|
|
2247
|
-
return this.request(
|
|
3155
|
+
post(path41, body, logger) {
|
|
3156
|
+
return this.request(path41, { method: "POST", body, logger });
|
|
2248
3157
|
}
|
|
2249
|
-
delete(
|
|
2250
|
-
return this.request(
|
|
3158
|
+
delete(path41, logger) {
|
|
3159
|
+
return this.request(path41, { method: "DELETE", logger });
|
|
2251
3160
|
}
|
|
2252
3161
|
};
|
|
2253
3162
|
function createLeonardoClient() {
|
|
@@ -2258,7 +3167,7 @@ function createLeonardoClient() {
|
|
|
2258
3167
|
function extFromUrl(url, fallback = ".png") {
|
|
2259
3168
|
try {
|
|
2260
3169
|
const u = new URL(url);
|
|
2261
|
-
const e =
|
|
3170
|
+
const e = path25.extname(u.pathname);
|
|
2262
3171
|
return e.length > 0 ? e : fallback;
|
|
2263
3172
|
} catch {
|
|
2264
3173
|
return fallback;
|
|
@@ -2343,7 +3252,7 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
2343
3252
|
if (!img) continue;
|
|
2344
3253
|
const ext = extFromUrl(img.url);
|
|
2345
3254
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
2346
|
-
const filepath =
|
|
3255
|
+
const filepath = path25.join(outDir, filename);
|
|
2347
3256
|
try {
|
|
2348
3257
|
await downloadFile(img.url, filepath);
|
|
2349
3258
|
saved.push(filepath);
|
|
@@ -2353,9 +3262,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
2353
3262
|
}
|
|
2354
3263
|
}
|
|
2355
3264
|
if (opts.output && saved[0]) {
|
|
2356
|
-
const dest =
|
|
2357
|
-
|
|
2358
|
-
|
|
3265
|
+
const dest = path25.resolve(opts.output);
|
|
3266
|
+
fs22.mkdirSync(path25.dirname(dest), { recursive: true });
|
|
3267
|
+
fs22.copyFileSync(saved[0], dest);
|
|
2359
3268
|
logger.success(`Copied to ${dest}`);
|
|
2360
3269
|
}
|
|
2361
3270
|
console.log(`
|
|
@@ -2367,11 +3276,11 @@ Generated ${saved.length} image(s):`);
|
|
|
2367
3276
|
|
|
2368
3277
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
2369
3278
|
init_http_client();
|
|
2370
|
-
import
|
|
2371
|
-
import
|
|
3279
|
+
import fs23 from "fs";
|
|
3280
|
+
import path26 from "path";
|
|
2372
3281
|
function extFromUrl2(url, fallback = ".png") {
|
|
2373
3282
|
try {
|
|
2374
|
-
return
|
|
3283
|
+
return path26.extname(new URL(url).pathname) || fallback;
|
|
2375
3284
|
} catch {
|
|
2376
3285
|
return fallback;
|
|
2377
3286
|
}
|
|
@@ -2433,7 +3342,7 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
2433
3342
|
if (!img) continue;
|
|
2434
3343
|
const ext = extFromUrl2(img.url);
|
|
2435
3344
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
2436
|
-
const filepath =
|
|
3345
|
+
const filepath = path26.join(outDir, filename);
|
|
2437
3346
|
try {
|
|
2438
3347
|
await downloadFile(img.url, filepath);
|
|
2439
3348
|
saved.push(filepath);
|
|
@@ -2443,9 +3352,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
2443
3352
|
}
|
|
2444
3353
|
}
|
|
2445
3354
|
if (opts.output && saved[0]) {
|
|
2446
|
-
const dest =
|
|
2447
|
-
|
|
2448
|
-
|
|
3355
|
+
const dest = path26.resolve(opts.output);
|
|
3356
|
+
fs23.mkdirSync(path26.dirname(dest), { recursive: true });
|
|
3357
|
+
fs23.copyFileSync(saved[0], dest);
|
|
2449
3358
|
logger.success(`Copied to ${dest}`);
|
|
2450
3359
|
}
|
|
2451
3360
|
console.log(`
|
|
@@ -2457,7 +3366,7 @@ Generated ${saved.length} image(s):`);
|
|
|
2457
3366
|
|
|
2458
3367
|
// src/providers/leonardo/commands/video.ts
|
|
2459
3368
|
init_http_client();
|
|
2460
|
-
import
|
|
3369
|
+
import path27 from "path";
|
|
2461
3370
|
function v2DimsFor(resolution) {
|
|
2462
3371
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
2463
3372
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -2568,7 +3477,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
2568
3477
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
2569
3478
|
process.exit(1);
|
|
2570
3479
|
}
|
|
2571
|
-
const outPath = opts.output ??
|
|
3480
|
+
const outPath = opts.output ?? path27.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
2572
3481
|
await downloadFile(videoUrl, outPath);
|
|
2573
3482
|
logger.success(`Saved ${outPath}`);
|
|
2574
3483
|
await maybeDownloadThumb(gen, outPath, {
|
|
@@ -2675,7 +3584,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
2675
3584
|
|
|
2676
3585
|
// src/providers/leonardo/commands/status.ts
|
|
2677
3586
|
init_http_client();
|
|
2678
|
-
import
|
|
3587
|
+
import path28 from "path";
|
|
2679
3588
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
2680
3589
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
2681
3590
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -2740,7 +3649,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
2740
3649
|
const outDir = getOutputDir();
|
|
2741
3650
|
let idx = 0;
|
|
2742
3651
|
for (const url of videos) {
|
|
2743
|
-
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output :
|
|
3652
|
+
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path28.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
|
|
2744
3653
|
await downloadFile(url, dest);
|
|
2745
3654
|
logger.success(`Saved ${dest}`);
|
|
2746
3655
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -2753,7 +3662,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
2753
3662
|
for (const url of images) {
|
|
2754
3663
|
const m = url.match(IMAGE_EXT_RE2);
|
|
2755
3664
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
2756
|
-
const dest =
|
|
3665
|
+
const dest = path28.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
2757
3666
|
await downloadFile(url, dest);
|
|
2758
3667
|
logger.success(`Saved ${dest}`);
|
|
2759
3668
|
idx++;
|
|
@@ -2841,7 +3750,7 @@ function requireMinimaxKey() {
|
|
|
2841
3750
|
}
|
|
2842
3751
|
return key;
|
|
2843
3752
|
}
|
|
2844
|
-
function
|
|
3753
|
+
function authHeaders3(apiKey) {
|
|
2845
3754
|
return {
|
|
2846
3755
|
Authorization: `Bearer ${apiKey}`,
|
|
2847
3756
|
"Content-Type": "application/json"
|
|
@@ -2853,7 +3762,7 @@ async function apiPost(endpoint, payload, apiKey, opts = {}) {
|
|
|
2853
3762
|
const data = await httpJson({
|
|
2854
3763
|
url,
|
|
2855
3764
|
method: "POST",
|
|
2856
|
-
headers:
|
|
3765
|
+
headers: authHeaders3(apiKey),
|
|
2857
3766
|
body: payload,
|
|
2858
3767
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2859
3768
|
});
|
|
@@ -2866,17 +3775,17 @@ async function apiPost(endpoint, payload, apiKey, opts = {}) {
|
|
|
2866
3775
|
}
|
|
2867
3776
|
return data;
|
|
2868
3777
|
}
|
|
2869
|
-
async function
|
|
3778
|
+
async function apiGet2(endpoint, params, apiKey) {
|
|
2870
3779
|
const url = new URL(`${MINIMAX_BASE_URL}/${endpoint}`);
|
|
2871
3780
|
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
2872
|
-
return httpJson({ url: url.toString(), headers:
|
|
3781
|
+
return httpJson({ url: url.toString(), headers: authHeaders3(apiKey) });
|
|
2873
3782
|
}
|
|
2874
3783
|
async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
2875
3784
|
const { intervalMs = 1e4, timeoutMs = 6e5, logger } = opts;
|
|
2876
3785
|
const deadline = Date.now() + timeoutMs;
|
|
2877
3786
|
let elapsed = 0;
|
|
2878
3787
|
while (Date.now() < deadline) {
|
|
2879
|
-
const result = await
|
|
3788
|
+
const result = await apiGet2(`query/${taskType}`, { task_id: taskId }, apiKey);
|
|
2880
3789
|
const status = result.status ?? "Unknown";
|
|
2881
3790
|
if (elapsed > 0 && elapsed % 3e4 < intervalMs) {
|
|
2882
3791
|
logger?.debug(`Polling... ${Math.round(elapsed / 1e3)}s elapsed, status: ${status}`);
|
|
@@ -2891,7 +3800,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
|
2891
3800
|
throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
|
|
2892
3801
|
}
|
|
2893
3802
|
async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
|
|
2894
|
-
const result = await
|
|
3803
|
+
const result = await apiGet2(
|
|
2895
3804
|
"files/retrieve",
|
|
2896
3805
|
{ file_id: fileId },
|
|
2897
3806
|
apiKey
|
|
@@ -2910,8 +3819,8 @@ function sleep4(ms) {
|
|
|
2910
3819
|
|
|
2911
3820
|
// src/providers/minimax/generators/music.ts
|
|
2912
3821
|
init_http_client();
|
|
2913
|
-
import
|
|
2914
|
-
import
|
|
3822
|
+
import fs24 from "fs";
|
|
3823
|
+
import path29 from "path";
|
|
2915
3824
|
async function generateMinimaxMusic(opts) {
|
|
2916
3825
|
const {
|
|
2917
3826
|
apiKey,
|
|
@@ -2956,14 +3865,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
2956
3865
|
}
|
|
2957
3866
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
2958
3867
|
const outDir = getOutputDir();
|
|
2959
|
-
const dest =
|
|
3868
|
+
const dest = path29.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
2960
3869
|
try {
|
|
2961
3870
|
if (audioData.startsWith("http")) {
|
|
2962
3871
|
const bytes = await fetchBytes(audioData);
|
|
2963
|
-
|
|
3872
|
+
fs24.writeFileSync(dest, bytes);
|
|
2964
3873
|
} else {
|
|
2965
3874
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
2966
|
-
|
|
3875
|
+
fs24.writeFileSync(dest, audioBytes);
|
|
2967
3876
|
}
|
|
2968
3877
|
} catch (e) {
|
|
2969
3878
|
return {
|
|
@@ -2974,8 +3883,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
2974
3883
|
const durSec = durationMs / 1e3;
|
|
2975
3884
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
2976
3885
|
if (output) {
|
|
2977
|
-
|
|
2978
|
-
|
|
3886
|
+
fs24.mkdirSync(path29.dirname(path29.resolve(output)), { recursive: true });
|
|
3887
|
+
fs24.copyFileSync(dest, output);
|
|
2979
3888
|
}
|
|
2980
3889
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
2981
3890
|
}
|
|
@@ -3003,7 +3912,7 @@ var ALL_MINIMAX_MODELS = /* @__PURE__ */ new Set([
|
|
|
3003
3912
|
...MINIMAX_SPEECH_MODELS,
|
|
3004
3913
|
...MINIMAX_MUSIC_MODELS
|
|
3005
3914
|
]);
|
|
3006
|
-
var
|
|
3915
|
+
var TASK_DEFAULTS2 = {
|
|
3007
3916
|
image: process.env.MINIMAX_IMAGE_MODEL ?? "image-01",
|
|
3008
3917
|
video: process.env.MINIMAX_VIDEO_MODEL ?? "MiniMax-Hailuo-2.3",
|
|
3009
3918
|
speech: process.env.MINIMAX_SPEECH_MODEL ?? "speech-2.8-hd",
|
|
@@ -3022,7 +3931,7 @@ function registerMinimaxGenerateMusicCommand(parent) {
|
|
|
3022
3931
|
if (!opts.lyrics && !opts.prompt) {
|
|
3023
3932
|
throw new ValidationError("Either --lyrics or --prompt is required");
|
|
3024
3933
|
}
|
|
3025
|
-
const model = opts.model ??
|
|
3934
|
+
const model = opts.model ?? TASK_DEFAULTS2.music;
|
|
3026
3935
|
const result = await generateMinimaxMusic({
|
|
3027
3936
|
apiKey,
|
|
3028
3937
|
lyrics: opts.lyrics,
|
|
@@ -3049,8 +3958,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
3049
3958
|
init_errors();
|
|
3050
3959
|
|
|
3051
3960
|
// src/providers/minimax/generators/speech.ts
|
|
3052
|
-
import
|
|
3053
|
-
import
|
|
3961
|
+
import fs25 from "fs";
|
|
3962
|
+
import path30 from "path";
|
|
3054
3963
|
async function generateMinimaxSpeech(opts) {
|
|
3055
3964
|
const {
|
|
3056
3965
|
apiKey,
|
|
@@ -3097,12 +4006,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
3097
4006
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
3098
4007
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
3099
4008
|
const outDir = getOutputDir();
|
|
3100
|
-
const dest =
|
|
3101
|
-
|
|
4009
|
+
const dest = path30.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
4010
|
+
fs25.writeFileSync(dest, audioBytes);
|
|
3102
4011
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
3103
4012
|
if (output) {
|
|
3104
|
-
|
|
3105
|
-
|
|
4013
|
+
fs25.mkdirSync(path30.dirname(path30.resolve(output)), { recursive: true });
|
|
4014
|
+
fs25.copyFileSync(dest, output);
|
|
3106
4015
|
}
|
|
3107
4016
|
return { status: "success", generatedAudio: dest, model };
|
|
3108
4017
|
}
|
|
@@ -3122,7 +4031,7 @@ function registerMinimaxGenerateSpeechCommand(parent) {
|
|
|
3122
4031
|
if (!SPEECH_FORMATS.includes(opts.outputFormat)) {
|
|
3123
4032
|
throw new ValidationError(`--output-format must be one of: ${SPEECH_FORMATS.join(", ")}`);
|
|
3124
4033
|
}
|
|
3125
|
-
const model = opts.model ??
|
|
4034
|
+
const model = opts.model ?? TASK_DEFAULTS2.speech;
|
|
3126
4035
|
const rate = Number.parseFloat(opts.rate);
|
|
3127
4036
|
const result = await generateMinimaxSpeech({
|
|
3128
4037
|
apiKey,
|
|
@@ -3149,8 +4058,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
3149
4058
|
init_errors();
|
|
3150
4059
|
|
|
3151
4060
|
// src/providers/minimax/generators/video.ts
|
|
3152
|
-
import
|
|
3153
|
-
import
|
|
4061
|
+
import fs26 from "fs";
|
|
4062
|
+
import path31 from "path";
|
|
3154
4063
|
async function generateMinimaxVideo(opts) {
|
|
3155
4064
|
const {
|
|
3156
4065
|
apiKey,
|
|
@@ -3196,18 +4105,18 @@ async function generateMinimaxVideo(opts) {
|
|
|
3196
4105
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
3197
4106
|
}
|
|
3198
4107
|
const outDir = getOutputDir();
|
|
3199
|
-
const dest =
|
|
4108
|
+
const dest = path31.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
3200
4109
|
try {
|
|
3201
4110
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
3202
4111
|
} catch (e) {
|
|
3203
4112
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
3204
4113
|
}
|
|
3205
4114
|
const elapsed = (Date.now() - start) / 1e3;
|
|
3206
|
-
const stat =
|
|
4115
|
+
const stat = fs26.statSync(dest);
|
|
3207
4116
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
3208
4117
|
if (output) {
|
|
3209
|
-
|
|
3210
|
-
|
|
4118
|
+
fs26.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
|
|
4119
|
+
fs26.copyFileSync(dest, output);
|
|
3211
4120
|
}
|
|
3212
4121
|
await maybeDownloadThumb(pollResult, dest, {
|
|
3213
4122
|
skip: thumb === false,
|
|
@@ -3230,7 +4139,7 @@ function registerMinimaxGenerateVideoCommand(parent) {
|
|
|
3230
4139
|
async (opts) => {
|
|
3231
4140
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3232
4141
|
const apiKey = requireMinimaxKey();
|
|
3233
|
-
const model = opts.model ??
|
|
4142
|
+
const model = opts.model ?? TASK_DEFAULTS2.video;
|
|
3234
4143
|
const duration = Number.parseInt(opts.duration, 10);
|
|
3235
4144
|
if (!VIDEO_DURATIONS.includes(duration)) {
|
|
3236
4145
|
throw new ValidationError(`--duration must be one of: ${VIDEO_DURATIONS.join(", ")}`);
|
|
@@ -3270,8 +4179,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
3270
4179
|
// src/providers/minimax/generators/image.ts
|
|
3271
4180
|
init_errors();
|
|
3272
4181
|
init_http_client();
|
|
3273
|
-
import
|
|
3274
|
-
import
|
|
4182
|
+
import fs27 from "fs";
|
|
4183
|
+
import path32 from "path";
|
|
3275
4184
|
async function generateMinimaxImage(opts) {
|
|
3276
4185
|
const {
|
|
3277
4186
|
apiKey,
|
|
@@ -3309,8 +4218,8 @@ async function generateMinimaxImage(opts) {
|
|
|
3309
4218
|
try {
|
|
3310
4219
|
const bytes = await fetchBytes(url);
|
|
3311
4220
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
3312
|
-
const dest =
|
|
3313
|
-
|
|
4221
|
+
const dest = path32.join(outDir, fname);
|
|
4222
|
+
fs27.writeFileSync(dest, bytes);
|
|
3314
4223
|
savedFiles.push(dest);
|
|
3315
4224
|
logger?.success(`Saved: ${dest}`);
|
|
3316
4225
|
} catch (e) {
|
|
@@ -3321,8 +4230,8 @@ async function generateMinimaxImage(opts) {
|
|
|
3321
4230
|
}
|
|
3322
4231
|
}
|
|
3323
4232
|
if (output && savedFiles[0]) {
|
|
3324
|
-
|
|
3325
|
-
|
|
4233
|
+
fs27.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
|
|
4234
|
+
fs27.copyFileSync(savedFiles[0], output);
|
|
3326
4235
|
}
|
|
3327
4236
|
return { status: "success", generatedImages: savedFiles, model };
|
|
3328
4237
|
}
|
|
@@ -3333,7 +4242,7 @@ function registerMinimaxGenerateCommand(parent) {
|
|
|
3333
4242
|
async (opts) => {
|
|
3334
4243
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3335
4244
|
const apiKey = requireMinimaxKey();
|
|
3336
|
-
const model = opts.model ??
|
|
4245
|
+
const model = opts.model ?? TASK_DEFAULTS2.image;
|
|
3337
4246
|
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
|
|
3338
4247
|
const result = await generateMinimaxImage({
|
|
3339
4248
|
apiKey,
|
|
@@ -3358,8 +4267,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
3358
4267
|
// src/providers/minimax/commands/image-to-image.ts
|
|
3359
4268
|
init_errors();
|
|
3360
4269
|
init_http_client();
|
|
3361
|
-
import
|
|
3362
|
-
import
|
|
4270
|
+
import fs28 from "fs";
|
|
4271
|
+
import path33 from "path";
|
|
3363
4272
|
function registerMinimaxImageToImageCommand(parent) {
|
|
3364
4273
|
parent.command("image-to-image").alias("i2i").description(
|
|
3365
4274
|
"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."
|
|
@@ -3370,7 +4279,7 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
3370
4279
|
async (opts) => {
|
|
3371
4280
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3372
4281
|
const apiKey = requireMinimaxKey();
|
|
3373
|
-
const model = opts.model ??
|
|
4282
|
+
const model = opts.model ?? TASK_DEFAULTS2.image;
|
|
3374
4283
|
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
|
|
3375
4284
|
logger.warn(
|
|
3376
4285
|
"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."
|
|
@@ -3414,8 +4323,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
3414
4323
|
if (!url) continue;
|
|
3415
4324
|
try {
|
|
3416
4325
|
const bytes = await fetchBytes(url);
|
|
3417
|
-
const dest =
|
|
3418
|
-
|
|
4326
|
+
const dest = path33.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
4327
|
+
fs28.writeFileSync(dest, bytes);
|
|
3419
4328
|
saved.push(dest);
|
|
3420
4329
|
logger.success(`Saved: ${dest}`);
|
|
3421
4330
|
} catch (e) {
|
|
@@ -3426,8 +4335,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
3426
4335
|
}
|
|
3427
4336
|
}
|
|
3428
4337
|
if (opts.output && saved[0]) {
|
|
3429
|
-
|
|
3430
|
-
|
|
4338
|
+
fs28.mkdirSync(path33.dirname(path33.resolve(opts.output)), { recursive: true });
|
|
4339
|
+
fs28.copyFileSync(saved[0], opts.output);
|
|
3431
4340
|
logger.success(`Copied to: ${opts.output}`);
|
|
3432
4341
|
}
|
|
3433
4342
|
console.log(`
|
|
@@ -3448,8 +4357,8 @@ function registerMinimaxCommands(program2) {
|
|
|
3448
4357
|
}
|
|
3449
4358
|
|
|
3450
4359
|
// src/providers/openrouter/client.ts
|
|
3451
|
-
import
|
|
3452
|
-
import
|
|
4360
|
+
import fs29 from "fs";
|
|
4361
|
+
import path34 from "path";
|
|
3453
4362
|
init_errors();
|
|
3454
4363
|
init_http_client();
|
|
3455
4364
|
var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
@@ -3538,8 +4447,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
3538
4447
|
if (!url) continue;
|
|
3539
4448
|
try {
|
|
3540
4449
|
const bytes = await fetchBytes(url);
|
|
3541
|
-
const dest =
|
|
3542
|
-
|
|
4450
|
+
const dest = path34.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
4451
|
+
fs29.writeFileSync(dest, bytes);
|
|
3543
4452
|
savedFiles.push(dest);
|
|
3544
4453
|
logger?.success(`Saved: ${dest}`);
|
|
3545
4454
|
} catch (e) {
|
|
@@ -3550,8 +4459,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
3550
4459
|
}
|
|
3551
4460
|
}
|
|
3552
4461
|
if (output && savedFiles[0]) {
|
|
3553
|
-
|
|
3554
|
-
|
|
4462
|
+
fs29.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
|
|
4463
|
+
fs29.writeFileSync(output, fs29.readFileSync(savedFiles[0]));
|
|
3555
4464
|
}
|
|
3556
4465
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
3557
4466
|
}
|
|
@@ -3586,8 +4495,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
3586
4495
|
}
|
|
3587
4496
|
|
|
3588
4497
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
3589
|
-
import
|
|
3590
|
-
import
|
|
4498
|
+
import fs30 from "fs";
|
|
4499
|
+
import path35 from "path";
|
|
3591
4500
|
init_errors();
|
|
3592
4501
|
init_http_client();
|
|
3593
4502
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -3674,14 +4583,14 @@ async function runImageToImage(opts) {
|
|
|
3674
4583
|
const url = messageImages[i]?.image_url?.url;
|
|
3675
4584
|
if (!url) continue;
|
|
3676
4585
|
const bytes = await fetchBytes(url);
|
|
3677
|
-
const dest =
|
|
3678
|
-
|
|
4586
|
+
const dest = path35.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
4587
|
+
fs30.writeFileSync(dest, bytes);
|
|
3679
4588
|
saved.push(dest);
|
|
3680
4589
|
opts.logger.success(`Saved: ${dest}`);
|
|
3681
4590
|
}
|
|
3682
4591
|
if (opts.output && saved[0]) {
|
|
3683
|
-
|
|
3684
|
-
|
|
4592
|
+
fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
|
|
4593
|
+
fs30.copyFileSync(saved[0], opts.output);
|
|
3685
4594
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
3686
4595
|
}
|
|
3687
4596
|
return saved;
|
|
@@ -3690,7 +4599,7 @@ async function runImageToImage(opts) {
|
|
|
3690
4599
|
// src/providers/openrouter/video-client.ts
|
|
3691
4600
|
init_http_client();
|
|
3692
4601
|
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
3693
|
-
function
|
|
4602
|
+
function authHeaders4(apiKey) {
|
|
3694
4603
|
const headers = {
|
|
3695
4604
|
Authorization: `Bearer ${apiKey}`,
|
|
3696
4605
|
"Content-Type": "application/json"
|
|
@@ -3705,7 +4614,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
3705
4614
|
return httpJson({
|
|
3706
4615
|
url: OPENROUTER_VIDEOS_URL,
|
|
3707
4616
|
method: "POST",
|
|
3708
|
-
headers:
|
|
4617
|
+
headers: authHeaders4(apiKey),
|
|
3709
4618
|
body,
|
|
3710
4619
|
timeoutMs: 12e4
|
|
3711
4620
|
});
|
|
@@ -3713,7 +4622,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
3713
4622
|
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
3714
4623
|
return httpJson({
|
|
3715
4624
|
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
3716
|
-
headers:
|
|
4625
|
+
headers: authHeaders4(apiKey)
|
|
3717
4626
|
});
|
|
3718
4627
|
}
|
|
3719
4628
|
function videoContentUrl(jobId) {
|
|
@@ -3722,18 +4631,18 @@ function videoContentUrl(jobId) {
|
|
|
3722
4631
|
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
3723
4632
|
return httpJson({
|
|
3724
4633
|
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
3725
|
-
headers:
|
|
4634
|
+
headers: authHeaders4(apiKey)
|
|
3726
4635
|
});
|
|
3727
4636
|
}
|
|
3728
4637
|
|
|
3729
4638
|
// src/providers/openrouter/commands/video-status.ts
|
|
3730
|
-
import
|
|
3731
|
-
import
|
|
4639
|
+
import fs31 from "fs";
|
|
4640
|
+
import path36 from "path";
|
|
3732
4641
|
async function downloadOpenRouterVideo(opts) {
|
|
3733
4642
|
const { jobId, logger, source } = opts;
|
|
3734
4643
|
const apiKey = requireOpenRouterKey();
|
|
3735
4644
|
const url = videoContentUrl(jobId);
|
|
3736
|
-
const dest =
|
|
4645
|
+
const dest = path36.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
3737
4646
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
3738
4647
|
if (!r.ok) {
|
|
3739
4648
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -3743,14 +4652,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
3743
4652
|
logger.error("Empty response body");
|
|
3744
4653
|
process.exit(1);
|
|
3745
4654
|
}
|
|
3746
|
-
|
|
3747
|
-
const ws =
|
|
4655
|
+
fs31.mkdirSync(path36.dirname(dest), { recursive: true });
|
|
4656
|
+
const ws = fs31.createWriteStream(dest);
|
|
3748
4657
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
3749
4658
|
await pipeline2(r.body, ws);
|
|
3750
4659
|
logger.success(`Saved ${dest}`);
|
|
3751
4660
|
if (opts.output) {
|
|
3752
|
-
|
|
3753
|
-
|
|
4661
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
|
|
4662
|
+
fs31.copyFileSync(dest, opts.output);
|
|
3754
4663
|
logger.success(`Copied to ${opts.output}`);
|
|
3755
4664
|
}
|
|
3756
4665
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -4012,6 +4921,12 @@ var PROVIDERS = [
|
|
|
4012
4921
|
envFallback: "ARK_API_KEY",
|
|
4013
4922
|
optional: true,
|
|
4014
4923
|
link: "https://console.byteplus.com/auth/api-keys"
|
|
4924
|
+
},
|
|
4925
|
+
{
|
|
4926
|
+
name: "ElevenLabs",
|
|
4927
|
+
envPrimary: "ELEVENLABS_API_KEY",
|
|
4928
|
+
optional: true,
|
|
4929
|
+
link: "https://elevenlabs.io/app/settings/api-keys"
|
|
4015
4930
|
}
|
|
4016
4931
|
];
|
|
4017
4932
|
function resolveProviderKey(p) {
|
|
@@ -4098,8 +5013,8 @@ function registerCheckCommand(program2) {
|
|
|
4098
5013
|
|
|
4099
5014
|
// src/commands/doc/convert.ts
|
|
4100
5015
|
init_errors();
|
|
4101
|
-
import
|
|
4102
|
-
import
|
|
5016
|
+
import fs32 from "fs";
|
|
5017
|
+
import path37 from "path";
|
|
4103
5018
|
|
|
4104
5019
|
// src/commands/doc/default-prompt.ts
|
|
4105
5020
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -4120,18 +5035,18 @@ async function convertDocuments(opts) {
|
|
|
4120
5035
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
4121
5036
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
4122
5037
|
for (const f of inputs) {
|
|
4123
|
-
if (!
|
|
5038
|
+
if (!fs32.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
4124
5039
|
}
|
|
4125
5040
|
let outputPath;
|
|
4126
5041
|
if (output) {
|
|
4127
|
-
outputPath =
|
|
5042
|
+
outputPath = path37.resolve(output);
|
|
4128
5043
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
4129
|
-
const base =
|
|
4130
|
-
outputPath =
|
|
5044
|
+
const base = path37.basename(inputs[0], path37.extname(inputs[0]));
|
|
5045
|
+
outputPath = path37.join(getOutputDir(), `${base}-extraction.md`);
|
|
4131
5046
|
} else {
|
|
4132
|
-
outputPath =
|
|
5047
|
+
outputPath = path37.join(getOutputDir(), "document-extraction.md");
|
|
4133
5048
|
}
|
|
4134
|
-
|
|
5049
|
+
fs32.mkdirSync(path37.dirname(outputPath), { recursive: true });
|
|
4135
5050
|
const results = [];
|
|
4136
5051
|
for (let i = 0; i < inputs.length; i++) {
|
|
4137
5052
|
const filePath = inputs[i];
|
|
@@ -4182,7 +5097,7 @@ async function convertDocuments(opts) {
|
|
|
4182
5097
|
`);
|
|
4183
5098
|
for (const r of results) {
|
|
4184
5099
|
lines.push(`
|
|
4185
|
-
## ${
|
|
5100
|
+
## ${path37.basename(r.file)}
|
|
4186
5101
|
`);
|
|
4187
5102
|
if (r.status === "success" && r.markdown) {
|
|
4188
5103
|
lines.push(`${r.markdown}
|
|
@@ -4195,7 +5110,7 @@ async function convertDocuments(opts) {
|
|
|
4195
5110
|
}
|
|
4196
5111
|
lines.push("\n---\n");
|
|
4197
5112
|
}
|
|
4198
|
-
|
|
5113
|
+
fs32.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
4199
5114
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
4200
5115
|
console.log(`
|
|
4201
5116
|
${"=".repeat(50)}`);
|
|
@@ -4241,12 +5156,12 @@ function registerDocCommands(program2) {
|
|
|
4241
5156
|
|
|
4242
5157
|
// src/commands/media/index.ts
|
|
4243
5158
|
init_errors();
|
|
4244
|
-
import
|
|
4245
|
-
import
|
|
5159
|
+
import fs35 from "fs";
|
|
5160
|
+
import path40 from "path";
|
|
4246
5161
|
|
|
4247
5162
|
// src/commands/media/batch.ts
|
|
4248
|
-
import
|
|
4249
|
-
import
|
|
5163
|
+
import fs33 from "fs";
|
|
5164
|
+
import path38 from "path";
|
|
4250
5165
|
|
|
4251
5166
|
// src/commands/media/detect-type.ts
|
|
4252
5167
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -4442,10 +5357,10 @@ async function batchOptimize(opts) {
|
|
|
4442
5357
|
verbose,
|
|
4443
5358
|
logger
|
|
4444
5359
|
} = opts;
|
|
4445
|
-
|
|
4446
|
-
const entries =
|
|
5360
|
+
fs33.mkdirSync(outputDir, { recursive: true });
|
|
5361
|
+
const entries = fs33.readdirSync(inputDir);
|
|
4447
5362
|
const mediaFiles = entries.filter((f) => {
|
|
4448
|
-
const ext =
|
|
5363
|
+
const ext = path38.extname(f).toLowerCase();
|
|
4449
5364
|
return ALL_EXTS.has(ext);
|
|
4450
5365
|
});
|
|
4451
5366
|
if (mediaFiles.length === 0) {
|
|
@@ -4456,9 +5371,9 @@ async function batchOptimize(opts) {
|
|
|
4456
5371
|
let succeeded = 0;
|
|
4457
5372
|
const errors = [];
|
|
4458
5373
|
for (const file of mediaFiles) {
|
|
4459
|
-
const inputPath =
|
|
4460
|
-
const outputPath =
|
|
4461
|
-
const ext =
|
|
5374
|
+
const inputPath = path38.join(inputDir, file);
|
|
5375
|
+
const outputPath = path38.join(outputDir, file);
|
|
5376
|
+
const ext = path38.extname(file).toLowerCase();
|
|
4462
5377
|
const kind = extToKind(ext);
|
|
4463
5378
|
logger?.debug(`Processing: ${file}`);
|
|
4464
5379
|
try {
|
|
@@ -4495,8 +5410,8 @@ async function batchOptimize(opts) {
|
|
|
4495
5410
|
|
|
4496
5411
|
// src/commands/media/split-video.ts
|
|
4497
5412
|
init_errors();
|
|
4498
|
-
import
|
|
4499
|
-
import
|
|
5413
|
+
import fs34 from "fs";
|
|
5414
|
+
import path39 from "path";
|
|
4500
5415
|
async function splitVideo(opts) {
|
|
4501
5416
|
await assertFfmpeg();
|
|
4502
5417
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -4507,10 +5422,10 @@ async function splitVideo(opts) {
|
|
|
4507
5422
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
4508
5423
|
return [input];
|
|
4509
5424
|
}
|
|
4510
|
-
|
|
4511
|
-
const ext =
|
|
4512
|
-
const baseName =
|
|
4513
|
-
const outputPattern =
|
|
5425
|
+
fs34.mkdirSync(outputDir, { recursive: true });
|
|
5426
|
+
const ext = path39.extname(input) || ".mp4";
|
|
5427
|
+
const baseName = path39.basename(input, ext);
|
|
5428
|
+
const outputPattern = path39.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
4514
5429
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
4515
5430
|
const args = [
|
|
4516
5431
|
"-i",
|
|
@@ -4529,8 +5444,8 @@ async function splitVideo(opts) {
|
|
|
4529
5444
|
await runFfmpeg(args, { verbose, logger });
|
|
4530
5445
|
const files = [];
|
|
4531
5446
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
4532
|
-
const fname =
|
|
4533
|
-
if (
|
|
5447
|
+
const fname = path39.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
5448
|
+
if (fs34.existsSync(fname)) files.push(fname);
|
|
4534
5449
|
}
|
|
4535
5450
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
4536
5451
|
return files;
|
|
@@ -4544,9 +5459,9 @@ function registerMediaCommands(program2) {
|
|
|
4544
5459
|
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(
|
|
4545
5460
|
async (opts) => {
|
|
4546
5461
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4547
|
-
if (!
|
|
4548
|
-
|
|
4549
|
-
const ext =
|
|
5462
|
+
if (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
5463
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5464
|
+
const ext = path40.extname(opts.input).toLowerCase();
|
|
4550
5465
|
const kind = extToKind(ext);
|
|
4551
5466
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
4552
5467
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -4586,7 +5501,7 @@ function registerMediaCommands(program2) {
|
|
|
4586
5501
|
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(
|
|
4587
5502
|
async (opts) => {
|
|
4588
5503
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4589
|
-
if (!
|
|
5504
|
+
if (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
4590
5505
|
const chunks = await splitVideo({
|
|
4591
5506
|
input: opts.input,
|
|
4592
5507
|
outputDir: opts.outputDir,
|
|
@@ -4602,7 +5517,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
4602
5517
|
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(
|
|
4603
5518
|
async (opts) => {
|
|
4604
5519
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4605
|
-
if (!
|
|
5520
|
+
if (!fs35.existsSync(opts.inputDir))
|
|
4606
5521
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
4607
5522
|
const result = await batchOptimize({
|
|
4608
5523
|
inputDir: opts.inputDir,
|
|
@@ -4631,6 +5546,7 @@ function registerCommands(program2) {
|
|
|
4631
5546
|
registerOpenRouterCommands(program2);
|
|
4632
5547
|
registerLeonardoCommands(program2);
|
|
4633
5548
|
registerBytePlusCommands(program2);
|
|
5549
|
+
registerElevenLabsCommands(program2);
|
|
4634
5550
|
registerMediaCommands(program2);
|
|
4635
5551
|
registerDocCommands(program2);
|
|
4636
5552
|
}
|