@mrgoonie/multix 0.0.6 → 0.0.8
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 +36 -0
- package/README.md +77 -3
- package/dist/cli.js +1109 -253
- package/package.json +2 -2
- package/skill/SKILL.md +144 -101
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,22 +4357,22 @@ 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
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
return
|
|
4364
|
+
|
|
4365
|
+
// src/providers/openrouter/payload.ts
|
|
4366
|
+
var IMAGE_ONLY_MODEL_PREFIXES = [
|
|
4367
|
+
"black-forest-labs/",
|
|
4368
|
+
"bytedance-seed/seedream",
|
|
4369
|
+
"sourceful/"
|
|
4370
|
+
];
|
|
4371
|
+
function resolveModalities(model) {
|
|
4372
|
+
const lower = model.toLowerCase();
|
|
4373
|
+
return IMAGE_ONLY_MODEL_PREFIXES.some((p) => lower.startsWith(p)) ? ["image"] : ["image", "text"];
|
|
3604
4374
|
}
|
|
3605
|
-
function
|
|
4375
|
+
function buildOpenRouterHeaders(apiKey) {
|
|
3606
4376
|
const headers = {
|
|
3607
4377
|
Authorization: `Bearer ${apiKey}`,
|
|
3608
4378
|
"Content-Type": "application/json"
|
|
@@ -3613,10 +4383,96 @@ function buildHeaders(apiKey) {
|
|
|
3613
4383
|
if (title) headers["X-Title"] = title;
|
|
3614
4384
|
return headers;
|
|
3615
4385
|
}
|
|
4386
|
+
function extractImagesFromResponse(data) {
|
|
4387
|
+
const out = { urls: [] };
|
|
4388
|
+
if (!data || typeof data !== "object") return out;
|
|
4389
|
+
const root = data;
|
|
4390
|
+
if (typeof root.model === "string") out.model = root.model;
|
|
4391
|
+
const choices = Array.isArray(root.choices) ? root.choices : [];
|
|
4392
|
+
const first = choices[0];
|
|
4393
|
+
if (!first || typeof first !== "object") return out;
|
|
4394
|
+
const choice = first;
|
|
4395
|
+
if (typeof choice.finish_reason === "string") out.finishReason = choice.finish_reason;
|
|
4396
|
+
const message = choice.message;
|
|
4397
|
+
if (!message) return out;
|
|
4398
|
+
const images = message.images;
|
|
4399
|
+
if (Array.isArray(images)) {
|
|
4400
|
+
for (const item of images) {
|
|
4401
|
+
const url = readImageUrl(item);
|
|
4402
|
+
if (url) out.urls.push(url);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
const content = message.content;
|
|
4406
|
+
if (Array.isArray(content)) {
|
|
4407
|
+
for (const part of content) {
|
|
4408
|
+
const url = readImageUrl(part);
|
|
4409
|
+
if (url) out.urls.push(url);
|
|
4410
|
+
}
|
|
4411
|
+
} else if (typeof content === "string") {
|
|
4412
|
+
out.textContent = content;
|
|
4413
|
+
}
|
|
4414
|
+
return out;
|
|
4415
|
+
}
|
|
4416
|
+
function readImageUrl(item) {
|
|
4417
|
+
if (!item || typeof item !== "object") return void 0;
|
|
4418
|
+
const rec = item;
|
|
4419
|
+
if (rec.type !== void 0 && rec.type !== "image_url") return void 0;
|
|
4420
|
+
const inner = rec.image_url;
|
|
4421
|
+
const url = inner?.url;
|
|
4422
|
+
return typeof url === "string" ? url : void 0;
|
|
4423
|
+
}
|
|
4424
|
+
function formatNoImagesError(model, parsed) {
|
|
4425
|
+
const parts = [
|
|
4426
|
+
`No images returned by '${model}' (finish_reason=${parsed.finishReason ?? "n/a"}).`
|
|
4427
|
+
];
|
|
4428
|
+
if (parsed.textContent) {
|
|
4429
|
+
parts.push(`Model said: "${truncate(parsed.textContent, 200)}".`);
|
|
4430
|
+
}
|
|
4431
|
+
parts.push(
|
|
4432
|
+
"Hint: verify the model supports image output and that 'modalities' includes 'image'."
|
|
4433
|
+
);
|
|
4434
|
+
return parts.join(" ");
|
|
4435
|
+
}
|
|
4436
|
+
function truncate(s, n) {
|
|
4437
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
4438
|
+
}
|
|
4439
|
+
function buildI2IPayload(opts) {
|
|
4440
|
+
const content = opts.refs.map((url) => ({
|
|
4441
|
+
type: "image_url",
|
|
4442
|
+
image_url: { url }
|
|
4443
|
+
}));
|
|
4444
|
+
content.push({ type: "text", text: opts.prompt });
|
|
4445
|
+
const payload = {
|
|
4446
|
+
messages: [{ role: "user", content }],
|
|
4447
|
+
modalities: resolveModalities(opts.model)
|
|
4448
|
+
};
|
|
4449
|
+
if (typeof opts.strength === "number") {
|
|
4450
|
+
payload.image_config = { strength: opts.strength };
|
|
4451
|
+
}
|
|
4452
|
+
if (opts.fallbackModels.length > 0) {
|
|
4453
|
+
payload.models = [opts.model, ...opts.fallbackModels];
|
|
4454
|
+
} else {
|
|
4455
|
+
payload.model = opts.model;
|
|
4456
|
+
}
|
|
4457
|
+
return payload;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
// src/providers/openrouter/client.ts
|
|
4461
|
+
var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
4462
|
+
var DEFAULT_OPENROUTER_MODEL = "google/gemini-3.1-flash-image-preview";
|
|
4463
|
+
function requireOpenRouterKey() {
|
|
4464
|
+
const key = resolveKey("OPENROUTER_API_KEY");
|
|
4465
|
+
if (!key) {
|
|
4466
|
+
throw new ConfigError(
|
|
4467
|
+
"OPENROUTER_API_KEY is not set. Get one at https://openrouter.ai/settings/keys"
|
|
4468
|
+
);
|
|
4469
|
+
}
|
|
4470
|
+
return key;
|
|
4471
|
+
}
|
|
3616
4472
|
function buildPayload(opts) {
|
|
3617
4473
|
const payload = {
|
|
3618
4474
|
messages: [{ role: "user", content: opts.prompt }],
|
|
3619
|
-
modalities: opts.model
|
|
4475
|
+
modalities: resolveModalities(opts.model),
|
|
3620
4476
|
image_config: { aspect_ratio: opts.aspectRatio }
|
|
3621
4477
|
};
|
|
3622
4478
|
if (opts.imageSize) {
|
|
@@ -3645,29 +4501,19 @@ async function generateOpenRouterImage(opts) {
|
|
|
3645
4501
|
data = await httpJson({
|
|
3646
4502
|
url: OPENROUTER_API_URL,
|
|
3647
4503
|
method: "POST",
|
|
3648
|
-
headers:
|
|
4504
|
+
headers: buildOpenRouterHeaders(apiKey),
|
|
3649
4505
|
body: payload,
|
|
3650
4506
|
timeoutMs: 24e4
|
|
3651
4507
|
});
|
|
3652
4508
|
} catch (e) {
|
|
3653
4509
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
3654
4510
|
}
|
|
3655
|
-
const
|
|
3656
|
-
if (
|
|
3657
|
-
return { status: "error", error:
|
|
3658
|
-
}
|
|
3659
|
-
const messageImages = choices[0]?.message?.images ?? [];
|
|
3660
|
-
if (messageImages.length === 0) {
|
|
3661
|
-
return {
|
|
3662
|
-
status: "error",
|
|
3663
|
-
error: `No images in response \u2014 model '${model}' may not support image generation`
|
|
3664
|
-
};
|
|
3665
|
-
}
|
|
3666
|
-
for (const img of messageImages) {
|
|
3667
|
-
const url = img.image_url?.url;
|
|
3668
|
-
if (url) imageUrls.push(url);
|
|
4511
|
+
const parsed = extractImagesFromResponse(data);
|
|
4512
|
+
if (parsed.urls.length === 0) {
|
|
4513
|
+
return { status: "error", error: formatNoImagesError(model, parsed) };
|
|
3669
4514
|
}
|
|
3670
|
-
|
|
4515
|
+
imageUrls.push(...parsed.urls);
|
|
4516
|
+
usedModel = parsed.model ?? usedModel;
|
|
3671
4517
|
if (imageUrls.length >= numImages) break;
|
|
3672
4518
|
}
|
|
3673
4519
|
const outDir = getOutputDir();
|
|
@@ -3677,8 +4523,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
3677
4523
|
if (!url) continue;
|
|
3678
4524
|
try {
|
|
3679
4525
|
const bytes = await fetchBytes(url);
|
|
3680
|
-
const dest =
|
|
3681
|
-
|
|
4526
|
+
const dest = path34.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
4527
|
+
fs29.writeFileSync(dest, bytes);
|
|
3682
4528
|
savedFiles.push(dest);
|
|
3683
4529
|
logger?.success(`Saved: ${dest}`);
|
|
3684
4530
|
} catch (e) {
|
|
@@ -3689,8 +4535,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
3689
4535
|
}
|
|
3690
4536
|
}
|
|
3691
4537
|
if (output && savedFiles[0]) {
|
|
3692
|
-
|
|
3693
|
-
|
|
4538
|
+
fs29.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
|
|
4539
|
+
fs29.writeFileSync(output, fs29.readFileSync(savedFiles[0]));
|
|
3694
4540
|
}
|
|
3695
4541
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
3696
4542
|
}
|
|
@@ -3725,8 +4571,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
3725
4571
|
}
|
|
3726
4572
|
|
|
3727
4573
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
3728
|
-
import
|
|
3729
|
-
import
|
|
4574
|
+
import fs30 from "fs";
|
|
4575
|
+
import path35 from "path";
|
|
3730
4576
|
init_errors();
|
|
3731
4577
|
init_http_client();
|
|
3732
4578
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -3738,13 +4584,15 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
3738
4584
|
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
3739
4585
|
collect5,
|
|
3740
4586
|
[]
|
|
3741
|
-
).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
4587
|
+
).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--strength <n>", "Recraft init-image strength 0..1 (Recraft models only)").option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
3742
4588
|
async (opts) => {
|
|
3743
4589
|
if (!opts.ref || opts.ref.length === 0) {
|
|
3744
|
-
throw new
|
|
4590
|
+
throw new ValidationError("At least one --ref is required for image-to-image.");
|
|
3745
4591
|
}
|
|
3746
4592
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3747
4593
|
const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_MODEL;
|
|
4594
|
+
const strength = parseStrength(opts.strength);
|
|
4595
|
+
const fallbackModels = (resolveKey("OPENROUTER_FALLBACK_MODELS") ?? "").split(",").map((m) => m.trim()).filter(Boolean);
|
|
3748
4596
|
const resolvedRefs = [];
|
|
3749
4597
|
for (const r of opts.ref) {
|
|
3750
4598
|
const resolved = await resolveImageInput(r, {
|
|
@@ -3754,11 +4602,17 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
3754
4602
|
});
|
|
3755
4603
|
resolvedRefs.push(refUrl(resolved));
|
|
3756
4604
|
}
|
|
3757
|
-
|
|
4605
|
+
const strengthDbg = strength !== void 0 ? `, strength=${strength}` : "";
|
|
4606
|
+
const fallbacksDbg = fallbackModels.length ? `, fallbacks=${fallbackModels.join(",")}` : "";
|
|
4607
|
+
logger.debug(
|
|
4608
|
+
`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}${strengthDbg}${fallbacksDbg}`
|
|
4609
|
+
);
|
|
3758
4610
|
const saved = await runImageToImage({
|
|
3759
4611
|
prompt: opts.prompt,
|
|
3760
4612
|
model,
|
|
3761
4613
|
refs: resolvedRefs,
|
|
4614
|
+
strength,
|
|
4615
|
+
fallbackModels,
|
|
3762
4616
|
output: opts.output,
|
|
3763
4617
|
logger
|
|
3764
4618
|
});
|
|
@@ -3772,26 +4626,24 @@ Generated ${saved.length} image(s):`);
|
|
|
3772
4626
|
function collect5(value, prev) {
|
|
3773
4627
|
return [...prev, value];
|
|
3774
4628
|
}
|
|
4629
|
+
function parseStrength(raw) {
|
|
4630
|
+
if (raw === void 0) return void 0;
|
|
4631
|
+
const n = Number.parseFloat(raw);
|
|
4632
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
4633
|
+
throw new ValidationError(`--strength must be a number in [0,1], got '${raw}'`);
|
|
4634
|
+
}
|
|
4635
|
+
return n;
|
|
4636
|
+
}
|
|
3775
4637
|
async function runImageToImage(opts) {
|
|
3776
4638
|
const apiKey = requireOpenRouterKey();
|
|
3777
|
-
const headers =
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
};
|
|
3781
|
-
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
3782
|
-
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
3783
|
-
if (referer) headers["HTTP-Referer"] = referer;
|
|
3784
|
-
if (title) headers["X-Title"] = title;
|
|
3785
|
-
const content = opts.refs.map((url) => ({
|
|
3786
|
-
type: "image_url",
|
|
3787
|
-
image_url: { url }
|
|
3788
|
-
}));
|
|
3789
|
-
content.push({ type: "text", text: opts.prompt });
|
|
3790
|
-
const payload = {
|
|
4639
|
+
const headers = buildOpenRouterHeaders(apiKey);
|
|
4640
|
+
const payload = buildI2IPayload({
|
|
4641
|
+
prompt: opts.prompt,
|
|
3791
4642
|
model: opts.model,
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
4643
|
+
refs: opts.refs,
|
|
4644
|
+
strength: opts.strength,
|
|
4645
|
+
fallbackModels: opts.fallbackModels
|
|
4646
|
+
});
|
|
3795
4647
|
const data = await httpJson({
|
|
3796
4648
|
url: OPENROUTER_API_URL,
|
|
3797
4649
|
method: "POST",
|
|
@@ -3799,28 +4651,25 @@ async function runImageToImage(opts) {
|
|
|
3799
4651
|
body: payload,
|
|
3800
4652
|
timeoutMs: 24e4
|
|
3801
4653
|
});
|
|
3802
|
-
const
|
|
3803
|
-
if (
|
|
3804
|
-
throw new ProviderError(
|
|
3805
|
-
`No images in response \u2014 model '${opts.model}' may not support image-to-image`,
|
|
3806
|
-
"openrouter"
|
|
3807
|
-
);
|
|
4654
|
+
const parsed = extractImagesFromResponse(data);
|
|
4655
|
+
if (parsed.urls.length === 0) {
|
|
4656
|
+
throw new ProviderError(formatNoImagesError(opts.model, parsed), "openrouter");
|
|
3808
4657
|
}
|
|
3809
4658
|
const outDir = getOutputDir();
|
|
3810
4659
|
const ts = Date.now();
|
|
3811
4660
|
const saved = [];
|
|
3812
|
-
for (let i = 0; i <
|
|
3813
|
-
const url =
|
|
4661
|
+
for (let i = 0; i < parsed.urls.length; i++) {
|
|
4662
|
+
const url = parsed.urls[i];
|
|
3814
4663
|
if (!url) continue;
|
|
3815
4664
|
const bytes = await fetchBytes(url);
|
|
3816
|
-
const dest =
|
|
3817
|
-
|
|
4665
|
+
const dest = path35.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
4666
|
+
fs30.writeFileSync(dest, bytes);
|
|
3818
4667
|
saved.push(dest);
|
|
3819
4668
|
opts.logger.success(`Saved: ${dest}`);
|
|
3820
4669
|
}
|
|
3821
4670
|
if (opts.output && saved[0]) {
|
|
3822
|
-
|
|
3823
|
-
|
|
4671
|
+
fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
|
|
4672
|
+
fs30.copyFileSync(saved[0], opts.output);
|
|
3824
4673
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
3825
4674
|
}
|
|
3826
4675
|
return saved;
|
|
@@ -3829,7 +4678,7 @@ async function runImageToImage(opts) {
|
|
|
3829
4678
|
// src/providers/openrouter/video-client.ts
|
|
3830
4679
|
init_http_client();
|
|
3831
4680
|
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
3832
|
-
function
|
|
4681
|
+
function authHeaders4(apiKey) {
|
|
3833
4682
|
const headers = {
|
|
3834
4683
|
Authorization: `Bearer ${apiKey}`,
|
|
3835
4684
|
"Content-Type": "application/json"
|
|
@@ -3844,7 +4693,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
3844
4693
|
return httpJson({
|
|
3845
4694
|
url: OPENROUTER_VIDEOS_URL,
|
|
3846
4695
|
method: "POST",
|
|
3847
|
-
headers:
|
|
4696
|
+
headers: authHeaders4(apiKey),
|
|
3848
4697
|
body,
|
|
3849
4698
|
timeoutMs: 12e4
|
|
3850
4699
|
});
|
|
@@ -3852,7 +4701,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
3852
4701
|
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
3853
4702
|
return httpJson({
|
|
3854
4703
|
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
3855
|
-
headers:
|
|
4704
|
+
headers: authHeaders4(apiKey)
|
|
3856
4705
|
});
|
|
3857
4706
|
}
|
|
3858
4707
|
function videoContentUrl(jobId) {
|
|
@@ -3861,18 +4710,18 @@ function videoContentUrl(jobId) {
|
|
|
3861
4710
|
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
3862
4711
|
return httpJson({
|
|
3863
4712
|
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
3864
|
-
headers:
|
|
4713
|
+
headers: authHeaders4(apiKey)
|
|
3865
4714
|
});
|
|
3866
4715
|
}
|
|
3867
4716
|
|
|
3868
4717
|
// src/providers/openrouter/commands/video-status.ts
|
|
3869
|
-
import
|
|
3870
|
-
import
|
|
4718
|
+
import fs31 from "fs";
|
|
4719
|
+
import path36 from "path";
|
|
3871
4720
|
async function downloadOpenRouterVideo(opts) {
|
|
3872
4721
|
const { jobId, logger, source } = opts;
|
|
3873
4722
|
const apiKey = requireOpenRouterKey();
|
|
3874
4723
|
const url = videoContentUrl(jobId);
|
|
3875
|
-
const dest =
|
|
4724
|
+
const dest = path36.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
3876
4725
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
3877
4726
|
if (!r.ok) {
|
|
3878
4727
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -3882,14 +4731,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
3882
4731
|
logger.error("Empty response body");
|
|
3883
4732
|
process.exit(1);
|
|
3884
4733
|
}
|
|
3885
|
-
|
|
3886
|
-
const ws =
|
|
4734
|
+
fs31.mkdirSync(path36.dirname(dest), { recursive: true });
|
|
4735
|
+
const ws = fs31.createWriteStream(dest);
|
|
3887
4736
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
3888
4737
|
await pipeline2(r.body, ws);
|
|
3889
4738
|
logger.success(`Saved ${dest}`);
|
|
3890
4739
|
if (opts.output) {
|
|
3891
|
-
|
|
3892
|
-
|
|
4740
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
|
|
4741
|
+
fs31.copyFileSync(dest, opts.output);
|
|
3893
4742
|
logger.success(`Copied to ${opts.output}`);
|
|
3894
4743
|
}
|
|
3895
4744
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -4151,6 +5000,12 @@ var PROVIDERS = [
|
|
|
4151
5000
|
envFallback: "ARK_API_KEY",
|
|
4152
5001
|
optional: true,
|
|
4153
5002
|
link: "https://console.byteplus.com/auth/api-keys"
|
|
5003
|
+
},
|
|
5004
|
+
{
|
|
5005
|
+
name: "ElevenLabs",
|
|
5006
|
+
envPrimary: "ELEVENLABS_API_KEY",
|
|
5007
|
+
optional: true,
|
|
5008
|
+
link: "https://elevenlabs.io/app/settings/api-keys"
|
|
4154
5009
|
}
|
|
4155
5010
|
];
|
|
4156
5011
|
function resolveProviderKey(p) {
|
|
@@ -4237,8 +5092,8 @@ function registerCheckCommand(program2) {
|
|
|
4237
5092
|
|
|
4238
5093
|
// src/commands/doc/convert.ts
|
|
4239
5094
|
init_errors();
|
|
4240
|
-
import
|
|
4241
|
-
import
|
|
5095
|
+
import fs32 from "fs";
|
|
5096
|
+
import path37 from "path";
|
|
4242
5097
|
|
|
4243
5098
|
// src/commands/doc/default-prompt.ts
|
|
4244
5099
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -4259,18 +5114,18 @@ async function convertDocuments(opts) {
|
|
|
4259
5114
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
4260
5115
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
4261
5116
|
for (const f of inputs) {
|
|
4262
|
-
if (!
|
|
5117
|
+
if (!fs32.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
4263
5118
|
}
|
|
4264
5119
|
let outputPath;
|
|
4265
5120
|
if (output) {
|
|
4266
|
-
outputPath =
|
|
5121
|
+
outputPath = path37.resolve(output);
|
|
4267
5122
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
4268
|
-
const base =
|
|
4269
|
-
outputPath =
|
|
5123
|
+
const base = path37.basename(inputs[0], path37.extname(inputs[0]));
|
|
5124
|
+
outputPath = path37.join(getOutputDir(), `${base}-extraction.md`);
|
|
4270
5125
|
} else {
|
|
4271
|
-
outputPath =
|
|
5126
|
+
outputPath = path37.join(getOutputDir(), "document-extraction.md");
|
|
4272
5127
|
}
|
|
4273
|
-
|
|
5128
|
+
fs32.mkdirSync(path37.dirname(outputPath), { recursive: true });
|
|
4274
5129
|
const results = [];
|
|
4275
5130
|
for (let i = 0; i < inputs.length; i++) {
|
|
4276
5131
|
const filePath = inputs[i];
|
|
@@ -4321,7 +5176,7 @@ async function convertDocuments(opts) {
|
|
|
4321
5176
|
`);
|
|
4322
5177
|
for (const r of results) {
|
|
4323
5178
|
lines.push(`
|
|
4324
|
-
## ${
|
|
5179
|
+
## ${path37.basename(r.file)}
|
|
4325
5180
|
`);
|
|
4326
5181
|
if (r.status === "success" && r.markdown) {
|
|
4327
5182
|
lines.push(`${r.markdown}
|
|
@@ -4334,7 +5189,7 @@ async function convertDocuments(opts) {
|
|
|
4334
5189
|
}
|
|
4335
5190
|
lines.push("\n---\n");
|
|
4336
5191
|
}
|
|
4337
|
-
|
|
5192
|
+
fs32.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
4338
5193
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
4339
5194
|
console.log(`
|
|
4340
5195
|
${"=".repeat(50)}`);
|
|
@@ -4380,12 +5235,12 @@ function registerDocCommands(program2) {
|
|
|
4380
5235
|
|
|
4381
5236
|
// src/commands/media/index.ts
|
|
4382
5237
|
init_errors();
|
|
4383
|
-
import
|
|
4384
|
-
import
|
|
5238
|
+
import fs35 from "fs";
|
|
5239
|
+
import path40 from "path";
|
|
4385
5240
|
|
|
4386
5241
|
// src/commands/media/batch.ts
|
|
4387
|
-
import
|
|
4388
|
-
import
|
|
5242
|
+
import fs33 from "fs";
|
|
5243
|
+
import path38 from "path";
|
|
4389
5244
|
|
|
4390
5245
|
// src/commands/media/detect-type.ts
|
|
4391
5246
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -4581,10 +5436,10 @@ async function batchOptimize(opts) {
|
|
|
4581
5436
|
verbose,
|
|
4582
5437
|
logger
|
|
4583
5438
|
} = opts;
|
|
4584
|
-
|
|
4585
|
-
const entries =
|
|
5439
|
+
fs33.mkdirSync(outputDir, { recursive: true });
|
|
5440
|
+
const entries = fs33.readdirSync(inputDir);
|
|
4586
5441
|
const mediaFiles = entries.filter((f) => {
|
|
4587
|
-
const ext =
|
|
5442
|
+
const ext = path38.extname(f).toLowerCase();
|
|
4588
5443
|
return ALL_EXTS.has(ext);
|
|
4589
5444
|
});
|
|
4590
5445
|
if (mediaFiles.length === 0) {
|
|
@@ -4595,9 +5450,9 @@ async function batchOptimize(opts) {
|
|
|
4595
5450
|
let succeeded = 0;
|
|
4596
5451
|
const errors = [];
|
|
4597
5452
|
for (const file of mediaFiles) {
|
|
4598
|
-
const inputPath =
|
|
4599
|
-
const outputPath =
|
|
4600
|
-
const ext =
|
|
5453
|
+
const inputPath = path38.join(inputDir, file);
|
|
5454
|
+
const outputPath = path38.join(outputDir, file);
|
|
5455
|
+
const ext = path38.extname(file).toLowerCase();
|
|
4601
5456
|
const kind = extToKind(ext);
|
|
4602
5457
|
logger?.debug(`Processing: ${file}`);
|
|
4603
5458
|
try {
|
|
@@ -4634,8 +5489,8 @@ async function batchOptimize(opts) {
|
|
|
4634
5489
|
|
|
4635
5490
|
// src/commands/media/split-video.ts
|
|
4636
5491
|
init_errors();
|
|
4637
|
-
import
|
|
4638
|
-
import
|
|
5492
|
+
import fs34 from "fs";
|
|
5493
|
+
import path39 from "path";
|
|
4639
5494
|
async function splitVideo(opts) {
|
|
4640
5495
|
await assertFfmpeg();
|
|
4641
5496
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -4646,10 +5501,10 @@ async function splitVideo(opts) {
|
|
|
4646
5501
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
4647
5502
|
return [input];
|
|
4648
5503
|
}
|
|
4649
|
-
|
|
4650
|
-
const ext =
|
|
4651
|
-
const baseName =
|
|
4652
|
-
const outputPattern =
|
|
5504
|
+
fs34.mkdirSync(outputDir, { recursive: true });
|
|
5505
|
+
const ext = path39.extname(input) || ".mp4";
|
|
5506
|
+
const baseName = path39.basename(input, ext);
|
|
5507
|
+
const outputPattern = path39.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
4653
5508
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
4654
5509
|
const args = [
|
|
4655
5510
|
"-i",
|
|
@@ -4668,8 +5523,8 @@ async function splitVideo(opts) {
|
|
|
4668
5523
|
await runFfmpeg(args, { verbose, logger });
|
|
4669
5524
|
const files = [];
|
|
4670
5525
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
4671
|
-
const fname =
|
|
4672
|
-
if (
|
|
5526
|
+
const fname = path39.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
5527
|
+
if (fs34.existsSync(fname)) files.push(fname);
|
|
4673
5528
|
}
|
|
4674
5529
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
4675
5530
|
return files;
|
|
@@ -4683,9 +5538,9 @@ function registerMediaCommands(program2) {
|
|
|
4683
5538
|
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
5539
|
async (opts) => {
|
|
4685
5540
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4686
|
-
if (!
|
|
4687
|
-
|
|
4688
|
-
const ext =
|
|
5541
|
+
if (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
5542
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5543
|
+
const ext = path40.extname(opts.input).toLowerCase();
|
|
4689
5544
|
const kind = extToKind(ext);
|
|
4690
5545
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
4691
5546
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -4725,7 +5580,7 @@ function registerMediaCommands(program2) {
|
|
|
4725
5580
|
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
5581
|
async (opts) => {
|
|
4727
5582
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4728
|
-
if (!
|
|
5583
|
+
if (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
4729
5584
|
const chunks = await splitVideo({
|
|
4730
5585
|
input: opts.input,
|
|
4731
5586
|
outputDir: opts.outputDir,
|
|
@@ -4741,7 +5596,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
4741
5596
|
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
5597
|
async (opts) => {
|
|
4743
5598
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4744
|
-
if (!
|
|
5599
|
+
if (!fs35.existsSync(opts.inputDir))
|
|
4745
5600
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
4746
5601
|
const result = await batchOptimize({
|
|
4747
5602
|
inputDir: opts.inputDir,
|
|
@@ -4770,6 +5625,7 @@ function registerCommands(program2) {
|
|
|
4770
5625
|
registerOpenRouterCommands(program2);
|
|
4771
5626
|
registerLeonardoCommands(program2);
|
|
4772
5627
|
registerBytePlusCommands(program2);
|
|
5628
|
+
registerElevenLabsCommands(program2);
|
|
4773
5629
|
registerMediaCommands(program2);
|
|
4774
5630
|
registerDocCommands(program2);
|
|
4775
5631
|
}
|