@mrgoonie/multix 0.0.8 → 0.1.1
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 +20 -0
- package/README.md +56 -3
- package/dist/cli.js +761 -106
- package/package.json +4 -2
- package/skill/SKILL.md +18 -3
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(path45, opts = {}) {
|
|
418
418
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
419
|
-
const url = new URL(base +
|
|
419
|
+
const url = new URL(base + path45);
|
|
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(path45, query, logger) {
|
|
438
|
+
return this.request(path45, { method: "GET", query, logger });
|
|
439
439
|
}
|
|
440
|
-
post(
|
|
441
|
-
return this.request(
|
|
440
|
+
post(path45, body, logger) {
|
|
441
|
+
return this.request(path45, { method: "POST", body, logger });
|
|
442
442
|
}
|
|
443
|
-
delete(
|
|
444
|
-
return this.request(
|
|
443
|
+
delete(path45, logger) {
|
|
444
|
+
return this.request(path45, { 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: path45, role } = parseRefSpec(spec);
|
|
994
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path45, logger), role);
|
|
995
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path45, logger), role);
|
|
996
|
+
return toRef(kind, await resolveAudioInput(path45, logger), role);
|
|
997
997
|
}
|
|
998
998
|
async function resolveRefSpecs(specs, logger) {
|
|
999
999
|
const out = [];
|
|
@@ -1325,8 +1325,8 @@ function requireElevenLabsKey() {
|
|
|
1325
1325
|
function authHeaders(apiKey, extra) {
|
|
1326
1326
|
return { "xi-api-key": apiKey, ...extra ?? {} };
|
|
1327
1327
|
}
|
|
1328
|
-
function apiGet(
|
|
1329
|
-
const url = new URL(`${ELEVENLABS_BASE_URL}/${
|
|
1328
|
+
function apiGet(endpoint2, apiKey, query) {
|
|
1329
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
|
|
1330
1330
|
if (query) {
|
|
1331
1331
|
for (const [k, v] of Object.entries(query)) {
|
|
1332
1332
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -1334,8 +1334,8 @@ function apiGet(endpoint, apiKey, query) {
|
|
|
1334
1334
|
}
|
|
1335
1335
|
return httpJson({ url: url.toString(), headers: authHeaders(apiKey) });
|
|
1336
1336
|
}
|
|
1337
|
-
function apiPostJson(
|
|
1338
|
-
const url = new URL(`${ELEVENLABS_BASE_URL}/${
|
|
1337
|
+
function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
|
|
1338
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
|
|
1339
1339
|
if (opts.query) {
|
|
1340
1340
|
for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
1341
1341
|
}
|
|
@@ -1347,8 +1347,8 @@ function apiPostJson(endpoint, payload, apiKey, opts = {}) {
|
|
|
1347
1347
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
1348
1348
|
});
|
|
1349
1349
|
}
|
|
1350
|
-
async function apiDelete(
|
|
1351
|
-
const url = `${ELEVENLABS_BASE_URL}/${
|
|
1350
|
+
async function apiDelete(endpoint2, apiKey) {
|
|
1351
|
+
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
1352
1352
|
const res = await globalThis.fetch(url, {
|
|
1353
1353
|
method: "DELETE",
|
|
1354
1354
|
headers: authHeaders(apiKey),
|
|
@@ -1359,8 +1359,8 @@ async function apiDelete(endpoint, apiKey) {
|
|
|
1359
1359
|
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
1360
1360
|
}
|
|
1361
1361
|
}
|
|
1362
|
-
async function apiPostBinary(
|
|
1363
|
-
const url = new URL(`${ELEVENLABS_BASE_URL}/${
|
|
1362
|
+
async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
|
|
1363
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
|
|
1364
1364
|
if (opts.query) {
|
|
1365
1365
|
for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
1366
1366
|
}
|
|
@@ -1394,8 +1394,8 @@ async function apiPostBinary(endpoint, body, apiKey, opts = {}) {
|
|
|
1394
1394
|
}
|
|
1395
1395
|
return Buffer.from(await res.arrayBuffer());
|
|
1396
1396
|
}
|
|
1397
|
-
async function apiPostMultipart(
|
|
1398
|
-
const url = `${ELEVENLABS_BASE_URL}/${
|
|
1397
|
+
async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
|
|
1398
|
+
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
1399
1399
|
let res;
|
|
1400
1400
|
try {
|
|
1401
1401
|
res = await globalThis.fetch(url, {
|
|
@@ -3126,11 +3126,11 @@ var LeonardoClient = class {
|
|
|
3126
3126
|
}
|
|
3127
3127
|
apiKey;
|
|
3128
3128
|
baseUrl;
|
|
3129
|
-
async request(
|
|
3129
|
+
async request(path45, opts = {}) {
|
|
3130
3130
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
3131
|
-
const v2Override =
|
|
3131
|
+
const v2Override = path45.startsWith("v2:");
|
|
3132
3132
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
3133
|
-
const finalPath = v2Override ?
|
|
3133
|
+
const finalPath = v2Override ? path45.slice(3) : path45;
|
|
3134
3134
|
const url = new URL(finalBase + finalPath);
|
|
3135
3135
|
if (opts.query) {
|
|
3136
3136
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -3149,14 +3149,14 @@ var LeonardoClient = class {
|
|
|
3149
3149
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
3150
3150
|
});
|
|
3151
3151
|
}
|
|
3152
|
-
get(
|
|
3153
|
-
return this.request(
|
|
3152
|
+
get(path45, query, logger) {
|
|
3153
|
+
return this.request(path45, { method: "GET", query, logger });
|
|
3154
3154
|
}
|
|
3155
|
-
post(
|
|
3156
|
-
return this.request(
|
|
3155
|
+
post(path45, body, logger) {
|
|
3156
|
+
return this.request(path45, { method: "POST", body, logger });
|
|
3157
3157
|
}
|
|
3158
|
-
delete(
|
|
3159
|
-
return this.request(
|
|
3158
|
+
delete(path45, logger) {
|
|
3159
|
+
return this.request(path45, { method: "DELETE", logger });
|
|
3160
3160
|
}
|
|
3161
3161
|
};
|
|
3162
3162
|
function createLeonardoClient() {
|
|
@@ -3756,8 +3756,8 @@ function authHeaders3(apiKey) {
|
|
|
3756
3756
|
"Content-Type": "application/json"
|
|
3757
3757
|
};
|
|
3758
3758
|
}
|
|
3759
|
-
async function apiPost(
|
|
3760
|
-
const url = `${MINIMAX_BASE_URL}/${
|
|
3759
|
+
async function apiPost(endpoint2, payload, apiKey, opts = {}) {
|
|
3760
|
+
const url = `${MINIMAX_BASE_URL}/${endpoint2}`;
|
|
3761
3761
|
opts.logger?.debug(`POST ${url}`);
|
|
3762
3762
|
const data = await httpJson({
|
|
3763
3763
|
url,
|
|
@@ -3775,8 +3775,8 @@ async function apiPost(endpoint, payload, apiKey, opts = {}) {
|
|
|
3775
3775
|
}
|
|
3776
3776
|
return data;
|
|
3777
3777
|
}
|
|
3778
|
-
async function apiGet2(
|
|
3779
|
-
const url = new URL(`${MINIMAX_BASE_URL}/${
|
|
3778
|
+
async function apiGet2(endpoint2, params, apiKey) {
|
|
3779
|
+
const url = new URL(`${MINIMAX_BASE_URL}/${endpoint2}`);
|
|
3780
3780
|
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
3781
3781
|
return httpJson({ url: url.toString(), headers: authHeaders3(apiKey) });
|
|
3782
3782
|
}
|
|
@@ -4356,11 +4356,638 @@ function registerMinimaxCommands(program2) {
|
|
|
4356
4356
|
registerMinimaxGenerateMusicCommand(minimax);
|
|
4357
4357
|
}
|
|
4358
4358
|
|
|
4359
|
-
// src/providers/
|
|
4359
|
+
// src/providers/openai/commands/generate-speech.ts
|
|
4360
|
+
init_errors();
|
|
4361
|
+
|
|
4362
|
+
// src/providers/openai/models.ts
|
|
4363
|
+
init_errors();
|
|
4364
|
+
var OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
4365
|
+
var DEFAULT_OPENAI_IMAGE_MODEL = "gpt-image-2";
|
|
4366
|
+
var DEFAULT_OPENAI_TTS_MODEL = "gpt-4o-mini-tts";
|
|
4367
|
+
var DEFAULT_OPENAI_STT_MODEL = "gpt-4o-transcribe";
|
|
4368
|
+
var OPENAI_IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp"];
|
|
4369
|
+
var OPENAI_IMAGE_QUALITIES = ["low", "medium", "high"];
|
|
4370
|
+
var OPENAI_IMAGE_SIZES = ["1024x1024", "1024x1536", "1536x1024", "auto"];
|
|
4371
|
+
var OPENAI_TTS_OUTPUT_FORMATS = ["mp3", "opus", "aac", "flac", "wav", "pcm"];
|
|
4372
|
+
var OPENAI_TTS_VOICES = [
|
|
4373
|
+
"alloy",
|
|
4374
|
+
"ash",
|
|
4375
|
+
"ballad",
|
|
4376
|
+
"cedar",
|
|
4377
|
+
"coral",
|
|
4378
|
+
"echo",
|
|
4379
|
+
"fable",
|
|
4380
|
+
"marin",
|
|
4381
|
+
"nova",
|
|
4382
|
+
"onyx",
|
|
4383
|
+
"sage",
|
|
4384
|
+
"shimmer",
|
|
4385
|
+
"verse"
|
|
4386
|
+
];
|
|
4387
|
+
var OPENAI_TRANSCRIPT_FORMATS = ["json", "text", "diarized_json"];
|
|
4388
|
+
function isOneOf(value, values) {
|
|
4389
|
+
return values.includes(value);
|
|
4390
|
+
}
|
|
4391
|
+
function extFromImageFormat(format) {
|
|
4392
|
+
if (format === "jpeg") return "jpg";
|
|
4393
|
+
return format;
|
|
4394
|
+
}
|
|
4395
|
+
function extFromAudioFormat(format) {
|
|
4396
|
+
return format;
|
|
4397
|
+
}
|
|
4398
|
+
function parseImageFormat(value) {
|
|
4399
|
+
if (!isOneOf(value, OPENAI_IMAGE_OUTPUT_FORMATS)) {
|
|
4400
|
+
throw new ValidationError(`--format must be one of: ${OPENAI_IMAGE_OUTPUT_FORMATS.join(", ")}`);
|
|
4401
|
+
}
|
|
4402
|
+
return value;
|
|
4403
|
+
}
|
|
4404
|
+
function parseImageQuality(value) {
|
|
4405
|
+
if (!isOneOf(value, OPENAI_IMAGE_QUALITIES)) {
|
|
4406
|
+
throw new ValidationError(`--quality must be one of: ${OPENAI_IMAGE_QUALITIES.join(", ")}`);
|
|
4407
|
+
}
|
|
4408
|
+
return value;
|
|
4409
|
+
}
|
|
4410
|
+
function parseImageSize(value) {
|
|
4411
|
+
if (!isOneOf(value, OPENAI_IMAGE_SIZES)) {
|
|
4412
|
+
throw new ValidationError(`--size must be one of: ${OPENAI_IMAGE_SIZES.join(", ")}`);
|
|
4413
|
+
}
|
|
4414
|
+
return value;
|
|
4415
|
+
}
|
|
4416
|
+
function parseDriver(value) {
|
|
4417
|
+
if (!isOneOf(value, ["api", "codex", "auto"])) {
|
|
4418
|
+
throw new ValidationError("--driver must be one of: api, codex, auto");
|
|
4419
|
+
}
|
|
4420
|
+
return value;
|
|
4421
|
+
}
|
|
4422
|
+
function parseNumImages(value) {
|
|
4423
|
+
if (!/^\d+$/.test(value)) {
|
|
4424
|
+
throw new ValidationError("--num-images must be an integer from 1 to 10");
|
|
4425
|
+
}
|
|
4426
|
+
const parsed = Number.parseInt(value, 10);
|
|
4427
|
+
if (parsed < 1 || parsed > 10) {
|
|
4428
|
+
throw new ValidationError("--num-images must be an integer from 1 to 10");
|
|
4429
|
+
}
|
|
4430
|
+
return parsed;
|
|
4431
|
+
}
|
|
4432
|
+
function parseTtsFormat(value) {
|
|
4433
|
+
if (!isOneOf(value, OPENAI_TTS_OUTPUT_FORMATS)) {
|
|
4434
|
+
throw new ValidationError(
|
|
4435
|
+
`--output-format must be one of: ${OPENAI_TTS_OUTPUT_FORMATS.join(", ")}`
|
|
4436
|
+
);
|
|
4437
|
+
}
|
|
4438
|
+
return value;
|
|
4439
|
+
}
|
|
4440
|
+
function validateTtsVoice(value) {
|
|
4441
|
+
if (!isOneOf(value, OPENAI_TTS_VOICES)) {
|
|
4442
|
+
throw new ValidationError(`--voice must be one of: ${OPENAI_TTS_VOICES.join(", ")}`);
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4445
|
+
function parseTranscriptFormat(value) {
|
|
4446
|
+
if (!isOneOf(value, OPENAI_TRANSCRIPT_FORMATS)) {
|
|
4447
|
+
throw new ValidationError(`--format must be one of: ${OPENAI_TRANSCRIPT_FORMATS.join(", ")}`);
|
|
4448
|
+
}
|
|
4449
|
+
return value;
|
|
4450
|
+
}
|
|
4451
|
+
function resolveTranscriptionRequestFormat(cliFormat, model) {
|
|
4452
|
+
if (cliFormat === "diarized_json") {
|
|
4453
|
+
if (model !== "gpt-4o-transcribe-diarize") {
|
|
4454
|
+
throw new ValidationError("--format diarized_json requires gpt-4o-transcribe-diarize");
|
|
4455
|
+
}
|
|
4456
|
+
return "diarized_json";
|
|
4457
|
+
}
|
|
4458
|
+
return "json";
|
|
4459
|
+
}
|
|
4460
|
+
function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
4461
|
+
if (names.length !== refs.length) {
|
|
4462
|
+
throw new ValidationError(
|
|
4463
|
+
"--known-speaker-name and --known-speaker-reference must have equal counts"
|
|
4464
|
+
);
|
|
4465
|
+
}
|
|
4466
|
+
if (names.length > 4) {
|
|
4467
|
+
throw new ValidationError("OpenAI diarization supports up to 4 known speakers");
|
|
4468
|
+
}
|
|
4469
|
+
}
|
|
4470
|
+
|
|
4471
|
+
// src/providers/openai/openai-audio.ts
|
|
4472
|
+
init_http_client();
|
|
4473
|
+
import fs30 from "fs";
|
|
4474
|
+
import path35 from "path";
|
|
4475
|
+
|
|
4476
|
+
// src/providers/openai/client.ts
|
|
4360
4477
|
import fs29 from "fs";
|
|
4361
4478
|
import path34 from "path";
|
|
4362
4479
|
init_errors();
|
|
4363
4480
|
init_http_client();
|
|
4481
|
+
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
4482
|
+
function requireOpenAIKey() {
|
|
4483
|
+
const key = resolveKey("OPENAI_API_KEY");
|
|
4484
|
+
if (!key) {
|
|
4485
|
+
throw new ConfigError(
|
|
4486
|
+
"OPENAI_API_KEY is not set. Get one at https://platform.openai.com/api-keys"
|
|
4487
|
+
);
|
|
4488
|
+
}
|
|
4489
|
+
return key;
|
|
4490
|
+
}
|
|
4491
|
+
function endpoint(pathname) {
|
|
4492
|
+
return `${OPENAI_BASE_URL}/${pathname.replace(/^\//, "")}`;
|
|
4493
|
+
}
|
|
4494
|
+
function authHeaders4(apiKey, extra) {
|
|
4495
|
+
return { Authorization: `Bearer ${apiKey}`, ...extra ?? {} };
|
|
4496
|
+
}
|
|
4497
|
+
function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
|
|
4498
|
+
return httpJson({
|
|
4499
|
+
url: endpoint(pathname),
|
|
4500
|
+
method: "POST",
|
|
4501
|
+
headers: authHeaders4(apiKey, { "Content-Type": "application/json" }),
|
|
4502
|
+
body,
|
|
4503
|
+
timeoutMs
|
|
4504
|
+
});
|
|
4505
|
+
}
|
|
4506
|
+
async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
|
|
4507
|
+
const url = endpoint(pathname);
|
|
4508
|
+
let res;
|
|
4509
|
+
try {
|
|
4510
|
+
res = await globalThis.fetch(url, {
|
|
4511
|
+
method: "POST",
|
|
4512
|
+
headers: authHeaders4(apiKey),
|
|
4513
|
+
body: form,
|
|
4514
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4515
|
+
});
|
|
4516
|
+
} catch (cause) {
|
|
4517
|
+
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
4518
|
+
}
|
|
4519
|
+
if (!res.ok) {
|
|
4520
|
+
const text = await res.text().catch(() => "");
|
|
4521
|
+
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
4522
|
+
}
|
|
4523
|
+
return res.json();
|
|
4524
|
+
}
|
|
4525
|
+
async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
|
|
4526
|
+
const url = endpoint(pathname);
|
|
4527
|
+
let res;
|
|
4528
|
+
try {
|
|
4529
|
+
res = await globalThis.fetch(url, {
|
|
4530
|
+
method: "POST",
|
|
4531
|
+
headers: authHeaders4(apiKey, { "Content-Type": "application/json" }),
|
|
4532
|
+
body: JSON.stringify(body),
|
|
4533
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4534
|
+
});
|
|
4535
|
+
} catch (cause) {
|
|
4536
|
+
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
4537
|
+
}
|
|
4538
|
+
if (!res.ok) {
|
|
4539
|
+
const text = await res.text().catch(() => "");
|
|
4540
|
+
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
4541
|
+
}
|
|
4542
|
+
return Buffer.from(await res.arrayBuffer());
|
|
4543
|
+
}
|
|
4544
|
+
function fileFromPath(filePath) {
|
|
4545
|
+
const abs = path34.resolve(filePath);
|
|
4546
|
+
if (!fs29.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
4547
|
+
const bytes = fs29.readFileSync(abs);
|
|
4548
|
+
return new File([new Uint8Array(bytes)], path34.basename(abs));
|
|
4549
|
+
}
|
|
4550
|
+
|
|
4551
|
+
// src/providers/openai/openai-audio.ts
|
|
4552
|
+
function buildSpeechBody(opts) {
|
|
4553
|
+
const body = {
|
|
4554
|
+
model: opts.model ?? DEFAULT_OPENAI_TTS_MODEL,
|
|
4555
|
+
input: opts.text,
|
|
4556
|
+
voice: opts.voice,
|
|
4557
|
+
response_format: opts.outputFormat
|
|
4558
|
+
};
|
|
4559
|
+
if (opts.instructions) body.instructions = opts.instructions;
|
|
4560
|
+
return body;
|
|
4561
|
+
}
|
|
4562
|
+
async function buildTranscriptionForm(opts) {
|
|
4563
|
+
const model = opts.model ?? DEFAULT_OPENAI_STT_MODEL;
|
|
4564
|
+
const form = new FormData();
|
|
4565
|
+
form.append("file", opts.input);
|
|
4566
|
+
form.append("model", model);
|
|
4567
|
+
form.append("response_format", resolveTranscriptionRequestFormat(opts.cliFormat, model));
|
|
4568
|
+
if (opts.language) form.append("language", opts.language);
|
|
4569
|
+
if (opts.chunkingStrategy) form.append("chunking_strategy", opts.chunkingStrategy);
|
|
4570
|
+
const names = opts.knownSpeakerNames ?? [];
|
|
4571
|
+
const refs = opts.knownSpeakerReferences ?? [];
|
|
4572
|
+
validateKnownSpeakerPairing(names, refs);
|
|
4573
|
+
for (const name of names) form.append("known_speaker_names[]", name);
|
|
4574
|
+
for (const ref of refs) {
|
|
4575
|
+
form.append("known_speaker_references[]", await resolveAudioReferenceDataUrl(ref));
|
|
4576
|
+
}
|
|
4577
|
+
return form;
|
|
4578
|
+
}
|
|
4579
|
+
async function resolveAudioReferenceDataUrl(ref) {
|
|
4580
|
+
const resolved = await resolveAudioInput(ref);
|
|
4581
|
+
if (resolved.kind === "data") return resolved.dataUrl;
|
|
4582
|
+
const bytes = await fetchBytes(resolved.url);
|
|
4583
|
+
return `data:${audioMimeFromReference(resolved.url)};base64,${bytes.toString("base64")}`;
|
|
4584
|
+
}
|
|
4585
|
+
function audioMimeFromReference(ref) {
|
|
4586
|
+
const pathname = new URL(ref).pathname;
|
|
4587
|
+
const ext = path35.extname(pathname).toLowerCase();
|
|
4588
|
+
switch (ext) {
|
|
4589
|
+
case ".mp3":
|
|
4590
|
+
return "audio/mpeg";
|
|
4591
|
+
case ".wav":
|
|
4592
|
+
return "audio/wav";
|
|
4593
|
+
case ".m4a":
|
|
4594
|
+
return "audio/mp4";
|
|
4595
|
+
case ".flac":
|
|
4596
|
+
return "audio/flac";
|
|
4597
|
+
case ".ogg":
|
|
4598
|
+
return "audio/ogg";
|
|
4599
|
+
default:
|
|
4600
|
+
return "application/octet-stream";
|
|
4601
|
+
}
|
|
4602
|
+
}
|
|
4603
|
+
async function generateOpenAISpeech(opts) {
|
|
4604
|
+
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
4605
|
+
const ext = extFromAudioFormat(opts.outputFormat);
|
|
4606
|
+
const dest = path35.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
4607
|
+
fs30.writeFileSync(dest, bytes);
|
|
4608
|
+
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
4609
|
+
if (opts.output) {
|
|
4610
|
+
fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
|
|
4611
|
+
fs30.copyFileSync(dest, opts.output);
|
|
4612
|
+
}
|
|
4613
|
+
return dest;
|
|
4614
|
+
}
|
|
4615
|
+
async function transcribeOpenAIAudio(opts) {
|
|
4616
|
+
const form = await buildTranscriptionForm({
|
|
4617
|
+
input: fileFromPath(opts.inputPath),
|
|
4618
|
+
model: opts.model,
|
|
4619
|
+
cliFormat: opts.format,
|
|
4620
|
+
language: opts.language,
|
|
4621
|
+
chunkingStrategy: opts.chunkingStrategy ?? (opts.model === "gpt-4o-transcribe-diarize" || opts.format === "diarized_json" ? "auto" : void 0),
|
|
4622
|
+
knownSpeakerNames: opts.knownSpeakerNames,
|
|
4623
|
+
knownSpeakerReferences: opts.knownSpeakerReferences
|
|
4624
|
+
});
|
|
4625
|
+
const data = await postOpenAIForm(
|
|
4626
|
+
"/audio/transcriptions",
|
|
4627
|
+
form,
|
|
4628
|
+
requireOpenAIKey(),
|
|
4629
|
+
6e5
|
|
4630
|
+
);
|
|
4631
|
+
const text = typeof data.text === "string" ? data.text : void 0;
|
|
4632
|
+
const ext = opts.format === "text" ? "txt" : "json";
|
|
4633
|
+
const dest = opts.output ? path35.resolve(opts.output) : path35.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
|
|
4634
|
+
fs30.mkdirSync(path35.dirname(dest), { recursive: true });
|
|
4635
|
+
fs30.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
4636
|
+
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
4637
|
+
return { path: dest, text };
|
|
4638
|
+
}
|
|
4639
|
+
|
|
4640
|
+
// src/providers/openai/commands/generate-speech.ts
|
|
4641
|
+
function registerOpenAIGenerateSpeechCommand(parent) {
|
|
4642
|
+
parent.command("generate-speech").description("Generate speech with OpenAI TTS").option("--text <str>", "Text to speak").option("--prompt <str>", "Alias for --text").option("--model <id>", "TTS model", process.env.OPENAI_TTS_MODEL ?? DEFAULT_OPENAI_TTS_MODEL).option("--voice <name>", "OpenAI voice", "marin").option("--instructions <text>", "Voice/style instructions").option("--output-format <fmt>", "mp3|opus|aac|flac|wav|pcm", "mp3").option("--output <path>", "Copy generated audio to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
4643
|
+
const text = opts.text ?? opts.prompt;
|
|
4644
|
+
if (!text) throw new ValidationError("Either --text or --prompt is required");
|
|
4645
|
+
validateTtsVoice(opts.voice);
|
|
4646
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4647
|
+
const dest = await generateOpenAISpeech({
|
|
4648
|
+
text,
|
|
4649
|
+
model: opts.model,
|
|
4650
|
+
voice: opts.voice,
|
|
4651
|
+
outputFormat: parseTtsFormat(opts.outputFormat),
|
|
4652
|
+
instructions: opts.instructions,
|
|
4653
|
+
output: opts.output,
|
|
4654
|
+
logger
|
|
4655
|
+
});
|
|
4656
|
+
console.log(`
|
|
4657
|
+
Generated audio: ${dest}`);
|
|
4658
|
+
});
|
|
4659
|
+
}
|
|
4660
|
+
|
|
4661
|
+
// src/providers/openai/codex-image-driver.ts
|
|
4662
|
+
import fs31 from "fs";
|
|
4663
|
+
import os2 from "os";
|
|
4664
|
+
import path36 from "path";
|
|
4665
|
+
import { execa } from "execa";
|
|
4666
|
+
init_errors();
|
|
4667
|
+
var defaultRunner = async (cmd, args, opts) => {
|
|
4668
|
+
const res = await execa(cmd, args, {
|
|
4669
|
+
cwd: opts?.cwd,
|
|
4670
|
+
env: opts?.env,
|
|
4671
|
+
reject: false,
|
|
4672
|
+
timeout: opts?.timeoutMs
|
|
4673
|
+
});
|
|
4674
|
+
return { stdout: res.stdout, stderr: res.stderr, exitCode: res.exitCode ?? 0 };
|
|
4675
|
+
};
|
|
4676
|
+
async function checkCodexAuthenticated(runner = defaultRunner) {
|
|
4677
|
+
try {
|
|
4678
|
+
const res = await runner("codex", ["login", "status"], { timeoutMs: 15e3 });
|
|
4679
|
+
return res.exitCode === 0 && /Logged in using ChatGPT/i.test(`${res.stdout}
|
|
4680
|
+
${res.stderr}`);
|
|
4681
|
+
} catch {
|
|
4682
|
+
return false;
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
async function resolveImageDriver(opts) {
|
|
4686
|
+
const hasApiKey = opts.hasApiKey ?? Boolean(resolveKey("OPENAI_API_KEY"));
|
|
4687
|
+
const codexAuthCheck = opts.codexAuthCheck ?? (() => checkCodexAuthenticated());
|
|
4688
|
+
if (opts.requested === "api") {
|
|
4689
|
+
if (!hasApiKey) throw new ConfigError("OPENAI_API_KEY is required for --driver api");
|
|
4690
|
+
return "api";
|
|
4691
|
+
}
|
|
4692
|
+
if (opts.requested === "codex") {
|
|
4693
|
+
if (!await codexAuthCheck()) throw new ConfigError("Codex driver requires `codex login`");
|
|
4694
|
+
return "codex";
|
|
4695
|
+
}
|
|
4696
|
+
if (hasApiKey) return "api";
|
|
4697
|
+
if (await codexAuthCheck()) return "codex";
|
|
4698
|
+
throw new ConfigError(
|
|
4699
|
+
"Set OPENAI_API_KEY or run `codex login` for the experimental image driver"
|
|
4700
|
+
);
|
|
4701
|
+
}
|
|
4702
|
+
function sanitizeCodexEnv() {
|
|
4703
|
+
const keep = ["PATH", "HOME", "CODEX_HOME", "TMPDIR", "TEMP", "LANG", "LC_ALL"];
|
|
4704
|
+
const env = {};
|
|
4705
|
+
for (const key of keep) {
|
|
4706
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
4707
|
+
}
|
|
4708
|
+
return env;
|
|
4709
|
+
}
|
|
4710
|
+
async function runCodexImageGeneration(opts) {
|
|
4711
|
+
const runner = opts.runner ?? defaultRunner;
|
|
4712
|
+
const ext = extFromImageFormat(opts.outputFormat);
|
|
4713
|
+
const dest = opts.output ? path36.resolve(opts.output) : path36.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
|
|
4714
|
+
fs31.mkdirSync(path36.dirname(dest), { recursive: true });
|
|
4715
|
+
const tmp = fs31.mkdtempSync(path36.join(os2.tmpdir(), "multix-codex-image-"));
|
|
4716
|
+
const schema = path36.join(tmp, "schema.json");
|
|
4717
|
+
const result = path36.join(tmp, "result.json");
|
|
4718
|
+
fs31.writeFileSync(
|
|
4719
|
+
schema,
|
|
4720
|
+
JSON.stringify({
|
|
4721
|
+
type: "object",
|
|
4722
|
+
properties: { status: { type: "string" }, path: { type: "string" } },
|
|
4723
|
+
required: ["status", "path"],
|
|
4724
|
+
additionalProperties: false
|
|
4725
|
+
})
|
|
4726
|
+
);
|
|
4727
|
+
const args = [
|
|
4728
|
+
"exec",
|
|
4729
|
+
"--ephemeral",
|
|
4730
|
+
"--skip-git-repo-check",
|
|
4731
|
+
"--cd",
|
|
4732
|
+
tmp,
|
|
4733
|
+
"--output-schema",
|
|
4734
|
+
schema,
|
|
4735
|
+
"--output-last-message",
|
|
4736
|
+
result
|
|
4737
|
+
];
|
|
4738
|
+
for (const ref of opts.refs ?? []) args.push("--image", ref);
|
|
4739
|
+
args.push(buildCodexPrompt(opts.prompt, dest));
|
|
4740
|
+
const res = await runner("codex", args, {
|
|
4741
|
+
cwd: tmp,
|
|
4742
|
+
env: sanitizeCodexEnv(),
|
|
4743
|
+
timeoutMs: 6e5
|
|
4744
|
+
});
|
|
4745
|
+
if (res.exitCode !== 0) {
|
|
4746
|
+
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
4747
|
+
}
|
|
4748
|
+
if (!fs31.existsSync(dest) || fs31.statSync(dest).size === 0) {
|
|
4749
|
+
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
4750
|
+
}
|
|
4751
|
+
return { path: dest };
|
|
4752
|
+
}
|
|
4753
|
+
function buildCodexPrompt(userPrompt, outputPath) {
|
|
4754
|
+
return [
|
|
4755
|
+
`Create exactly one image at ${outputPath}.`,
|
|
4756
|
+
"Do not modify any repository files.",
|
|
4757
|
+
"Use the attached reference images if present.",
|
|
4758
|
+
`Image request: ${userPrompt}`,
|
|
4759
|
+
`Reply with JSON only: {"status":"ok","path":"${outputPath}"}.`
|
|
4760
|
+
].join(" ");
|
|
4761
|
+
}
|
|
4762
|
+
|
|
4763
|
+
// src/providers/openai/openai-image.ts
|
|
4764
|
+
init_errors();
|
|
4765
|
+
init_http_client();
|
|
4766
|
+
import fs32 from "fs";
|
|
4767
|
+
import path37 from "path";
|
|
4768
|
+
function buildImageGenerationBody(opts) {
|
|
4769
|
+
return {
|
|
4770
|
+
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
4771
|
+
prompt: opts.prompt,
|
|
4772
|
+
size: opts.size ?? "1024x1024",
|
|
4773
|
+
quality: opts.quality ?? "medium",
|
|
4774
|
+
output_format: opts.outputFormat,
|
|
4775
|
+
n: opts.numImages ?? 1
|
|
4776
|
+
};
|
|
4777
|
+
}
|
|
4778
|
+
async function buildImageEditForm(opts) {
|
|
4779
|
+
const form = new FormData();
|
|
4780
|
+
form.append("model", opts.model);
|
|
4781
|
+
form.append("prompt", opts.prompt);
|
|
4782
|
+
form.append("size", opts.size);
|
|
4783
|
+
form.append("quality", opts.quality);
|
|
4784
|
+
form.append("output_format", opts.outputFormat);
|
|
4785
|
+
for (const ref of opts.refs) {
|
|
4786
|
+
form.append("image[]", new File([new Uint8Array(ref.bytes)], ref.filename, { type: ref.mime }));
|
|
4787
|
+
}
|
|
4788
|
+
return form;
|
|
4789
|
+
}
|
|
4790
|
+
function saveOpenAIImages(opts) {
|
|
4791
|
+
const items = opts.data ?? [];
|
|
4792
|
+
if (items.length === 0) throw new ProviderError("No images in OpenAI response", "openai");
|
|
4793
|
+
const outDir = getOutputDir();
|
|
4794
|
+
const ext = extFromImageFormat(opts.format);
|
|
4795
|
+
const ts = Date.now();
|
|
4796
|
+
const saved = [];
|
|
4797
|
+
for (let i = 0; i < items.length; i++) {
|
|
4798
|
+
const b64 = items[i]?.b64_json;
|
|
4799
|
+
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
4800
|
+
const dest = path37.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
4801
|
+
fs32.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
4802
|
+
saved.push(dest);
|
|
4803
|
+
opts.logger?.success(`Saved: ${dest}`);
|
|
4804
|
+
}
|
|
4805
|
+
if (opts.output && saved[0]) {
|
|
4806
|
+
fs32.mkdirSync(path37.dirname(path37.resolve(opts.output)), { recursive: true });
|
|
4807
|
+
fs32.copyFileSync(saved[0], opts.output);
|
|
4808
|
+
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
4809
|
+
}
|
|
4810
|
+
return saved;
|
|
4811
|
+
}
|
|
4812
|
+
async function resolveImageRefBytes(ref) {
|
|
4813
|
+
if (isHttpUrl(ref)) {
|
|
4814
|
+
const bytes2 = await fetchBytes(ref);
|
|
4815
|
+
const pathname = new URL(ref).pathname;
|
|
4816
|
+
const filename = path37.basename(pathname) || "reference.png";
|
|
4817
|
+
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
4818
|
+
}
|
|
4819
|
+
const file = fileFromPath(ref);
|
|
4820
|
+
const bytes = Buffer.from(await file.arrayBuffer());
|
|
4821
|
+
return { filename: file.name, bytes, mime: file.type || sniffImageMime2(bytes, file.name) };
|
|
4822
|
+
}
|
|
4823
|
+
async function generateOpenAIImage(opts) {
|
|
4824
|
+
const data = await postOpenAIJson(
|
|
4825
|
+
"/images/generations",
|
|
4826
|
+
buildImageGenerationBody(opts),
|
|
4827
|
+
requireOpenAIKey()
|
|
4828
|
+
);
|
|
4829
|
+
return saveOpenAIImages({
|
|
4830
|
+
data: data.data,
|
|
4831
|
+
task: "image",
|
|
4832
|
+
format: opts.outputFormat,
|
|
4833
|
+
output: opts.output,
|
|
4834
|
+
logger: opts.logger
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
async function editOpenAIImage(opts) {
|
|
4838
|
+
const refs = await Promise.all(opts.refs.map((ref) => resolveImageRefBytes(ref)));
|
|
4839
|
+
const form = await buildImageEditForm({
|
|
4840
|
+
prompt: opts.prompt,
|
|
4841
|
+
refs,
|
|
4842
|
+
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
4843
|
+
size: opts.size ?? "1024x1024",
|
|
4844
|
+
quality: opts.quality ?? "medium",
|
|
4845
|
+
outputFormat: opts.outputFormat
|
|
4846
|
+
});
|
|
4847
|
+
const data = await postOpenAIForm("/images/edits", form, requireOpenAIKey());
|
|
4848
|
+
return saveOpenAIImages({
|
|
4849
|
+
data: data.data,
|
|
4850
|
+
task: "image-edit",
|
|
4851
|
+
format: opts.outputFormat,
|
|
4852
|
+
output: opts.output,
|
|
4853
|
+
logger: opts.logger
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
function sniffImageMime2(buf, name) {
|
|
4857
|
+
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
4858
|
+
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
4859
|
+
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
4860
|
+
return "image/webp";
|
|
4861
|
+
const ext = path37.extname(name).toLowerCase();
|
|
4862
|
+
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
4863
|
+
if (ext === ".webp") return "image/webp";
|
|
4864
|
+
return "image/png";
|
|
4865
|
+
}
|
|
4866
|
+
|
|
4867
|
+
// src/providers/openai/commands/generate.ts
|
|
4868
|
+
function registerOpenAIGenerateCommand(parent) {
|
|
4869
|
+
parent.command("generate").description("Generate images with OpenAI Image API or experimental Codex driver").requiredOption("--prompt <text>", "Text prompt").option("--driver <driver>", "api|codex|auto", "auto").option(
|
|
4870
|
+
"-m, --model <id>",
|
|
4871
|
+
"Image model",
|
|
4872
|
+
resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
|
|
4873
|
+
).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option("--format <fmt>", "png|jpeg|webp", "png").option("-n, --num-images <n>", "Number of images for API driver", "1").option("--output <path>", "Save first image to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
4874
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4875
|
+
const outputFormat = parseImageFormat(opts.format);
|
|
4876
|
+
const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
|
|
4877
|
+
const saved = driver === "codex" ? [
|
|
4878
|
+
(await runCodexImageGeneration({
|
|
4879
|
+
prompt: opts.prompt,
|
|
4880
|
+
outputFormat,
|
|
4881
|
+
output: opts.output
|
|
4882
|
+
})).path
|
|
4883
|
+
] : await generateOpenAIImage({
|
|
4884
|
+
prompt: opts.prompt,
|
|
4885
|
+
model: opts.model,
|
|
4886
|
+
size: parseImageSize(opts.size),
|
|
4887
|
+
quality: parseImageQuality(opts.quality),
|
|
4888
|
+
outputFormat,
|
|
4889
|
+
numImages: parseNumImages(opts.numImages),
|
|
4890
|
+
output: opts.output,
|
|
4891
|
+
logger
|
|
4892
|
+
});
|
|
4893
|
+
console.log(`
|
|
4894
|
+
Generated ${saved.length} image(s):`);
|
|
4895
|
+
for (const file of saved) console.log(` ${file}`);
|
|
4896
|
+
});
|
|
4897
|
+
}
|
|
4898
|
+
|
|
4899
|
+
// src/providers/openai/commands/image-to-image.ts
|
|
4900
|
+
init_errors();
|
|
4901
|
+
function registerOpenAIImageToImageCommand(parent) {
|
|
4902
|
+
parent.command("image-to-image").alias("i2i").description("Edit images with OpenAI Image API or experimental Codex driver").requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
4903
|
+
"--ref <path>",
|
|
4904
|
+
"Reference image path or URL; repeatable",
|
|
4905
|
+
collect5,
|
|
4906
|
+
[]
|
|
4907
|
+
).option("--driver <driver>", "api|codex|auto", "auto").option(
|
|
4908
|
+
"-m, --model <id>",
|
|
4909
|
+
"Image model",
|
|
4910
|
+
resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
|
|
4911
|
+
).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option("--format <fmt>", "png|jpeg|webp", "png").option("--output <path>", "Save first image to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
4912
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
4913
|
+
throw new ValidationError("At least one --ref is required for image-to-image.");
|
|
4914
|
+
}
|
|
4915
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4916
|
+
const outputFormat = parseImageFormat(opts.format);
|
|
4917
|
+
const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
|
|
4918
|
+
const saved = driver === "codex" ? [
|
|
4919
|
+
(await runCodexImageGeneration({
|
|
4920
|
+
prompt: opts.prompt,
|
|
4921
|
+
refs: opts.ref,
|
|
4922
|
+
outputFormat,
|
|
4923
|
+
output: opts.output
|
|
4924
|
+
})).path
|
|
4925
|
+
] : await editOpenAIImage({
|
|
4926
|
+
prompt: opts.prompt,
|
|
4927
|
+
refs: opts.ref,
|
|
4928
|
+
model: opts.model,
|
|
4929
|
+
size: parseImageSize(opts.size),
|
|
4930
|
+
quality: parseImageQuality(opts.quality),
|
|
4931
|
+
outputFormat,
|
|
4932
|
+
output: opts.output,
|
|
4933
|
+
logger
|
|
4934
|
+
});
|
|
4935
|
+
console.log(`
|
|
4936
|
+
Generated ${saved.length} image(s):`);
|
|
4937
|
+
for (const file of saved) console.log(` ${file}`);
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
function collect5(value, prev) {
|
|
4941
|
+
return [...prev, value];
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4944
|
+
// src/providers/openai/commands/transcribe.ts
|
|
4945
|
+
function registerOpenAITranscribeCommand(parent) {
|
|
4946
|
+
parent.command("transcribe").description("Transcribe audio/video with OpenAI STT").requiredOption("--input <path>", "Audio or video file").option("--model <id>", "STT model", process.env.OPENAI_STT_MODEL ?? DEFAULT_OPENAI_STT_MODEL).option("--format <fmt>", "json|text|diarized_json", "text").option("--language <code>", "ISO language code").option("--chunking-strategy <strategy>", "e.g. auto").option(
|
|
4947
|
+
"--known-speaker-name <name>",
|
|
4948
|
+
"Known speaker name; repeatable",
|
|
4949
|
+
collect6,
|
|
4950
|
+
[]
|
|
4951
|
+
).option(
|
|
4952
|
+
"--known-speaker-reference <path>",
|
|
4953
|
+
"2-10s reference audio; repeatable",
|
|
4954
|
+
collect6,
|
|
4955
|
+
[]
|
|
4956
|
+
).option("--output <path>", "Save transcript to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
4957
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4958
|
+
const result = await transcribeOpenAIAudio({
|
|
4959
|
+
inputPath: opts.input,
|
|
4960
|
+
model: opts.model,
|
|
4961
|
+
format: parseTranscriptFormat(opts.format),
|
|
4962
|
+
language: opts.language,
|
|
4963
|
+
chunkingStrategy: opts.chunkingStrategy,
|
|
4964
|
+
knownSpeakerNames: opts.knownSpeakerName,
|
|
4965
|
+
knownSpeakerReferences: opts.knownSpeakerReference,
|
|
4966
|
+
output: opts.output,
|
|
4967
|
+
logger
|
|
4968
|
+
});
|
|
4969
|
+
if (opts.format === "text" && result.text) console.log(`
|
|
4970
|
+
${result.text.trim()}`);
|
|
4971
|
+
});
|
|
4972
|
+
}
|
|
4973
|
+
function collect6(value, prev) {
|
|
4974
|
+
return [...prev, value];
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4977
|
+
// src/providers/openai/commands/index.ts
|
|
4978
|
+
function registerOpenAICommands(program2) {
|
|
4979
|
+
const openai = program2.command("openai").description("OpenAI image generation/editing, TTS, STT, and experimental Codex image driver");
|
|
4980
|
+
registerOpenAIGenerateCommand(openai);
|
|
4981
|
+
registerOpenAIImageToImageCommand(openai);
|
|
4982
|
+
registerOpenAIGenerateSpeechCommand(openai);
|
|
4983
|
+
registerOpenAITranscribeCommand(openai);
|
|
4984
|
+
}
|
|
4985
|
+
|
|
4986
|
+
// src/providers/openrouter/client.ts
|
|
4987
|
+
import fs33 from "fs";
|
|
4988
|
+
import path38 from "path";
|
|
4989
|
+
init_errors();
|
|
4990
|
+
init_http_client();
|
|
4364
4991
|
|
|
4365
4992
|
// src/providers/openrouter/payload.ts
|
|
4366
4993
|
var IMAGE_ONLY_MODEL_PREFIXES = [
|
|
@@ -4523,8 +5150,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
4523
5150
|
if (!url) continue;
|
|
4524
5151
|
try {
|
|
4525
5152
|
const bytes = await fetchBytes(url);
|
|
4526
|
-
const dest =
|
|
4527
|
-
|
|
5153
|
+
const dest = path38.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
5154
|
+
fs33.writeFileSync(dest, bytes);
|
|
4528
5155
|
savedFiles.push(dest);
|
|
4529
5156
|
logger?.success(`Saved: ${dest}`);
|
|
4530
5157
|
} catch (e) {
|
|
@@ -4535,8 +5162,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
4535
5162
|
}
|
|
4536
5163
|
}
|
|
4537
5164
|
if (output && savedFiles[0]) {
|
|
4538
|
-
|
|
4539
|
-
|
|
5165
|
+
fs33.mkdirSync(path38.dirname(path38.resolve(output)), { recursive: true });
|
|
5166
|
+
fs33.writeFileSync(output, fs33.readFileSync(savedFiles[0]));
|
|
4540
5167
|
}
|
|
4541
5168
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
4542
5169
|
}
|
|
@@ -4571,8 +5198,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
4571
5198
|
}
|
|
4572
5199
|
|
|
4573
5200
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
4574
|
-
import
|
|
4575
|
-
import
|
|
5201
|
+
import fs34 from "fs";
|
|
5202
|
+
import path39 from "path";
|
|
4576
5203
|
init_errors();
|
|
4577
5204
|
init_http_client();
|
|
4578
5205
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -4582,7 +5209,7 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
4582
5209
|
parent.command("image-to-image").alias("i2i").description("Edit/transform images via OpenRouter (default: google/gemini-2.5-flash-image)").requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
4583
5210
|
"--ref <path>",
|
|
4584
5211
|
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
4585
|
-
|
|
5212
|
+
collect7,
|
|
4586
5213
|
[]
|
|
4587
5214
|
).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--strength <n>", "Recraft init-image strength 0..1 (Recraft models only)").option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
4588
5215
|
async (opts) => {
|
|
@@ -4623,7 +5250,7 @@ Generated ${saved.length} image(s):`);
|
|
|
4623
5250
|
}
|
|
4624
5251
|
);
|
|
4625
5252
|
}
|
|
4626
|
-
function
|
|
5253
|
+
function collect7(value, prev) {
|
|
4627
5254
|
return [...prev, value];
|
|
4628
5255
|
}
|
|
4629
5256
|
function parseStrength(raw) {
|
|
@@ -4662,14 +5289,14 @@ async function runImageToImage(opts) {
|
|
|
4662
5289
|
const url = parsed.urls[i];
|
|
4663
5290
|
if (!url) continue;
|
|
4664
5291
|
const bytes = await fetchBytes(url);
|
|
4665
|
-
const dest =
|
|
4666
|
-
|
|
5292
|
+
const dest = path39.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
5293
|
+
fs34.writeFileSync(dest, bytes);
|
|
4667
5294
|
saved.push(dest);
|
|
4668
5295
|
opts.logger.success(`Saved: ${dest}`);
|
|
4669
5296
|
}
|
|
4670
5297
|
if (opts.output && saved[0]) {
|
|
4671
|
-
|
|
4672
|
-
|
|
5298
|
+
fs34.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
|
|
5299
|
+
fs34.copyFileSync(saved[0], opts.output);
|
|
4673
5300
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
4674
5301
|
}
|
|
4675
5302
|
return saved;
|
|
@@ -4678,7 +5305,7 @@ async function runImageToImage(opts) {
|
|
|
4678
5305
|
// src/providers/openrouter/video-client.ts
|
|
4679
5306
|
init_http_client();
|
|
4680
5307
|
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
4681
|
-
function
|
|
5308
|
+
function authHeaders5(apiKey) {
|
|
4682
5309
|
const headers = {
|
|
4683
5310
|
Authorization: `Bearer ${apiKey}`,
|
|
4684
5311
|
"Content-Type": "application/json"
|
|
@@ -4693,7 +5320,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
4693
5320
|
return httpJson({
|
|
4694
5321
|
url: OPENROUTER_VIDEOS_URL,
|
|
4695
5322
|
method: "POST",
|
|
4696
|
-
headers:
|
|
5323
|
+
headers: authHeaders5(apiKey),
|
|
4697
5324
|
body,
|
|
4698
5325
|
timeoutMs: 12e4
|
|
4699
5326
|
});
|
|
@@ -4701,7 +5328,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
4701
5328
|
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
4702
5329
|
return httpJson({
|
|
4703
5330
|
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
4704
|
-
headers:
|
|
5331
|
+
headers: authHeaders5(apiKey)
|
|
4705
5332
|
});
|
|
4706
5333
|
}
|
|
4707
5334
|
function videoContentUrl(jobId) {
|
|
@@ -4710,18 +5337,18 @@ function videoContentUrl(jobId) {
|
|
|
4710
5337
|
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
4711
5338
|
return httpJson({
|
|
4712
5339
|
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
4713
|
-
headers:
|
|
5340
|
+
headers: authHeaders5(apiKey)
|
|
4714
5341
|
});
|
|
4715
5342
|
}
|
|
4716
5343
|
|
|
4717
5344
|
// src/providers/openrouter/commands/video-status.ts
|
|
4718
|
-
import
|
|
4719
|
-
import
|
|
5345
|
+
import fs35 from "fs";
|
|
5346
|
+
import path40 from "path";
|
|
4720
5347
|
async function downloadOpenRouterVideo(opts) {
|
|
4721
5348
|
const { jobId, logger, source } = opts;
|
|
4722
5349
|
const apiKey = requireOpenRouterKey();
|
|
4723
5350
|
const url = videoContentUrl(jobId);
|
|
4724
|
-
const dest =
|
|
5351
|
+
const dest = path40.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
4725
5352
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
4726
5353
|
if (!r.ok) {
|
|
4727
5354
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -4731,14 +5358,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
4731
5358
|
logger.error("Empty response body");
|
|
4732
5359
|
process.exit(1);
|
|
4733
5360
|
}
|
|
4734
|
-
|
|
4735
|
-
const ws =
|
|
5361
|
+
fs35.mkdirSync(path40.dirname(dest), { recursive: true });
|
|
5362
|
+
const ws = fs35.createWriteStream(dest);
|
|
4736
5363
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
4737
5364
|
await pipeline2(r.body, ws);
|
|
4738
5365
|
logger.success(`Saved ${dest}`);
|
|
4739
5366
|
if (opts.output) {
|
|
4740
|
-
|
|
4741
|
-
|
|
5367
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5368
|
+
fs35.copyFileSync(dest, opts.output);
|
|
4742
5369
|
logger.success(`Copied to ${opts.output}`);
|
|
4743
5370
|
}
|
|
4744
5371
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -4898,10 +5525,10 @@ function registerOpenRouterCommands(program2) {
|
|
|
4898
5525
|
}
|
|
4899
5526
|
|
|
4900
5527
|
// src/commands/check-helpers/binary-check.ts
|
|
4901
|
-
import { execa } from "execa";
|
|
5528
|
+
import { execa as execa2 } from "execa";
|
|
4902
5529
|
async function checkBinary(binary) {
|
|
4903
5530
|
try {
|
|
4904
|
-
const result = await
|
|
5531
|
+
const result = await execa2(binary, ["--version"], { reject: true, all: true });
|
|
4905
5532
|
const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
|
|
4906
5533
|
return { available: true, version, binary };
|
|
4907
5534
|
} catch (e) {
|
|
@@ -4982,6 +5609,12 @@ var PROVIDERS = [
|
|
|
4982
5609
|
optional: true,
|
|
4983
5610
|
link: "https://openrouter.ai/settings/keys"
|
|
4984
5611
|
},
|
|
5612
|
+
{
|
|
5613
|
+
name: "OpenAI",
|
|
5614
|
+
envPrimary: "OPENAI_API_KEY",
|
|
5615
|
+
optional: true,
|
|
5616
|
+
link: "https://platform.openai.com/api-keys"
|
|
5617
|
+
},
|
|
4985
5618
|
{
|
|
4986
5619
|
name: "MiniMax",
|
|
4987
5620
|
envPrimary: "MINIMAX_API_KEY",
|
|
@@ -5017,6 +5650,10 @@ function resolveProviderKey(p) {
|
|
|
5017
5650
|
}
|
|
5018
5651
|
return {};
|
|
5019
5652
|
}
|
|
5653
|
+
async function checkCodexImageReadiness(hasAnyProviderKey, verbose = false, checker = checkCodexAuthenticated) {
|
|
5654
|
+
if (hasAnyProviderKey && !verbose) return false;
|
|
5655
|
+
return checker();
|
|
5656
|
+
}
|
|
5020
5657
|
function registerCheckCommand(program2) {
|
|
5021
5658
|
program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
|
|
5022
5659
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -5053,10 +5690,25 @@ function registerCheckCommand(program2) {
|
|
|
5053
5690
|
logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
|
|
5054
5691
|
}
|
|
5055
5692
|
}
|
|
5693
|
+
const shouldCheckCodex = !anyKey || opts.verbose;
|
|
5694
|
+
let codexReady = false;
|
|
5695
|
+
if (shouldCheckCodex) {
|
|
5696
|
+
logger.header("Codex Image Driver");
|
|
5697
|
+
logger.info("Checking Codex login status...");
|
|
5698
|
+
codexReady = await checkCodexImageReadiness(anyKey, opts.verbose ?? false);
|
|
5699
|
+
if (codexReady) {
|
|
5700
|
+
logger.success("Codex authenticated \u2014 experimental OpenAI image driver available");
|
|
5701
|
+
} else {
|
|
5702
|
+
logger.info("Codex image driver unavailable (run `codex login` to enable it)");
|
|
5703
|
+
}
|
|
5704
|
+
}
|
|
5056
5705
|
if (!anyKey) {
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5706
|
+
if (!codexReady) {
|
|
5707
|
+
logger.header("No Provider Keys Configured");
|
|
5708
|
+
console.log(SETUP_HINTS);
|
|
5709
|
+
process.exit(1);
|
|
5710
|
+
}
|
|
5711
|
+
anyKey = true;
|
|
5060
5712
|
}
|
|
5061
5713
|
const geminiKey = resolved.get("Gemini")?.key;
|
|
5062
5714
|
if (geminiKey) {
|
|
@@ -5079,9 +5731,11 @@ function registerCheckCommand(program2) {
|
|
|
5079
5731
|
console.log(
|
|
5080
5732
|
" Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
|
|
5081
5733
|
);
|
|
5734
|
+
} else if (resolved.get("OpenAI")?.key) {
|
|
5735
|
+
console.log(" OpenAI API setup available (images, TTS, STT)");
|
|
5082
5736
|
} else {
|
|
5083
|
-
console.log(" Image generation via
|
|
5084
|
-
console.log(" Add
|
|
5737
|
+
console.log(" Image generation via configured providers or experimental Codex driver");
|
|
5738
|
+
console.log(" Add API keys for full provider features");
|
|
5085
5739
|
}
|
|
5086
5740
|
} else {
|
|
5087
5741
|
logger.error("One or more checks failed \u2014 see above");
|
|
@@ -5092,8 +5746,8 @@ function registerCheckCommand(program2) {
|
|
|
5092
5746
|
|
|
5093
5747
|
// src/commands/doc/convert.ts
|
|
5094
5748
|
init_errors();
|
|
5095
|
-
import
|
|
5096
|
-
import
|
|
5749
|
+
import fs36 from "fs";
|
|
5750
|
+
import path41 from "path";
|
|
5097
5751
|
|
|
5098
5752
|
// src/commands/doc/default-prompt.ts
|
|
5099
5753
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -5114,18 +5768,18 @@ async function convertDocuments(opts) {
|
|
|
5114
5768
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
5115
5769
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
5116
5770
|
for (const f of inputs) {
|
|
5117
|
-
if (!
|
|
5771
|
+
if (!fs36.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
5118
5772
|
}
|
|
5119
5773
|
let outputPath;
|
|
5120
5774
|
if (output) {
|
|
5121
|
-
outputPath =
|
|
5775
|
+
outputPath = path41.resolve(output);
|
|
5122
5776
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
5123
|
-
const base =
|
|
5124
|
-
outputPath =
|
|
5777
|
+
const base = path41.basename(inputs[0], path41.extname(inputs[0]));
|
|
5778
|
+
outputPath = path41.join(getOutputDir(), `${base}-extraction.md`);
|
|
5125
5779
|
} else {
|
|
5126
|
-
outputPath =
|
|
5780
|
+
outputPath = path41.join(getOutputDir(), "document-extraction.md");
|
|
5127
5781
|
}
|
|
5128
|
-
|
|
5782
|
+
fs36.mkdirSync(path41.dirname(outputPath), { recursive: true });
|
|
5129
5783
|
const results = [];
|
|
5130
5784
|
for (let i = 0; i < inputs.length; i++) {
|
|
5131
5785
|
const filePath = inputs[i];
|
|
@@ -5176,7 +5830,7 @@ async function convertDocuments(opts) {
|
|
|
5176
5830
|
`);
|
|
5177
5831
|
for (const r of results) {
|
|
5178
5832
|
lines.push(`
|
|
5179
|
-
## ${
|
|
5833
|
+
## ${path41.basename(r.file)}
|
|
5180
5834
|
`);
|
|
5181
5835
|
if (r.status === "success" && r.markdown) {
|
|
5182
5836
|
lines.push(`${r.markdown}
|
|
@@ -5189,7 +5843,7 @@ async function convertDocuments(opts) {
|
|
|
5189
5843
|
}
|
|
5190
5844
|
lines.push("\n---\n");
|
|
5191
5845
|
}
|
|
5192
|
-
|
|
5846
|
+
fs36.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
5193
5847
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
5194
5848
|
console.log(`
|
|
5195
5849
|
${"=".repeat(50)}`);
|
|
@@ -5235,12 +5889,12 @@ function registerDocCommands(program2) {
|
|
|
5235
5889
|
|
|
5236
5890
|
// src/commands/media/index.ts
|
|
5237
5891
|
init_errors();
|
|
5238
|
-
import
|
|
5239
|
-
import
|
|
5892
|
+
import fs39 from "fs";
|
|
5893
|
+
import path44 from "path";
|
|
5240
5894
|
|
|
5241
5895
|
// src/commands/media/batch.ts
|
|
5242
|
-
import
|
|
5243
|
-
import
|
|
5896
|
+
import fs37 from "fs";
|
|
5897
|
+
import path42 from "path";
|
|
5244
5898
|
|
|
5245
5899
|
// src/commands/media/detect-type.ts
|
|
5246
5900
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -5259,10 +5913,10 @@ init_errors();
|
|
|
5259
5913
|
|
|
5260
5914
|
// src/commands/media/ffmpeg-runner.ts
|
|
5261
5915
|
init_errors();
|
|
5262
|
-
import { execa as
|
|
5916
|
+
import { execa as execa3 } from "execa";
|
|
5263
5917
|
async function assertFfmpeg() {
|
|
5264
5918
|
try {
|
|
5265
|
-
await
|
|
5919
|
+
await execa3("ffmpeg", ["-version"], { reject: true });
|
|
5266
5920
|
} catch (e) {
|
|
5267
5921
|
if (e.code === "ENOENT") {
|
|
5268
5922
|
throw new ValidationError(
|
|
@@ -5275,7 +5929,7 @@ async function runFfmpeg(args, opts = {}) {
|
|
|
5275
5929
|
const { verbose, logger } = opts;
|
|
5276
5930
|
logger?.debug(`ffmpeg ${args.join(" ")}`);
|
|
5277
5931
|
try {
|
|
5278
|
-
const result = await
|
|
5932
|
+
const result = await execa3("ffmpeg", args, { reject: true, all: true });
|
|
5279
5933
|
if (verbose && result.stderr) logger?.debug(result.stderr);
|
|
5280
5934
|
} catch (e) {
|
|
5281
5935
|
const err2 = e;
|
|
@@ -5285,7 +5939,7 @@ async function runFfmpeg(args, opts = {}) {
|
|
|
5285
5939
|
}
|
|
5286
5940
|
async function getMediaInfo(filePath) {
|
|
5287
5941
|
try {
|
|
5288
|
-
const result = await
|
|
5942
|
+
const result = await execa3(
|
|
5289
5943
|
"ffprobe",
|
|
5290
5944
|
["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath],
|
|
5291
5945
|
{ reject: true }
|
|
@@ -5348,10 +6002,10 @@ async function optimizeAudio(opts) {
|
|
|
5348
6002
|
|
|
5349
6003
|
// src/commands/media/magick-runner.ts
|
|
5350
6004
|
init_errors();
|
|
5351
|
-
import { execa as
|
|
6005
|
+
import { execa as execa4 } from "execa";
|
|
5352
6006
|
async function assertMagick() {
|
|
5353
6007
|
try {
|
|
5354
|
-
await
|
|
6008
|
+
await execa4("magick", ["-version"], { reject: true });
|
|
5355
6009
|
} catch (e) {
|
|
5356
6010
|
if (e.code === "ENOENT") {
|
|
5357
6011
|
throw new ValidationError(
|
|
@@ -5364,7 +6018,7 @@ async function runMagick(args, opts = {}) {
|
|
|
5364
6018
|
const { verbose, logger } = opts;
|
|
5365
6019
|
logger?.debug(`magick ${args.join(" ")}`);
|
|
5366
6020
|
try {
|
|
5367
|
-
const result = await
|
|
6021
|
+
const result = await execa4("magick", args, { reject: true, all: true });
|
|
5368
6022
|
if (verbose && result.stderr) logger?.debug(result.stderr);
|
|
5369
6023
|
} catch (e) {
|
|
5370
6024
|
const err2 = e;
|
|
@@ -5436,10 +6090,10 @@ async function batchOptimize(opts) {
|
|
|
5436
6090
|
verbose,
|
|
5437
6091
|
logger
|
|
5438
6092
|
} = opts;
|
|
5439
|
-
|
|
5440
|
-
const entries =
|
|
6093
|
+
fs37.mkdirSync(outputDir, { recursive: true });
|
|
6094
|
+
const entries = fs37.readdirSync(inputDir);
|
|
5441
6095
|
const mediaFiles = entries.filter((f) => {
|
|
5442
|
-
const ext =
|
|
6096
|
+
const ext = path42.extname(f).toLowerCase();
|
|
5443
6097
|
return ALL_EXTS.has(ext);
|
|
5444
6098
|
});
|
|
5445
6099
|
if (mediaFiles.length === 0) {
|
|
@@ -5450,9 +6104,9 @@ async function batchOptimize(opts) {
|
|
|
5450
6104
|
let succeeded = 0;
|
|
5451
6105
|
const errors = [];
|
|
5452
6106
|
for (const file of mediaFiles) {
|
|
5453
|
-
const inputPath =
|
|
5454
|
-
const outputPath =
|
|
5455
|
-
const ext =
|
|
6107
|
+
const inputPath = path42.join(inputDir, file);
|
|
6108
|
+
const outputPath = path42.join(outputDir, file);
|
|
6109
|
+
const ext = path42.extname(file).toLowerCase();
|
|
5456
6110
|
const kind = extToKind(ext);
|
|
5457
6111
|
logger?.debug(`Processing: ${file}`);
|
|
5458
6112
|
try {
|
|
@@ -5489,8 +6143,8 @@ async function batchOptimize(opts) {
|
|
|
5489
6143
|
|
|
5490
6144
|
// src/commands/media/split-video.ts
|
|
5491
6145
|
init_errors();
|
|
5492
|
-
import
|
|
5493
|
-
import
|
|
6146
|
+
import fs38 from "fs";
|
|
6147
|
+
import path43 from "path";
|
|
5494
6148
|
async function splitVideo(opts) {
|
|
5495
6149
|
await assertFfmpeg();
|
|
5496
6150
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -5501,10 +6155,10 @@ async function splitVideo(opts) {
|
|
|
5501
6155
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
5502
6156
|
return [input];
|
|
5503
6157
|
}
|
|
5504
|
-
|
|
5505
|
-
const ext =
|
|
5506
|
-
const baseName =
|
|
5507
|
-
const outputPattern =
|
|
6158
|
+
fs38.mkdirSync(outputDir, { recursive: true });
|
|
6159
|
+
const ext = path43.extname(input) || ".mp4";
|
|
6160
|
+
const baseName = path43.basename(input, ext);
|
|
6161
|
+
const outputPattern = path43.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
5508
6162
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
5509
6163
|
const args = [
|
|
5510
6164
|
"-i",
|
|
@@ -5523,8 +6177,8 @@ async function splitVideo(opts) {
|
|
|
5523
6177
|
await runFfmpeg(args, { verbose, logger });
|
|
5524
6178
|
const files = [];
|
|
5525
6179
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
5526
|
-
const fname =
|
|
5527
|
-
if (
|
|
6180
|
+
const fname = path43.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
6181
|
+
if (fs38.existsSync(fname)) files.push(fname);
|
|
5528
6182
|
}
|
|
5529
6183
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
5530
6184
|
return files;
|
|
@@ -5538,9 +6192,9 @@ function registerMediaCommands(program2) {
|
|
|
5538
6192
|
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(
|
|
5539
6193
|
async (opts) => {
|
|
5540
6194
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5541
|
-
if (!
|
|
5542
|
-
|
|
5543
|
-
const ext =
|
|
6195
|
+
if (!fs39.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
6196
|
+
fs39.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
|
|
6197
|
+
const ext = path44.extname(opts.input).toLowerCase();
|
|
5544
6198
|
const kind = extToKind(ext);
|
|
5545
6199
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
5546
6200
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -5580,7 +6234,7 @@ function registerMediaCommands(program2) {
|
|
|
5580
6234
|
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(
|
|
5581
6235
|
async (opts) => {
|
|
5582
6236
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5583
|
-
if (!
|
|
6237
|
+
if (!fs39.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
5584
6238
|
const chunks = await splitVideo({
|
|
5585
6239
|
input: opts.input,
|
|
5586
6240
|
outputDir: opts.outputDir,
|
|
@@ -5596,7 +6250,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
5596
6250
|
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(
|
|
5597
6251
|
async (opts) => {
|
|
5598
6252
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5599
|
-
if (!
|
|
6253
|
+
if (!fs39.existsSync(opts.inputDir))
|
|
5600
6254
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
5601
6255
|
const result = await batchOptimize({
|
|
5602
6256
|
inputDir: opts.inputDir,
|
|
@@ -5622,6 +6276,7 @@ function registerCommands(program2) {
|
|
|
5622
6276
|
registerCheckCommand(program2);
|
|
5623
6277
|
registerGeminiCommands(program2);
|
|
5624
6278
|
registerMinimaxCommands(program2);
|
|
6279
|
+
registerOpenAICommands(program2);
|
|
5625
6280
|
registerOpenRouterCommands(program2);
|
|
5626
6281
|
registerLeonardoCommands(program2);
|
|
5627
6282
|
registerBytePlusCommands(program2);
|