@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/dist/cli.js CHANGED
@@ -414,9 +414,9 @@ var BytePlusClient = class {
414
414
  }
415
415
  apiKey;
416
416
  baseUrl;
417
- async request(path41, opts = {}) {
417
+ async request(path45, opts = {}) {
418
418
  const base = this.baseUrl.replace(/\/$/, "");
419
- const url = new URL(base + path41);
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(path41, query, logger) {
438
- return this.request(path41, { method: "GET", query, logger });
437
+ get(path45, query, logger) {
438
+ return this.request(path45, { method: "GET", query, logger });
439
439
  }
440
- post(path41, body, logger) {
441
- return this.request(path41, { method: "POST", body, logger });
440
+ post(path45, body, logger) {
441
+ return this.request(path45, { method: "POST", body, logger });
442
442
  }
443
- delete(path41, logger) {
444
- return this.request(path41, { method: "DELETE", logger });
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: path41, role } = parseRefSpec(spec);
994
- if (kind === "image") return toRef(kind, await resolveImageInput(path41, logger), role);
995
- if (kind === "video") return toRef(kind, await resolveVideoInput(path41, logger), role);
996
- return toRef(kind, await resolveAudioInput(path41, logger), role);
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(endpoint, apiKey, query) {
1329
- const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
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(endpoint, payload, apiKey, opts = {}) {
1338
- const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
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(endpoint, apiKey) {
1351
- const url = `${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`;
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(endpoint, body, apiKey, opts = {}) {
1363
- const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
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(endpoint, form, apiKey, opts = {}) {
1398
- const url = `${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`;
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(path41, opts = {}) {
3129
+ async request(path45, opts = {}) {
3130
3130
  const base = this.baseUrl.replace(/\/$/, "");
3131
- const v2Override = path41.startsWith("v2:");
3131
+ const v2Override = path45.startsWith("v2:");
3132
3132
  const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
3133
- const finalPath = v2Override ? path41.slice(3) : path41;
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(path41, query, logger) {
3153
- return this.request(path41, { method: "GET", query, logger });
3152
+ get(path45, query, logger) {
3153
+ return this.request(path45, { method: "GET", query, logger });
3154
3154
  }
3155
- post(path41, body, logger) {
3156
- return this.request(path41, { method: "POST", body, logger });
3155
+ post(path45, body, logger) {
3156
+ return this.request(path45, { method: "POST", body, logger });
3157
3157
  }
3158
- delete(path41, logger) {
3159
- return this.request(path41, { method: "DELETE", logger });
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(endpoint, payload, apiKey, opts = {}) {
3760
- const url = `${MINIMAX_BASE_URL}/${endpoint}`;
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(endpoint, params, apiKey) {
3779
- const url = new URL(`${MINIMAX_BASE_URL}/${endpoint}`);
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/openrouter/client.ts
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 = path34.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
4527
- fs29.writeFileSync(dest, bytes);
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
- fs29.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
4539
- fs29.writeFileSync(output, fs29.readFileSync(savedFiles[0]));
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 fs30 from "fs";
4575
- import path35 from "path";
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
- collect5,
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 collect5(value, prev) {
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 = path35.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
4666
- fs30.writeFileSync(dest, bytes);
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
- fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
4672
- fs30.copyFileSync(saved[0], opts.output);
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 authHeaders4(apiKey) {
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: authHeaders4(apiKey),
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: authHeaders4(apiKey)
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: authHeaders4(apiKey)
5340
+ headers: authHeaders5(apiKey)
4714
5341
  });
4715
5342
  }
4716
5343
 
4717
5344
  // src/providers/openrouter/commands/video-status.ts
4718
- import fs31 from "fs";
4719
- import path36 from "path";
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 = path36.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
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
- fs31.mkdirSync(path36.dirname(dest), { recursive: true });
4735
- const ws = fs31.createWriteStream(dest);
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
- fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
4741
- fs31.copyFileSync(dest, opts.output);
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 execa(binary, ["--version"], { reject: true, all: true });
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
- logger.header("No Provider Keys Configured");
5058
- console.log(SETUP_HINTS);
5059
- process.exit(1);
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 OpenRouter/MiniMax/Leonardo/BytePlus available");
5084
- console.log(" Add GEMINI_API_KEY for analysis, transcription, and doc conversion");
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 fs32 from "fs";
5096
- import path37 from "path";
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 (!fs32.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
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 = path37.resolve(output);
5775
+ outputPath = path41.resolve(output);
5122
5776
  } else if (autoName && inputs.length === 1 && inputs[0]) {
5123
- const base = path37.basename(inputs[0], path37.extname(inputs[0]));
5124
- outputPath = path37.join(getOutputDir(), `${base}-extraction.md`);
5777
+ const base = path41.basename(inputs[0], path41.extname(inputs[0]));
5778
+ outputPath = path41.join(getOutputDir(), `${base}-extraction.md`);
5125
5779
  } else {
5126
- outputPath = path37.join(getOutputDir(), "document-extraction.md");
5780
+ outputPath = path41.join(getOutputDir(), "document-extraction.md");
5127
5781
  }
5128
- fs32.mkdirSync(path37.dirname(outputPath), { recursive: true });
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
- ## ${path37.basename(r.file)}
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
- fs32.writeFileSync(outputPath, lines.join("\n"), "utf8");
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 fs35 from "fs";
5239
- import path40 from "path";
5892
+ import fs39 from "fs";
5893
+ import path44 from "path";
5240
5894
 
5241
5895
  // src/commands/media/batch.ts
5242
- import fs33 from "fs";
5243
- import path38 from "path";
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 execa2 } from "execa";
5916
+ import { execa as execa3 } from "execa";
5263
5917
  async function assertFfmpeg() {
5264
5918
  try {
5265
- await execa2("ffmpeg", ["-version"], { reject: true });
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 execa2("ffmpeg", args, { reject: true, all: true });
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 execa2(
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 execa3 } from "execa";
6005
+ import { execa as execa4 } from "execa";
5352
6006
  async function assertMagick() {
5353
6007
  try {
5354
- await execa3("magick", ["-version"], { reject: true });
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 execa3("magick", args, { reject: true, all: true });
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
- fs33.mkdirSync(outputDir, { recursive: true });
5440
- const entries = fs33.readdirSync(inputDir);
6093
+ fs37.mkdirSync(outputDir, { recursive: true });
6094
+ const entries = fs37.readdirSync(inputDir);
5441
6095
  const mediaFiles = entries.filter((f) => {
5442
- const ext = path38.extname(f).toLowerCase();
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 = path38.join(inputDir, file);
5454
- const outputPath = path38.join(outputDir, file);
5455
- const ext = path38.extname(file).toLowerCase();
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 fs34 from "fs";
5493
- import path39 from "path";
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
- fs34.mkdirSync(outputDir, { recursive: true });
5505
- const ext = path39.extname(input) || ".mp4";
5506
- const baseName = path39.basename(input, ext);
5507
- const outputPattern = path39.join(outputDir, `${baseName}_chunk_%03d${ext}`);
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 = path39.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
5527
- if (fs34.existsSync(fname)) files.push(fname);
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 (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
5542
- fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
5543
- const ext = path40.extname(opts.input).toLowerCase();
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 (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
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 (!fs35.existsSync(opts.inputDir))
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);