@mrgoonie/multix 0.0.7 → 0.1.0-beta
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 +24 -0
- package/README.md +60 -5
- package/dist/cli.js +891 -157
- package/package.json +4 -2
- package/skill/SKILL.md +159 -101
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,23 +4356,650 @@ 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();
|
|
4364
|
-
var
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
const key = resolveKey("OPENROUTER_API_KEY");
|
|
4481
|
+
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
4482
|
+
function requireOpenAIKey() {
|
|
4483
|
+
const key = resolveKey("OPENAI_API_KEY");
|
|
4368
4484
|
if (!key) {
|
|
4369
4485
|
throw new ConfigError(
|
|
4370
|
-
"
|
|
4486
|
+
"OPENAI_API_KEY is not set. Get one at https://platform.openai.com/api-keys"
|
|
4371
4487
|
);
|
|
4372
4488
|
}
|
|
4373
4489
|
return key;
|
|
4374
4490
|
}
|
|
4375
|
-
function
|
|
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();
|
|
4991
|
+
|
|
4992
|
+
// src/providers/openrouter/payload.ts
|
|
4993
|
+
var IMAGE_ONLY_MODEL_PREFIXES = [
|
|
4994
|
+
"black-forest-labs/",
|
|
4995
|
+
"bytedance-seed/seedream",
|
|
4996
|
+
"sourceful/"
|
|
4997
|
+
];
|
|
4998
|
+
function resolveModalities(model) {
|
|
4999
|
+
const lower = model.toLowerCase();
|
|
5000
|
+
return IMAGE_ONLY_MODEL_PREFIXES.some((p) => lower.startsWith(p)) ? ["image"] : ["image", "text"];
|
|
5001
|
+
}
|
|
5002
|
+
function buildOpenRouterHeaders(apiKey) {
|
|
4376
5003
|
const headers = {
|
|
4377
5004
|
Authorization: `Bearer ${apiKey}`,
|
|
4378
5005
|
"Content-Type": "application/json"
|
|
@@ -4383,10 +5010,96 @@ function buildHeaders(apiKey) {
|
|
|
4383
5010
|
if (title) headers["X-Title"] = title;
|
|
4384
5011
|
return headers;
|
|
4385
5012
|
}
|
|
5013
|
+
function extractImagesFromResponse(data) {
|
|
5014
|
+
const out = { urls: [] };
|
|
5015
|
+
if (!data || typeof data !== "object") return out;
|
|
5016
|
+
const root = data;
|
|
5017
|
+
if (typeof root.model === "string") out.model = root.model;
|
|
5018
|
+
const choices = Array.isArray(root.choices) ? root.choices : [];
|
|
5019
|
+
const first = choices[0];
|
|
5020
|
+
if (!first || typeof first !== "object") return out;
|
|
5021
|
+
const choice = first;
|
|
5022
|
+
if (typeof choice.finish_reason === "string") out.finishReason = choice.finish_reason;
|
|
5023
|
+
const message = choice.message;
|
|
5024
|
+
if (!message) return out;
|
|
5025
|
+
const images = message.images;
|
|
5026
|
+
if (Array.isArray(images)) {
|
|
5027
|
+
for (const item of images) {
|
|
5028
|
+
const url = readImageUrl(item);
|
|
5029
|
+
if (url) out.urls.push(url);
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
const content = message.content;
|
|
5033
|
+
if (Array.isArray(content)) {
|
|
5034
|
+
for (const part of content) {
|
|
5035
|
+
const url = readImageUrl(part);
|
|
5036
|
+
if (url) out.urls.push(url);
|
|
5037
|
+
}
|
|
5038
|
+
} else if (typeof content === "string") {
|
|
5039
|
+
out.textContent = content;
|
|
5040
|
+
}
|
|
5041
|
+
return out;
|
|
5042
|
+
}
|
|
5043
|
+
function readImageUrl(item) {
|
|
5044
|
+
if (!item || typeof item !== "object") return void 0;
|
|
5045
|
+
const rec = item;
|
|
5046
|
+
if (rec.type !== void 0 && rec.type !== "image_url") return void 0;
|
|
5047
|
+
const inner = rec.image_url;
|
|
5048
|
+
const url = inner?.url;
|
|
5049
|
+
return typeof url === "string" ? url : void 0;
|
|
5050
|
+
}
|
|
5051
|
+
function formatNoImagesError(model, parsed) {
|
|
5052
|
+
const parts = [
|
|
5053
|
+
`No images returned by '${model}' (finish_reason=${parsed.finishReason ?? "n/a"}).`
|
|
5054
|
+
];
|
|
5055
|
+
if (parsed.textContent) {
|
|
5056
|
+
parts.push(`Model said: "${truncate(parsed.textContent, 200)}".`);
|
|
5057
|
+
}
|
|
5058
|
+
parts.push(
|
|
5059
|
+
"Hint: verify the model supports image output and that 'modalities' includes 'image'."
|
|
5060
|
+
);
|
|
5061
|
+
return parts.join(" ");
|
|
5062
|
+
}
|
|
5063
|
+
function truncate(s, n) {
|
|
5064
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
5065
|
+
}
|
|
5066
|
+
function buildI2IPayload(opts) {
|
|
5067
|
+
const content = opts.refs.map((url) => ({
|
|
5068
|
+
type: "image_url",
|
|
5069
|
+
image_url: { url }
|
|
5070
|
+
}));
|
|
5071
|
+
content.push({ type: "text", text: opts.prompt });
|
|
5072
|
+
const payload = {
|
|
5073
|
+
messages: [{ role: "user", content }],
|
|
5074
|
+
modalities: resolveModalities(opts.model)
|
|
5075
|
+
};
|
|
5076
|
+
if (typeof opts.strength === "number") {
|
|
5077
|
+
payload.image_config = { strength: opts.strength };
|
|
5078
|
+
}
|
|
5079
|
+
if (opts.fallbackModels.length > 0) {
|
|
5080
|
+
payload.models = [opts.model, ...opts.fallbackModels];
|
|
5081
|
+
} else {
|
|
5082
|
+
payload.model = opts.model;
|
|
5083
|
+
}
|
|
5084
|
+
return payload;
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
// src/providers/openrouter/client.ts
|
|
5088
|
+
var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
5089
|
+
var DEFAULT_OPENROUTER_MODEL = "google/gemini-3.1-flash-image-preview";
|
|
5090
|
+
function requireOpenRouterKey() {
|
|
5091
|
+
const key = resolveKey("OPENROUTER_API_KEY");
|
|
5092
|
+
if (!key) {
|
|
5093
|
+
throw new ConfigError(
|
|
5094
|
+
"OPENROUTER_API_KEY is not set. Get one at https://openrouter.ai/settings/keys"
|
|
5095
|
+
);
|
|
5096
|
+
}
|
|
5097
|
+
return key;
|
|
5098
|
+
}
|
|
4386
5099
|
function buildPayload(opts) {
|
|
4387
5100
|
const payload = {
|
|
4388
5101
|
messages: [{ role: "user", content: opts.prompt }],
|
|
4389
|
-
modalities: opts.model
|
|
5102
|
+
modalities: resolveModalities(opts.model),
|
|
4390
5103
|
image_config: { aspect_ratio: opts.aspectRatio }
|
|
4391
5104
|
};
|
|
4392
5105
|
if (opts.imageSize) {
|
|
@@ -4415,29 +5128,19 @@ async function generateOpenRouterImage(opts) {
|
|
|
4415
5128
|
data = await httpJson({
|
|
4416
5129
|
url: OPENROUTER_API_URL,
|
|
4417
5130
|
method: "POST",
|
|
4418
|
-
headers:
|
|
5131
|
+
headers: buildOpenRouterHeaders(apiKey),
|
|
4419
5132
|
body: payload,
|
|
4420
5133
|
timeoutMs: 24e4
|
|
4421
5134
|
});
|
|
4422
5135
|
} catch (e) {
|
|
4423
5136
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
4424
5137
|
}
|
|
4425
|
-
const
|
|
4426
|
-
if (
|
|
4427
|
-
return { status: "error", error:
|
|
5138
|
+
const parsed = extractImagesFromResponse(data);
|
|
5139
|
+
if (parsed.urls.length === 0) {
|
|
5140
|
+
return { status: "error", error: formatNoImagesError(model, parsed) };
|
|
4428
5141
|
}
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
return {
|
|
4432
|
-
status: "error",
|
|
4433
|
-
error: `No images in response \u2014 model '${model}' may not support image generation`
|
|
4434
|
-
};
|
|
4435
|
-
}
|
|
4436
|
-
for (const img of messageImages) {
|
|
4437
|
-
const url = img.image_url?.url;
|
|
4438
|
-
if (url) imageUrls.push(url);
|
|
4439
|
-
}
|
|
4440
|
-
usedModel = data.model ?? usedModel;
|
|
5142
|
+
imageUrls.push(...parsed.urls);
|
|
5143
|
+
usedModel = parsed.model ?? usedModel;
|
|
4441
5144
|
if (imageUrls.length >= numImages) break;
|
|
4442
5145
|
}
|
|
4443
5146
|
const outDir = getOutputDir();
|
|
@@ -4447,8 +5150,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
4447
5150
|
if (!url) continue;
|
|
4448
5151
|
try {
|
|
4449
5152
|
const bytes = await fetchBytes(url);
|
|
4450
|
-
const dest =
|
|
4451
|
-
|
|
5153
|
+
const dest = path38.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
5154
|
+
fs33.writeFileSync(dest, bytes);
|
|
4452
5155
|
savedFiles.push(dest);
|
|
4453
5156
|
logger?.success(`Saved: ${dest}`);
|
|
4454
5157
|
} catch (e) {
|
|
@@ -4459,8 +5162,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
4459
5162
|
}
|
|
4460
5163
|
}
|
|
4461
5164
|
if (output && savedFiles[0]) {
|
|
4462
|
-
|
|
4463
|
-
|
|
5165
|
+
fs33.mkdirSync(path38.dirname(path38.resolve(output)), { recursive: true });
|
|
5166
|
+
fs33.writeFileSync(output, fs33.readFileSync(savedFiles[0]));
|
|
4464
5167
|
}
|
|
4465
5168
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
4466
5169
|
}
|
|
@@ -4495,8 +5198,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
4495
5198
|
}
|
|
4496
5199
|
|
|
4497
5200
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
4498
|
-
import
|
|
4499
|
-
import
|
|
5201
|
+
import fs34 from "fs";
|
|
5202
|
+
import path39 from "path";
|
|
4500
5203
|
init_errors();
|
|
4501
5204
|
init_http_client();
|
|
4502
5205
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -4506,15 +5209,17 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
4506
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(
|
|
4507
5210
|
"--ref <path>",
|
|
4508
5211
|
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
4509
|
-
|
|
5212
|
+
collect7,
|
|
4510
5213
|
[]
|
|
4511
|
-
).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
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(
|
|
4512
5215
|
async (opts) => {
|
|
4513
5216
|
if (!opts.ref || opts.ref.length === 0) {
|
|
4514
|
-
throw new
|
|
5217
|
+
throw new ValidationError("At least one --ref is required for image-to-image.");
|
|
4515
5218
|
}
|
|
4516
5219
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4517
5220
|
const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_MODEL;
|
|
5221
|
+
const strength = parseStrength(opts.strength);
|
|
5222
|
+
const fallbackModels = (resolveKey("OPENROUTER_FALLBACK_MODELS") ?? "").split(",").map((m) => m.trim()).filter(Boolean);
|
|
4518
5223
|
const resolvedRefs = [];
|
|
4519
5224
|
for (const r of opts.ref) {
|
|
4520
5225
|
const resolved = await resolveImageInput(r, {
|
|
@@ -4524,11 +5229,17 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
4524
5229
|
});
|
|
4525
5230
|
resolvedRefs.push(refUrl(resolved));
|
|
4526
5231
|
}
|
|
4527
|
-
|
|
5232
|
+
const strengthDbg = strength !== void 0 ? `, strength=${strength}` : "";
|
|
5233
|
+
const fallbacksDbg = fallbackModels.length ? `, fallbacks=${fallbackModels.join(",")}` : "";
|
|
5234
|
+
logger.debug(
|
|
5235
|
+
`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}${strengthDbg}${fallbacksDbg}`
|
|
5236
|
+
);
|
|
4528
5237
|
const saved = await runImageToImage({
|
|
4529
5238
|
prompt: opts.prompt,
|
|
4530
5239
|
model,
|
|
4531
5240
|
refs: resolvedRefs,
|
|
5241
|
+
strength,
|
|
5242
|
+
fallbackModels,
|
|
4532
5243
|
output: opts.output,
|
|
4533
5244
|
logger
|
|
4534
5245
|
});
|
|
@@ -4539,29 +5250,27 @@ Generated ${saved.length} image(s):`);
|
|
|
4539
5250
|
}
|
|
4540
5251
|
);
|
|
4541
5252
|
}
|
|
4542
|
-
function
|
|
5253
|
+
function collect7(value, prev) {
|
|
4543
5254
|
return [...prev, value];
|
|
4544
5255
|
}
|
|
5256
|
+
function parseStrength(raw) {
|
|
5257
|
+
if (raw === void 0) return void 0;
|
|
5258
|
+
const n = Number.parseFloat(raw);
|
|
5259
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
5260
|
+
throw new ValidationError(`--strength must be a number in [0,1], got '${raw}'`);
|
|
5261
|
+
}
|
|
5262
|
+
return n;
|
|
5263
|
+
}
|
|
4545
5264
|
async function runImageToImage(opts) {
|
|
4546
5265
|
const apiKey = requireOpenRouterKey();
|
|
4547
|
-
const headers =
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
};
|
|
4551
|
-
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
4552
|
-
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
4553
|
-
if (referer) headers["HTTP-Referer"] = referer;
|
|
4554
|
-
if (title) headers["X-Title"] = title;
|
|
4555
|
-
const content = opts.refs.map((url) => ({
|
|
4556
|
-
type: "image_url",
|
|
4557
|
-
image_url: { url }
|
|
4558
|
-
}));
|
|
4559
|
-
content.push({ type: "text", text: opts.prompt });
|
|
4560
|
-
const payload = {
|
|
5266
|
+
const headers = buildOpenRouterHeaders(apiKey);
|
|
5267
|
+
const payload = buildI2IPayload({
|
|
5268
|
+
prompt: opts.prompt,
|
|
4561
5269
|
model: opts.model,
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
5270
|
+
refs: opts.refs,
|
|
5271
|
+
strength: opts.strength,
|
|
5272
|
+
fallbackModels: opts.fallbackModels
|
|
5273
|
+
});
|
|
4565
5274
|
const data = await httpJson({
|
|
4566
5275
|
url: OPENROUTER_API_URL,
|
|
4567
5276
|
method: "POST",
|
|
@@ -4569,28 +5278,25 @@ async function runImageToImage(opts) {
|
|
|
4569
5278
|
body: payload,
|
|
4570
5279
|
timeoutMs: 24e4
|
|
4571
5280
|
});
|
|
4572
|
-
const
|
|
4573
|
-
if (
|
|
4574
|
-
throw new ProviderError(
|
|
4575
|
-
`No images in response \u2014 model '${opts.model}' may not support image-to-image`,
|
|
4576
|
-
"openrouter"
|
|
4577
|
-
);
|
|
5281
|
+
const parsed = extractImagesFromResponse(data);
|
|
5282
|
+
if (parsed.urls.length === 0) {
|
|
5283
|
+
throw new ProviderError(formatNoImagesError(opts.model, parsed), "openrouter");
|
|
4578
5284
|
}
|
|
4579
5285
|
const outDir = getOutputDir();
|
|
4580
5286
|
const ts = Date.now();
|
|
4581
5287
|
const saved = [];
|
|
4582
|
-
for (let i = 0; i <
|
|
4583
|
-
const url =
|
|
5288
|
+
for (let i = 0; i < parsed.urls.length; i++) {
|
|
5289
|
+
const url = parsed.urls[i];
|
|
4584
5290
|
if (!url) continue;
|
|
4585
5291
|
const bytes = await fetchBytes(url);
|
|
4586
|
-
const dest =
|
|
4587
|
-
|
|
5292
|
+
const dest = path39.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
5293
|
+
fs34.writeFileSync(dest, bytes);
|
|
4588
5294
|
saved.push(dest);
|
|
4589
5295
|
opts.logger.success(`Saved: ${dest}`);
|
|
4590
5296
|
}
|
|
4591
5297
|
if (opts.output && saved[0]) {
|
|
4592
|
-
|
|
4593
|
-
|
|
5298
|
+
fs34.mkdirSync(path39.dirname(path39.resolve(opts.output)), { recursive: true });
|
|
5299
|
+
fs34.copyFileSync(saved[0], opts.output);
|
|
4594
5300
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
4595
5301
|
}
|
|
4596
5302
|
return saved;
|
|
@@ -4599,7 +5305,7 @@ async function runImageToImage(opts) {
|
|
|
4599
5305
|
// src/providers/openrouter/video-client.ts
|
|
4600
5306
|
init_http_client();
|
|
4601
5307
|
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
4602
|
-
function
|
|
5308
|
+
function authHeaders5(apiKey) {
|
|
4603
5309
|
const headers = {
|
|
4604
5310
|
Authorization: `Bearer ${apiKey}`,
|
|
4605
5311
|
"Content-Type": "application/json"
|
|
@@ -4614,7 +5320,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
4614
5320
|
return httpJson({
|
|
4615
5321
|
url: OPENROUTER_VIDEOS_URL,
|
|
4616
5322
|
method: "POST",
|
|
4617
|
-
headers:
|
|
5323
|
+
headers: authHeaders5(apiKey),
|
|
4618
5324
|
body,
|
|
4619
5325
|
timeoutMs: 12e4
|
|
4620
5326
|
});
|
|
@@ -4622,7 +5328,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
4622
5328
|
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
4623
5329
|
return httpJson({
|
|
4624
5330
|
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
4625
|
-
headers:
|
|
5331
|
+
headers: authHeaders5(apiKey)
|
|
4626
5332
|
});
|
|
4627
5333
|
}
|
|
4628
5334
|
function videoContentUrl(jobId) {
|
|
@@ -4631,18 +5337,18 @@ function videoContentUrl(jobId) {
|
|
|
4631
5337
|
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
4632
5338
|
return httpJson({
|
|
4633
5339
|
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
4634
|
-
headers:
|
|
5340
|
+
headers: authHeaders5(apiKey)
|
|
4635
5341
|
});
|
|
4636
5342
|
}
|
|
4637
5343
|
|
|
4638
5344
|
// src/providers/openrouter/commands/video-status.ts
|
|
4639
|
-
import
|
|
4640
|
-
import
|
|
5345
|
+
import fs35 from "fs";
|
|
5346
|
+
import path40 from "path";
|
|
4641
5347
|
async function downloadOpenRouterVideo(opts) {
|
|
4642
5348
|
const { jobId, logger, source } = opts;
|
|
4643
5349
|
const apiKey = requireOpenRouterKey();
|
|
4644
5350
|
const url = videoContentUrl(jobId);
|
|
4645
|
-
const dest =
|
|
5351
|
+
const dest = path40.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
4646
5352
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
4647
5353
|
if (!r.ok) {
|
|
4648
5354
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -4652,14 +5358,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
4652
5358
|
logger.error("Empty response body");
|
|
4653
5359
|
process.exit(1);
|
|
4654
5360
|
}
|
|
4655
|
-
|
|
4656
|
-
const ws =
|
|
5361
|
+
fs35.mkdirSync(path40.dirname(dest), { recursive: true });
|
|
5362
|
+
const ws = fs35.createWriteStream(dest);
|
|
4657
5363
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
4658
5364
|
await pipeline2(r.body, ws);
|
|
4659
5365
|
logger.success(`Saved ${dest}`);
|
|
4660
5366
|
if (opts.output) {
|
|
4661
|
-
|
|
4662
|
-
|
|
5367
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5368
|
+
fs35.copyFileSync(dest, opts.output);
|
|
4663
5369
|
logger.success(`Copied to ${opts.output}`);
|
|
4664
5370
|
}
|
|
4665
5371
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -4819,10 +5525,10 @@ function registerOpenRouterCommands(program2) {
|
|
|
4819
5525
|
}
|
|
4820
5526
|
|
|
4821
5527
|
// src/commands/check-helpers/binary-check.ts
|
|
4822
|
-
import { execa } from "execa";
|
|
5528
|
+
import { execa as execa2 } from "execa";
|
|
4823
5529
|
async function checkBinary(binary) {
|
|
4824
5530
|
try {
|
|
4825
|
-
const result = await
|
|
5531
|
+
const result = await execa2(binary, ["--version"], { reject: true, all: true });
|
|
4826
5532
|
const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
|
|
4827
5533
|
return { available: true, version, binary };
|
|
4828
5534
|
} catch (e) {
|
|
@@ -4903,6 +5609,12 @@ var PROVIDERS = [
|
|
|
4903
5609
|
optional: true,
|
|
4904
5610
|
link: "https://openrouter.ai/settings/keys"
|
|
4905
5611
|
},
|
|
5612
|
+
{
|
|
5613
|
+
name: "OpenAI",
|
|
5614
|
+
envPrimary: "OPENAI_API_KEY",
|
|
5615
|
+
optional: true,
|
|
5616
|
+
link: "https://platform.openai.com/api-keys"
|
|
5617
|
+
},
|
|
4906
5618
|
{
|
|
4907
5619
|
name: "MiniMax",
|
|
4908
5620
|
envPrimary: "MINIMAX_API_KEY",
|
|
@@ -4938,6 +5650,10 @@ function resolveProviderKey(p) {
|
|
|
4938
5650
|
}
|
|
4939
5651
|
return {};
|
|
4940
5652
|
}
|
|
5653
|
+
async function checkCodexImageReadiness(hasAnyProviderKey, verbose = false, checker = checkCodexAuthenticated) {
|
|
5654
|
+
if (hasAnyProviderKey && !verbose) return false;
|
|
5655
|
+
return checker();
|
|
5656
|
+
}
|
|
4941
5657
|
function registerCheckCommand(program2) {
|
|
4942
5658
|
program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
|
|
4943
5659
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -4974,10 +5690,25 @@ function registerCheckCommand(program2) {
|
|
|
4974
5690
|
logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
|
|
4975
5691
|
}
|
|
4976
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
|
+
}
|
|
4977
5705
|
if (!anyKey) {
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
5706
|
+
if (!codexReady) {
|
|
5707
|
+
logger.header("No Provider Keys Configured");
|
|
5708
|
+
console.log(SETUP_HINTS);
|
|
5709
|
+
process.exit(1);
|
|
5710
|
+
}
|
|
5711
|
+
anyKey = true;
|
|
4981
5712
|
}
|
|
4982
5713
|
const geminiKey = resolved.get("Gemini")?.key;
|
|
4983
5714
|
if (geminiKey) {
|
|
@@ -5000,9 +5731,11 @@ function registerCheckCommand(program2) {
|
|
|
5000
5731
|
console.log(
|
|
5001
5732
|
" Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
|
|
5002
5733
|
);
|
|
5734
|
+
} else if (resolved.get("OpenAI")?.key) {
|
|
5735
|
+
console.log(" OpenAI API setup available (images, TTS, STT)");
|
|
5003
5736
|
} else {
|
|
5004
|
-
console.log(" Image generation via
|
|
5005
|
-
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");
|
|
5006
5739
|
}
|
|
5007
5740
|
} else {
|
|
5008
5741
|
logger.error("One or more checks failed \u2014 see above");
|
|
@@ -5013,8 +5746,8 @@ function registerCheckCommand(program2) {
|
|
|
5013
5746
|
|
|
5014
5747
|
// src/commands/doc/convert.ts
|
|
5015
5748
|
init_errors();
|
|
5016
|
-
import
|
|
5017
|
-
import
|
|
5749
|
+
import fs36 from "fs";
|
|
5750
|
+
import path41 from "path";
|
|
5018
5751
|
|
|
5019
5752
|
// src/commands/doc/default-prompt.ts
|
|
5020
5753
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -5035,18 +5768,18 @@ async function convertDocuments(opts) {
|
|
|
5035
5768
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
5036
5769
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
5037
5770
|
for (const f of inputs) {
|
|
5038
|
-
if (!
|
|
5771
|
+
if (!fs36.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
5039
5772
|
}
|
|
5040
5773
|
let outputPath;
|
|
5041
5774
|
if (output) {
|
|
5042
|
-
outputPath =
|
|
5775
|
+
outputPath = path41.resolve(output);
|
|
5043
5776
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
5044
|
-
const base =
|
|
5045
|
-
outputPath =
|
|
5777
|
+
const base = path41.basename(inputs[0], path41.extname(inputs[0]));
|
|
5778
|
+
outputPath = path41.join(getOutputDir(), `${base}-extraction.md`);
|
|
5046
5779
|
} else {
|
|
5047
|
-
outputPath =
|
|
5780
|
+
outputPath = path41.join(getOutputDir(), "document-extraction.md");
|
|
5048
5781
|
}
|
|
5049
|
-
|
|
5782
|
+
fs36.mkdirSync(path41.dirname(outputPath), { recursive: true });
|
|
5050
5783
|
const results = [];
|
|
5051
5784
|
for (let i = 0; i < inputs.length; i++) {
|
|
5052
5785
|
const filePath = inputs[i];
|
|
@@ -5097,7 +5830,7 @@ async function convertDocuments(opts) {
|
|
|
5097
5830
|
`);
|
|
5098
5831
|
for (const r of results) {
|
|
5099
5832
|
lines.push(`
|
|
5100
|
-
## ${
|
|
5833
|
+
## ${path41.basename(r.file)}
|
|
5101
5834
|
`);
|
|
5102
5835
|
if (r.status === "success" && r.markdown) {
|
|
5103
5836
|
lines.push(`${r.markdown}
|
|
@@ -5110,7 +5843,7 @@ async function convertDocuments(opts) {
|
|
|
5110
5843
|
}
|
|
5111
5844
|
lines.push("\n---\n");
|
|
5112
5845
|
}
|
|
5113
|
-
|
|
5846
|
+
fs36.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
5114
5847
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
5115
5848
|
console.log(`
|
|
5116
5849
|
${"=".repeat(50)}`);
|
|
@@ -5156,12 +5889,12 @@ function registerDocCommands(program2) {
|
|
|
5156
5889
|
|
|
5157
5890
|
// src/commands/media/index.ts
|
|
5158
5891
|
init_errors();
|
|
5159
|
-
import
|
|
5160
|
-
import
|
|
5892
|
+
import fs39 from "fs";
|
|
5893
|
+
import path44 from "path";
|
|
5161
5894
|
|
|
5162
5895
|
// src/commands/media/batch.ts
|
|
5163
|
-
import
|
|
5164
|
-
import
|
|
5896
|
+
import fs37 from "fs";
|
|
5897
|
+
import path42 from "path";
|
|
5165
5898
|
|
|
5166
5899
|
// src/commands/media/detect-type.ts
|
|
5167
5900
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -5180,10 +5913,10 @@ init_errors();
|
|
|
5180
5913
|
|
|
5181
5914
|
// src/commands/media/ffmpeg-runner.ts
|
|
5182
5915
|
init_errors();
|
|
5183
|
-
import { execa as
|
|
5916
|
+
import { execa as execa3 } from "execa";
|
|
5184
5917
|
async function assertFfmpeg() {
|
|
5185
5918
|
try {
|
|
5186
|
-
await
|
|
5919
|
+
await execa3("ffmpeg", ["-version"], { reject: true });
|
|
5187
5920
|
} catch (e) {
|
|
5188
5921
|
if (e.code === "ENOENT") {
|
|
5189
5922
|
throw new ValidationError(
|
|
@@ -5196,7 +5929,7 @@ async function runFfmpeg(args, opts = {}) {
|
|
|
5196
5929
|
const { verbose, logger } = opts;
|
|
5197
5930
|
logger?.debug(`ffmpeg ${args.join(" ")}`);
|
|
5198
5931
|
try {
|
|
5199
|
-
const result = await
|
|
5932
|
+
const result = await execa3("ffmpeg", args, { reject: true, all: true });
|
|
5200
5933
|
if (verbose && result.stderr) logger?.debug(result.stderr);
|
|
5201
5934
|
} catch (e) {
|
|
5202
5935
|
const err2 = e;
|
|
@@ -5206,7 +5939,7 @@ async function runFfmpeg(args, opts = {}) {
|
|
|
5206
5939
|
}
|
|
5207
5940
|
async function getMediaInfo(filePath) {
|
|
5208
5941
|
try {
|
|
5209
|
-
const result = await
|
|
5942
|
+
const result = await execa3(
|
|
5210
5943
|
"ffprobe",
|
|
5211
5944
|
["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath],
|
|
5212
5945
|
{ reject: true }
|
|
@@ -5269,10 +6002,10 @@ async function optimizeAudio(opts) {
|
|
|
5269
6002
|
|
|
5270
6003
|
// src/commands/media/magick-runner.ts
|
|
5271
6004
|
init_errors();
|
|
5272
|
-
import { execa as
|
|
6005
|
+
import { execa as execa4 } from "execa";
|
|
5273
6006
|
async function assertMagick() {
|
|
5274
6007
|
try {
|
|
5275
|
-
await
|
|
6008
|
+
await execa4("magick", ["-version"], { reject: true });
|
|
5276
6009
|
} catch (e) {
|
|
5277
6010
|
if (e.code === "ENOENT") {
|
|
5278
6011
|
throw new ValidationError(
|
|
@@ -5285,7 +6018,7 @@ async function runMagick(args, opts = {}) {
|
|
|
5285
6018
|
const { verbose, logger } = opts;
|
|
5286
6019
|
logger?.debug(`magick ${args.join(" ")}`);
|
|
5287
6020
|
try {
|
|
5288
|
-
const result = await
|
|
6021
|
+
const result = await execa4("magick", args, { reject: true, all: true });
|
|
5289
6022
|
if (verbose && result.stderr) logger?.debug(result.stderr);
|
|
5290
6023
|
} catch (e) {
|
|
5291
6024
|
const err2 = e;
|
|
@@ -5357,10 +6090,10 @@ async function batchOptimize(opts) {
|
|
|
5357
6090
|
verbose,
|
|
5358
6091
|
logger
|
|
5359
6092
|
} = opts;
|
|
5360
|
-
|
|
5361
|
-
const entries =
|
|
6093
|
+
fs37.mkdirSync(outputDir, { recursive: true });
|
|
6094
|
+
const entries = fs37.readdirSync(inputDir);
|
|
5362
6095
|
const mediaFiles = entries.filter((f) => {
|
|
5363
|
-
const ext =
|
|
6096
|
+
const ext = path42.extname(f).toLowerCase();
|
|
5364
6097
|
return ALL_EXTS.has(ext);
|
|
5365
6098
|
});
|
|
5366
6099
|
if (mediaFiles.length === 0) {
|
|
@@ -5371,9 +6104,9 @@ async function batchOptimize(opts) {
|
|
|
5371
6104
|
let succeeded = 0;
|
|
5372
6105
|
const errors = [];
|
|
5373
6106
|
for (const file of mediaFiles) {
|
|
5374
|
-
const inputPath =
|
|
5375
|
-
const outputPath =
|
|
5376
|
-
const ext =
|
|
6107
|
+
const inputPath = path42.join(inputDir, file);
|
|
6108
|
+
const outputPath = path42.join(outputDir, file);
|
|
6109
|
+
const ext = path42.extname(file).toLowerCase();
|
|
5377
6110
|
const kind = extToKind(ext);
|
|
5378
6111
|
logger?.debug(`Processing: ${file}`);
|
|
5379
6112
|
try {
|
|
@@ -5410,8 +6143,8 @@ async function batchOptimize(opts) {
|
|
|
5410
6143
|
|
|
5411
6144
|
// src/commands/media/split-video.ts
|
|
5412
6145
|
init_errors();
|
|
5413
|
-
import
|
|
5414
|
-
import
|
|
6146
|
+
import fs38 from "fs";
|
|
6147
|
+
import path43 from "path";
|
|
5415
6148
|
async function splitVideo(opts) {
|
|
5416
6149
|
await assertFfmpeg();
|
|
5417
6150
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -5422,10 +6155,10 @@ async function splitVideo(opts) {
|
|
|
5422
6155
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
5423
6156
|
return [input];
|
|
5424
6157
|
}
|
|
5425
|
-
|
|
5426
|
-
const ext =
|
|
5427
|
-
const baseName =
|
|
5428
|
-
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}`);
|
|
5429
6162
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
5430
6163
|
const args = [
|
|
5431
6164
|
"-i",
|
|
@@ -5444,8 +6177,8 @@ async function splitVideo(opts) {
|
|
|
5444
6177
|
await runFfmpeg(args, { verbose, logger });
|
|
5445
6178
|
const files = [];
|
|
5446
6179
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
5447
|
-
const fname =
|
|
5448
|
-
if (
|
|
6180
|
+
const fname = path43.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
6181
|
+
if (fs38.existsSync(fname)) files.push(fname);
|
|
5449
6182
|
}
|
|
5450
6183
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
5451
6184
|
return files;
|
|
@@ -5459,9 +6192,9 @@ function registerMediaCommands(program2) {
|
|
|
5459
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(
|
|
5460
6193
|
async (opts) => {
|
|
5461
6194
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5462
|
-
if (!
|
|
5463
|
-
|
|
5464
|
-
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();
|
|
5465
6198
|
const kind = extToKind(ext);
|
|
5466
6199
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
5467
6200
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -5501,7 +6234,7 @@ function registerMediaCommands(program2) {
|
|
|
5501
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(
|
|
5502
6235
|
async (opts) => {
|
|
5503
6236
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5504
|
-
if (!
|
|
6237
|
+
if (!fs39.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
5505
6238
|
const chunks = await splitVideo({
|
|
5506
6239
|
input: opts.input,
|
|
5507
6240
|
outputDir: opts.outputDir,
|
|
@@ -5517,7 +6250,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
5517
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(
|
|
5518
6251
|
async (opts) => {
|
|
5519
6252
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5520
|
-
if (!
|
|
6253
|
+
if (!fs39.existsSync(opts.inputDir))
|
|
5521
6254
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
5522
6255
|
const result = await batchOptimize({
|
|
5523
6256
|
inputDir: opts.inputDir,
|
|
@@ -5543,6 +6276,7 @@ function registerCommands(program2) {
|
|
|
5543
6276
|
registerCheckCommand(program2);
|
|
5544
6277
|
registerGeminiCommands(program2);
|
|
5545
6278
|
registerMinimaxCommands(program2);
|
|
6279
|
+
registerOpenAICommands(program2);
|
|
5546
6280
|
registerOpenRouterCommands(program2);
|
|
5547
6281
|
registerLeonardoCommands(program2);
|
|
5548
6282
|
registerBytePlusCommands(program2);
|