@mrgoonie/multix 0.0.6 → 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 +19 -0
- package/README.md +73 -1
- package/dist/cli.js +974 -197
- package/package.json +2 -2
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"() {
|
|
@@ -406,9 +414,9 @@ var BytePlusClient = class {
|
|
|
406
414
|
}
|
|
407
415
|
apiKey;
|
|
408
416
|
baseUrl;
|
|
409
|
-
async request(
|
|
417
|
+
async request(path41, opts = {}) {
|
|
410
418
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
411
|
-
const url = new URL(base +
|
|
419
|
+
const url = new URL(base + path41);
|
|
412
420
|
if (opts.query) {
|
|
413
421
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
414
422
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -426,14 +434,14 @@ var BytePlusClient = class {
|
|
|
426
434
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
427
435
|
});
|
|
428
436
|
}
|
|
429
|
-
get(
|
|
430
|
-
return this.request(
|
|
437
|
+
get(path41, query, logger) {
|
|
438
|
+
return this.request(path41, { method: "GET", query, logger });
|
|
431
439
|
}
|
|
432
|
-
post(
|
|
433
|
-
return this.request(
|
|
440
|
+
post(path41, body, logger) {
|
|
441
|
+
return this.request(path41, { method: "POST", body, logger });
|
|
434
442
|
}
|
|
435
|
-
delete(
|
|
436
|
-
return this.request(
|
|
443
|
+
delete(path41, logger) {
|
|
444
|
+
return this.request(path41, { method: "DELETE", logger });
|
|
437
445
|
}
|
|
438
446
|
};
|
|
439
447
|
function createBytePlusClient() {
|
|
@@ -982,10 +990,10 @@ function toRef(kind, resolved, role) {
|
|
|
982
990
|
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
983
991
|
}
|
|
984
992
|
async function resolveRefSpec(kind, spec, logger) {
|
|
985
|
-
const { path:
|
|
986
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
987
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
988
|
-
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);
|
|
989
997
|
}
|
|
990
998
|
async function resolveRefSpecs(specs, logger) {
|
|
991
999
|
const out = [];
|
|
@@ -1243,18 +1251,780 @@ function registerBytePlusCommands(program2) {
|
|
|
1243
1251
|
registerBytePlusStatusCommand(byteplus);
|
|
1244
1252
|
}
|
|
1245
1253
|
|
|
1246
|
-
// src/providers/
|
|
1254
|
+
// src/providers/elevenlabs/client.ts
|
|
1255
|
+
import fs7 from "fs";
|
|
1256
|
+
import path12 from "path";
|
|
1247
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();
|
|
1248
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");
|
|
1812
|
+
}
|
|
1813
|
+
|
|
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
|
|
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");
|
|
1890
|
+
}
|
|
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
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
|
|
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
|
|
1967
|
+
});
|
|
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(",");
|
|
1983
|
+
}
|
|
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
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
|
|
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);
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
// src/providers/gemini/commands/analyze.ts
|
|
2017
|
+
init_errors();
|
|
2018
|
+
import fs14 from "fs";
|
|
1249
2019
|
|
|
1250
2020
|
// src/providers/gemini/client.ts
|
|
1251
|
-
import
|
|
1252
|
-
import
|
|
2021
|
+
import fs12 from "fs";
|
|
2022
|
+
import path18 from "path";
|
|
1253
2023
|
init_errors();
|
|
1254
2024
|
init_http_client();
|
|
1255
2025
|
|
|
1256
2026
|
// src/providers/gemini/task-resolver.ts
|
|
1257
|
-
import
|
|
2027
|
+
import path17 from "path";
|
|
1258
2028
|
var MIME_MAP = {
|
|
1259
2029
|
// Audio
|
|
1260
2030
|
".mp3": "audio/mp3",
|
|
@@ -1295,7 +2065,7 @@ var MIME_MAP = {
|
|
|
1295
2065
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
1296
2066
|
};
|
|
1297
2067
|
function getMimeType(filePath) {
|
|
1298
|
-
const ext =
|
|
2068
|
+
const ext = path17.extname(filePath).toLowerCase();
|
|
1299
2069
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
1300
2070
|
}
|
|
1301
2071
|
function requiresProcessingWait(mimeType) {
|
|
@@ -1314,15 +2084,15 @@ function requireApiKey() {
|
|
|
1314
2084
|
);
|
|
1315
2085
|
return key;
|
|
1316
2086
|
}
|
|
1317
|
-
function
|
|
2087
|
+
function authHeaders2(apiKey) {
|
|
1318
2088
|
return { "x-goog-api-key": apiKey };
|
|
1319
2089
|
}
|
|
1320
2090
|
async function uploadFile(filePath, opts = {}) {
|
|
1321
2091
|
const apiKey = requireApiKey();
|
|
1322
2092
|
const { timeoutMs = 3e5, logger } = opts;
|
|
1323
2093
|
const mimeType = getMimeType(filePath);
|
|
1324
|
-
const fileBytes =
|
|
1325
|
-
const displayName =
|
|
2094
|
+
const fileBytes = fs12.readFileSync(filePath);
|
|
2095
|
+
const displayName = path18.basename(filePath);
|
|
1326
2096
|
const boundary = `multix_${Date.now()}`;
|
|
1327
2097
|
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
1328
2098
|
const body = Buffer.concat([
|
|
@@ -1367,7 +2137,7 @@ Content-Type: ${mimeType}\r
|
|
|
1367
2137
|
await sleep(2e3);
|
|
1368
2138
|
fileRef = await httpJson({
|
|
1369
2139
|
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
1370
|
-
headers:
|
|
2140
|
+
headers: authHeaders2(apiKey)
|
|
1371
2141
|
}).then((r) => r.file);
|
|
1372
2142
|
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
1373
2143
|
}
|
|
@@ -1384,7 +2154,7 @@ async function generateContent(req) {
|
|
|
1384
2154
|
return httpJson({
|
|
1385
2155
|
url: `${GENERATE_BASE}/${req.model}:generateContent`,
|
|
1386
2156
|
method: "POST",
|
|
1387
|
-
headers:
|
|
2157
|
+
headers: authHeaders2(apiKey),
|
|
1388
2158
|
body: {
|
|
1389
2159
|
contents: req.contents,
|
|
1390
2160
|
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
@@ -1411,12 +2181,12 @@ function extractImages(resp) {
|
|
|
1411
2181
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
1412
2182
|
}
|
|
1413
2183
|
function inlinePart(filePath) {
|
|
1414
|
-
const data =
|
|
2184
|
+
const data = fs12.readFileSync(filePath).toString("base64");
|
|
1415
2185
|
const mimeType = getMimeType(filePath);
|
|
1416
2186
|
return { inlineData: { mimeType, data } };
|
|
1417
2187
|
}
|
|
1418
2188
|
function shouldUseFileApi(filePath) {
|
|
1419
|
-
const stat =
|
|
2189
|
+
const stat = fs12.statSync(filePath);
|
|
1420
2190
|
return stat.size > 20 * 1024 * 1024;
|
|
1421
2191
|
}
|
|
1422
2192
|
function sleep(ms) {
|
|
@@ -1469,13 +2239,13 @@ function getDefaultModel(task) {
|
|
|
1469
2239
|
}
|
|
1470
2240
|
|
|
1471
2241
|
// src/providers/gemini/result-formatter.ts
|
|
1472
|
-
import
|
|
1473
|
-
import
|
|
2242
|
+
import fs13 from "fs";
|
|
2243
|
+
import path19 from "path";
|
|
1474
2244
|
function formatResults(results, format, outputPath) {
|
|
1475
2245
|
const content = renderFormat(results, format);
|
|
1476
2246
|
if (outputPath) {
|
|
1477
|
-
|
|
1478
|
-
|
|
2247
|
+
fs13.mkdirSync(path19.dirname(path19.resolve(outputPath)), { recursive: true });
|
|
2248
|
+
fs13.writeFileSync(outputPath, content, "utf8");
|
|
1479
2249
|
} else {
|
|
1480
2250
|
process.stdout.write(content);
|
|
1481
2251
|
}
|
|
@@ -1542,7 +2312,7 @@ function registerAnalyzeCommand(parent) {
|
|
|
1542
2312
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1543
2313
|
const model = opts.model ?? getDefaultModel("analyze");
|
|
1544
2314
|
for (const f of opts.files) {
|
|
1545
|
-
if (!
|
|
2315
|
+
if (!fs14.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1546
2316
|
}
|
|
1547
2317
|
const results = [];
|
|
1548
2318
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -1588,14 +2358,14 @@ function registerAnalyzeCommand(parent) {
|
|
|
1588
2358
|
|
|
1589
2359
|
// src/providers/gemini/commands/extract.ts
|
|
1590
2360
|
init_errors();
|
|
1591
|
-
import
|
|
2361
|
+
import fs15 from "fs";
|
|
1592
2362
|
function registerExtractCommand(parent) {
|
|
1593
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(
|
|
1594
2364
|
async (opts) => {
|
|
1595
2365
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1596
2366
|
const model = opts.model ?? getDefaultModel("extract");
|
|
1597
2367
|
for (const f of opts.files) {
|
|
1598
|
-
if (!
|
|
2368
|
+
if (!fs15.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
1599
2369
|
}
|
|
1600
2370
|
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
1601
2371
|
const results = [];
|
|
@@ -1637,8 +2407,8 @@ function registerExtractCommand(parent) {
|
|
|
1637
2407
|
init_errors();
|
|
1638
2408
|
|
|
1639
2409
|
// src/providers/gemini/generators/speech.ts
|
|
1640
|
-
import
|
|
1641
|
-
import
|
|
2410
|
+
import fs16 from "fs";
|
|
2411
|
+
import path20 from "path";
|
|
1642
2412
|
|
|
1643
2413
|
// src/providers/gemini/voices.ts
|
|
1644
2414
|
var GEMINI_TTS_VOICES = [
|
|
@@ -1721,12 +2491,12 @@ async function generateGeminiSpeech(opts) {
|
|
|
1721
2491
|
const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
|
|
1722
2492
|
const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
|
|
1723
2493
|
const outDir = getOutputDir();
|
|
1724
|
-
const dest =
|
|
1725
|
-
|
|
2494
|
+
const dest = path20.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
2495
|
+
fs16.writeFileSync(dest, outBytes);
|
|
1726
2496
|
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
1727
2497
|
if (output) {
|
|
1728
|
-
|
|
1729
|
-
|
|
2498
|
+
fs16.mkdirSync(path20.dirname(path20.resolve(output)), { recursive: true });
|
|
2499
|
+
fs16.copyFileSync(dest, output);
|
|
1730
2500
|
logger?.success(`Copied to: ${output}`);
|
|
1731
2501
|
}
|
|
1732
2502
|
return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
|
|
@@ -1825,8 +2595,8 @@ function collectSpeakers(value, prev) {
|
|
|
1825
2595
|
}
|
|
1826
2596
|
|
|
1827
2597
|
// src/providers/gemini/commands/generate-video.ts
|
|
1828
|
-
import
|
|
1829
|
-
import
|
|
2598
|
+
import fs17 from "fs";
|
|
2599
|
+
import path21 from "path";
|
|
1830
2600
|
init_errors();
|
|
1831
2601
|
init_http_client();
|
|
1832
2602
|
var BASE2 = "https://generativelanguage.googleapis.com";
|
|
@@ -1860,10 +2630,10 @@ function registerGenerateVideoCommand(parent) {
|
|
|
1860
2630
|
const contentParts = [{ text: opts.prompt }];
|
|
1861
2631
|
if (opts.referenceImages?.length) {
|
|
1862
2632
|
for (const imgPath of opts.referenceImages.slice(0, 2)) {
|
|
1863
|
-
if (!
|
|
2633
|
+
if (!fs17.existsSync(imgPath))
|
|
1864
2634
|
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
1865
|
-
const data =
|
|
1866
|
-
const ext =
|
|
2635
|
+
const data = fs17.readFileSync(imgPath).toString("base64");
|
|
2636
|
+
const ext = path21.extname(imgPath).toLowerCase();
|
|
1867
2637
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
1868
2638
|
contentParts.push({ inlineData: { mimeType, data } });
|
|
1869
2639
|
}
|
|
@@ -1912,13 +2682,13 @@ function registerGenerateVideoCommand(parent) {
|
|
|
1912
2682
|
);
|
|
1913
2683
|
}
|
|
1914
2684
|
const outDir = getOutputDir();
|
|
1915
|
-
const dest =
|
|
2685
|
+
const dest = path21.join(outDir, `veo_generated_${Date.now()}.mp4`);
|
|
1916
2686
|
const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
|
|
1917
2687
|
await downloadFile2(videoUri, dest);
|
|
1918
2688
|
logger.success(`Saved video: ${dest}`);
|
|
1919
2689
|
if (opts.output) {
|
|
1920
|
-
|
|
1921
|
-
|
|
2690
|
+
fs17.mkdirSync(path21.dirname(path21.resolve(opts.output)), { recursive: true });
|
|
2691
|
+
fs17.copyFileSync(dest, opts.output);
|
|
1922
2692
|
logger.success(`Copied to: ${opts.output}`);
|
|
1923
2693
|
}
|
|
1924
2694
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -1937,8 +2707,8 @@ function sleep2(ms) {
|
|
|
1937
2707
|
|
|
1938
2708
|
// src/providers/gemini/commands/generate.ts
|
|
1939
2709
|
init_errors();
|
|
1940
|
-
import
|
|
1941
|
-
import
|
|
2710
|
+
import fs18 from "fs";
|
|
2711
|
+
import path22 from "path";
|
|
1942
2712
|
function registerGenerateCommand(parent) {
|
|
1943
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(
|
|
1944
2714
|
async (opts) => {
|
|
@@ -1980,8 +2750,8 @@ function registerGenerateCommand(parent) {
|
|
|
1980
2750
|
const outDir = getOutputDir();
|
|
1981
2751
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
1982
2752
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
1983
|
-
const dest =
|
|
1984
|
-
|
|
2753
|
+
const dest = path22.join(outDir, fname);
|
|
2754
|
+
fs18.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
1985
2755
|
savedFiles.push(dest);
|
|
1986
2756
|
logger.success(`Saved: ${dest}`);
|
|
1987
2757
|
}
|
|
@@ -1996,8 +2766,8 @@ function registerGenerateCommand(parent) {
|
|
|
1996
2766
|
process.exit(1);
|
|
1997
2767
|
}
|
|
1998
2768
|
if (opts.output && savedFiles[0]) {
|
|
1999
|
-
|
|
2000
|
-
|
|
2769
|
+
fs18.mkdirSync(path22.dirname(path22.resolve(opts.output)), { recursive: true });
|
|
2770
|
+
fs18.copyFileSync(savedFiles[0], opts.output);
|
|
2001
2771
|
logger.success(`Copied to: ${opts.output}`);
|
|
2002
2772
|
}
|
|
2003
2773
|
console.log(`
|
|
@@ -2008,8 +2778,8 @@ Generated ${savedFiles.length} image(s):`);
|
|
|
2008
2778
|
}
|
|
2009
2779
|
|
|
2010
2780
|
// src/providers/gemini/commands/image-to-image.ts
|
|
2011
|
-
import
|
|
2012
|
-
import
|
|
2781
|
+
import fs19 from "fs";
|
|
2782
|
+
import path23 from "path";
|
|
2013
2783
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
2014
2784
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
2015
2785
|
function registerGeminiImageToImageCommand(parent) {
|
|
@@ -2070,14 +2840,14 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
2070
2840
|
const img = images[i];
|
|
2071
2841
|
if (!img) continue;
|
|
2072
2842
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
2073
|
-
const dest =
|
|
2074
|
-
|
|
2843
|
+
const dest = path23.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
2844
|
+
fs19.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
2075
2845
|
saved.push(dest);
|
|
2076
2846
|
logger.success(`Saved: ${dest}`);
|
|
2077
2847
|
}
|
|
2078
2848
|
if (opts.output && saved[0]) {
|
|
2079
|
-
|
|
2080
|
-
|
|
2849
|
+
fs19.mkdirSync(path23.dirname(path23.resolve(opts.output)), { recursive: true });
|
|
2850
|
+
fs19.copyFileSync(saved[0], opts.output);
|
|
2081
2851
|
logger.success(`Copied to: ${opts.output}`);
|
|
2082
2852
|
}
|
|
2083
2853
|
console.log(`
|
|
@@ -2104,7 +2874,7 @@ function sniffImageMime(buf, url) {
|
|
|
2104
2874
|
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
2105
2875
|
return "image/webp";
|
|
2106
2876
|
}
|
|
2107
|
-
const ext =
|
|
2877
|
+
const ext = path23.extname(new URL(url).pathname).toLowerCase();
|
|
2108
2878
|
if (ext === ".png") return "image/png";
|
|
2109
2879
|
if (ext === ".webp") return "image/webp";
|
|
2110
2880
|
if (ext === ".gif") return "image/gif";
|
|
@@ -2112,17 +2882,17 @@ function sniffImageMime(buf, url) {
|
|
|
2112
2882
|
}
|
|
2113
2883
|
|
|
2114
2884
|
// src/providers/gemini/commands/image-to-video.ts
|
|
2115
|
-
import
|
|
2116
|
-
import
|
|
2885
|
+
import fs20 from "fs";
|
|
2886
|
+
import path24 from "path";
|
|
2117
2887
|
init_errors();
|
|
2118
2888
|
init_http_client();
|
|
2119
2889
|
var BASE3 = "https://generativelanguage.googleapis.com";
|
|
2120
2890
|
function imagePart(imgPath) {
|
|
2121
|
-
if (!
|
|
2891
|
+
if (!fs20.existsSync(imgPath)) {
|
|
2122
2892
|
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
2123
2893
|
}
|
|
2124
|
-
const data =
|
|
2125
|
-
const ext =
|
|
2894
|
+
const data = fs20.readFileSync(imgPath).toString("base64");
|
|
2895
|
+
const ext = path24.extname(imgPath).toLowerCase();
|
|
2126
2896
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
2127
2897
|
return { inlineData: { mimeType, data } };
|
|
2128
2898
|
}
|
|
@@ -2195,12 +2965,12 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
2195
2965
|
"gemini"
|
|
2196
2966
|
);
|
|
2197
2967
|
}
|
|
2198
|
-
const dest =
|
|
2968
|
+
const dest = path24.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
2199
2969
|
await downloadFile(videoUri, dest);
|
|
2200
2970
|
logger.success(`Saved video: ${dest}`);
|
|
2201
2971
|
if (opts.output) {
|
|
2202
|
-
|
|
2203
|
-
|
|
2972
|
+
fs20.mkdirSync(path24.dirname(path24.resolve(opts.output)), { recursive: true });
|
|
2973
|
+
fs20.copyFileSync(dest, opts.output);
|
|
2204
2974
|
logger.success(`Copied to: ${opts.output}`);
|
|
2205
2975
|
}
|
|
2206
2976
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -2216,14 +2986,14 @@ Generated video: ${dest}`);
|
|
|
2216
2986
|
|
|
2217
2987
|
// src/providers/gemini/commands/transcribe.ts
|
|
2218
2988
|
init_errors();
|
|
2219
|
-
import
|
|
2220
|
-
function
|
|
2989
|
+
import fs21 from "fs";
|
|
2990
|
+
function registerTranscribeCommand2(parent) {
|
|
2221
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(
|
|
2222
2992
|
async (opts) => {
|
|
2223
2993
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2224
2994
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
2225
2995
|
for (const f of opts.files) {
|
|
2226
|
-
if (!
|
|
2996
|
+
if (!fs21.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2227
2997
|
}
|
|
2228
2998
|
const results = [];
|
|
2229
2999
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -2270,7 +3040,7 @@ function registerGeminiCommands(program2) {
|
|
|
2270
3040
|
"Gemini AI operations: analyze, transcribe, extract, generate images/video/speech"
|
|
2271
3041
|
);
|
|
2272
3042
|
registerAnalyzeCommand(gemini);
|
|
2273
|
-
|
|
3043
|
+
registerTranscribeCommand2(gemini);
|
|
2274
3044
|
registerExtractCommand(gemini);
|
|
2275
3045
|
registerGenerateCommand(gemini);
|
|
2276
3046
|
registerGeminiImageToImageCommand(gemini);
|
|
@@ -2281,8 +3051,8 @@ function registerGeminiCommands(program2) {
|
|
|
2281
3051
|
|
|
2282
3052
|
// src/providers/leonardo/commands/generate.ts
|
|
2283
3053
|
init_http_client();
|
|
2284
|
-
import
|
|
2285
|
-
import
|
|
3054
|
+
import fs22 from "fs";
|
|
3055
|
+
import path25 from "path";
|
|
2286
3056
|
|
|
2287
3057
|
// src/providers/leonardo/client.ts
|
|
2288
3058
|
init_errors();
|
|
@@ -2356,11 +3126,11 @@ var LeonardoClient = class {
|
|
|
2356
3126
|
}
|
|
2357
3127
|
apiKey;
|
|
2358
3128
|
baseUrl;
|
|
2359
|
-
async request(
|
|
3129
|
+
async request(path41, opts = {}) {
|
|
2360
3130
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
2361
|
-
const v2Override =
|
|
3131
|
+
const v2Override = path41.startsWith("v2:");
|
|
2362
3132
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
2363
|
-
const finalPath = v2Override ?
|
|
3133
|
+
const finalPath = v2Override ? path41.slice(3) : path41;
|
|
2364
3134
|
const url = new URL(finalBase + finalPath);
|
|
2365
3135
|
if (opts.query) {
|
|
2366
3136
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -2379,14 +3149,14 @@ var LeonardoClient = class {
|
|
|
2379
3149
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2380
3150
|
});
|
|
2381
3151
|
}
|
|
2382
|
-
get(
|
|
2383
|
-
return this.request(
|
|
3152
|
+
get(path41, query, logger) {
|
|
3153
|
+
return this.request(path41, { method: "GET", query, logger });
|
|
2384
3154
|
}
|
|
2385
|
-
post(
|
|
2386
|
-
return this.request(
|
|
3155
|
+
post(path41, body, logger) {
|
|
3156
|
+
return this.request(path41, { method: "POST", body, logger });
|
|
2387
3157
|
}
|
|
2388
|
-
delete(
|
|
2389
|
-
return this.request(
|
|
3158
|
+
delete(path41, logger) {
|
|
3159
|
+
return this.request(path41, { method: "DELETE", logger });
|
|
2390
3160
|
}
|
|
2391
3161
|
};
|
|
2392
3162
|
function createLeonardoClient() {
|
|
@@ -2397,7 +3167,7 @@ function createLeonardoClient() {
|
|
|
2397
3167
|
function extFromUrl(url, fallback = ".png") {
|
|
2398
3168
|
try {
|
|
2399
3169
|
const u = new URL(url);
|
|
2400
|
-
const e =
|
|
3170
|
+
const e = path25.extname(u.pathname);
|
|
2401
3171
|
return e.length > 0 ? e : fallback;
|
|
2402
3172
|
} catch {
|
|
2403
3173
|
return fallback;
|
|
@@ -2482,7 +3252,7 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
2482
3252
|
if (!img) continue;
|
|
2483
3253
|
const ext = extFromUrl(img.url);
|
|
2484
3254
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
2485
|
-
const filepath =
|
|
3255
|
+
const filepath = path25.join(outDir, filename);
|
|
2486
3256
|
try {
|
|
2487
3257
|
await downloadFile(img.url, filepath);
|
|
2488
3258
|
saved.push(filepath);
|
|
@@ -2492,9 +3262,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
2492
3262
|
}
|
|
2493
3263
|
}
|
|
2494
3264
|
if (opts.output && saved[0]) {
|
|
2495
|
-
const dest =
|
|
2496
|
-
|
|
2497
|
-
|
|
3265
|
+
const dest = path25.resolve(opts.output);
|
|
3266
|
+
fs22.mkdirSync(path25.dirname(dest), { recursive: true });
|
|
3267
|
+
fs22.copyFileSync(saved[0], dest);
|
|
2498
3268
|
logger.success(`Copied to ${dest}`);
|
|
2499
3269
|
}
|
|
2500
3270
|
console.log(`
|
|
@@ -2506,11 +3276,11 @@ Generated ${saved.length} image(s):`);
|
|
|
2506
3276
|
|
|
2507
3277
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
2508
3278
|
init_http_client();
|
|
2509
|
-
import
|
|
2510
|
-
import
|
|
3279
|
+
import fs23 from "fs";
|
|
3280
|
+
import path26 from "path";
|
|
2511
3281
|
function extFromUrl2(url, fallback = ".png") {
|
|
2512
3282
|
try {
|
|
2513
|
-
return
|
|
3283
|
+
return path26.extname(new URL(url).pathname) || fallback;
|
|
2514
3284
|
} catch {
|
|
2515
3285
|
return fallback;
|
|
2516
3286
|
}
|
|
@@ -2572,7 +3342,7 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
2572
3342
|
if (!img) continue;
|
|
2573
3343
|
const ext = extFromUrl2(img.url);
|
|
2574
3344
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
2575
|
-
const filepath =
|
|
3345
|
+
const filepath = path26.join(outDir, filename);
|
|
2576
3346
|
try {
|
|
2577
3347
|
await downloadFile(img.url, filepath);
|
|
2578
3348
|
saved.push(filepath);
|
|
@@ -2582,9 +3352,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
2582
3352
|
}
|
|
2583
3353
|
}
|
|
2584
3354
|
if (opts.output && saved[0]) {
|
|
2585
|
-
const dest =
|
|
2586
|
-
|
|
2587
|
-
|
|
3355
|
+
const dest = path26.resolve(opts.output);
|
|
3356
|
+
fs23.mkdirSync(path26.dirname(dest), { recursive: true });
|
|
3357
|
+
fs23.copyFileSync(saved[0], dest);
|
|
2588
3358
|
logger.success(`Copied to ${dest}`);
|
|
2589
3359
|
}
|
|
2590
3360
|
console.log(`
|
|
@@ -2596,7 +3366,7 @@ Generated ${saved.length} image(s):`);
|
|
|
2596
3366
|
|
|
2597
3367
|
// src/providers/leonardo/commands/video.ts
|
|
2598
3368
|
init_http_client();
|
|
2599
|
-
import
|
|
3369
|
+
import path27 from "path";
|
|
2600
3370
|
function v2DimsFor(resolution) {
|
|
2601
3371
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
2602
3372
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -2707,7 +3477,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
2707
3477
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
2708
3478
|
process.exit(1);
|
|
2709
3479
|
}
|
|
2710
|
-
const outPath = opts.output ??
|
|
3480
|
+
const outPath = opts.output ?? path27.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
2711
3481
|
await downloadFile(videoUrl, outPath);
|
|
2712
3482
|
logger.success(`Saved ${outPath}`);
|
|
2713
3483
|
await maybeDownloadThumb(gen, outPath, {
|
|
@@ -2814,7 +3584,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
2814
3584
|
|
|
2815
3585
|
// src/providers/leonardo/commands/status.ts
|
|
2816
3586
|
init_http_client();
|
|
2817
|
-
import
|
|
3587
|
+
import path28 from "path";
|
|
2818
3588
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
2819
3589
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
2820
3590
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -2879,7 +3649,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
2879
3649
|
const outDir = getOutputDir();
|
|
2880
3650
|
let idx = 0;
|
|
2881
3651
|
for (const url of videos) {
|
|
2882
|
-
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`);
|
|
2883
3653
|
await downloadFile(url, dest);
|
|
2884
3654
|
logger.success(`Saved ${dest}`);
|
|
2885
3655
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -2892,7 +3662,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
2892
3662
|
for (const url of images) {
|
|
2893
3663
|
const m = url.match(IMAGE_EXT_RE2);
|
|
2894
3664
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
2895
|
-
const dest =
|
|
3665
|
+
const dest = path28.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
2896
3666
|
await downloadFile(url, dest);
|
|
2897
3667
|
logger.success(`Saved ${dest}`);
|
|
2898
3668
|
idx++;
|
|
@@ -2980,7 +3750,7 @@ function requireMinimaxKey() {
|
|
|
2980
3750
|
}
|
|
2981
3751
|
return key;
|
|
2982
3752
|
}
|
|
2983
|
-
function
|
|
3753
|
+
function authHeaders3(apiKey) {
|
|
2984
3754
|
return {
|
|
2985
3755
|
Authorization: `Bearer ${apiKey}`,
|
|
2986
3756
|
"Content-Type": "application/json"
|
|
@@ -2992,7 +3762,7 @@ async function apiPost(endpoint, payload, apiKey, opts = {}) {
|
|
|
2992
3762
|
const data = await httpJson({
|
|
2993
3763
|
url,
|
|
2994
3764
|
method: "POST",
|
|
2995
|
-
headers:
|
|
3765
|
+
headers: authHeaders3(apiKey),
|
|
2996
3766
|
body: payload,
|
|
2997
3767
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2998
3768
|
});
|
|
@@ -3005,17 +3775,17 @@ async function apiPost(endpoint, payload, apiKey, opts = {}) {
|
|
|
3005
3775
|
}
|
|
3006
3776
|
return data;
|
|
3007
3777
|
}
|
|
3008
|
-
async function
|
|
3778
|
+
async function apiGet2(endpoint, params, apiKey) {
|
|
3009
3779
|
const url = new URL(`${MINIMAX_BASE_URL}/${endpoint}`);
|
|
3010
3780
|
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
3011
|
-
return httpJson({ url: url.toString(), headers:
|
|
3781
|
+
return httpJson({ url: url.toString(), headers: authHeaders3(apiKey) });
|
|
3012
3782
|
}
|
|
3013
3783
|
async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
3014
3784
|
const { intervalMs = 1e4, timeoutMs = 6e5, logger } = opts;
|
|
3015
3785
|
const deadline = Date.now() + timeoutMs;
|
|
3016
3786
|
let elapsed = 0;
|
|
3017
3787
|
while (Date.now() < deadline) {
|
|
3018
|
-
const result = await
|
|
3788
|
+
const result = await apiGet2(`query/${taskType}`, { task_id: taskId }, apiKey);
|
|
3019
3789
|
const status = result.status ?? "Unknown";
|
|
3020
3790
|
if (elapsed > 0 && elapsed % 3e4 < intervalMs) {
|
|
3021
3791
|
logger?.debug(`Polling... ${Math.round(elapsed / 1e3)}s elapsed, status: ${status}`);
|
|
@@ -3030,7 +3800,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
|
3030
3800
|
throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
|
|
3031
3801
|
}
|
|
3032
3802
|
async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
|
|
3033
|
-
const result = await
|
|
3803
|
+
const result = await apiGet2(
|
|
3034
3804
|
"files/retrieve",
|
|
3035
3805
|
{ file_id: fileId },
|
|
3036
3806
|
apiKey
|
|
@@ -3049,8 +3819,8 @@ function sleep4(ms) {
|
|
|
3049
3819
|
|
|
3050
3820
|
// src/providers/minimax/generators/music.ts
|
|
3051
3821
|
init_http_client();
|
|
3052
|
-
import
|
|
3053
|
-
import
|
|
3822
|
+
import fs24 from "fs";
|
|
3823
|
+
import path29 from "path";
|
|
3054
3824
|
async function generateMinimaxMusic(opts) {
|
|
3055
3825
|
const {
|
|
3056
3826
|
apiKey,
|
|
@@ -3095,14 +3865,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
3095
3865
|
}
|
|
3096
3866
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
3097
3867
|
const outDir = getOutputDir();
|
|
3098
|
-
const dest =
|
|
3868
|
+
const dest = path29.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
3099
3869
|
try {
|
|
3100
3870
|
if (audioData.startsWith("http")) {
|
|
3101
3871
|
const bytes = await fetchBytes(audioData);
|
|
3102
|
-
|
|
3872
|
+
fs24.writeFileSync(dest, bytes);
|
|
3103
3873
|
} else {
|
|
3104
3874
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
3105
|
-
|
|
3875
|
+
fs24.writeFileSync(dest, audioBytes);
|
|
3106
3876
|
}
|
|
3107
3877
|
} catch (e) {
|
|
3108
3878
|
return {
|
|
@@ -3113,8 +3883,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
3113
3883
|
const durSec = durationMs / 1e3;
|
|
3114
3884
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
3115
3885
|
if (output) {
|
|
3116
|
-
|
|
3117
|
-
|
|
3886
|
+
fs24.mkdirSync(path29.dirname(path29.resolve(output)), { recursive: true });
|
|
3887
|
+
fs24.copyFileSync(dest, output);
|
|
3118
3888
|
}
|
|
3119
3889
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
3120
3890
|
}
|
|
@@ -3142,7 +3912,7 @@ var ALL_MINIMAX_MODELS = /* @__PURE__ */ new Set([
|
|
|
3142
3912
|
...MINIMAX_SPEECH_MODELS,
|
|
3143
3913
|
...MINIMAX_MUSIC_MODELS
|
|
3144
3914
|
]);
|
|
3145
|
-
var
|
|
3915
|
+
var TASK_DEFAULTS2 = {
|
|
3146
3916
|
image: process.env.MINIMAX_IMAGE_MODEL ?? "image-01",
|
|
3147
3917
|
video: process.env.MINIMAX_VIDEO_MODEL ?? "MiniMax-Hailuo-2.3",
|
|
3148
3918
|
speech: process.env.MINIMAX_SPEECH_MODEL ?? "speech-2.8-hd",
|
|
@@ -3161,7 +3931,7 @@ function registerMinimaxGenerateMusicCommand(parent) {
|
|
|
3161
3931
|
if (!opts.lyrics && !opts.prompt) {
|
|
3162
3932
|
throw new ValidationError("Either --lyrics or --prompt is required");
|
|
3163
3933
|
}
|
|
3164
|
-
const model = opts.model ??
|
|
3934
|
+
const model = opts.model ?? TASK_DEFAULTS2.music;
|
|
3165
3935
|
const result = await generateMinimaxMusic({
|
|
3166
3936
|
apiKey,
|
|
3167
3937
|
lyrics: opts.lyrics,
|
|
@@ -3188,8 +3958,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
3188
3958
|
init_errors();
|
|
3189
3959
|
|
|
3190
3960
|
// src/providers/minimax/generators/speech.ts
|
|
3191
|
-
import
|
|
3192
|
-
import
|
|
3961
|
+
import fs25 from "fs";
|
|
3962
|
+
import path30 from "path";
|
|
3193
3963
|
async function generateMinimaxSpeech(opts) {
|
|
3194
3964
|
const {
|
|
3195
3965
|
apiKey,
|
|
@@ -3236,12 +4006,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
3236
4006
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
3237
4007
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
3238
4008
|
const outDir = getOutputDir();
|
|
3239
|
-
const dest =
|
|
3240
|
-
|
|
4009
|
+
const dest = path30.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
4010
|
+
fs25.writeFileSync(dest, audioBytes);
|
|
3241
4011
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
3242
4012
|
if (output) {
|
|
3243
|
-
|
|
3244
|
-
|
|
4013
|
+
fs25.mkdirSync(path30.dirname(path30.resolve(output)), { recursive: true });
|
|
4014
|
+
fs25.copyFileSync(dest, output);
|
|
3245
4015
|
}
|
|
3246
4016
|
return { status: "success", generatedAudio: dest, model };
|
|
3247
4017
|
}
|
|
@@ -3261,7 +4031,7 @@ function registerMinimaxGenerateSpeechCommand(parent) {
|
|
|
3261
4031
|
if (!SPEECH_FORMATS.includes(opts.outputFormat)) {
|
|
3262
4032
|
throw new ValidationError(`--output-format must be one of: ${SPEECH_FORMATS.join(", ")}`);
|
|
3263
4033
|
}
|
|
3264
|
-
const model = opts.model ??
|
|
4034
|
+
const model = opts.model ?? TASK_DEFAULTS2.speech;
|
|
3265
4035
|
const rate = Number.parseFloat(opts.rate);
|
|
3266
4036
|
const result = await generateMinimaxSpeech({
|
|
3267
4037
|
apiKey,
|
|
@@ -3288,8 +4058,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
3288
4058
|
init_errors();
|
|
3289
4059
|
|
|
3290
4060
|
// src/providers/minimax/generators/video.ts
|
|
3291
|
-
import
|
|
3292
|
-
import
|
|
4061
|
+
import fs26 from "fs";
|
|
4062
|
+
import path31 from "path";
|
|
3293
4063
|
async function generateMinimaxVideo(opts) {
|
|
3294
4064
|
const {
|
|
3295
4065
|
apiKey,
|
|
@@ -3335,18 +4105,18 @@ async function generateMinimaxVideo(opts) {
|
|
|
3335
4105
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
3336
4106
|
}
|
|
3337
4107
|
const outDir = getOutputDir();
|
|
3338
|
-
const dest =
|
|
4108
|
+
const dest = path31.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
3339
4109
|
try {
|
|
3340
4110
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
3341
4111
|
} catch (e) {
|
|
3342
4112
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
3343
4113
|
}
|
|
3344
4114
|
const elapsed = (Date.now() - start) / 1e3;
|
|
3345
|
-
const stat =
|
|
4115
|
+
const stat = fs26.statSync(dest);
|
|
3346
4116
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
3347
4117
|
if (output) {
|
|
3348
|
-
|
|
3349
|
-
|
|
4118
|
+
fs26.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
|
|
4119
|
+
fs26.copyFileSync(dest, output);
|
|
3350
4120
|
}
|
|
3351
4121
|
await maybeDownloadThumb(pollResult, dest, {
|
|
3352
4122
|
skip: thumb === false,
|
|
@@ -3369,7 +4139,7 @@ function registerMinimaxGenerateVideoCommand(parent) {
|
|
|
3369
4139
|
async (opts) => {
|
|
3370
4140
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3371
4141
|
const apiKey = requireMinimaxKey();
|
|
3372
|
-
const model = opts.model ??
|
|
4142
|
+
const model = opts.model ?? TASK_DEFAULTS2.video;
|
|
3373
4143
|
const duration = Number.parseInt(opts.duration, 10);
|
|
3374
4144
|
if (!VIDEO_DURATIONS.includes(duration)) {
|
|
3375
4145
|
throw new ValidationError(`--duration must be one of: ${VIDEO_DURATIONS.join(", ")}`);
|
|
@@ -3409,8 +4179,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
3409
4179
|
// src/providers/minimax/generators/image.ts
|
|
3410
4180
|
init_errors();
|
|
3411
4181
|
init_http_client();
|
|
3412
|
-
import
|
|
3413
|
-
import
|
|
4182
|
+
import fs27 from "fs";
|
|
4183
|
+
import path32 from "path";
|
|
3414
4184
|
async function generateMinimaxImage(opts) {
|
|
3415
4185
|
const {
|
|
3416
4186
|
apiKey,
|
|
@@ -3448,8 +4218,8 @@ async function generateMinimaxImage(opts) {
|
|
|
3448
4218
|
try {
|
|
3449
4219
|
const bytes = await fetchBytes(url);
|
|
3450
4220
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
3451
|
-
const dest =
|
|
3452
|
-
|
|
4221
|
+
const dest = path32.join(outDir, fname);
|
|
4222
|
+
fs27.writeFileSync(dest, bytes);
|
|
3453
4223
|
savedFiles.push(dest);
|
|
3454
4224
|
logger?.success(`Saved: ${dest}`);
|
|
3455
4225
|
} catch (e) {
|
|
@@ -3460,8 +4230,8 @@ async function generateMinimaxImage(opts) {
|
|
|
3460
4230
|
}
|
|
3461
4231
|
}
|
|
3462
4232
|
if (output && savedFiles[0]) {
|
|
3463
|
-
|
|
3464
|
-
|
|
4233
|
+
fs27.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
|
|
4234
|
+
fs27.copyFileSync(savedFiles[0], output);
|
|
3465
4235
|
}
|
|
3466
4236
|
return { status: "success", generatedImages: savedFiles, model };
|
|
3467
4237
|
}
|
|
@@ -3472,7 +4242,7 @@ function registerMinimaxGenerateCommand(parent) {
|
|
|
3472
4242
|
async (opts) => {
|
|
3473
4243
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3474
4244
|
const apiKey = requireMinimaxKey();
|
|
3475
|
-
const model = opts.model ??
|
|
4245
|
+
const model = opts.model ?? TASK_DEFAULTS2.image;
|
|
3476
4246
|
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
|
|
3477
4247
|
const result = await generateMinimaxImage({
|
|
3478
4248
|
apiKey,
|
|
@@ -3497,8 +4267,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
3497
4267
|
// src/providers/minimax/commands/image-to-image.ts
|
|
3498
4268
|
init_errors();
|
|
3499
4269
|
init_http_client();
|
|
3500
|
-
import
|
|
3501
|
-
import
|
|
4270
|
+
import fs28 from "fs";
|
|
4271
|
+
import path33 from "path";
|
|
3502
4272
|
function registerMinimaxImageToImageCommand(parent) {
|
|
3503
4273
|
parent.command("image-to-image").alias("i2i").description(
|
|
3504
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."
|
|
@@ -3509,7 +4279,7 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
3509
4279
|
async (opts) => {
|
|
3510
4280
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3511
4281
|
const apiKey = requireMinimaxKey();
|
|
3512
|
-
const model = opts.model ??
|
|
4282
|
+
const model = opts.model ?? TASK_DEFAULTS2.image;
|
|
3513
4283
|
const numImages = Math.min(Math.max(Number.parseInt(opts.numImages, 10) || 1, 1), 9);
|
|
3514
4284
|
logger.warn(
|
|
3515
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."
|
|
@@ -3553,8 +4323,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
3553
4323
|
if (!url) continue;
|
|
3554
4324
|
try {
|
|
3555
4325
|
const bytes = await fetchBytes(url);
|
|
3556
|
-
const dest =
|
|
3557
|
-
|
|
4326
|
+
const dest = path33.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
4327
|
+
fs28.writeFileSync(dest, bytes);
|
|
3558
4328
|
saved.push(dest);
|
|
3559
4329
|
logger.success(`Saved: ${dest}`);
|
|
3560
4330
|
} catch (e) {
|
|
@@ -3565,8 +4335,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
3565
4335
|
}
|
|
3566
4336
|
}
|
|
3567
4337
|
if (opts.output && saved[0]) {
|
|
3568
|
-
|
|
3569
|
-
|
|
4338
|
+
fs28.mkdirSync(path33.dirname(path33.resolve(opts.output)), { recursive: true });
|
|
4339
|
+
fs28.copyFileSync(saved[0], opts.output);
|
|
3570
4340
|
logger.success(`Copied to: ${opts.output}`);
|
|
3571
4341
|
}
|
|
3572
4342
|
console.log(`
|
|
@@ -3587,8 +4357,8 @@ function registerMinimaxCommands(program2) {
|
|
|
3587
4357
|
}
|
|
3588
4358
|
|
|
3589
4359
|
// src/providers/openrouter/client.ts
|
|
3590
|
-
import
|
|
3591
|
-
import
|
|
4360
|
+
import fs29 from "fs";
|
|
4361
|
+
import path34 from "path";
|
|
3592
4362
|
init_errors();
|
|
3593
4363
|
init_http_client();
|
|
3594
4364
|
var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
@@ -3677,8 +4447,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
3677
4447
|
if (!url) continue;
|
|
3678
4448
|
try {
|
|
3679
4449
|
const bytes = await fetchBytes(url);
|
|
3680
|
-
const dest =
|
|
3681
|
-
|
|
4450
|
+
const dest = path34.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
4451
|
+
fs29.writeFileSync(dest, bytes);
|
|
3682
4452
|
savedFiles.push(dest);
|
|
3683
4453
|
logger?.success(`Saved: ${dest}`);
|
|
3684
4454
|
} catch (e) {
|
|
@@ -3689,8 +4459,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
3689
4459
|
}
|
|
3690
4460
|
}
|
|
3691
4461
|
if (output && savedFiles[0]) {
|
|
3692
|
-
|
|
3693
|
-
|
|
4462
|
+
fs29.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
|
|
4463
|
+
fs29.writeFileSync(output, fs29.readFileSync(savedFiles[0]));
|
|
3694
4464
|
}
|
|
3695
4465
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
3696
4466
|
}
|
|
@@ -3725,8 +4495,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
3725
4495
|
}
|
|
3726
4496
|
|
|
3727
4497
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
3728
|
-
import
|
|
3729
|
-
import
|
|
4498
|
+
import fs30 from "fs";
|
|
4499
|
+
import path35 from "path";
|
|
3730
4500
|
init_errors();
|
|
3731
4501
|
init_http_client();
|
|
3732
4502
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -3813,14 +4583,14 @@ async function runImageToImage(opts) {
|
|
|
3813
4583
|
const url = messageImages[i]?.image_url?.url;
|
|
3814
4584
|
if (!url) continue;
|
|
3815
4585
|
const bytes = await fetchBytes(url);
|
|
3816
|
-
const dest =
|
|
3817
|
-
|
|
4586
|
+
const dest = path35.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
4587
|
+
fs30.writeFileSync(dest, bytes);
|
|
3818
4588
|
saved.push(dest);
|
|
3819
4589
|
opts.logger.success(`Saved: ${dest}`);
|
|
3820
4590
|
}
|
|
3821
4591
|
if (opts.output && saved[0]) {
|
|
3822
|
-
|
|
3823
|
-
|
|
4592
|
+
fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
|
|
4593
|
+
fs30.copyFileSync(saved[0], opts.output);
|
|
3824
4594
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
3825
4595
|
}
|
|
3826
4596
|
return saved;
|
|
@@ -3829,7 +4599,7 @@ async function runImageToImage(opts) {
|
|
|
3829
4599
|
// src/providers/openrouter/video-client.ts
|
|
3830
4600
|
init_http_client();
|
|
3831
4601
|
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
3832
|
-
function
|
|
4602
|
+
function authHeaders4(apiKey) {
|
|
3833
4603
|
const headers = {
|
|
3834
4604
|
Authorization: `Bearer ${apiKey}`,
|
|
3835
4605
|
"Content-Type": "application/json"
|
|
@@ -3844,7 +4614,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
3844
4614
|
return httpJson({
|
|
3845
4615
|
url: OPENROUTER_VIDEOS_URL,
|
|
3846
4616
|
method: "POST",
|
|
3847
|
-
headers:
|
|
4617
|
+
headers: authHeaders4(apiKey),
|
|
3848
4618
|
body,
|
|
3849
4619
|
timeoutMs: 12e4
|
|
3850
4620
|
});
|
|
@@ -3852,7 +4622,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
3852
4622
|
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
3853
4623
|
return httpJson({
|
|
3854
4624
|
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
3855
|
-
headers:
|
|
4625
|
+
headers: authHeaders4(apiKey)
|
|
3856
4626
|
});
|
|
3857
4627
|
}
|
|
3858
4628
|
function videoContentUrl(jobId) {
|
|
@@ -3861,18 +4631,18 @@ function videoContentUrl(jobId) {
|
|
|
3861
4631
|
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
3862
4632
|
return httpJson({
|
|
3863
4633
|
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
3864
|
-
headers:
|
|
4634
|
+
headers: authHeaders4(apiKey)
|
|
3865
4635
|
});
|
|
3866
4636
|
}
|
|
3867
4637
|
|
|
3868
4638
|
// src/providers/openrouter/commands/video-status.ts
|
|
3869
|
-
import
|
|
3870
|
-
import
|
|
4639
|
+
import fs31 from "fs";
|
|
4640
|
+
import path36 from "path";
|
|
3871
4641
|
async function downloadOpenRouterVideo(opts) {
|
|
3872
4642
|
const { jobId, logger, source } = opts;
|
|
3873
4643
|
const apiKey = requireOpenRouterKey();
|
|
3874
4644
|
const url = videoContentUrl(jobId);
|
|
3875
|
-
const dest =
|
|
4645
|
+
const dest = path36.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
3876
4646
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
3877
4647
|
if (!r.ok) {
|
|
3878
4648
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -3882,14 +4652,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
3882
4652
|
logger.error("Empty response body");
|
|
3883
4653
|
process.exit(1);
|
|
3884
4654
|
}
|
|
3885
|
-
|
|
3886
|
-
const ws =
|
|
4655
|
+
fs31.mkdirSync(path36.dirname(dest), { recursive: true });
|
|
4656
|
+
const ws = fs31.createWriteStream(dest);
|
|
3887
4657
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
3888
4658
|
await pipeline2(r.body, ws);
|
|
3889
4659
|
logger.success(`Saved ${dest}`);
|
|
3890
4660
|
if (opts.output) {
|
|
3891
|
-
|
|
3892
|
-
|
|
4661
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
|
|
4662
|
+
fs31.copyFileSync(dest, opts.output);
|
|
3893
4663
|
logger.success(`Copied to ${opts.output}`);
|
|
3894
4664
|
}
|
|
3895
4665
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -4151,6 +4921,12 @@ var PROVIDERS = [
|
|
|
4151
4921
|
envFallback: "ARK_API_KEY",
|
|
4152
4922
|
optional: true,
|
|
4153
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"
|
|
4154
4930
|
}
|
|
4155
4931
|
];
|
|
4156
4932
|
function resolveProviderKey(p) {
|
|
@@ -4237,8 +5013,8 @@ function registerCheckCommand(program2) {
|
|
|
4237
5013
|
|
|
4238
5014
|
// src/commands/doc/convert.ts
|
|
4239
5015
|
init_errors();
|
|
4240
|
-
import
|
|
4241
|
-
import
|
|
5016
|
+
import fs32 from "fs";
|
|
5017
|
+
import path37 from "path";
|
|
4242
5018
|
|
|
4243
5019
|
// src/commands/doc/default-prompt.ts
|
|
4244
5020
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -4259,18 +5035,18 @@ async function convertDocuments(opts) {
|
|
|
4259
5035
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
4260
5036
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
4261
5037
|
for (const f of inputs) {
|
|
4262
|
-
if (!
|
|
5038
|
+
if (!fs32.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
4263
5039
|
}
|
|
4264
5040
|
let outputPath;
|
|
4265
5041
|
if (output) {
|
|
4266
|
-
outputPath =
|
|
5042
|
+
outputPath = path37.resolve(output);
|
|
4267
5043
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
4268
|
-
const base =
|
|
4269
|
-
outputPath =
|
|
5044
|
+
const base = path37.basename(inputs[0], path37.extname(inputs[0]));
|
|
5045
|
+
outputPath = path37.join(getOutputDir(), `${base}-extraction.md`);
|
|
4270
5046
|
} else {
|
|
4271
|
-
outputPath =
|
|
5047
|
+
outputPath = path37.join(getOutputDir(), "document-extraction.md");
|
|
4272
5048
|
}
|
|
4273
|
-
|
|
5049
|
+
fs32.mkdirSync(path37.dirname(outputPath), { recursive: true });
|
|
4274
5050
|
const results = [];
|
|
4275
5051
|
for (let i = 0; i < inputs.length; i++) {
|
|
4276
5052
|
const filePath = inputs[i];
|
|
@@ -4321,7 +5097,7 @@ async function convertDocuments(opts) {
|
|
|
4321
5097
|
`);
|
|
4322
5098
|
for (const r of results) {
|
|
4323
5099
|
lines.push(`
|
|
4324
|
-
## ${
|
|
5100
|
+
## ${path37.basename(r.file)}
|
|
4325
5101
|
`);
|
|
4326
5102
|
if (r.status === "success" && r.markdown) {
|
|
4327
5103
|
lines.push(`${r.markdown}
|
|
@@ -4334,7 +5110,7 @@ async function convertDocuments(opts) {
|
|
|
4334
5110
|
}
|
|
4335
5111
|
lines.push("\n---\n");
|
|
4336
5112
|
}
|
|
4337
|
-
|
|
5113
|
+
fs32.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
4338
5114
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
4339
5115
|
console.log(`
|
|
4340
5116
|
${"=".repeat(50)}`);
|
|
@@ -4380,12 +5156,12 @@ function registerDocCommands(program2) {
|
|
|
4380
5156
|
|
|
4381
5157
|
// src/commands/media/index.ts
|
|
4382
5158
|
init_errors();
|
|
4383
|
-
import
|
|
4384
|
-
import
|
|
5159
|
+
import fs35 from "fs";
|
|
5160
|
+
import path40 from "path";
|
|
4385
5161
|
|
|
4386
5162
|
// src/commands/media/batch.ts
|
|
4387
|
-
import
|
|
4388
|
-
import
|
|
5163
|
+
import fs33 from "fs";
|
|
5164
|
+
import path38 from "path";
|
|
4389
5165
|
|
|
4390
5166
|
// src/commands/media/detect-type.ts
|
|
4391
5167
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -4581,10 +5357,10 @@ async function batchOptimize(opts) {
|
|
|
4581
5357
|
verbose,
|
|
4582
5358
|
logger
|
|
4583
5359
|
} = opts;
|
|
4584
|
-
|
|
4585
|
-
const entries =
|
|
5360
|
+
fs33.mkdirSync(outputDir, { recursive: true });
|
|
5361
|
+
const entries = fs33.readdirSync(inputDir);
|
|
4586
5362
|
const mediaFiles = entries.filter((f) => {
|
|
4587
|
-
const ext =
|
|
5363
|
+
const ext = path38.extname(f).toLowerCase();
|
|
4588
5364
|
return ALL_EXTS.has(ext);
|
|
4589
5365
|
});
|
|
4590
5366
|
if (mediaFiles.length === 0) {
|
|
@@ -4595,9 +5371,9 @@ async function batchOptimize(opts) {
|
|
|
4595
5371
|
let succeeded = 0;
|
|
4596
5372
|
const errors = [];
|
|
4597
5373
|
for (const file of mediaFiles) {
|
|
4598
|
-
const inputPath =
|
|
4599
|
-
const outputPath =
|
|
4600
|
-
const ext =
|
|
5374
|
+
const inputPath = path38.join(inputDir, file);
|
|
5375
|
+
const outputPath = path38.join(outputDir, file);
|
|
5376
|
+
const ext = path38.extname(file).toLowerCase();
|
|
4601
5377
|
const kind = extToKind(ext);
|
|
4602
5378
|
logger?.debug(`Processing: ${file}`);
|
|
4603
5379
|
try {
|
|
@@ -4634,8 +5410,8 @@ async function batchOptimize(opts) {
|
|
|
4634
5410
|
|
|
4635
5411
|
// src/commands/media/split-video.ts
|
|
4636
5412
|
init_errors();
|
|
4637
|
-
import
|
|
4638
|
-
import
|
|
5413
|
+
import fs34 from "fs";
|
|
5414
|
+
import path39 from "path";
|
|
4639
5415
|
async function splitVideo(opts) {
|
|
4640
5416
|
await assertFfmpeg();
|
|
4641
5417
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -4646,10 +5422,10 @@ async function splitVideo(opts) {
|
|
|
4646
5422
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
4647
5423
|
return [input];
|
|
4648
5424
|
}
|
|
4649
|
-
|
|
4650
|
-
const ext =
|
|
4651
|
-
const baseName =
|
|
4652
|
-
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}`);
|
|
4653
5429
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
4654
5430
|
const args = [
|
|
4655
5431
|
"-i",
|
|
@@ -4668,8 +5444,8 @@ async function splitVideo(opts) {
|
|
|
4668
5444
|
await runFfmpeg(args, { verbose, logger });
|
|
4669
5445
|
const files = [];
|
|
4670
5446
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
4671
|
-
const fname =
|
|
4672
|
-
if (
|
|
5447
|
+
const fname = path39.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
5448
|
+
if (fs34.existsSync(fname)) files.push(fname);
|
|
4673
5449
|
}
|
|
4674
5450
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
4675
5451
|
return files;
|
|
@@ -4683,9 +5459,9 @@ function registerMediaCommands(program2) {
|
|
|
4683
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(
|
|
4684
5460
|
async (opts) => {
|
|
4685
5461
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4686
|
-
if (!
|
|
4687
|
-
|
|
4688
|
-
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();
|
|
4689
5465
|
const kind = extToKind(ext);
|
|
4690
5466
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
4691
5467
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -4725,7 +5501,7 @@ function registerMediaCommands(program2) {
|
|
|
4725
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(
|
|
4726
5502
|
async (opts) => {
|
|
4727
5503
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4728
|
-
if (!
|
|
5504
|
+
if (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
4729
5505
|
const chunks = await splitVideo({
|
|
4730
5506
|
input: opts.input,
|
|
4731
5507
|
outputDir: opts.outputDir,
|
|
@@ -4741,7 +5517,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
4741
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(
|
|
4742
5518
|
async (opts) => {
|
|
4743
5519
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4744
|
-
if (!
|
|
5520
|
+
if (!fs35.existsSync(opts.inputDir))
|
|
4745
5521
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
4746
5522
|
const result = await batchOptimize({
|
|
4747
5523
|
inputDir: opts.inputDir,
|
|
@@ -4770,6 +5546,7 @@ function registerCommands(program2) {
|
|
|
4770
5546
|
registerOpenRouterCommands(program2);
|
|
4771
5547
|
registerLeonardoCommands(program2);
|
|
4772
5548
|
registerBytePlusCommands(program2);
|
|
5549
|
+
registerElevenLabsCommands(program2);
|
|
4773
5550
|
registerMediaCommands(program2);
|
|
4774
5551
|
registerDocCommands(program2);
|
|
4775
5552
|
}
|