@mrgoonie/multix 0.1.1 → 0.3.0
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 +14 -0
- package/README.md +40 -2
- package/dist/cli.js +676 -269
- package/package.json +1 -1
- package/skill/SKILL.md +105 -7
package/dist/cli.js
CHANGED
|
@@ -414,9 +414,9 @@ var BytePlusClient = class {
|
|
|
414
414
|
}
|
|
415
415
|
apiKey;
|
|
416
416
|
baseUrl;
|
|
417
|
-
async request(
|
|
417
|
+
async request(path46, opts = {}) {
|
|
418
418
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
419
|
-
const url = new URL(base +
|
|
419
|
+
const url = new URL(base + path46);
|
|
420
420
|
if (opts.query) {
|
|
421
421
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
422
422
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -434,14 +434,14 @@ var BytePlusClient = class {
|
|
|
434
434
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
435
435
|
});
|
|
436
436
|
}
|
|
437
|
-
get(
|
|
438
|
-
return this.request(
|
|
437
|
+
get(path46, query, logger) {
|
|
438
|
+
return this.request(path46, { method: "GET", query, logger });
|
|
439
439
|
}
|
|
440
|
-
post(
|
|
441
|
-
return this.request(
|
|
440
|
+
post(path46, body, logger) {
|
|
441
|
+
return this.request(path46, { method: "POST", body, logger });
|
|
442
442
|
}
|
|
443
|
-
delete(
|
|
444
|
-
return this.request(
|
|
443
|
+
delete(path46, logger) {
|
|
444
|
+
return this.request(path46, { method: "DELETE", logger });
|
|
445
445
|
}
|
|
446
446
|
};
|
|
447
447
|
function createBytePlusClient() {
|
|
@@ -990,10 +990,10 @@ function toRef(kind, resolved, role) {
|
|
|
990
990
|
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
991
991
|
}
|
|
992
992
|
async function resolveRefSpec(kind, spec, logger) {
|
|
993
|
-
const { path:
|
|
994
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
995
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
996
|
-
return toRef(kind, await resolveAudioInput(
|
|
993
|
+
const { path: path46, role } = parseRefSpec(spec);
|
|
994
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path46, logger), role);
|
|
995
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path46, logger), role);
|
|
996
|
+
return toRef(kind, await resolveAudioInput(path46, logger), role);
|
|
997
997
|
}
|
|
998
998
|
async function resolveRefSpecs(specs, logger) {
|
|
999
999
|
const out = [];
|
|
@@ -1131,11 +1131,11 @@ function registerBytePlusStatusCommand(parent) {
|
|
|
1131
1131
|
console.log(JSON.stringify(status, null, 2));
|
|
1132
1132
|
if (opts.download && status.status === "succeeded") {
|
|
1133
1133
|
const content = status.content;
|
|
1134
|
-
const
|
|
1134
|
+
const videoUrl2 = content?.video_url;
|
|
1135
1135
|
const fileUrl = content?.file_url;
|
|
1136
|
-
if (
|
|
1136
|
+
if (videoUrl2) {
|
|
1137
1137
|
const outPath = opts.output ?? path10.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
|
|
1138
|
-
await downloadVideo(
|
|
1138
|
+
await downloadVideo(videoUrl2, outPath, logger);
|
|
1139
1139
|
await maybeDownloadThumb(status, outPath, {
|
|
1140
1140
|
skip: opts.thumb === false,
|
|
1141
1141
|
copyTo: opts.output,
|
|
@@ -1240,7 +1240,7 @@ Task: ${taskId}`);
|
|
|
1240
1240
|
// src/providers/byteplus/commands/index.ts
|
|
1241
1241
|
function registerBytePlusCommands(program2) {
|
|
1242
1242
|
const byteplus = program2.command("byteplus").description(
|
|
1243
|
-
"BytePlus ModelArk: Seedream
|
|
1243
|
+
"BytePlus ModelArk: image (Seedream 4.0), video (Seedance 2.0 text/image/reference-to-video), and 3D (Hyper3D / Hitem3d)"
|
|
1244
1244
|
);
|
|
1245
1245
|
registerBytePlusGenerateCommand(byteplus);
|
|
1246
1246
|
registerBytePlusImageToImageCommand(byteplus);
|
|
@@ -1251,9 +1251,375 @@ function registerBytePlusCommands(program2) {
|
|
|
1251
1251
|
registerBytePlusStatusCommand(byteplus);
|
|
1252
1252
|
}
|
|
1253
1253
|
|
|
1254
|
-
// src/providers/
|
|
1254
|
+
// src/providers/cloudflare/commands/generate-speech.ts
|
|
1255
|
+
init_errors();
|
|
1256
|
+
|
|
1257
|
+
// src/providers/cloudflare/client.ts
|
|
1258
|
+
init_errors();
|
|
1259
|
+
|
|
1260
|
+
// src/providers/cloudflare/models.ts
|
|
1261
|
+
init_errors();
|
|
1262
|
+
var CLOUDFLARE_IMAGE_MODEL = "@cf/black-forest-labs/flux-1-schnell";
|
|
1263
|
+
var CLOUDFLARE_TTS_MODEL = "@cf/myshell-ai/melotts";
|
|
1264
|
+
var CLOUDFLARE_VIDEO_MODEL = "prunaai/p-video";
|
|
1265
|
+
function required(name) {
|
|
1266
|
+
const value = resolveKey(name)?.trim();
|
|
1267
|
+
if (!value) throw new ConfigError(`${name} is required for Cloudflare media generation`);
|
|
1268
|
+
return value;
|
|
1269
|
+
}
|
|
1270
|
+
function getCloudflareModel(kind, requested) {
|
|
1271
|
+
const expected = kind === "image" ? CLOUDFLARE_IMAGE_MODEL : CLOUDFLARE_TTS_MODEL;
|
|
1272
|
+
const overrideName = kind === "image" ? "CLOUDFLARE_AI_IMAGE_MODEL" : "CLOUDFLARE_AI_TTS_MODEL";
|
|
1273
|
+
const model = requested ?? resolveKey(overrideName) ?? expected;
|
|
1274
|
+
if (model !== expected) {
|
|
1275
|
+
throw new ValidationError(`Unsupported Cloudflare ${kind} model: ${model}`);
|
|
1276
|
+
}
|
|
1277
|
+
return model;
|
|
1278
|
+
}
|
|
1279
|
+
function validateImageSteps(value) {
|
|
1280
|
+
if (!/^\d+$/.test(value)) {
|
|
1281
|
+
throw new ValidationError("--steps must be an integer from 1 to 8 for flux-1-schnell");
|
|
1282
|
+
}
|
|
1283
|
+
const steps = Number.parseInt(value, 10);
|
|
1284
|
+
if (!Number.isInteger(steps) || steps < 1 || steps > 8) {
|
|
1285
|
+
throw new ValidationError("--steps must be an integer from 1 to 8 for flux-1-schnell");
|
|
1286
|
+
}
|
|
1287
|
+
return steps;
|
|
1288
|
+
}
|
|
1289
|
+
function requireCloudflareConfig() {
|
|
1290
|
+
return {
|
|
1291
|
+
accountId: required("CLOUDFLARE_ACCOUNT_ID"),
|
|
1292
|
+
apiToken: required("CLOUDFLARE_API_TOKEN"),
|
|
1293
|
+
gatewayId: resolveKey("CLOUDFLARE_AI_GATEWAY_ID")?.trim() || void 0
|
|
1294
|
+
};
|
|
1295
|
+
}
|
|
1296
|
+
function requireCloudflareVideoConfig() {
|
|
1297
|
+
const config = requireCloudflareConfig();
|
|
1298
|
+
if (!config.gatewayId) {
|
|
1299
|
+
throw new ConfigError("CLOUDFLARE_AI_GATEWAY_ID is required for Cloudflare video generation");
|
|
1300
|
+
}
|
|
1301
|
+
return {
|
|
1302
|
+
...config,
|
|
1303
|
+
gatewayId: config.gatewayId,
|
|
1304
|
+
replicateApiToken: required("REPLICATE_API_TOKEN")
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// src/providers/cloudflare/client.ts
|
|
1309
|
+
function collectPayloadHeader() {
|
|
1310
|
+
const envName = ["CLOUDFLARE", "AI", "GATEWAY", "COLLECT", "LOG", "PAYLOAD"].join("_");
|
|
1311
|
+
return resolveKey(envName) === "true" ? "true" : "false";
|
|
1312
|
+
}
|
|
1313
|
+
function gatewayHeaders(config) {
|
|
1314
|
+
return {
|
|
1315
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
1316
|
+
"Content-Type": "application/json",
|
|
1317
|
+
"cf-aig-gateway-id": config.gatewayId ?? "",
|
|
1318
|
+
"cf-aig-collect-log-payload": collectPayloadHeader()
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
function providerFailure(status) {
|
|
1322
|
+
return new ProviderError(`Cloudflare request failed with HTTP ${status}`, "Cloudflare");
|
|
1323
|
+
}
|
|
1324
|
+
async function readJson(response) {
|
|
1325
|
+
if (!response.ok) throw providerFailure(response.status);
|
|
1326
|
+
try {
|
|
1327
|
+
return await response.json();
|
|
1328
|
+
} catch {
|
|
1329
|
+
throw new ProviderError("Cloudflare returned an invalid JSON response", "Cloudflare");
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
function nativeUrl(accountId, model) {
|
|
1333
|
+
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/run/${model}`;
|
|
1334
|
+
}
|
|
1335
|
+
function gatewayUrl(accountId) {
|
|
1336
|
+
return `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/run`;
|
|
1337
|
+
}
|
|
1338
|
+
async function runCloudflareJson(model, input) {
|
|
1339
|
+
const config = requireCloudflareConfig();
|
|
1340
|
+
const useGateway = Boolean(config.gatewayId);
|
|
1341
|
+
const response = await globalThis.fetch(
|
|
1342
|
+
useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
|
|
1343
|
+
{
|
|
1344
|
+
method: "POST",
|
|
1345
|
+
headers: useGateway ? gatewayHeaders(config) : { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" },
|
|
1346
|
+
body: JSON.stringify(useGateway ? { model, input } : input)
|
|
1347
|
+
}
|
|
1348
|
+
);
|
|
1349
|
+
const payload = await readJson(response);
|
|
1350
|
+
if (payload.success === false || payload.result === void 0) {
|
|
1351
|
+
throw new ProviderError("Cloudflare returned no usable result", "Cloudflare");
|
|
1352
|
+
}
|
|
1353
|
+
return payload.result;
|
|
1354
|
+
}
|
|
1355
|
+
async function runCloudflareBytes(model, input) {
|
|
1356
|
+
const config = requireCloudflareConfig();
|
|
1357
|
+
const useGateway = Boolean(config.gatewayId);
|
|
1358
|
+
const response = await globalThis.fetch(
|
|
1359
|
+
useGateway ? gatewayUrl(config.accountId) : nativeUrl(config.accountId, model),
|
|
1360
|
+
{
|
|
1361
|
+
method: "POST",
|
|
1362
|
+
headers: useGateway ? gatewayHeaders(config) : { Authorization: `Bearer ${config.apiToken}`, "Content-Type": "application/json" },
|
|
1363
|
+
body: JSON.stringify(useGateway ? { model, input } : input)
|
|
1364
|
+
}
|
|
1365
|
+
);
|
|
1366
|
+
if (!response.ok) throw providerFailure(response.status);
|
|
1367
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1368
|
+
if (!contentType.includes("audio/mpeg")) {
|
|
1369
|
+
throw new ProviderError("Cloudflare did not return MPEG audio", "Cloudflare");
|
|
1370
|
+
}
|
|
1371
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1372
|
+
}
|
|
1373
|
+
function videoUrl(config, suffix = "") {
|
|
1374
|
+
return `https://gateway.ai.cloudflare.com/v1/${encodeURIComponent(config.accountId)}/${encodeURIComponent(config.gatewayId)}/replicate/predictions${suffix}`;
|
|
1375
|
+
}
|
|
1376
|
+
function videoHeaders(config, wait = false) {
|
|
1377
|
+
return {
|
|
1378
|
+
Authorization: `Bearer ${config.replicateApiToken}`,
|
|
1379
|
+
"cf-aig-authorization": `Bearer ${config.apiToken}`,
|
|
1380
|
+
"cf-aig-collect-log-payload": collectPayloadHeader(),
|
|
1381
|
+
"Content-Type": "application/json",
|
|
1382
|
+
...wait ? { Prefer: "wait" } : {}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
function prediction(value) {
|
|
1386
|
+
if (!value || typeof value !== "object") {
|
|
1387
|
+
throw new ProviderError("Cloudflare returned an invalid video prediction", "Cloudflare");
|
|
1388
|
+
}
|
|
1389
|
+
const result = value;
|
|
1390
|
+
if (typeof result.id !== "string" || typeof result.status !== "string") {
|
|
1391
|
+
throw new ProviderError("Cloudflare returned an invalid video prediction", "Cloudflare");
|
|
1392
|
+
}
|
|
1393
|
+
if (result.output !== void 0 && typeof result.output !== "string" && (!Array.isArray(result.output) || !result.output.every((item) => typeof item === "string"))) {
|
|
1394
|
+
throw new ProviderError("Cloudflare returned an invalid video output", "Cloudflare");
|
|
1395
|
+
}
|
|
1396
|
+
return { id: result.id, status: result.status, output: result.output };
|
|
1397
|
+
}
|
|
1398
|
+
async function createVideoPrediction(input, wait = false) {
|
|
1399
|
+
const config = requireCloudflareVideoConfig();
|
|
1400
|
+
const response = await globalThis.fetch(videoUrl(config), {
|
|
1401
|
+
method: "POST",
|
|
1402
|
+
headers: videoHeaders(config, wait),
|
|
1403
|
+
body: JSON.stringify({ version: CLOUDFLARE_VIDEO_MODEL, input })
|
|
1404
|
+
});
|
|
1405
|
+
return prediction(await readJson(response));
|
|
1406
|
+
}
|
|
1407
|
+
async function getVideoPrediction(id) {
|
|
1408
|
+
const config = requireCloudflareVideoConfig();
|
|
1409
|
+
const response = await globalThis.fetch(videoUrl(config, `/${encodeURIComponent(id)}`), {
|
|
1410
|
+
headers: videoHeaders(config)
|
|
1411
|
+
});
|
|
1412
|
+
return prediction(await readJson(response));
|
|
1413
|
+
}
|
|
1414
|
+
async function downloadVideo2(url) {
|
|
1415
|
+
let response;
|
|
1416
|
+
try {
|
|
1417
|
+
response = await globalThis.fetch(url);
|
|
1418
|
+
} catch {
|
|
1419
|
+
throw new ProviderError("Cloudflare video download could not be started", "Cloudflare");
|
|
1420
|
+
}
|
|
1421
|
+
if (!response.ok)
|
|
1422
|
+
throw new ProviderError(
|
|
1423
|
+
`Cloudflare video download failed with HTTP ${response.status}`,
|
|
1424
|
+
"Cloudflare"
|
|
1425
|
+
);
|
|
1426
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// src/providers/cloudflare/media-output.ts
|
|
1430
|
+
init_errors();
|
|
1255
1431
|
import fs7 from "fs";
|
|
1256
1432
|
import path12 from "path";
|
|
1433
|
+
function destination(filename, output) {
|
|
1434
|
+
return output ? path12.resolve(output) : path12.join(getOutputDir(), filename);
|
|
1435
|
+
}
|
|
1436
|
+
function saveBytes(bytes, filename, output) {
|
|
1437
|
+
const dest = destination(filename, output);
|
|
1438
|
+
fs7.mkdirSync(path12.dirname(dest), { recursive: true });
|
|
1439
|
+
fs7.writeFileSync(dest, bytes);
|
|
1440
|
+
return dest;
|
|
1441
|
+
}
|
|
1442
|
+
function saveBase64Image(image, output) {
|
|
1443
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(image) || image.length === 0 || image.length % 4 !== 0) {
|
|
1444
|
+
throw new ProviderError("Cloudflare returned an invalid image result", "Cloudflare");
|
|
1445
|
+
}
|
|
1446
|
+
let bytes;
|
|
1447
|
+
try {
|
|
1448
|
+
bytes = Buffer.from(image, "base64");
|
|
1449
|
+
} catch {
|
|
1450
|
+
throw new ProviderError("Cloudflare returned an invalid image result", "Cloudflare");
|
|
1451
|
+
}
|
|
1452
|
+
if (bytes.length === 0)
|
|
1453
|
+
throw new ProviderError("Cloudflare returned an empty image result", "Cloudflare");
|
|
1454
|
+
return saveBytes(bytes, `cloudflare_image_${Date.now()}.jpg`, output);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/providers/cloudflare/commands/generate-speech.ts
|
|
1458
|
+
function registerCloudflareSpeechCommand(parent) {
|
|
1459
|
+
parent.command("generate-speech").description("Generate MPEG speech with Cloudflare Workers AI MeloTTS").requiredOption("--text <text>", "Text to speak").option("--lang <code>", "MeloTTS language code").option("--model <id>", "Cloudflare TTS model").option("--output <path>", "Save MP3 at this path").option("-v, --verbose", "Verbose logging").action(
|
|
1460
|
+
async (opts) => {
|
|
1461
|
+
if (!opts.text.trim()) throw new ValidationError("--text must not be empty");
|
|
1462
|
+
if (opts.lang !== void 0 && !opts.lang.trim())
|
|
1463
|
+
throw new ValidationError("--lang must not be empty");
|
|
1464
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1465
|
+
const model = getCloudflareModel("speech", opts.model);
|
|
1466
|
+
const input = { prompt: opts.text };
|
|
1467
|
+
if (opts.lang) input.lang = opts.lang;
|
|
1468
|
+
logger.debug(`Cloudflare speech model=${model}`);
|
|
1469
|
+
const bytes = await runCloudflareBytes(model, input);
|
|
1470
|
+
const destination2 = saveBytes(bytes, `cloudflare_speech_${Date.now()}.mp3`, opts.output);
|
|
1471
|
+
logger.success(`Saved ${destination2}`);
|
|
1472
|
+
console.log(destination2);
|
|
1473
|
+
}
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
// src/providers/cloudflare/commands/generate-video.ts
|
|
1478
|
+
init_errors();
|
|
1479
|
+
|
|
1480
|
+
// src/providers/cloudflare/commands/video-helpers.ts
|
|
1481
|
+
init_errors();
|
|
1482
|
+
function positiveInteger(value, flag) {
|
|
1483
|
+
if (!/^\d+$/.test(value)) throw new ValidationError(`${flag} must be a positive integer`);
|
|
1484
|
+
const parsed = Number.parseInt(value, 10);
|
|
1485
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
1486
|
+
throw new ValidationError(`${flag} must be a positive integer`);
|
|
1487
|
+
return parsed;
|
|
1488
|
+
}
|
|
1489
|
+
function outputUrl(prediction2) {
|
|
1490
|
+
if (typeof prediction2.output === "string") return prediction2.output;
|
|
1491
|
+
return prediction2.output?.find((value) => typeof value === "string");
|
|
1492
|
+
}
|
|
1493
|
+
async function saveCompletedVideo(prediction2, output) {
|
|
1494
|
+
const url = outputUrl(prediction2);
|
|
1495
|
+
if (!url)
|
|
1496
|
+
throw new ProviderError("Cloudflare completed the video without an output", "Cloudflare");
|
|
1497
|
+
const bytes = await downloadVideo2(url);
|
|
1498
|
+
return saveBytes(bytes, `cloudflare_video_${prediction2.id}.mp4`, output);
|
|
1499
|
+
}
|
|
1500
|
+
async function waitForVideo(prediction2, timeoutMs, onTick) {
|
|
1501
|
+
const deadline = Date.now() + timeoutMs;
|
|
1502
|
+
let current = prediction2;
|
|
1503
|
+
while (!isTerminalVideoStatus(current.status)) {
|
|
1504
|
+
if (Date.now() >= deadline)
|
|
1505
|
+
throw new ProviderError("Cloudflare video polling timed out", "Cloudflare");
|
|
1506
|
+
await new Promise((resolve) => setTimeout(resolve, 5e3));
|
|
1507
|
+
current = await getVideoPrediction(current.id);
|
|
1508
|
+
onTick?.(current.status);
|
|
1509
|
+
}
|
|
1510
|
+
if (current.status !== "succeeded")
|
|
1511
|
+
throw new ProviderError("Cloudflare video generation failed", "Cloudflare");
|
|
1512
|
+
return current;
|
|
1513
|
+
}
|
|
1514
|
+
function isTerminalVideoStatus(status) {
|
|
1515
|
+
return status === "succeeded" || status === "failed" || status === "canceled" || status === "cancelled" || status === "aborted";
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// src/providers/cloudflare/commands/generate-video.ts
|
|
1519
|
+
function registerCloudflareVideoCommand(parent) {
|
|
1520
|
+
parent.command("generate-video").description("Create a prunaai/p-video prediction through Cloudflare AI Gateway").requiredOption("--prompt <text>", "Video prompt").option("--duration <seconds>", "Video duration in seconds", "5").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option("--resolution <value>", "Resolution", "720p").option("--fps <n>", "Frames per second", "24").option("--wait", "Poll until the prediction succeeds or fails").option("--wait-timeout <ms>", "Polling timeout in ms", "600000").option("--download", "Download the completed video (implies --wait)").option("--output <path>", "Save video at this path").option("-v, --verbose", "Verbose logging").action(
|
|
1521
|
+
async (opts) => {
|
|
1522
|
+
if (!opts.prompt.trim()) throw new ValidationError("--prompt must not be empty");
|
|
1523
|
+
if (!opts.aspectRatio.trim()) throw new ValidationError("--aspect-ratio must not be empty");
|
|
1524
|
+
if (!opts.resolution.trim()) throw new ValidationError("--resolution must not be empty");
|
|
1525
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1526
|
+
const input = {
|
|
1527
|
+
prompt: opts.prompt,
|
|
1528
|
+
duration: positiveInteger(opts.duration, "--duration"),
|
|
1529
|
+
aspect_ratio: opts.aspectRatio,
|
|
1530
|
+
resolution: opts.resolution,
|
|
1531
|
+
fps: positiveInteger(opts.fps, "--fps")
|
|
1532
|
+
};
|
|
1533
|
+
const shouldWait = Boolean(opts.wait || opts.download);
|
|
1534
|
+
const prediction2 = await createVideoPrediction(input, shouldWait);
|
|
1535
|
+
logger.success(`Video job ${prediction2.id} (${prediction2.status})`);
|
|
1536
|
+
if (!shouldWait) {
|
|
1537
|
+
console.log(prediction2.id);
|
|
1538
|
+
console.error(`Poll with: multix cloudflare video-status ${prediction2.id}`);
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const completed = prediction2.status === "succeeded" ? prediction2 : await waitForVideo(
|
|
1542
|
+
prediction2,
|
|
1543
|
+
positiveInteger(opts.waitTimeout, "--wait-timeout"),
|
|
1544
|
+
(status) => logger.debug(`Video status: ${status}`)
|
|
1545
|
+
);
|
|
1546
|
+
const destination2 = await saveCompletedVideo(completed, opts.output);
|
|
1547
|
+
logger.success(`Saved ${destination2}`);
|
|
1548
|
+
console.log(destination2);
|
|
1549
|
+
}
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
// src/providers/cloudflare/commands/generate.ts
|
|
1554
|
+
init_errors();
|
|
1555
|
+
function registerCloudflareGenerateCommand(parent) {
|
|
1556
|
+
parent.command("generate").description("Generate an image with Cloudflare Workers AI FLUX.1 Schnell").requiredOption("--prompt <text>", "Image prompt").option("--model <id>", "Cloudflare image model").option("--steps <n>", "Inference steps (1-8)", "4").option("--seed <n>", "Optional non-negative integer seed").option("--output <path>", "Save image at this path").option("-v, --verbose", "Verbose logging").action(
|
|
1557
|
+
async (opts) => {
|
|
1558
|
+
if (!opts.prompt.trim()) throw new ValidationError("--prompt must not be empty");
|
|
1559
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1560
|
+
const model = getCloudflareModel("image", opts.model);
|
|
1561
|
+
const steps = validateImageSteps(opts.steps);
|
|
1562
|
+
const input = { prompt: opts.prompt, steps };
|
|
1563
|
+
if (opts.seed !== void 0) {
|
|
1564
|
+
if (!/^\d+$/.test(opts.seed)) {
|
|
1565
|
+
throw new ValidationError("--seed must be a non-negative integer");
|
|
1566
|
+
}
|
|
1567
|
+
const seed = Number.parseInt(opts.seed, 10);
|
|
1568
|
+
if (!Number.isInteger(seed) || seed < 0) {
|
|
1569
|
+
throw new ValidationError("--seed must be a non-negative integer");
|
|
1570
|
+
}
|
|
1571
|
+
input.seed = seed;
|
|
1572
|
+
}
|
|
1573
|
+
logger.debug(`Cloudflare image model=${model} steps=${steps}`);
|
|
1574
|
+
const result = await runCloudflareJson(model, input);
|
|
1575
|
+
if (typeof result.image !== "string") {
|
|
1576
|
+
throw new ProviderError("Cloudflare returned no image result", "Cloudflare");
|
|
1577
|
+
}
|
|
1578
|
+
const destination2 = saveBase64Image(result.image, opts.output);
|
|
1579
|
+
logger.success(`Saved ${destination2}`);
|
|
1580
|
+
console.log(destination2);
|
|
1581
|
+
}
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
// src/providers/cloudflare/commands/video-status.ts
|
|
1586
|
+
function registerCloudflareVideoStatusCommand(parent) {
|
|
1587
|
+
parent.command("video-status <id>").description("Check, wait for, or download a Cloudflare AI Gateway video prediction").option("--wait", "Poll until the prediction succeeds or fails").option("--wait-timeout <ms>", "Polling timeout in ms", "600000").option("--download", "Download the completed video (implies --wait)").option("--output <path>", "Save video at this path").option("-v, --verbose", "Verbose logging").action(
|
|
1588
|
+
async (id, opts) => {
|
|
1589
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1590
|
+
const shouldWait = Boolean(opts.wait || opts.download);
|
|
1591
|
+
const prediction2 = await getVideoPrediction(id);
|
|
1592
|
+
const completed = shouldWait && prediction2.status !== "succeeded" ? await waitForVideo(
|
|
1593
|
+
prediction2,
|
|
1594
|
+
positiveInteger(opts.waitTimeout, "--wait-timeout"),
|
|
1595
|
+
(status) => logger.debug(`Video status: ${status}`)
|
|
1596
|
+
) : prediction2;
|
|
1597
|
+
console.log(`${completed.id}: ${completed.status}`);
|
|
1598
|
+
if (opts.download) {
|
|
1599
|
+
if (completed.status !== "succeeded") {
|
|
1600
|
+
logger.warn(`Cannot download while status is ${completed.status}`);
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
const destination2 = await saveCompletedVideo(completed, opts.output);
|
|
1604
|
+
logger.success(`Saved ${destination2}`);
|
|
1605
|
+
console.log(destination2);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// src/providers/cloudflare/commands/index.ts
|
|
1612
|
+
function registerCloudflareCommands(program2) {
|
|
1613
|
+
const cloudflare = program2.command("cloudflare").description("Cloudflare Workers AI image and speech, plus AI Gateway video generation");
|
|
1614
|
+
registerCloudflareGenerateCommand(cloudflare);
|
|
1615
|
+
registerCloudflareSpeechCommand(cloudflare);
|
|
1616
|
+
registerCloudflareVideoCommand(cloudflare);
|
|
1617
|
+
registerCloudflareVideoStatusCommand(cloudflare);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
// src/providers/elevenlabs/client.ts
|
|
1621
|
+
import fs8 from "fs";
|
|
1622
|
+
import path13 from "path";
|
|
1257
1623
|
init_errors();
|
|
1258
1624
|
init_http_client();
|
|
1259
1625
|
|
|
@@ -1415,10 +1781,10 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
|
|
|
1415
1781
|
return res.json();
|
|
1416
1782
|
}
|
|
1417
1783
|
function readFileAsBlob(filePath, fieldFilename) {
|
|
1418
|
-
const abs =
|
|
1419
|
-
if (!
|
|
1420
|
-
const buf =
|
|
1421
|
-
const name = fieldFilename ??
|
|
1784
|
+
const abs = path13.resolve(filePath);
|
|
1785
|
+
if (!fs8.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
1786
|
+
const buf = fs8.readFileSync(abs);
|
|
1787
|
+
const name = fieldFilename ?? path13.basename(abs);
|
|
1422
1788
|
return new File([new Uint8Array(buf)], name);
|
|
1423
1789
|
}
|
|
1424
1790
|
async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
|
|
@@ -1439,15 +1805,15 @@ async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
|
|
|
1439
1805
|
}
|
|
1440
1806
|
throw new ProviderError(`${label} timed out after ${timeoutMs / 1e3}s`, "elevenlabs");
|
|
1441
1807
|
}
|
|
1442
|
-
function
|
|
1808
|
+
function saveBytes2(opts) {
|
|
1443
1809
|
const { bytes, task, ext, outputCopy, logger } = opts;
|
|
1444
1810
|
const dir = getOutputDir();
|
|
1445
|
-
const dest =
|
|
1446
|
-
|
|
1811
|
+
const dest = path13.join(dir, `elevenlabs_${task}_${Date.now()}.${ext}`);
|
|
1812
|
+
fs8.writeFileSync(dest, bytes);
|
|
1447
1813
|
logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
1448
1814
|
if (outputCopy) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1815
|
+
fs8.mkdirSync(path13.dirname(path13.resolve(outputCopy)), { recursive: true });
|
|
1816
|
+
fs8.copyFileSync(dest, outputCopy);
|
|
1451
1817
|
}
|
|
1452
1818
|
return dest;
|
|
1453
1819
|
}
|
|
@@ -1486,14 +1852,14 @@ ${models.length} models`);
|
|
|
1486
1852
|
}
|
|
1487
1853
|
|
|
1488
1854
|
// src/providers/elevenlabs/commands/align.ts
|
|
1489
|
-
import
|
|
1490
|
-
import
|
|
1855
|
+
import fs9 from "fs";
|
|
1856
|
+
import path14 from "path";
|
|
1491
1857
|
function registerAlignCommand(parent) {
|
|
1492
1858
|
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
1859
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1494
1860
|
const apiKey = requireElevenLabsKey();
|
|
1495
1861
|
let text = opts.text;
|
|
1496
|
-
if (opts.textFile) text =
|
|
1862
|
+
if (opts.textFile) text = fs9.readFileSync(path14.resolve(opts.textFile), "utf8");
|
|
1497
1863
|
const form = new FormData();
|
|
1498
1864
|
form.append("file", readFileAsBlob(opts.input));
|
|
1499
1865
|
form.append("text", text);
|
|
@@ -1501,9 +1867,9 @@ function registerAlignCommand(parent) {
|
|
|
1501
1867
|
const data = await apiPostMultipart("forced-alignment", form, apiKey, {
|
|
1502
1868
|
timeoutMs: 6e5
|
|
1503
1869
|
});
|
|
1504
|
-
const dest = opts.output ?
|
|
1505
|
-
|
|
1506
|
-
|
|
1870
|
+
const dest = opts.output ? path14.resolve(opts.output) : path14.join(getOutputDir(), `elevenlabs_align_${Date.now()}.json`);
|
|
1871
|
+
fs9.mkdirSync(path14.dirname(dest), { recursive: true });
|
|
1872
|
+
fs9.writeFileSync(dest, JSON.stringify(data, null, 2));
|
|
1507
1873
|
logger.success(`Saved alignment: ${dest}`);
|
|
1508
1874
|
if (data.words) console.log(`Aligned ${data.words.length} words (loss=${data.loss ?? "?"})`);
|
|
1509
1875
|
});
|
|
@@ -1546,8 +1912,8 @@ function registerCloneCommand(parent) {
|
|
|
1546
1912
|
// src/providers/elevenlabs/commands/dub.ts
|
|
1547
1913
|
init_errors();
|
|
1548
1914
|
init_http_client();
|
|
1549
|
-
import
|
|
1550
|
-
import
|
|
1915
|
+
import fs10 from "fs";
|
|
1916
|
+
import path15 from "path";
|
|
1551
1917
|
function registerDubCommand(parent) {
|
|
1552
1918
|
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
1919
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -1590,7 +1956,7 @@ function registerDubCommand(parent) {
|
|
|
1590
1956
|
);
|
|
1591
1957
|
logger.success(`Dub complete: ${start.dubbing_id}`);
|
|
1592
1958
|
if (opts.download) {
|
|
1593
|
-
const dest = opts.output ?
|
|
1959
|
+
const dest = opts.output ? path15.resolve(opts.output) : path15.join(getOutputDir(), `elevenlabs_dub_${start.dubbing_id}_${opts.targetLang}.mp4`);
|
|
1594
1960
|
const url = `${ELEVENLABS_BASE_URL}/dubbing/${start.dubbing_id}/audio/${opts.targetLang}`;
|
|
1595
1961
|
await downloadDub(url, apiKey, dest);
|
|
1596
1962
|
logger.success(`Saved: ${dest}`);
|
|
@@ -1605,7 +1971,7 @@ Dubbed file: ${dest}`);
|
|
|
1605
1971
|
const status = await apiGet(`dubbing/${dubbingId}`, apiKey);
|
|
1606
1972
|
console.log(JSON.stringify(status, null, 2));
|
|
1607
1973
|
if (opts.download) {
|
|
1608
|
-
const dest = opts.output ?
|
|
1974
|
+
const dest = opts.output ? path15.resolve(opts.output) : path15.join(getOutputDir(), `elevenlabs_dub_${dubbingId}_${opts.download}.mp4`);
|
|
1609
1975
|
const url = `${ELEVENLABS_BASE_URL}/dubbing/${dubbingId}/audio/${opts.download}`;
|
|
1610
1976
|
await downloadDub(url, apiKey, dest);
|
|
1611
1977
|
console.log(`Saved: ${dest}`);
|
|
@@ -1613,7 +1979,7 @@ Dubbed file: ${dest}`);
|
|
|
1613
1979
|
});
|
|
1614
1980
|
}
|
|
1615
1981
|
async function downloadDub(url, apiKey, dest) {
|
|
1616
|
-
|
|
1982
|
+
fs10.mkdirSync(path15.dirname(dest), { recursive: true });
|
|
1617
1983
|
const res = await globalThis.fetch(url, {
|
|
1618
1984
|
headers: { "xi-api-key": apiKey },
|
|
1619
1985
|
signal: AbortSignal.timeout(6e5)
|
|
@@ -1622,7 +1988,7 @@ async function downloadDub(url, apiKey, dest) {
|
|
|
1622
1988
|
throw new Error(`Failed to download dub: HTTP ${res.status}`);
|
|
1623
1989
|
}
|
|
1624
1990
|
const arr = new Uint8Array(await res.arrayBuffer());
|
|
1625
|
-
|
|
1991
|
+
fs10.writeFileSync(dest, arr);
|
|
1626
1992
|
void downloadFile;
|
|
1627
1993
|
}
|
|
1628
1994
|
|
|
@@ -1637,7 +2003,7 @@ function registerIsolateCommand(parent) {
|
|
|
1637
2003
|
const bytes = await apiPostBinary("audio-isolation", form, apiKey, {
|
|
1638
2004
|
timeoutMs: 6e5
|
|
1639
2005
|
});
|
|
1640
|
-
const dest =
|
|
2006
|
+
const dest = saveBytes2({
|
|
1641
2007
|
bytes,
|
|
1642
2008
|
task: "isolate",
|
|
1643
2009
|
ext: "mp3",
|
|
@@ -1651,8 +2017,8 @@ Isolated audio: ${dest}`);
|
|
|
1651
2017
|
|
|
1652
2018
|
// src/providers/elevenlabs/commands/music.ts
|
|
1653
2019
|
init_errors();
|
|
1654
|
-
import
|
|
1655
|
-
import
|
|
2020
|
+
import fs11 from "fs";
|
|
2021
|
+
import path16 from "path";
|
|
1656
2022
|
function registerMusicCommand(parent) {
|
|
1657
2023
|
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
2024
|
"--format <fmt>",
|
|
@@ -1667,9 +2033,9 @@ function registerMusicCommand(parent) {
|
|
|
1667
2033
|
const payload = { model_id: opts.model };
|
|
1668
2034
|
if (opts.prompt) payload.prompt = opts.prompt;
|
|
1669
2035
|
if (opts.plan) {
|
|
1670
|
-
const planPath =
|
|
1671
|
-
if (!
|
|
1672
|
-
const planJson = JSON.parse(
|
|
2036
|
+
const planPath = path16.resolve(opts.plan);
|
|
2037
|
+
if (!fs11.existsSync(planPath)) throw new ValidationError(`Plan file not found: ${planPath}`);
|
|
2038
|
+
const planJson = JSON.parse(fs11.readFileSync(planPath, "utf8"));
|
|
1673
2039
|
payload.composition_plan = planJson;
|
|
1674
2040
|
}
|
|
1675
2041
|
if (opts.musicLengthMs) payload.music_length_ms = Number.parseInt(opts.musicLengthMs, 10);
|
|
@@ -1678,7 +2044,7 @@ function registerMusicCommand(parent) {
|
|
|
1678
2044
|
query: { output_format: opts.format },
|
|
1679
2045
|
timeoutMs: 6e5
|
|
1680
2046
|
});
|
|
1681
|
-
const dest =
|
|
2047
|
+
const dest = saveBytes2({
|
|
1682
2048
|
bytes,
|
|
1683
2049
|
task: "music",
|
|
1684
2050
|
ext: extFromFormat(opts.format),
|
|
@@ -1715,7 +2081,7 @@ function registerSfxCommand(parent) {
|
|
|
1715
2081
|
query: { output_format: opts.format },
|
|
1716
2082
|
timeoutMs: 3e5
|
|
1717
2083
|
});
|
|
1718
|
-
const dest =
|
|
2084
|
+
const dest = saveBytes2({
|
|
1719
2085
|
bytes,
|
|
1720
2086
|
task: "sfx",
|
|
1721
2087
|
ext: extFromFormat(opts.format),
|
|
@@ -1728,8 +2094,8 @@ Generated SFX: ${dest}`);
|
|
|
1728
2094
|
}
|
|
1729
2095
|
|
|
1730
2096
|
// src/providers/elevenlabs/commands/transcribe.ts
|
|
1731
|
-
import
|
|
1732
|
-
import
|
|
2097
|
+
import fs12 from "fs";
|
|
2098
|
+
import path17 from "path";
|
|
1733
2099
|
function registerTranscribeCommand(parent) {
|
|
1734
2100
|
parent.command("transcribe").description("Speech-to-text transcription with Scribe").requiredOption("--input <path>", "Audio or video file to transcribe").option(
|
|
1735
2101
|
"--model <id>",
|
|
@@ -1752,7 +2118,7 @@ function registerTranscribeCommand(parent) {
|
|
|
1752
2118
|
});
|
|
1753
2119
|
const ext = opts.format === "json" ? "json" : opts.format === "srt" ? "srt" : opts.format === "vtt" ? "vtt" : "txt";
|
|
1754
2120
|
const dir = getOutputDir();
|
|
1755
|
-
const dest = opts.output ?
|
|
2121
|
+
const dest = opts.output ? path17.resolve(opts.output) : path17.join(dir, `elevenlabs_transcribe_${Date.now()}.${ext}`);
|
|
1756
2122
|
let body;
|
|
1757
2123
|
switch (opts.format) {
|
|
1758
2124
|
case "json":
|
|
@@ -1767,8 +2133,8 @@ function registerTranscribeCommand(parent) {
|
|
|
1767
2133
|
default:
|
|
1768
2134
|
body = data.text ?? "";
|
|
1769
2135
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
2136
|
+
fs12.mkdirSync(path17.dirname(dest), { recursive: true });
|
|
2137
|
+
fs12.writeFileSync(dest, body);
|
|
1772
2138
|
logger.success(`Saved transcript: ${dest}`);
|
|
1773
2139
|
if (data.language_code) {
|
|
1774
2140
|
logger.info(
|
|
@@ -1846,7 +2212,7 @@ function registerTtsCommand(parent) {
|
|
|
1846
2212
|
const bytes = await apiPostBinary(`text-to-speech/${opts.voice}`, payload, apiKey, {
|
|
1847
2213
|
query: { output_format: opts.format }
|
|
1848
2214
|
});
|
|
1849
|
-
const dest =
|
|
2215
|
+
const dest = saveBytes2({
|
|
1850
2216
|
bytes,
|
|
1851
2217
|
task: "tts",
|
|
1852
2218
|
ext: extFromFormat(opts.format),
|
|
@@ -1888,7 +2254,7 @@ function registerVoiceChangerCommand(parent) {
|
|
|
1888
2254
|
if (!bytes || bytes.length === 0) {
|
|
1889
2255
|
throw new ValidationError("Empty response from voice-changer");
|
|
1890
2256
|
}
|
|
1891
|
-
const dest =
|
|
2257
|
+
const dest = saveBytes2({
|
|
1892
2258
|
bytes,
|
|
1893
2259
|
task: "voice_changer",
|
|
1894
2260
|
ext: extFromFormat(opts.format),
|
|
@@ -1959,7 +2325,7 @@ ${data.voices.length} voices${data.has_more ? " (more available)" : ""}`);
|
|
|
1959
2325
|
logger.success(`${data.previews.length} previews generated`);
|
|
1960
2326
|
data.previews.forEach((p, i) => {
|
|
1961
2327
|
const bytes = Buffer.from(p.audio_base_64, "base64");
|
|
1962
|
-
const dest =
|
|
2328
|
+
const dest = saveBytes2({
|
|
1963
2329
|
bytes,
|
|
1964
2330
|
task: `${opts.outputPrefix}_${i + 1}_${p.generated_voice_id}`,
|
|
1965
2331
|
ext: extFromFormat(TASK_DEFAULTS.ttsFormat),
|
|
@@ -1998,7 +2364,7 @@ ${data.voices.length} voices${data.has_more ? " (more available)" : ""}`);
|
|
|
1998
2364
|
// src/providers/elevenlabs/commands/index.ts
|
|
1999
2365
|
function registerElevenLabsCommands(program2) {
|
|
2000
2366
|
const el = program2.command("elevenlabs").description(
|
|
2001
|
-
"ElevenLabs:
|
|
2367
|
+
"ElevenLabs: text-to-speech, voice cloning, speech-to-text, voice changer, sound effects, music, dubbing, voice isolation, forced alignment"
|
|
2002
2368
|
);
|
|
2003
2369
|
registerTtsCommand(el);
|
|
2004
2370
|
registerVoicesCommand(el);
|
|
@@ -2015,16 +2381,16 @@ function registerElevenLabsCommands(program2) {
|
|
|
2015
2381
|
|
|
2016
2382
|
// src/providers/gemini/commands/analyze.ts
|
|
2017
2383
|
init_errors();
|
|
2018
|
-
import
|
|
2384
|
+
import fs15 from "fs";
|
|
2019
2385
|
|
|
2020
2386
|
// src/providers/gemini/client.ts
|
|
2021
|
-
import
|
|
2022
|
-
import
|
|
2387
|
+
import fs13 from "fs";
|
|
2388
|
+
import path19 from "path";
|
|
2023
2389
|
init_errors();
|
|
2024
2390
|
init_http_client();
|
|
2025
2391
|
|
|
2026
2392
|
// src/providers/gemini/task-resolver.ts
|
|
2027
|
-
import
|
|
2393
|
+
import path18 from "path";
|
|
2028
2394
|
var MIME_MAP = {
|
|
2029
2395
|
// Audio
|
|
2030
2396
|
".mp3": "audio/mp3",
|
|
@@ -2065,7 +2431,7 @@ var MIME_MAP = {
|
|
|
2065
2431
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
2066
2432
|
};
|
|
2067
2433
|
function getMimeType(filePath) {
|
|
2068
|
-
const ext =
|
|
2434
|
+
const ext = path18.extname(filePath).toLowerCase();
|
|
2069
2435
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
2070
2436
|
}
|
|
2071
2437
|
function requiresProcessingWait(mimeType) {
|
|
@@ -2091,8 +2457,8 @@ async function uploadFile(filePath, opts = {}) {
|
|
|
2091
2457
|
const apiKey = requireApiKey();
|
|
2092
2458
|
const { timeoutMs = 3e5, logger } = opts;
|
|
2093
2459
|
const mimeType = getMimeType(filePath);
|
|
2094
|
-
const fileBytes =
|
|
2095
|
-
const displayName =
|
|
2460
|
+
const fileBytes = fs13.readFileSync(filePath);
|
|
2461
|
+
const displayName = path19.basename(filePath);
|
|
2096
2462
|
const boundary = `multix_${Date.now()}`;
|
|
2097
2463
|
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
2098
2464
|
const body = Buffer.concat([
|
|
@@ -2181,12 +2547,12 @@ function extractImages(resp) {
|
|
|
2181
2547
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
2182
2548
|
}
|
|
2183
2549
|
function inlinePart(filePath) {
|
|
2184
|
-
const data =
|
|
2550
|
+
const data = fs13.readFileSync(filePath).toString("base64");
|
|
2185
2551
|
const mimeType = getMimeType(filePath);
|
|
2186
2552
|
return { inlineData: { mimeType, data } };
|
|
2187
2553
|
}
|
|
2188
2554
|
function shouldUseFileApi(filePath) {
|
|
2189
|
-
const stat =
|
|
2555
|
+
const stat = fs13.statSync(filePath);
|
|
2190
2556
|
return stat.size > 20 * 1024 * 1024;
|
|
2191
2557
|
}
|
|
2192
2558
|
function sleep(ms) {
|
|
@@ -2239,13 +2605,13 @@ function getDefaultModel(task) {
|
|
|
2239
2605
|
}
|
|
2240
2606
|
|
|
2241
2607
|
// src/providers/gemini/result-formatter.ts
|
|
2242
|
-
import
|
|
2243
|
-
import
|
|
2608
|
+
import fs14 from "fs";
|
|
2609
|
+
import path20 from "path";
|
|
2244
2610
|
function formatResults(results, format, outputPath) {
|
|
2245
2611
|
const content = renderFormat(results, format);
|
|
2246
2612
|
if (outputPath) {
|
|
2247
|
-
|
|
2248
|
-
|
|
2613
|
+
fs14.mkdirSync(path20.dirname(path20.resolve(outputPath)), { recursive: true });
|
|
2614
|
+
fs14.writeFileSync(outputPath, content, "utf8");
|
|
2249
2615
|
} else {
|
|
2250
2616
|
process.stdout.write(content);
|
|
2251
2617
|
}
|
|
@@ -2312,7 +2678,7 @@ function registerAnalyzeCommand(parent) {
|
|
|
2312
2678
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2313
2679
|
const model = opts.model ?? getDefaultModel("analyze");
|
|
2314
2680
|
for (const f of opts.files) {
|
|
2315
|
-
if (!
|
|
2681
|
+
if (!fs15.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2316
2682
|
}
|
|
2317
2683
|
const results = [];
|
|
2318
2684
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -2358,14 +2724,14 @@ function registerAnalyzeCommand(parent) {
|
|
|
2358
2724
|
|
|
2359
2725
|
// src/providers/gemini/commands/extract.ts
|
|
2360
2726
|
init_errors();
|
|
2361
|
-
import
|
|
2727
|
+
import fs16 from "fs";
|
|
2362
2728
|
function registerExtractCommand(parent) {
|
|
2363
2729
|
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(
|
|
2364
2730
|
async (opts) => {
|
|
2365
2731
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2366
2732
|
const model = opts.model ?? getDefaultModel("extract");
|
|
2367
2733
|
for (const f of opts.files) {
|
|
2368
|
-
if (!
|
|
2734
|
+
if (!fs16.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2369
2735
|
}
|
|
2370
2736
|
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
2371
2737
|
const results = [];
|
|
@@ -2407,8 +2773,8 @@ function registerExtractCommand(parent) {
|
|
|
2407
2773
|
init_errors();
|
|
2408
2774
|
|
|
2409
2775
|
// src/providers/gemini/generators/speech.ts
|
|
2410
|
-
import
|
|
2411
|
-
import
|
|
2776
|
+
import fs17 from "fs";
|
|
2777
|
+
import path21 from "path";
|
|
2412
2778
|
|
|
2413
2779
|
// src/providers/gemini/voices.ts
|
|
2414
2780
|
var GEMINI_TTS_VOICES = [
|
|
@@ -2491,12 +2857,12 @@ async function generateGeminiSpeech(opts) {
|
|
|
2491
2857
|
const sampleRate = parseSampleRate(audio.mimeType) ?? TTS_PCM_SAMPLE_RATE;
|
|
2492
2858
|
const outBytes = outputFormat === "wav" ? wrapPcmInWav(pcmBytes, sampleRate, TTS_PCM_CHANNELS, TTS_PCM_BITS_PER_SAMPLE) : pcmBytes;
|
|
2493
2859
|
const outDir = getOutputDir();
|
|
2494
|
-
const dest =
|
|
2495
|
-
|
|
2860
|
+
const dest = path21.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
2861
|
+
fs17.writeFileSync(dest, outBytes);
|
|
2496
2862
|
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
2497
2863
|
if (output) {
|
|
2498
|
-
|
|
2499
|
-
|
|
2864
|
+
fs17.mkdirSync(path21.dirname(path21.resolve(output)), { recursive: true });
|
|
2865
|
+
fs17.copyFileSync(dest, output);
|
|
2500
2866
|
logger?.success(`Copied to: ${output}`);
|
|
2501
2867
|
}
|
|
2502
2868
|
return { status: "success", generatedAudio: dest, model, mimeType: audio.mimeType };
|
|
@@ -2595,8 +2961,8 @@ function collectSpeakers(value, prev) {
|
|
|
2595
2961
|
}
|
|
2596
2962
|
|
|
2597
2963
|
// src/providers/gemini/commands/generate-video.ts
|
|
2598
|
-
import
|
|
2599
|
-
import
|
|
2964
|
+
import fs18 from "fs";
|
|
2965
|
+
import path22 from "path";
|
|
2600
2966
|
init_errors();
|
|
2601
2967
|
init_http_client();
|
|
2602
2968
|
var BASE2 = "https://generativelanguage.googleapis.com";
|
|
@@ -2630,10 +2996,10 @@ function registerGenerateVideoCommand(parent) {
|
|
|
2630
2996
|
const contentParts = [{ text: opts.prompt }];
|
|
2631
2997
|
if (opts.referenceImages?.length) {
|
|
2632
2998
|
for (const imgPath of opts.referenceImages.slice(0, 2)) {
|
|
2633
|
-
if (!
|
|
2999
|
+
if (!fs18.existsSync(imgPath))
|
|
2634
3000
|
throw new ValidationError(`Reference image not found: ${imgPath}`);
|
|
2635
|
-
const data =
|
|
2636
|
-
const ext =
|
|
3001
|
+
const data = fs18.readFileSync(imgPath).toString("base64");
|
|
3002
|
+
const ext = path22.extname(imgPath).toLowerCase();
|
|
2637
3003
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
2638
3004
|
contentParts.push({ inlineData: { mimeType, data } });
|
|
2639
3005
|
}
|
|
@@ -2682,13 +3048,13 @@ function registerGenerateVideoCommand(parent) {
|
|
|
2682
3048
|
);
|
|
2683
3049
|
}
|
|
2684
3050
|
const outDir = getOutputDir();
|
|
2685
|
-
const dest =
|
|
3051
|
+
const dest = path22.join(outDir, `veo_generated_${Date.now()}.mp4`);
|
|
2686
3052
|
const { downloadFile: downloadFile2 } = await Promise.resolve().then(() => (init_http_client(), http_client_exports));
|
|
2687
3053
|
await downloadFile2(videoUri, dest);
|
|
2688
3054
|
logger.success(`Saved video: ${dest}`);
|
|
2689
3055
|
if (opts.output) {
|
|
2690
|
-
|
|
2691
|
-
|
|
3056
|
+
fs18.mkdirSync(path22.dirname(path22.resolve(opts.output)), { recursive: true });
|
|
3057
|
+
fs18.copyFileSync(dest, opts.output);
|
|
2692
3058
|
logger.success(`Copied to: ${opts.output}`);
|
|
2693
3059
|
}
|
|
2694
3060
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -2707,8 +3073,8 @@ function sleep2(ms) {
|
|
|
2707
3073
|
|
|
2708
3074
|
// src/providers/gemini/commands/generate.ts
|
|
2709
3075
|
init_errors();
|
|
2710
|
-
import
|
|
2711
|
-
import
|
|
3076
|
+
import fs19 from "fs";
|
|
3077
|
+
import path23 from "path";
|
|
2712
3078
|
function registerGenerateCommand(parent) {
|
|
2713
3079
|
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(
|
|
2714
3080
|
async (opts) => {
|
|
@@ -2750,8 +3116,8 @@ function registerGenerateCommand(parent) {
|
|
|
2750
3116
|
const outDir = getOutputDir();
|
|
2751
3117
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
2752
3118
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
2753
|
-
const dest =
|
|
2754
|
-
|
|
3119
|
+
const dest = path23.join(outDir, fname);
|
|
3120
|
+
fs19.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
2755
3121
|
savedFiles.push(dest);
|
|
2756
3122
|
logger.success(`Saved: ${dest}`);
|
|
2757
3123
|
}
|
|
@@ -2766,8 +3132,8 @@ function registerGenerateCommand(parent) {
|
|
|
2766
3132
|
process.exit(1);
|
|
2767
3133
|
}
|
|
2768
3134
|
if (opts.output && savedFiles[0]) {
|
|
2769
|
-
|
|
2770
|
-
|
|
3135
|
+
fs19.mkdirSync(path23.dirname(path23.resolve(opts.output)), { recursive: true });
|
|
3136
|
+
fs19.copyFileSync(savedFiles[0], opts.output);
|
|
2771
3137
|
logger.success(`Copied to: ${opts.output}`);
|
|
2772
3138
|
}
|
|
2773
3139
|
console.log(`
|
|
@@ -2778,8 +3144,8 @@ Generated ${savedFiles.length} image(s):`);
|
|
|
2778
3144
|
}
|
|
2779
3145
|
|
|
2780
3146
|
// src/providers/gemini/commands/image-to-image.ts
|
|
2781
|
-
import
|
|
2782
|
-
import
|
|
3147
|
+
import fs20 from "fs";
|
|
3148
|
+
import path24 from "path";
|
|
2783
3149
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
2784
3150
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
2785
3151
|
function registerGeminiImageToImageCommand(parent) {
|
|
@@ -2840,14 +3206,14 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
2840
3206
|
const img = images[i];
|
|
2841
3207
|
if (!img) continue;
|
|
2842
3208
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
2843
|
-
const dest =
|
|
2844
|
-
|
|
3209
|
+
const dest = path24.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
3210
|
+
fs20.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
2845
3211
|
saved.push(dest);
|
|
2846
3212
|
logger.success(`Saved: ${dest}`);
|
|
2847
3213
|
}
|
|
2848
3214
|
if (opts.output && saved[0]) {
|
|
2849
|
-
|
|
2850
|
-
|
|
3215
|
+
fs20.mkdirSync(path24.dirname(path24.resolve(opts.output)), { recursive: true });
|
|
3216
|
+
fs20.copyFileSync(saved[0], opts.output);
|
|
2851
3217
|
logger.success(`Copied to: ${opts.output}`);
|
|
2852
3218
|
}
|
|
2853
3219
|
console.log(`
|
|
@@ -2874,7 +3240,7 @@ function sniffImageMime(buf, url) {
|
|
|
2874
3240
|
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
2875
3241
|
return "image/webp";
|
|
2876
3242
|
}
|
|
2877
|
-
const ext =
|
|
3243
|
+
const ext = path24.extname(new URL(url).pathname).toLowerCase();
|
|
2878
3244
|
if (ext === ".png") return "image/png";
|
|
2879
3245
|
if (ext === ".webp") return "image/webp";
|
|
2880
3246
|
if (ext === ".gif") return "image/gif";
|
|
@@ -2882,17 +3248,17 @@ function sniffImageMime(buf, url) {
|
|
|
2882
3248
|
}
|
|
2883
3249
|
|
|
2884
3250
|
// src/providers/gemini/commands/image-to-video.ts
|
|
2885
|
-
import
|
|
2886
|
-
import
|
|
3251
|
+
import fs21 from "fs";
|
|
3252
|
+
import path25 from "path";
|
|
2887
3253
|
init_errors();
|
|
2888
3254
|
init_http_client();
|
|
2889
3255
|
var BASE3 = "https://generativelanguage.googleapis.com";
|
|
2890
3256
|
function imagePart(imgPath) {
|
|
2891
|
-
if (!
|
|
3257
|
+
if (!fs21.existsSync(imgPath)) {
|
|
2892
3258
|
throw new ValidationError(`Image not found: ${imgPath}`);
|
|
2893
3259
|
}
|
|
2894
|
-
const data =
|
|
2895
|
-
const ext =
|
|
3260
|
+
const data = fs21.readFileSync(imgPath).toString("base64");
|
|
3261
|
+
const ext = path25.extname(imgPath).toLowerCase();
|
|
2896
3262
|
const mimeType = ext === ".png" ? "image/png" : "image/jpeg";
|
|
2897
3263
|
return { inlineData: { mimeType, data } };
|
|
2898
3264
|
}
|
|
@@ -2965,12 +3331,12 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
2965
3331
|
"gemini"
|
|
2966
3332
|
);
|
|
2967
3333
|
}
|
|
2968
|
-
const dest =
|
|
3334
|
+
const dest = path25.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
2969
3335
|
await downloadFile(videoUri, dest);
|
|
2970
3336
|
logger.success(`Saved video: ${dest}`);
|
|
2971
3337
|
if (opts.output) {
|
|
2972
|
-
|
|
2973
|
-
|
|
3338
|
+
fs21.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
|
|
3339
|
+
fs21.copyFileSync(dest, opts.output);
|
|
2974
3340
|
logger.success(`Copied to: ${opts.output}`);
|
|
2975
3341
|
}
|
|
2976
3342
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -2986,14 +3352,14 @@ Generated video: ${dest}`);
|
|
|
2986
3352
|
|
|
2987
3353
|
// src/providers/gemini/commands/transcribe.ts
|
|
2988
3354
|
init_errors();
|
|
2989
|
-
import
|
|
3355
|
+
import fs22 from "fs";
|
|
2990
3356
|
function registerTranscribeCommand2(parent) {
|
|
2991
3357
|
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(
|
|
2992
3358
|
async (opts) => {
|
|
2993
3359
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2994
3360
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
2995
3361
|
for (const f of opts.files) {
|
|
2996
|
-
if (!
|
|
3362
|
+
if (!fs22.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2997
3363
|
}
|
|
2998
3364
|
const results = [];
|
|
2999
3365
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -3037,7 +3403,7 @@ function registerTranscribeCommand2(parent) {
|
|
|
3037
3403
|
// src/providers/gemini/commands/index.ts
|
|
3038
3404
|
function registerGeminiCommands(program2) {
|
|
3039
3405
|
const gemini = program2.command("gemini").description(
|
|
3040
|
-
"Gemini
|
|
3406
|
+
"Gemini: analyze, transcribe, extract data, generate images (Imagen/Nano Banana), video (Veo), and speech (Flash TTS)"
|
|
3041
3407
|
);
|
|
3042
3408
|
registerAnalyzeCommand(gemini);
|
|
3043
3409
|
registerTranscribeCommand2(gemini);
|
|
@@ -3051,8 +3417,8 @@ function registerGeminiCommands(program2) {
|
|
|
3051
3417
|
|
|
3052
3418
|
// src/providers/leonardo/commands/generate.ts
|
|
3053
3419
|
init_http_client();
|
|
3054
|
-
import
|
|
3055
|
-
import
|
|
3420
|
+
import fs23 from "fs";
|
|
3421
|
+
import path26 from "path";
|
|
3056
3422
|
|
|
3057
3423
|
// src/providers/leonardo/client.ts
|
|
3058
3424
|
init_errors();
|
|
@@ -3126,11 +3492,11 @@ var LeonardoClient = class {
|
|
|
3126
3492
|
}
|
|
3127
3493
|
apiKey;
|
|
3128
3494
|
baseUrl;
|
|
3129
|
-
async request(
|
|
3495
|
+
async request(path46, opts = {}) {
|
|
3130
3496
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
3131
|
-
const v2Override =
|
|
3497
|
+
const v2Override = path46.startsWith("v2:");
|
|
3132
3498
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
3133
|
-
const finalPath = v2Override ?
|
|
3499
|
+
const finalPath = v2Override ? path46.slice(3) : path46;
|
|
3134
3500
|
const url = new URL(finalBase + finalPath);
|
|
3135
3501
|
if (opts.query) {
|
|
3136
3502
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -3149,14 +3515,14 @@ var LeonardoClient = class {
|
|
|
3149
3515
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
3150
3516
|
});
|
|
3151
3517
|
}
|
|
3152
|
-
get(
|
|
3153
|
-
return this.request(
|
|
3518
|
+
get(path46, query, logger) {
|
|
3519
|
+
return this.request(path46, { method: "GET", query, logger });
|
|
3154
3520
|
}
|
|
3155
|
-
post(
|
|
3156
|
-
return this.request(
|
|
3521
|
+
post(path46, body, logger) {
|
|
3522
|
+
return this.request(path46, { method: "POST", body, logger });
|
|
3157
3523
|
}
|
|
3158
|
-
delete(
|
|
3159
|
-
return this.request(
|
|
3524
|
+
delete(path46, logger) {
|
|
3525
|
+
return this.request(path46, { method: "DELETE", logger });
|
|
3160
3526
|
}
|
|
3161
3527
|
};
|
|
3162
3528
|
function createLeonardoClient() {
|
|
@@ -3167,7 +3533,7 @@ function createLeonardoClient() {
|
|
|
3167
3533
|
function extFromUrl(url, fallback = ".png") {
|
|
3168
3534
|
try {
|
|
3169
3535
|
const u = new URL(url);
|
|
3170
|
-
const e =
|
|
3536
|
+
const e = path26.extname(u.pathname);
|
|
3171
3537
|
return e.length > 0 ? e : fallback;
|
|
3172
3538
|
} catch {
|
|
3173
3539
|
return fallback;
|
|
@@ -3252,7 +3618,7 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
3252
3618
|
if (!img) continue;
|
|
3253
3619
|
const ext = extFromUrl(img.url);
|
|
3254
3620
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
3255
|
-
const filepath =
|
|
3621
|
+
const filepath = path26.join(outDir, filename);
|
|
3256
3622
|
try {
|
|
3257
3623
|
await downloadFile(img.url, filepath);
|
|
3258
3624
|
saved.push(filepath);
|
|
@@ -3262,9 +3628,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
3262
3628
|
}
|
|
3263
3629
|
}
|
|
3264
3630
|
if (opts.output && saved[0]) {
|
|
3265
|
-
const dest =
|
|
3266
|
-
|
|
3267
|
-
|
|
3631
|
+
const dest = path26.resolve(opts.output);
|
|
3632
|
+
fs23.mkdirSync(path26.dirname(dest), { recursive: true });
|
|
3633
|
+
fs23.copyFileSync(saved[0], dest);
|
|
3268
3634
|
logger.success(`Copied to ${dest}`);
|
|
3269
3635
|
}
|
|
3270
3636
|
console.log(`
|
|
@@ -3276,11 +3642,11 @@ Generated ${saved.length} image(s):`);
|
|
|
3276
3642
|
|
|
3277
3643
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
3278
3644
|
init_http_client();
|
|
3279
|
-
import
|
|
3280
|
-
import
|
|
3645
|
+
import fs24 from "fs";
|
|
3646
|
+
import path27 from "path";
|
|
3281
3647
|
function extFromUrl2(url, fallback = ".png") {
|
|
3282
3648
|
try {
|
|
3283
|
-
return
|
|
3649
|
+
return path27.extname(new URL(url).pathname) || fallback;
|
|
3284
3650
|
} catch {
|
|
3285
3651
|
return fallback;
|
|
3286
3652
|
}
|
|
@@ -3342,7 +3708,7 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
3342
3708
|
if (!img) continue;
|
|
3343
3709
|
const ext = extFromUrl2(img.url);
|
|
3344
3710
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
3345
|
-
const filepath =
|
|
3711
|
+
const filepath = path27.join(outDir, filename);
|
|
3346
3712
|
try {
|
|
3347
3713
|
await downloadFile(img.url, filepath);
|
|
3348
3714
|
saved.push(filepath);
|
|
@@ -3352,9 +3718,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
3352
3718
|
}
|
|
3353
3719
|
}
|
|
3354
3720
|
if (opts.output && saved[0]) {
|
|
3355
|
-
const dest =
|
|
3356
|
-
|
|
3357
|
-
|
|
3721
|
+
const dest = path27.resolve(opts.output);
|
|
3722
|
+
fs24.mkdirSync(path27.dirname(dest), { recursive: true });
|
|
3723
|
+
fs24.copyFileSync(saved[0], dest);
|
|
3358
3724
|
logger.success(`Copied to ${dest}`);
|
|
3359
3725
|
}
|
|
3360
3726
|
console.log(`
|
|
@@ -3366,7 +3732,7 @@ Generated ${saved.length} image(s):`);
|
|
|
3366
3732
|
|
|
3367
3733
|
// src/providers/leonardo/commands/video.ts
|
|
3368
3734
|
init_http_client();
|
|
3369
|
-
import
|
|
3735
|
+
import path28 from "path";
|
|
3370
3736
|
function v2DimsFor(resolution) {
|
|
3371
3737
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
3372
3738
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -3466,19 +3832,19 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
3466
3832
|
logger.error("No generation in response");
|
|
3467
3833
|
process.exit(1);
|
|
3468
3834
|
}
|
|
3469
|
-
const
|
|
3835
|
+
const videoUrl2 = gen.generated_images?.find(
|
|
3470
3836
|
(img) => typeof img.url === "string" && /\.(mp4|webm|mov)(\?|$)/i.test(img.url)
|
|
3471
3837
|
)?.url ?? gen.generated_images?.[0]?.motionMP4URL;
|
|
3472
3838
|
if (!opts.download) {
|
|
3473
|
-
if (typeof
|
|
3839
|
+
if (typeof videoUrl2 === "string") console.log(videoUrl2);
|
|
3474
3840
|
return;
|
|
3475
3841
|
}
|
|
3476
|
-
if (typeof
|
|
3842
|
+
if (typeof videoUrl2 !== "string") {
|
|
3477
3843
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
3478
3844
|
process.exit(1);
|
|
3479
3845
|
}
|
|
3480
|
-
const outPath = opts.output ??
|
|
3481
|
-
await downloadFile(
|
|
3846
|
+
const outPath = opts.output ?? path28.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
3847
|
+
await downloadFile(videoUrl2, outPath);
|
|
3482
3848
|
logger.success(`Saved ${outPath}`);
|
|
3483
3849
|
await maybeDownloadThumb(gen, outPath, {
|
|
3484
3850
|
skip: !opts.thumb,
|
|
@@ -3584,7 +3950,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
3584
3950
|
|
|
3585
3951
|
// src/providers/leonardo/commands/status.ts
|
|
3586
3952
|
init_http_client();
|
|
3587
|
-
import
|
|
3953
|
+
import path29 from "path";
|
|
3588
3954
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
3589
3955
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
3590
3956
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -3649,7 +4015,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
3649
4015
|
const outDir = getOutputDir();
|
|
3650
4016
|
let idx = 0;
|
|
3651
4017
|
for (const url of videos) {
|
|
3652
|
-
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output :
|
|
4018
|
+
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
|
|
3653
4019
|
await downloadFile(url, dest);
|
|
3654
4020
|
logger.success(`Saved ${dest}`);
|
|
3655
4021
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -3662,7 +4028,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
3662
4028
|
for (const url of images) {
|
|
3663
4029
|
const m = url.match(IMAGE_EXT_RE2);
|
|
3664
4030
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
3665
|
-
const dest =
|
|
4031
|
+
const dest = path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
3666
4032
|
await downloadFile(url, dest);
|
|
3667
4033
|
logger.success(`Saved ${dest}`);
|
|
3668
4034
|
idx++;
|
|
@@ -3722,7 +4088,9 @@ Resolutions: ${LEONARDO_VIDEO_RESOLUTIONS.join(", ")}`);
|
|
|
3722
4088
|
|
|
3723
4089
|
// src/providers/leonardo/commands/index.ts
|
|
3724
4090
|
function registerLeonardoCommands(program2) {
|
|
3725
|
-
const leonardo = program2.command("leonardo").description(
|
|
4091
|
+
const leonardo = program2.command("leonardo").description(
|
|
4092
|
+
"Leonardo: image generation, video (Motion, Veo, Kling, Hailuo, Seedance, LTX), upscaling, and account info"
|
|
4093
|
+
);
|
|
3726
4094
|
registerLeonardoGenerateCommand(leonardo);
|
|
3727
4095
|
registerLeonardoImageToImageCommand(leonardo);
|
|
3728
4096
|
registerLeonardoVideoCommand(leonardo);
|
|
@@ -3819,8 +4187,8 @@ function sleep4(ms) {
|
|
|
3819
4187
|
|
|
3820
4188
|
// src/providers/minimax/generators/music.ts
|
|
3821
4189
|
init_http_client();
|
|
3822
|
-
import
|
|
3823
|
-
import
|
|
4190
|
+
import fs25 from "fs";
|
|
4191
|
+
import path30 from "path";
|
|
3824
4192
|
async function generateMinimaxMusic(opts) {
|
|
3825
4193
|
const {
|
|
3826
4194
|
apiKey,
|
|
@@ -3865,14 +4233,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
3865
4233
|
}
|
|
3866
4234
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
3867
4235
|
const outDir = getOutputDir();
|
|
3868
|
-
const dest =
|
|
4236
|
+
const dest = path30.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
3869
4237
|
try {
|
|
3870
4238
|
if (audioData.startsWith("http")) {
|
|
3871
4239
|
const bytes = await fetchBytes(audioData);
|
|
3872
|
-
|
|
4240
|
+
fs25.writeFileSync(dest, bytes);
|
|
3873
4241
|
} else {
|
|
3874
4242
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
3875
|
-
|
|
4243
|
+
fs25.writeFileSync(dest, audioBytes);
|
|
3876
4244
|
}
|
|
3877
4245
|
} catch (e) {
|
|
3878
4246
|
return {
|
|
@@ -3883,8 +4251,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
3883
4251
|
const durSec = durationMs / 1e3;
|
|
3884
4252
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
3885
4253
|
if (output) {
|
|
3886
|
-
|
|
3887
|
-
|
|
4254
|
+
fs25.mkdirSync(path30.dirname(path30.resolve(output)), { recursive: true });
|
|
4255
|
+
fs25.copyFileSync(dest, output);
|
|
3888
4256
|
}
|
|
3889
4257
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
3890
4258
|
}
|
|
@@ -3958,8 +4326,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
3958
4326
|
init_errors();
|
|
3959
4327
|
|
|
3960
4328
|
// src/providers/minimax/generators/speech.ts
|
|
3961
|
-
import
|
|
3962
|
-
import
|
|
4329
|
+
import fs26 from "fs";
|
|
4330
|
+
import path31 from "path";
|
|
3963
4331
|
async function generateMinimaxSpeech(opts) {
|
|
3964
4332
|
const {
|
|
3965
4333
|
apiKey,
|
|
@@ -4006,12 +4374,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
4006
4374
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
4007
4375
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
4008
4376
|
const outDir = getOutputDir();
|
|
4009
|
-
const dest =
|
|
4010
|
-
|
|
4377
|
+
const dest = path31.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
4378
|
+
fs26.writeFileSync(dest, audioBytes);
|
|
4011
4379
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
4012
4380
|
if (output) {
|
|
4013
|
-
|
|
4014
|
-
|
|
4381
|
+
fs26.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
|
|
4382
|
+
fs26.copyFileSync(dest, output);
|
|
4015
4383
|
}
|
|
4016
4384
|
return { status: "success", generatedAudio: dest, model };
|
|
4017
4385
|
}
|
|
@@ -4058,8 +4426,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
4058
4426
|
init_errors();
|
|
4059
4427
|
|
|
4060
4428
|
// src/providers/minimax/generators/video.ts
|
|
4061
|
-
import
|
|
4062
|
-
import
|
|
4429
|
+
import fs27 from "fs";
|
|
4430
|
+
import path32 from "path";
|
|
4063
4431
|
async function generateMinimaxVideo(opts) {
|
|
4064
4432
|
const {
|
|
4065
4433
|
apiKey,
|
|
@@ -4105,18 +4473,18 @@ async function generateMinimaxVideo(opts) {
|
|
|
4105
4473
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
4106
4474
|
}
|
|
4107
4475
|
const outDir = getOutputDir();
|
|
4108
|
-
const dest =
|
|
4476
|
+
const dest = path32.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
4109
4477
|
try {
|
|
4110
4478
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
4111
4479
|
} catch (e) {
|
|
4112
4480
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
4113
4481
|
}
|
|
4114
4482
|
const elapsed = (Date.now() - start) / 1e3;
|
|
4115
|
-
const stat =
|
|
4483
|
+
const stat = fs27.statSync(dest);
|
|
4116
4484
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
4117
4485
|
if (output) {
|
|
4118
|
-
|
|
4119
|
-
|
|
4486
|
+
fs27.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
|
|
4487
|
+
fs27.copyFileSync(dest, output);
|
|
4120
4488
|
}
|
|
4121
4489
|
await maybeDownloadThumb(pollResult, dest, {
|
|
4122
4490
|
skip: thumb === false,
|
|
@@ -4179,8 +4547,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
4179
4547
|
// src/providers/minimax/generators/image.ts
|
|
4180
4548
|
init_errors();
|
|
4181
4549
|
init_http_client();
|
|
4182
|
-
import
|
|
4183
|
-
import
|
|
4550
|
+
import fs28 from "fs";
|
|
4551
|
+
import path33 from "path";
|
|
4184
4552
|
async function generateMinimaxImage(opts) {
|
|
4185
4553
|
const {
|
|
4186
4554
|
apiKey,
|
|
@@ -4218,8 +4586,8 @@ async function generateMinimaxImage(opts) {
|
|
|
4218
4586
|
try {
|
|
4219
4587
|
const bytes = await fetchBytes(url);
|
|
4220
4588
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
4221
|
-
const dest =
|
|
4222
|
-
|
|
4589
|
+
const dest = path33.join(outDir, fname);
|
|
4590
|
+
fs28.writeFileSync(dest, bytes);
|
|
4223
4591
|
savedFiles.push(dest);
|
|
4224
4592
|
logger?.success(`Saved: ${dest}`);
|
|
4225
4593
|
} catch (e) {
|
|
@@ -4230,8 +4598,8 @@ async function generateMinimaxImage(opts) {
|
|
|
4230
4598
|
}
|
|
4231
4599
|
}
|
|
4232
4600
|
if (output && savedFiles[0]) {
|
|
4233
|
-
|
|
4234
|
-
|
|
4601
|
+
fs28.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
|
|
4602
|
+
fs28.copyFileSync(savedFiles[0], output);
|
|
4235
4603
|
}
|
|
4236
4604
|
return { status: "success", generatedImages: savedFiles, model };
|
|
4237
4605
|
}
|
|
@@ -4267,8 +4635,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
4267
4635
|
// src/providers/minimax/commands/image-to-image.ts
|
|
4268
4636
|
init_errors();
|
|
4269
4637
|
init_http_client();
|
|
4270
|
-
import
|
|
4271
|
-
import
|
|
4638
|
+
import fs29 from "fs";
|
|
4639
|
+
import path34 from "path";
|
|
4272
4640
|
function registerMinimaxImageToImageCommand(parent) {
|
|
4273
4641
|
parent.command("image-to-image").alias("i2i").description(
|
|
4274
4642
|
"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."
|
|
@@ -4323,8 +4691,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
4323
4691
|
if (!url) continue;
|
|
4324
4692
|
try {
|
|
4325
4693
|
const bytes = await fetchBytes(url);
|
|
4326
|
-
const dest =
|
|
4327
|
-
|
|
4694
|
+
const dest = path34.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
4695
|
+
fs29.writeFileSync(dest, bytes);
|
|
4328
4696
|
saved.push(dest);
|
|
4329
4697
|
logger.success(`Saved: ${dest}`);
|
|
4330
4698
|
} catch (e) {
|
|
@@ -4335,8 +4703,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
4335
4703
|
}
|
|
4336
4704
|
}
|
|
4337
4705
|
if (opts.output && saved[0]) {
|
|
4338
|
-
|
|
4339
|
-
|
|
4706
|
+
fs29.mkdirSync(path34.dirname(path34.resolve(opts.output)), { recursive: true });
|
|
4707
|
+
fs29.copyFileSync(saved[0], opts.output);
|
|
4340
4708
|
logger.success(`Copied to: ${opts.output}`);
|
|
4341
4709
|
}
|
|
4342
4710
|
console.log(`
|
|
@@ -4348,7 +4716,7 @@ Generated ${saved.length} image(s):`);
|
|
|
4348
4716
|
|
|
4349
4717
|
// src/providers/minimax/commands/index.ts
|
|
4350
4718
|
function registerMinimaxCommands(program2) {
|
|
4351
|
-
const minimax = program2.command("minimax").description("MiniMax
|
|
4719
|
+
const minimax = program2.command("minimax").description("MiniMax: generate images, video (Hailuo), speech (TTS), and music");
|
|
4352
4720
|
registerMinimaxGenerateCommand(minimax);
|
|
4353
4721
|
registerMinimaxImageToImageCommand(minimax);
|
|
4354
4722
|
registerMinimaxGenerateVideoCommand(minimax);
|
|
@@ -4470,12 +4838,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
4470
4838
|
|
|
4471
4839
|
// src/providers/openai/openai-audio.ts
|
|
4472
4840
|
init_http_client();
|
|
4473
|
-
import
|
|
4474
|
-
import
|
|
4841
|
+
import fs31 from "fs";
|
|
4842
|
+
import path36 from "path";
|
|
4475
4843
|
|
|
4476
4844
|
// src/providers/openai/client.ts
|
|
4477
|
-
import
|
|
4478
|
-
import
|
|
4845
|
+
import fs30 from "fs";
|
|
4846
|
+
import path35 from "path";
|
|
4479
4847
|
init_errors();
|
|
4480
4848
|
init_http_client();
|
|
4481
4849
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -4542,10 +4910,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
4542
4910
|
return Buffer.from(await res.arrayBuffer());
|
|
4543
4911
|
}
|
|
4544
4912
|
function fileFromPath(filePath) {
|
|
4545
|
-
const abs =
|
|
4546
|
-
if (!
|
|
4547
|
-
const bytes =
|
|
4548
|
-
return new File([new Uint8Array(bytes)],
|
|
4913
|
+
const abs = path35.resolve(filePath);
|
|
4914
|
+
if (!fs30.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
4915
|
+
const bytes = fs30.readFileSync(abs);
|
|
4916
|
+
return new File([new Uint8Array(bytes)], path35.basename(abs));
|
|
4549
4917
|
}
|
|
4550
4918
|
|
|
4551
4919
|
// src/providers/openai/openai-audio.ts
|
|
@@ -4584,7 +4952,7 @@ async function resolveAudioReferenceDataUrl(ref) {
|
|
|
4584
4952
|
}
|
|
4585
4953
|
function audioMimeFromReference(ref) {
|
|
4586
4954
|
const pathname = new URL(ref).pathname;
|
|
4587
|
-
const ext =
|
|
4955
|
+
const ext = path36.extname(pathname).toLowerCase();
|
|
4588
4956
|
switch (ext) {
|
|
4589
4957
|
case ".mp3":
|
|
4590
4958
|
return "audio/mpeg";
|
|
@@ -4603,12 +4971,12 @@ function audioMimeFromReference(ref) {
|
|
|
4603
4971
|
async function generateOpenAISpeech(opts) {
|
|
4604
4972
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
4605
4973
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
4606
|
-
const dest =
|
|
4607
|
-
|
|
4974
|
+
const dest = path36.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
4975
|
+
fs31.writeFileSync(dest, bytes);
|
|
4608
4976
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
4609
4977
|
if (opts.output) {
|
|
4610
|
-
|
|
4611
|
-
|
|
4978
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
|
|
4979
|
+
fs31.copyFileSync(dest, opts.output);
|
|
4612
4980
|
}
|
|
4613
4981
|
return dest;
|
|
4614
4982
|
}
|
|
@@ -4630,9 +4998,9 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
4630
4998
|
);
|
|
4631
4999
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
4632
5000
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
4633
|
-
const dest = opts.output ?
|
|
4634
|
-
|
|
4635
|
-
|
|
5001
|
+
const dest = opts.output ? path36.resolve(opts.output) : path36.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
|
|
5002
|
+
fs31.mkdirSync(path36.dirname(dest), { recursive: true });
|
|
5003
|
+
fs31.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
4636
5004
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
4637
5005
|
return { path: dest, text };
|
|
4638
5006
|
}
|
|
@@ -4659,9 +5027,9 @@ Generated audio: ${dest}`);
|
|
|
4659
5027
|
}
|
|
4660
5028
|
|
|
4661
5029
|
// src/providers/openai/codex-image-driver.ts
|
|
4662
|
-
import
|
|
5030
|
+
import fs32 from "fs";
|
|
4663
5031
|
import os2 from "os";
|
|
4664
|
-
import
|
|
5032
|
+
import path37 from "path";
|
|
4665
5033
|
import { execa } from "execa";
|
|
4666
5034
|
init_errors();
|
|
4667
5035
|
var defaultRunner = async (cmd, args, opts) => {
|
|
@@ -4710,12 +5078,12 @@ function sanitizeCodexEnv() {
|
|
|
4710
5078
|
async function runCodexImageGeneration(opts) {
|
|
4711
5079
|
const runner = opts.runner ?? defaultRunner;
|
|
4712
5080
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
4713
|
-
const dest = opts.output ?
|
|
4714
|
-
|
|
4715
|
-
const tmp =
|
|
4716
|
-
const schema =
|
|
4717
|
-
const result =
|
|
4718
|
-
|
|
5081
|
+
const dest = opts.output ? path37.resolve(opts.output) : path37.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
|
|
5082
|
+
fs32.mkdirSync(path37.dirname(dest), { recursive: true });
|
|
5083
|
+
const tmp = fs32.mkdtempSync(path37.join(os2.tmpdir(), "multix-codex-image-"));
|
|
5084
|
+
const schema = path37.join(tmp, "schema.json");
|
|
5085
|
+
const result = path37.join(tmp, "result.json");
|
|
5086
|
+
fs32.writeFileSync(
|
|
4719
5087
|
schema,
|
|
4720
5088
|
JSON.stringify({
|
|
4721
5089
|
type: "object",
|
|
@@ -4745,7 +5113,7 @@ async function runCodexImageGeneration(opts) {
|
|
|
4745
5113
|
if (res.exitCode !== 0) {
|
|
4746
5114
|
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
4747
5115
|
}
|
|
4748
|
-
if (!
|
|
5116
|
+
if (!fs32.existsSync(dest) || fs32.statSync(dest).size === 0) {
|
|
4749
5117
|
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
4750
5118
|
}
|
|
4751
5119
|
return { path: dest };
|
|
@@ -4763,8 +5131,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
4763
5131
|
// src/providers/openai/openai-image.ts
|
|
4764
5132
|
init_errors();
|
|
4765
5133
|
init_http_client();
|
|
4766
|
-
import
|
|
4767
|
-
import
|
|
5134
|
+
import fs33 from "fs";
|
|
5135
|
+
import path38 from "path";
|
|
4768
5136
|
function buildImageGenerationBody(opts) {
|
|
4769
5137
|
return {
|
|
4770
5138
|
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
@@ -4797,14 +5165,14 @@ function saveOpenAIImages(opts) {
|
|
|
4797
5165
|
for (let i = 0; i < items.length; i++) {
|
|
4798
5166
|
const b64 = items[i]?.b64_json;
|
|
4799
5167
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
4800
|
-
const dest =
|
|
4801
|
-
|
|
5168
|
+
const dest = path38.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
5169
|
+
fs33.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
4802
5170
|
saved.push(dest);
|
|
4803
5171
|
opts.logger?.success(`Saved: ${dest}`);
|
|
4804
5172
|
}
|
|
4805
5173
|
if (opts.output && saved[0]) {
|
|
4806
|
-
|
|
4807
|
-
|
|
5174
|
+
fs33.mkdirSync(path38.dirname(path38.resolve(opts.output)), { recursive: true });
|
|
5175
|
+
fs33.copyFileSync(saved[0], opts.output);
|
|
4808
5176
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
4809
5177
|
}
|
|
4810
5178
|
return saved;
|
|
@@ -4813,7 +5181,7 @@ async function resolveImageRefBytes(ref) {
|
|
|
4813
5181
|
if (isHttpUrl(ref)) {
|
|
4814
5182
|
const bytes2 = await fetchBytes(ref);
|
|
4815
5183
|
const pathname = new URL(ref).pathname;
|
|
4816
|
-
const filename =
|
|
5184
|
+
const filename = path38.basename(pathname) || "reference.png";
|
|
4817
5185
|
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
4818
5186
|
}
|
|
4819
5187
|
const file = fileFromPath(ref);
|
|
@@ -4858,7 +5226,7 @@ function sniffImageMime2(buf, name) {
|
|
|
4858
5226
|
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
4859
5227
|
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
4860
5228
|
return "image/webp";
|
|
4861
|
-
const ext =
|
|
5229
|
+
const ext = path38.extname(name).toLowerCase();
|
|
4862
5230
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
4863
5231
|
if (ext === ".webp") return "image/webp";
|
|
4864
5232
|
return "image/png";
|
|
@@ -4976,7 +5344,9 @@ function collect6(value, prev) {
|
|
|
4976
5344
|
|
|
4977
5345
|
// src/providers/openai/commands/index.ts
|
|
4978
5346
|
function registerOpenAICommands(program2) {
|
|
4979
|
-
const openai = program2.command("openai").description(
|
|
5347
|
+
const openai = program2.command("openai").description(
|
|
5348
|
+
"OpenAI: image generation/editing (GPT-Image, Codex driver), text-to-speech (TTS), speech-to-text (STT)"
|
|
5349
|
+
);
|
|
4980
5350
|
registerOpenAIGenerateCommand(openai);
|
|
4981
5351
|
registerOpenAIImageToImageCommand(openai);
|
|
4982
5352
|
registerOpenAIGenerateSpeechCommand(openai);
|
|
@@ -4984,8 +5354,8 @@ function registerOpenAICommands(program2) {
|
|
|
4984
5354
|
}
|
|
4985
5355
|
|
|
4986
5356
|
// src/providers/openrouter/client.ts
|
|
4987
|
-
import
|
|
4988
|
-
import
|
|
5357
|
+
import fs34 from "fs";
|
|
5358
|
+
import path39 from "path";
|
|
4989
5359
|
init_errors();
|
|
4990
5360
|
init_http_client();
|
|
4991
5361
|
|
|
@@ -5150,8 +5520,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
5150
5520
|
if (!url) continue;
|
|
5151
5521
|
try {
|
|
5152
5522
|
const bytes = await fetchBytes(url);
|
|
5153
|
-
const dest =
|
|
5154
|
-
|
|
5523
|
+
const dest = path39.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
5524
|
+
fs34.writeFileSync(dest, bytes);
|
|
5155
5525
|
savedFiles.push(dest);
|
|
5156
5526
|
logger?.success(`Saved: ${dest}`);
|
|
5157
5527
|
} catch (e) {
|
|
@@ -5162,15 +5532,17 @@ async function generateOpenRouterImage(opts) {
|
|
|
5162
5532
|
}
|
|
5163
5533
|
}
|
|
5164
5534
|
if (output && savedFiles[0]) {
|
|
5165
|
-
|
|
5166
|
-
|
|
5535
|
+
fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
|
|
5536
|
+
fs34.writeFileSync(output, fs34.readFileSync(savedFiles[0]));
|
|
5167
5537
|
}
|
|
5168
5538
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
5169
5539
|
}
|
|
5170
5540
|
|
|
5171
5541
|
// src/providers/openrouter/commands/generate.ts
|
|
5172
5542
|
function registerOpenRouterGenerateCommand(parent) {
|
|
5173
|
-
parent.command("generate").description(
|
|
5543
|
+
parent.command("generate").description(
|
|
5544
|
+
"Generate images via OpenRouter (text-to-image); models: Gemini, GPT-Image, Flux, Recraft, Seedream"
|
|
5545
|
+
).requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "OpenRouter model id (e.g. google/gemini-3.1-flash-image-preview)").option("--aspect-ratio <ratio>", "Aspect ratio (e.g. 1:1, 16:9)", "1:1").option("--image-size <size>", "Image size hint (e.g. 1K, 2K)").option("--num-images <n>", "Number of images to generate", "1").option("--output <path>", "Copy first image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
5174
5546
|
async (opts) => {
|
|
5175
5547
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5176
5548
|
const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_OPENROUTER_MODEL;
|
|
@@ -5198,15 +5570,17 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
5198
5570
|
}
|
|
5199
5571
|
|
|
5200
5572
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
5201
|
-
import
|
|
5202
|
-
import
|
|
5573
|
+
import fs35 from "fs";
|
|
5574
|
+
import path40 from "path";
|
|
5203
5575
|
init_errors();
|
|
5204
5576
|
init_http_client();
|
|
5205
5577
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
5206
5578
|
var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
|
|
5207
5579
|
var HARD_LIMIT_BYTES2 = 8 * 1024 * 1024;
|
|
5208
5580
|
function registerOpenRouterImageToImageCommand(parent) {
|
|
5209
|
-
parent.command("image-to-image").alias("i2i").description(
|
|
5581
|
+
parent.command("image-to-image").alias("i2i").description(
|
|
5582
|
+
"Edit/transform images via OpenRouter (image-to-image); models: Gemini, GPT-Image, Flux, Recraft, Seedream"
|
|
5583
|
+
).requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
5210
5584
|
"--ref <path>",
|
|
5211
5585
|
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
5212
5586
|
collect7,
|
|
@@ -5289,14 +5663,14 @@ async function runImageToImage(opts) {
|
|
|
5289
5663
|
const url = parsed.urls[i];
|
|
5290
5664
|
if (!url) continue;
|
|
5291
5665
|
const bytes = await fetchBytes(url);
|
|
5292
|
-
const dest =
|
|
5293
|
-
|
|
5666
|
+
const dest = path40.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
5667
|
+
fs35.writeFileSync(dest, bytes);
|
|
5294
5668
|
saved.push(dest);
|
|
5295
5669
|
opts.logger.success(`Saved: ${dest}`);
|
|
5296
5670
|
}
|
|
5297
5671
|
if (opts.output && saved[0]) {
|
|
5298
|
-
|
|
5299
|
-
|
|
5672
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5673
|
+
fs35.copyFileSync(saved[0], opts.output);
|
|
5300
5674
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
5301
5675
|
}
|
|
5302
5676
|
return saved;
|
|
@@ -5342,13 +5716,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
5342
5716
|
}
|
|
5343
5717
|
|
|
5344
5718
|
// src/providers/openrouter/commands/video-status.ts
|
|
5345
|
-
import
|
|
5346
|
-
import
|
|
5719
|
+
import fs36 from "fs";
|
|
5720
|
+
import path41 from "path";
|
|
5347
5721
|
async function downloadOpenRouterVideo(opts) {
|
|
5348
5722
|
const { jobId, logger, source } = opts;
|
|
5349
5723
|
const apiKey = requireOpenRouterKey();
|
|
5350
5724
|
const url = videoContentUrl(jobId);
|
|
5351
|
-
const dest =
|
|
5725
|
+
const dest = path41.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
5352
5726
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
5353
5727
|
if (!r.ok) {
|
|
5354
5728
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -5358,14 +5732,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
5358
5732
|
logger.error("Empty response body");
|
|
5359
5733
|
process.exit(1);
|
|
5360
5734
|
}
|
|
5361
|
-
|
|
5362
|
-
const ws =
|
|
5735
|
+
fs36.mkdirSync(path41.dirname(dest), { recursive: true });
|
|
5736
|
+
const ws = fs36.createWriteStream(dest);
|
|
5363
5737
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
5364
5738
|
await pipeline2(r.body, ws);
|
|
5365
5739
|
logger.success(`Saved ${dest}`);
|
|
5366
5740
|
if (opts.output) {
|
|
5367
|
-
|
|
5368
|
-
|
|
5741
|
+
fs36.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
|
|
5742
|
+
fs36.copyFileSync(dest, opts.output);
|
|
5369
5743
|
logger.success(`Copied to ${opts.output}`);
|
|
5370
5744
|
}
|
|
5371
5745
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -5376,7 +5750,7 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
5376
5750
|
return dest;
|
|
5377
5751
|
}
|
|
5378
5752
|
function registerOpenRouterVideoStatusCommand(parent) {
|
|
5379
|
-
parent.command("video-status <jobId>").description("Poll an OpenRouter video generation job").option("--wait", "Poll until status is 'completed' or 'failed'").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "If completed, download the video (implies --wait)").option("--output <path>", "Copy downloaded video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
5753
|
+
parent.command("video-status <jobId>").description("Poll or download an OpenRouter video generation job by job ID").option("--wait", "Poll until status is 'completed' or 'failed'").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "If completed, download the video (implies --wait)").option("--output <path>", "Copy downloaded video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
5380
5754
|
async (jobId, opts) => {
|
|
5381
5755
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5382
5756
|
const shouldWait = !!(opts.wait || opts.download);
|
|
@@ -5426,7 +5800,9 @@ function registerOpenRouterVideoStatusCommand(parent) {
|
|
|
5426
5800
|
|
|
5427
5801
|
// src/providers/openrouter/commands/image-to-video.ts
|
|
5428
5802
|
function registerOpenRouterImageToVideoCommand(parent) {
|
|
5429
|
-
parent.command("image-to-video").alias("i2v").description(
|
|
5803
|
+
parent.command("image-to-video").alias("i2v").description(
|
|
5804
|
+
"Generate video from an image via OpenRouter (image-to-video, async); models: Veo, Seedance, Kling \u2014 use video-models to list all"
|
|
5805
|
+
).requiredOption("--prompt <text>", "Motion/scene prompt").requiredOption("--image-url <url>", "First-frame image URL (https)").option("--last-frame-url <url>", "Optional last-frame image URL").option("--model <id>", "OpenRouter video model id (e.g. google/veo-3.1)").option("--resolution <r>", "Resolution hint (e.g. 720p, 1080p)").option("--aspect-ratio <r>", "Aspect ratio hint (e.g. 16:9, 9:16)").option("--duration <n>", "Duration seconds").option("--seed <n>", "Fixed seed").option("--wait", "Poll until status is 'completed' or 'failed'").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download the generated video on success (implies --wait)").option("--output <path>", "Copy downloaded video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
5430
5806
|
async (opts) => {
|
|
5431
5807
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5432
5808
|
const model = opts.model ?? resolveKey("OPENROUTER_VIDEO_MODEL") ?? "google/veo-3.1";
|
|
@@ -5503,7 +5879,9 @@ function registerOpenRouterImageToVideoCommand(parent) {
|
|
|
5503
5879
|
|
|
5504
5880
|
// src/providers/openrouter/commands/video-models.ts
|
|
5505
5881
|
function registerOpenRouterVideoModelsCommand(parent) {
|
|
5506
|
-
parent.command("video-models").description(
|
|
5882
|
+
parent.command("video-models").description(
|
|
5883
|
+
"List available OpenRouter video-generation models (Veo, Seedance, Kling, and others)"
|
|
5884
|
+
).option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
5507
5885
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5508
5886
|
const res = await listVideoModels();
|
|
5509
5887
|
const list = res.data ?? res.models ?? [];
|
|
@@ -5516,7 +5894,9 @@ function registerOpenRouterVideoModelsCommand(parent) {
|
|
|
5516
5894
|
|
|
5517
5895
|
// src/providers/openrouter/commands/index.ts
|
|
5518
5896
|
function registerOpenRouterCommands(program2) {
|
|
5519
|
-
const openrouter = program2.command("openrouter").description(
|
|
5897
|
+
const openrouter = program2.command("openrouter").description(
|
|
5898
|
+
"OpenRouter: text-to-image, image-to-image, and video generation (image-to-video) via routed models \u2014 Veo, Seedance, Kling, and others"
|
|
5899
|
+
);
|
|
5520
5900
|
registerOpenRouterGenerateCommand(openrouter);
|
|
5521
5901
|
registerOpenRouterImageToImageCommand(openrouter);
|
|
5522
5902
|
registerOpenRouterImageToVideoCommand(openrouter);
|
|
@@ -5650,6 +6030,16 @@ function resolveProviderKey(p) {
|
|
|
5650
6030
|
}
|
|
5651
6031
|
return {};
|
|
5652
6032
|
}
|
|
6033
|
+
function cloudflareReadiness() {
|
|
6034
|
+
const accountId = resolveKey("CLOUDFLARE_ACCOUNT_ID")?.trim();
|
|
6035
|
+
const apiToken = resolveKey("CLOUDFLARE_API_TOKEN")?.trim();
|
|
6036
|
+
const gatewayId = resolveKey("CLOUDFLARE_AI_GATEWAY_ID")?.trim();
|
|
6037
|
+
const replicateToken = resolveKey("REPLICATE_API_TOKEN")?.trim();
|
|
6038
|
+
return {
|
|
6039
|
+
native: Boolean(accountId && apiToken),
|
|
6040
|
+
video: Boolean(accountId && apiToken && gatewayId && replicateToken)
|
|
6041
|
+
};
|
|
6042
|
+
}
|
|
5653
6043
|
async function checkCodexImageReadiness(hasAnyProviderKey, verbose = false, checker = checkCodexAuthenticated) {
|
|
5654
6044
|
if (hasAnyProviderKey && !verbose) return false;
|
|
5655
6045
|
return checker();
|
|
@@ -5690,6 +6080,20 @@ function registerCheckCommand(program2) {
|
|
|
5690
6080
|
logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
|
|
5691
6081
|
}
|
|
5692
6082
|
}
|
|
6083
|
+
const cloudflare = cloudflareReadiness();
|
|
6084
|
+
if (cloudflare.native) {
|
|
6085
|
+
anyKey = true;
|
|
6086
|
+
logger.success("Cloudflare Workers AI credentials configured (image and speech)");
|
|
6087
|
+
if (cloudflare.video) {
|
|
6088
|
+
logger.success("Cloudflare AI Gateway video credentials configured");
|
|
6089
|
+
} else {
|
|
6090
|
+
logger.info("Cloudflare video needs CLOUDFLARE_AI_GATEWAY_ID and REPLICATE_API_TOKEN");
|
|
6091
|
+
}
|
|
6092
|
+
} else {
|
|
6093
|
+
logger.info(
|
|
6094
|
+
"Cloudflare media needs CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN (optional)"
|
|
6095
|
+
);
|
|
6096
|
+
}
|
|
5693
6097
|
const shouldCheckCodex = !anyKey || opts.verbose;
|
|
5694
6098
|
let codexReady = false;
|
|
5695
6099
|
if (shouldCheckCodex) {
|
|
@@ -5746,8 +6150,8 @@ function registerCheckCommand(program2) {
|
|
|
5746
6150
|
|
|
5747
6151
|
// src/commands/doc/convert.ts
|
|
5748
6152
|
init_errors();
|
|
5749
|
-
import
|
|
5750
|
-
import
|
|
6153
|
+
import fs37 from "fs";
|
|
6154
|
+
import path42 from "path";
|
|
5751
6155
|
|
|
5752
6156
|
// src/commands/doc/default-prompt.ts
|
|
5753
6157
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -5768,18 +6172,18 @@ async function convertDocuments(opts) {
|
|
|
5768
6172
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
5769
6173
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
5770
6174
|
for (const f of inputs) {
|
|
5771
|
-
if (!
|
|
6175
|
+
if (!fs37.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
5772
6176
|
}
|
|
5773
6177
|
let outputPath;
|
|
5774
6178
|
if (output) {
|
|
5775
|
-
outputPath =
|
|
6179
|
+
outputPath = path42.resolve(output);
|
|
5776
6180
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
5777
|
-
const base =
|
|
5778
|
-
outputPath =
|
|
6181
|
+
const base = path42.basename(inputs[0], path42.extname(inputs[0]));
|
|
6182
|
+
outputPath = path42.join(getOutputDir(), `${base}-extraction.md`);
|
|
5779
6183
|
} else {
|
|
5780
|
-
outputPath =
|
|
6184
|
+
outputPath = path42.join(getOutputDir(), "document-extraction.md");
|
|
5781
6185
|
}
|
|
5782
|
-
|
|
6186
|
+
fs37.mkdirSync(path42.dirname(outputPath), { recursive: true });
|
|
5783
6187
|
const results = [];
|
|
5784
6188
|
for (let i = 0; i < inputs.length; i++) {
|
|
5785
6189
|
const filePath = inputs[i];
|
|
@@ -5830,7 +6234,7 @@ async function convertDocuments(opts) {
|
|
|
5830
6234
|
`);
|
|
5831
6235
|
for (const r of results) {
|
|
5832
6236
|
lines.push(`
|
|
5833
|
-
## ${
|
|
6237
|
+
## ${path42.basename(r.file)}
|
|
5834
6238
|
`);
|
|
5835
6239
|
if (r.status === "success" && r.markdown) {
|
|
5836
6240
|
lines.push(`${r.markdown}
|
|
@@ -5843,7 +6247,7 @@ async function convertDocuments(opts) {
|
|
|
5843
6247
|
}
|
|
5844
6248
|
lines.push("\n---\n");
|
|
5845
6249
|
}
|
|
5846
|
-
|
|
6250
|
+
fs37.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
5847
6251
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
5848
6252
|
console.log(`
|
|
5849
6253
|
${"=".repeat(50)}`);
|
|
@@ -5865,7 +6269,9 @@ function sleep5(ms) {
|
|
|
5865
6269
|
|
|
5866
6270
|
// src/commands/doc/index.ts
|
|
5867
6271
|
function registerDocCommands(program2) {
|
|
5868
|
-
const doc = program2.command("doc").description(
|
|
6272
|
+
const doc = program2.command("doc").description(
|
|
6273
|
+
"Document-to-Markdown: convert PDFs, images, DOCX, PPTX, and other docs to Markdown via Gemini"
|
|
6274
|
+
);
|
|
5869
6275
|
doc.command("convert").description("Convert documents to Markdown using Gemini Files API").requiredOption(
|
|
5870
6276
|
"--input <files...>",
|
|
5871
6277
|
"Input file(s) to convert",
|
|
@@ -5889,12 +6295,12 @@ function registerDocCommands(program2) {
|
|
|
5889
6295
|
|
|
5890
6296
|
// src/commands/media/index.ts
|
|
5891
6297
|
init_errors();
|
|
5892
|
-
import
|
|
5893
|
-
import
|
|
6298
|
+
import fs40 from "fs";
|
|
6299
|
+
import path45 from "path";
|
|
5894
6300
|
|
|
5895
6301
|
// src/commands/media/batch.ts
|
|
5896
|
-
import
|
|
5897
|
-
import
|
|
6302
|
+
import fs38 from "fs";
|
|
6303
|
+
import path43 from "path";
|
|
5898
6304
|
|
|
5899
6305
|
// src/commands/media/detect-type.ts
|
|
5900
6306
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -6090,10 +6496,10 @@ async function batchOptimize(opts) {
|
|
|
6090
6496
|
verbose,
|
|
6091
6497
|
logger
|
|
6092
6498
|
} = opts;
|
|
6093
|
-
|
|
6094
|
-
const entries =
|
|
6499
|
+
fs38.mkdirSync(outputDir, { recursive: true });
|
|
6500
|
+
const entries = fs38.readdirSync(inputDir);
|
|
6095
6501
|
const mediaFiles = entries.filter((f) => {
|
|
6096
|
-
const ext =
|
|
6502
|
+
const ext = path43.extname(f).toLowerCase();
|
|
6097
6503
|
return ALL_EXTS.has(ext);
|
|
6098
6504
|
});
|
|
6099
6505
|
if (mediaFiles.length === 0) {
|
|
@@ -6104,9 +6510,9 @@ async function batchOptimize(opts) {
|
|
|
6104
6510
|
let succeeded = 0;
|
|
6105
6511
|
const errors = [];
|
|
6106
6512
|
for (const file of mediaFiles) {
|
|
6107
|
-
const inputPath =
|
|
6108
|
-
const outputPath =
|
|
6109
|
-
const ext =
|
|
6513
|
+
const inputPath = path43.join(inputDir, file);
|
|
6514
|
+
const outputPath = path43.join(outputDir, file);
|
|
6515
|
+
const ext = path43.extname(file).toLowerCase();
|
|
6110
6516
|
const kind = extToKind(ext);
|
|
6111
6517
|
logger?.debug(`Processing: ${file}`);
|
|
6112
6518
|
try {
|
|
@@ -6143,8 +6549,8 @@ async function batchOptimize(opts) {
|
|
|
6143
6549
|
|
|
6144
6550
|
// src/commands/media/split-video.ts
|
|
6145
6551
|
init_errors();
|
|
6146
|
-
import
|
|
6147
|
-
import
|
|
6552
|
+
import fs39 from "fs";
|
|
6553
|
+
import path44 from "path";
|
|
6148
6554
|
async function splitVideo(opts) {
|
|
6149
6555
|
await assertFfmpeg();
|
|
6150
6556
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -6155,10 +6561,10 @@ async function splitVideo(opts) {
|
|
|
6155
6561
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
6156
6562
|
return [input];
|
|
6157
6563
|
}
|
|
6158
|
-
|
|
6159
|
-
const ext =
|
|
6160
|
-
const baseName =
|
|
6161
|
-
const outputPattern =
|
|
6564
|
+
fs39.mkdirSync(outputDir, { recursive: true });
|
|
6565
|
+
const ext = path44.extname(input) || ".mp4";
|
|
6566
|
+
const baseName = path44.basename(input, ext);
|
|
6567
|
+
const outputPattern = path44.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
6162
6568
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
6163
6569
|
const args = [
|
|
6164
6570
|
"-i",
|
|
@@ -6177,8 +6583,8 @@ async function splitVideo(opts) {
|
|
|
6177
6583
|
await runFfmpeg(args, { verbose, logger });
|
|
6178
6584
|
const files = [];
|
|
6179
6585
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
6180
|
-
const fname =
|
|
6181
|
-
if (
|
|
6586
|
+
const fname = path44.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
6587
|
+
if (fs39.existsSync(fname)) files.push(fname);
|
|
6182
6588
|
}
|
|
6183
6589
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
6184
6590
|
return files;
|
|
@@ -6187,14 +6593,14 @@ async function splitVideo(opts) {
|
|
|
6187
6593
|
// src/commands/media/index.ts
|
|
6188
6594
|
function registerMediaCommands(program2) {
|
|
6189
6595
|
const media = program2.command("media").description(
|
|
6190
|
-
"Media
|
|
6596
|
+
"Media processing via ffmpeg/ImageMagick: compress video/audio/images, split long videos, batch optimize directories"
|
|
6191
6597
|
);
|
|
6192
6598
|
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(
|
|
6193
6599
|
async (opts) => {
|
|
6194
6600
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6195
|
-
if (!
|
|
6196
|
-
|
|
6197
|
-
const ext =
|
|
6601
|
+
if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
6602
|
+
fs40.mkdirSync(path45.dirname(path45.resolve(opts.output)), { recursive: true });
|
|
6603
|
+
const ext = path45.extname(opts.input).toLowerCase();
|
|
6198
6604
|
const kind = extToKind(ext);
|
|
6199
6605
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
6200
6606
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -6234,7 +6640,7 @@ function registerMediaCommands(program2) {
|
|
|
6234
6640
|
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(
|
|
6235
6641
|
async (opts) => {
|
|
6236
6642
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6237
|
-
if (!
|
|
6643
|
+
if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
6238
6644
|
const chunks = await splitVideo({
|
|
6239
6645
|
input: opts.input,
|
|
6240
6646
|
outputDir: opts.outputDir,
|
|
@@ -6250,7 +6656,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
6250
6656
|
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(
|
|
6251
6657
|
async (opts) => {
|
|
6252
6658
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6253
|
-
if (!
|
|
6659
|
+
if (!fs40.existsSync(opts.inputDir))
|
|
6254
6660
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
6255
6661
|
const result = await batchOptimize({
|
|
6256
6662
|
inputDir: opts.inputDir,
|
|
@@ -6280,6 +6686,7 @@ function registerCommands(program2) {
|
|
|
6280
6686
|
registerOpenRouterCommands(program2);
|
|
6281
6687
|
registerLeonardoCommands(program2);
|
|
6282
6688
|
registerBytePlusCommands(program2);
|
|
6689
|
+
registerCloudflareCommands(program2);
|
|
6283
6690
|
registerElevenLabsCommands(program2);
|
|
6284
6691
|
registerMediaCommands(program2);
|
|
6285
6692
|
registerDocCommands(program2);
|
|
@@ -6294,7 +6701,7 @@ loadEnv();
|
|
|
6294
6701
|
var require2 = createRequire(import.meta.url);
|
|
6295
6702
|
var pkg = require2("../package.json");
|
|
6296
6703
|
var program = new Command().name("multix").description(
|
|
6297
|
-
"AI multimodal CLI \u2014 Gemini
|
|
6704
|
+
"AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, ffmpeg, and ImageMagick"
|
|
6298
6705
|
).version(pkg.version);
|
|
6299
6706
|
registerCommands(program);
|
|
6300
6707
|
program.parseAsync(process.argv).catch((err2) => {
|