@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/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,23 +4356,650 @@ 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();
4364
- var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
4365
- var DEFAULT_OPENROUTER_MODEL = "google/gemini-3.1-flash-image-preview";
4366
- function requireOpenRouterKey() {
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
- "OPENROUTER_API_KEY is not set. Get one at https://openrouter.ai/settings/keys"
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 buildHeaders(apiKey) {
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.includes("gemini") ? ["image", "text"] : ["image"],
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: buildHeaders(apiKey),
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 choices = data.choices ?? [];
4426
- if (choices.length === 0) {
4427
- return { status: "error", error: `No choices in response: ${JSON.stringify(data)}` };
5138
+ const parsed = extractImagesFromResponse(data);
5139
+ if (parsed.urls.length === 0) {
5140
+ return { status: "error", error: formatNoImagesError(model, parsed) };
4428
5141
  }
4429
- const messageImages = choices[0]?.message?.images ?? [];
4430
- if (messageImages.length === 0) {
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 = path34.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
4451
- fs29.writeFileSync(dest, bytes);
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
- fs29.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
4463
- 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]));
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 fs30 from "fs";
4499
- import path35 from "path";
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
- collect5,
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 Error("At least one --ref is required for image-to-image.");
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
- logger.debug(`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}`);
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 collect5(value, prev) {
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
- Authorization: `Bearer ${apiKey}`,
4549
- "Content-Type": "application/json"
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
- messages: [{ role: "user", content }],
4563
- modalities: opts.model.includes("gemini") ? ["image", "text"] : ["image"]
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 messageImages = data.choices?.[0]?.message?.images ?? [];
4573
- if (messageImages.length === 0) {
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 < messageImages.length; i++) {
4583
- const url = messageImages[i]?.image_url?.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 = path35.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
4587
- fs30.writeFileSync(dest, bytes);
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
- fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
4593
- fs30.copyFileSync(saved[0], opts.output);
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 authHeaders4(apiKey) {
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: authHeaders4(apiKey),
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: authHeaders4(apiKey)
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: authHeaders4(apiKey)
5340
+ headers: authHeaders5(apiKey)
4635
5341
  });
4636
5342
  }
4637
5343
 
4638
5344
  // src/providers/openrouter/commands/video-status.ts
4639
- import fs31 from "fs";
4640
- import path36 from "path";
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 = path36.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
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
- fs31.mkdirSync(path36.dirname(dest), { recursive: true });
4656
- const ws = fs31.createWriteStream(dest);
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
- fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
4662
- fs31.copyFileSync(dest, opts.output);
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 execa(binary, ["--version"], { reject: true, all: true });
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
- logger.header("No Provider Keys Configured");
4979
- console.log(SETUP_HINTS);
4980
- 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;
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 OpenRouter/MiniMax/Leonardo/BytePlus available");
5005
- 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");
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 fs32 from "fs";
5017
- import path37 from "path";
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 (!fs32.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
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 = path37.resolve(output);
5775
+ outputPath = path41.resolve(output);
5043
5776
  } else if (autoName && inputs.length === 1 && inputs[0]) {
5044
- const base = path37.basename(inputs[0], path37.extname(inputs[0]));
5045
- 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`);
5046
5779
  } else {
5047
- outputPath = path37.join(getOutputDir(), "document-extraction.md");
5780
+ outputPath = path41.join(getOutputDir(), "document-extraction.md");
5048
5781
  }
5049
- fs32.mkdirSync(path37.dirname(outputPath), { recursive: true });
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
- ## ${path37.basename(r.file)}
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
- fs32.writeFileSync(outputPath, lines.join("\n"), "utf8");
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 fs35 from "fs";
5160
- import path40 from "path";
5892
+ import fs39 from "fs";
5893
+ import path44 from "path";
5161
5894
 
5162
5895
  // src/commands/media/batch.ts
5163
- import fs33 from "fs";
5164
- import path38 from "path";
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 execa2 } from "execa";
5916
+ import { execa as execa3 } from "execa";
5184
5917
  async function assertFfmpeg() {
5185
5918
  try {
5186
- await execa2("ffmpeg", ["-version"], { reject: true });
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 execa2("ffmpeg", args, { reject: true, all: true });
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 execa2(
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 execa3 } from "execa";
6005
+ import { execa as execa4 } from "execa";
5273
6006
  async function assertMagick() {
5274
6007
  try {
5275
- await execa3("magick", ["-version"], { reject: true });
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 execa3("magick", args, { reject: true, all: true });
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
- fs33.mkdirSync(outputDir, { recursive: true });
5361
- const entries = fs33.readdirSync(inputDir);
6093
+ fs37.mkdirSync(outputDir, { recursive: true });
6094
+ const entries = fs37.readdirSync(inputDir);
5362
6095
  const mediaFiles = entries.filter((f) => {
5363
- const ext = path38.extname(f).toLowerCase();
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 = path38.join(inputDir, file);
5375
- const outputPath = path38.join(outputDir, file);
5376
- 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();
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 fs34 from "fs";
5414
- import path39 from "path";
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
- fs34.mkdirSync(outputDir, { recursive: true });
5426
- const ext = path39.extname(input) || ".mp4";
5427
- const baseName = path39.basename(input, ext);
5428
- 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}`);
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 = path39.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
5448
- 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);
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 (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
5463
- fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
5464
- 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();
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 (!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}`);
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 (!fs35.existsSync(opts.inputDir))
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);