@mrgoonie/multix 0.0.8 → 0.1.0-beta.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
@@ -110,11 +110,14 @@ async function httpJson(opts) {
110
110
  }
111
111
  return response.json();
112
112
  }
113
- async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
113
+ async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers = {}) {
114
114
  fs4.mkdirSync(path4.dirname(dest), { recursive: true });
115
115
  let response;
116
116
  try {
117
- response = await globalThis.fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
117
+ response = await globalThis.fetch(url, {
118
+ headers,
119
+ signal: AbortSignal.timeout(timeoutMs)
120
+ });
118
121
  } catch (cause) {
119
122
  const msg = cause instanceof Error ? cause.message : String(cause);
120
123
  throw new HttpError(0, msg, url);
@@ -414,9 +417,9 @@ var BytePlusClient = class {
414
417
  }
415
418
  apiKey;
416
419
  baseUrl;
417
- async request(path41, opts = {}) {
420
+ async request(path46, opts = {}) {
418
421
  const base = this.baseUrl.replace(/\/$/, "");
419
- const url = new URL(base + path41);
422
+ const url = new URL(base + path46);
420
423
  if (opts.query) {
421
424
  for (const [k, v] of Object.entries(opts.query)) {
422
425
  if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
@@ -434,14 +437,14 @@ var BytePlusClient = class {
434
437
  timeoutMs: opts.timeoutMs ?? 12e4
435
438
  });
436
439
  }
437
- get(path41, query, logger) {
438
- return this.request(path41, { method: "GET", query, logger });
440
+ get(path46, query, logger) {
441
+ return this.request(path46, { method: "GET", query, logger });
439
442
  }
440
- post(path41, body, logger) {
441
- return this.request(path41, { method: "POST", body, logger });
443
+ post(path46, body, logger) {
444
+ return this.request(path46, { method: "POST", body, logger });
442
445
  }
443
- delete(path41, logger) {
444
- return this.request(path41, { method: "DELETE", logger });
446
+ delete(path46, logger) {
447
+ return this.request(path46, { method: "DELETE", logger });
445
448
  }
446
449
  };
447
450
  function createBytePlusClient() {
@@ -990,10 +993,10 @@ function toRef(kind, resolved, role) {
990
993
  return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
991
994
  }
992
995
  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);
996
+ const { path: path46, role } = parseRefSpec(spec);
997
+ if (kind === "image") return toRef(kind, await resolveImageInput(path46, logger), role);
998
+ if (kind === "video") return toRef(kind, await resolveVideoInput(path46, logger), role);
999
+ return toRef(kind, await resolveAudioInput(path46, logger), role);
997
1000
  }
998
1001
  async function resolveRefSpecs(specs, logger) {
999
1002
  const out = [];
@@ -1325,8 +1328,8 @@ function requireElevenLabsKey() {
1325
1328
  function authHeaders(apiKey, extra) {
1326
1329
  return { "xi-api-key": apiKey, ...extra ?? {} };
1327
1330
  }
1328
- function apiGet(endpoint, apiKey, query) {
1329
- const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
1331
+ function apiGet(endpoint2, apiKey, query) {
1332
+ const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
1330
1333
  if (query) {
1331
1334
  for (const [k, v] of Object.entries(query)) {
1332
1335
  if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
@@ -1334,8 +1337,8 @@ function apiGet(endpoint, apiKey, query) {
1334
1337
  }
1335
1338
  return httpJson({ url: url.toString(), headers: authHeaders(apiKey) });
1336
1339
  }
1337
- function apiPostJson(endpoint, payload, apiKey, opts = {}) {
1338
- const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
1340
+ function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
1341
+ const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
1339
1342
  if (opts.query) {
1340
1343
  for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
1341
1344
  }
@@ -1347,8 +1350,8 @@ function apiPostJson(endpoint, payload, apiKey, opts = {}) {
1347
1350
  timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
1348
1351
  });
1349
1352
  }
1350
- async function apiDelete(endpoint, apiKey) {
1351
- const url = `${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`;
1353
+ async function apiDelete(endpoint2, apiKey) {
1354
+ const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
1352
1355
  const res = await globalThis.fetch(url, {
1353
1356
  method: "DELETE",
1354
1357
  headers: authHeaders(apiKey),
@@ -1359,8 +1362,8 @@ async function apiDelete(endpoint, apiKey) {
1359
1362
  throw new HttpError(res.status, text.slice(0, 500), url);
1360
1363
  }
1361
1364
  }
1362
- async function apiPostBinary(endpoint, body, apiKey, opts = {}) {
1363
- const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`);
1365
+ async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
1366
+ const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
1364
1367
  if (opts.query) {
1365
1368
  for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
1366
1369
  }
@@ -1394,8 +1397,8 @@ async function apiPostBinary(endpoint, body, apiKey, opts = {}) {
1394
1397
  }
1395
1398
  return Buffer.from(await res.arrayBuffer());
1396
1399
  }
1397
- async function apiPostMultipart(endpoint, form, apiKey, opts = {}) {
1398
- const url = `${ELEVENLABS_BASE_URL}/${endpoint.replace(/^\//, "")}`;
1400
+ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
1401
+ const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
1399
1402
  let res;
1400
1403
  try {
1401
1404
  res = await globalThis.fetch(url, {
@@ -2076,7 +2079,7 @@ function requiresProcessingWait(mimeType) {
2076
2079
  var BASE = "https://generativelanguage.googleapis.com";
2077
2080
  var FILES_BASE = `${BASE}/upload/v1beta/files`;
2078
2081
  var GENERATE_BASE = `${BASE}/v1beta/models`;
2079
- function requireApiKey() {
2082
+ function requireGeminiApiKey() {
2080
2083
  const key = resolveKey("GEMINI_API_KEY");
2081
2084
  if (!key)
2082
2085
  throw new ConfigError(
@@ -2084,11 +2087,14 @@ function requireApiKey() {
2084
2087
  );
2085
2088
  return key;
2086
2089
  }
2087
- function authHeaders2(apiKey) {
2090
+ function geminiAuthHeaders(apiKey) {
2088
2091
  return { "x-goog-api-key": apiKey };
2089
2092
  }
2093
+ function unwrapGeminiFileRef(response) {
2094
+ return "file" in response ? response.file : response;
2095
+ }
2090
2096
  async function uploadFile(filePath, opts = {}) {
2091
- const apiKey = requireApiKey();
2097
+ const apiKey = requireGeminiApiKey();
2092
2098
  const { timeoutMs = 3e5, logger } = opts;
2093
2099
  const mimeType = getMimeType(filePath);
2094
2100
  const fileBytes = fs12.readFileSync(filePath);
@@ -2137,8 +2143,8 @@ Content-Type: ${mimeType}\r
2137
2143
  await sleep(2e3);
2138
2144
  fileRef = await httpJson({
2139
2145
  url: `${BASE}/v1beta/${fileRef.name}`,
2140
- headers: authHeaders2(apiKey)
2141
- }).then((r) => r.file);
2146
+ headers: geminiAuthHeaders(apiKey)
2147
+ }).then(unwrapGeminiFileRef);
2142
2148
  logger?.debug(`Processing... state=${fileRef.state}`);
2143
2149
  }
2144
2150
  if (fileRef.state === "FAILED")
@@ -2150,11 +2156,11 @@ Content-Type: ${mimeType}\r
2150
2156
  return fileRef;
2151
2157
  }
2152
2158
  async function generateContent(req) {
2153
- const apiKey = requireApiKey();
2159
+ const apiKey = requireGeminiApiKey();
2154
2160
  return httpJson({
2155
2161
  url: `${GENERATE_BASE}/${req.model}:generateContent`,
2156
2162
  method: "POST",
2157
- headers: authHeaders2(apiKey),
2163
+ headers: geminiAuthHeaders(apiKey),
2158
2164
  body: {
2159
2165
  contents: req.contents,
2160
2166
  ...req.generationConfig ? { generationConfig: req.generationConfig } : {}
@@ -2196,10 +2202,12 @@ function sleep(ms) {
2196
2202
  // src/providers/gemini/models.ts
2197
2203
  var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
2198
2204
  var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
2205
+ var IMAGE_MODEL_LITE = "gemini-3.1-flash-lite-image";
2199
2206
  var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
2200
- var ANALYSIS_MODEL_DEFAULT = "gemini-2.5-flash";
2207
+ var TEXT_MODEL_DEFAULT = "gemini-3.5-flash";
2208
+ var ANALYSIS_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
2201
2209
  var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
2202
- var DOC_MODEL_DEFAULT = "gemini-2.5-flash";
2210
+ var DOC_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
2203
2211
  var IMAGEN_MODELS = /* @__PURE__ */ new Set([
2204
2212
  "imagen-4.0-generate-001",
2205
2213
  "imagen-4.0-ultra-generate-001",
@@ -2210,6 +2218,7 @@ var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
2210
2218
  "gemini-3-pro-image-preview",
2211
2219
  "gemini-2.5-flash-image",
2212
2220
  "gemini-2.5-flash-image-preview",
2221
+ IMAGE_MODEL_LITE,
2213
2222
  ...IMAGEN_MODELS
2214
2223
  ]);
2215
2224
  var ASPECT_RATIOS = [
@@ -2225,6 +2234,15 @@ var ASPECT_RATIOS = [
2225
2234
  "21:9"
2226
2235
  ];
2227
2236
  var IMAGE_SIZES = ["1K", "2K", "4K"];
2237
+ function resolveImageOptions(model, aspectRatio, requestedSize) {
2238
+ if (model !== IMAGE_MODEL_LITE) {
2239
+ return { aspectRatio, ...requestedSize ? { imageSize: requestedSize } : {} };
2240
+ }
2241
+ if (requestedSize && requestedSize !== "1K") {
2242
+ throw new Error(`${IMAGE_MODEL_LITE} supports only 1K image output.`);
2243
+ }
2244
+ return { aspectRatio, imageSize: "1K" };
2245
+ }
2228
2246
  function getDefaultModel(task) {
2229
2247
  if (task === "generate") {
2230
2248
  return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
@@ -2709,6 +2727,13 @@ function sleep2(ms) {
2709
2727
  init_errors();
2710
2728
  import fs18 from "fs";
2711
2729
  import path22 from "path";
2730
+ function buildImageGenerationConfig(model, aspectRatio, size) {
2731
+ const imageOptions = resolveImageOptions(model, aspectRatio, size);
2732
+ return {
2733
+ responseModalities: ["IMAGE"],
2734
+ imageConfig: imageOptions
2735
+ };
2736
+ }
2712
2737
  function registerGenerateCommand(parent) {
2713
2738
  parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").option("-v, --verbose", "Verbose logging").action(
2714
2739
  async (opts) => {
@@ -2725,12 +2750,11 @@ function registerGenerateCommand(parent) {
2725
2750
  }
2726
2751
  logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
2727
2752
  logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
2728
- const imageConfig = { aspectRatio: opts.aspectRatio };
2729
- if (opts.size) imageConfig.imageSize = opts.size;
2730
- const generationConfig = {
2731
- responseModalities: ["IMAGE"],
2732
- imageConfig
2733
- };
2753
+ const generationConfig = buildImageGenerationConfig(
2754
+ model,
2755
+ opts.aspectRatio,
2756
+ opts.size
2757
+ );
2734
2758
  const savedFiles = [];
2735
2759
  for (let i = 0; i < numImages; i++) {
2736
2760
  try {
@@ -2782,6 +2806,10 @@ import fs19 from "fs";
2782
2806
  import path23 from "path";
2783
2807
  var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
2784
2808
  var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
2809
+ function buildImageToImageGenerationConfig(model, aspectRatio, size) {
2810
+ const imageOptions = resolveImageOptions(model, aspectRatio, size);
2811
+ return imageOptions.imageSize ? { responseModalities: ["IMAGE"], imageConfig: imageOptions } : void 0;
2812
+ }
2785
2813
  function registerGeminiImageToImageCommand(parent) {
2786
2814
  parent.command("image-to-image").alias("i2i").description(
2787
2815
  "Edit/compose images with Gemini 2.5 Flash Image (Nano Banana) using one or more reference images"
@@ -2790,13 +2818,26 @@ function registerGeminiImageToImageCommand(parent) {
2790
2818
  "Reference image (URL or local path); repeatable for multi-ref",
2791
2819
  collect4,
2792
2820
  []
2793
- ).option("-m, --model <id>", `Gemini image model (default ${IMAGE_MODEL_FALLBACK})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
2821
+ ).option("-m, --model <id>", `Gemini image model (default ${IMAGE_MODEL_FALLBACK})`).option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
2794
2822
  async (opts) => {
2795
2823
  if (!opts.ref || opts.ref.length === 0) {
2796
2824
  throw new Error("At least one --ref is required for image-to-image.");
2797
2825
  }
2798
2826
  const logger = createLogger({ verbose: opts.verbose ?? false });
2799
2827
  const model = opts.model ?? IMAGE_MODEL_FALLBACK;
2828
+ if (model === IMAGE_MODEL_LITE && opts.ref.length > 1) {
2829
+ logger.warn(
2830
+ `${IMAGE_MODEL_LITE} is not optimized for multiple reference images; use one when possible.`
2831
+ );
2832
+ }
2833
+ if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
2834
+ throw new Error(
2835
+ `Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
2836
+ );
2837
+ }
2838
+ if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
2839
+ throw new Error(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
2840
+ }
2800
2841
  const parts = [];
2801
2842
  for (const r of opts.ref) {
2802
2843
  const resolved = await resolveImageInput(r, {
@@ -2820,10 +2861,15 @@ function registerGeminiImageToImageCommand(parent) {
2820
2861
  }
2821
2862
  parts.push({ text: opts.prompt });
2822
2863
  logger.debug(`Calling ${model} with ${opts.ref.length} reference image(s)`);
2864
+ const generationConfig = buildImageToImageGenerationConfig(
2865
+ model,
2866
+ opts.aspectRatio,
2867
+ opts.size
2868
+ );
2823
2869
  const req = {
2824
2870
  model,
2825
2871
  contents: [{ parts }],
2826
- generationConfig: { responseModalities: ["IMAGE"] }
2872
+ ...generationConfig ? { generationConfig } : {}
2827
2873
  };
2828
2874
  const resp = await generateContent(req);
2829
2875
  const images = extractImages(resp);
@@ -2984,16 +3030,186 @@ Generated video: ${dest}`);
2984
3030
  );
2985
3031
  }
2986
3032
 
2987
- // src/providers/gemini/commands/transcribe.ts
3033
+ // src/providers/gemini/commands/omni-video.ts
2988
3034
  init_errors();
2989
3035
  import fs21 from "fs";
3036
+ import path25 from "path";
3037
+
3038
+ // src/providers/gemini/interactions.ts
3039
+ init_errors();
3040
+ init_http_client();
3041
+ var BASE4 = "https://generativelanguage.googleapis.com/v1beta";
3042
+ var OMNI_MODEL = "gemini-omni-flash-preview";
3043
+ function buildOmniInteractionPayload(request) {
3044
+ return {
3045
+ model: OMNI_MODEL,
3046
+ input: buildOmniInput(request),
3047
+ response_format: { type: "video", delivery: "uri" },
3048
+ store: true,
3049
+ ...request.previousInteractionId ? { previous_interaction_id: request.previousInteractionId } : {}
3050
+ };
3051
+ }
3052
+ async function createOmniInteraction(request) {
3053
+ const apiKey = requireGeminiApiKey();
3054
+ const raw = await httpJson({
3055
+ url: `${BASE4}/interactions`,
3056
+ method: "POST",
3057
+ headers: geminiAuthHeaders(apiKey),
3058
+ body: buildOmniInteractionPayload(request)
3059
+ });
3060
+ if (!raw.id)
3061
+ throw new ProviderError("Omni response did not include an interaction ID.", "gemini");
3062
+ return { interactionId: raw.id, video: extractOmniVideo(raw) };
3063
+ }
3064
+ function buildOmniInput(request) {
3065
+ const parts = [{ type: "text", text: request.prompt }];
3066
+ for (const image of request.images ?? []) {
3067
+ parts.push({ type: "image", data: image.data, mime_type: image.mimeType });
3068
+ }
3069
+ if (request.videoFileUri) parts.push({ type: "document", uri: request.videoFileUri });
3070
+ return parts;
3071
+ }
3072
+ function extractOmniVideo(raw) {
3073
+ for (const step of raw.steps ?? []) {
3074
+ if (step.type !== "model_output") continue;
3075
+ for (const content of step.content ?? []) {
3076
+ if (content.type !== "video") continue;
3077
+ const mimeType = content.mime_type ?? "video/mp4";
3078
+ if (content.data) return { kind: "inline", data: content.data, mimeType };
3079
+ if (content.uri) return { kind: "uri", uri: content.uri, mimeType };
3080
+ }
3081
+ }
3082
+ throw new ProviderError("Omni response did not include a generated video.", "gemini");
3083
+ }
3084
+ async function waitForGeminiFile(fileUri, timeoutMs = 3e5) {
3085
+ const apiKey = requireGeminiApiKey();
3086
+ const name = fileNameFromUri(fileUri);
3087
+ const deadline = Date.now() + timeoutMs;
3088
+ while (Date.now() < deadline) {
3089
+ const response = await httpJson({
3090
+ url: `${BASE4}/${name}`,
3091
+ headers: geminiAuthHeaders(apiKey)
3092
+ });
3093
+ const file = unwrapGeminiFile(response);
3094
+ if (file.state === "ACTIVE") return file;
3095
+ if (file.state === "FAILED") throw new ProviderError("Generated Omni video failed.", "gemini");
3096
+ await sleep4(5e3);
3097
+ }
3098
+ throw new ProviderError("Timed out waiting for generated Omni video.", "gemini");
3099
+ }
3100
+ async function downloadGeminiFile(fileUri, destination) {
3101
+ const apiKey = requireGeminiApiKey();
3102
+ await downloadFile(fileUri, destination, void 0, geminiAuthHeaders(apiKey));
3103
+ }
3104
+ function fileNameFromUri(fileUri) {
3105
+ const pathname = new URL(fileUri).pathname;
3106
+ const match = pathname.match(/\/(files\/[^/:]+)(?::download)?$/);
3107
+ if (!match?.[1]) throw new ProviderError(`Unexpected Gemini file URI: ${fileUri}`, "gemini");
3108
+ return match[1];
3109
+ }
3110
+ function unwrapGeminiFile(response) {
3111
+ return "file" in response ? response.file : response;
3112
+ }
3113
+ function selectGeminiDownloadUri(file, fallbackUri) {
3114
+ return file.downloadUri ?? fallbackUri;
3115
+ }
3116
+ function sleep4(ms) {
3117
+ return new Promise((resolve) => setTimeout(resolve, ms));
3118
+ }
3119
+
3120
+ // src/providers/gemini/commands/omni-video.ts
3121
+ function registerOmniVideoCommand(parent) {
3122
+ parent.command("omni-video").description("Generate or edit video with Gemini Omni Flash [PAID PREVIEW]").requiredOption("--prompt <text>", "Video prompt or concise edit instruction").option("--image <paths...>", "Local image guidance files").option("--video <path>", "One local video to edit (not available in EEA/CH/UK)").option("--previous-interaction-id <id>", "Continue a prior Gemini-stored interaction").option("--upload-timeout <ms>", "Files API processing timeout in ms", "300000").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").addHelpText(
3123
+ "after",
3124
+ "\nPreview, paid-tier API. Outputs 3-10 second 720p videos. Audio references, video extension/interpolation, voice editing, and multiple videos are unsupported. Follow-up edits require provider-side stored interactions; no local history is kept."
3125
+ ).action(async (opts) => {
3126
+ const logger = createLogger({ verbose: opts.verbose ?? false });
3127
+ validateInputs(opts);
3128
+ const images = (opts.image ?? []).map(readImage);
3129
+ const videoFile = opts.video ? await uploadFile(opts.video, { timeoutMs: parseTimeout(opts.uploadTimeout), logger }) : void 0;
3130
+ const result = await createOmniInteraction({
3131
+ prompt: opts.prompt,
3132
+ images,
3133
+ videoFileUri: videoFile?.uri,
3134
+ previousInteractionId: opts.previousInteractionId
3135
+ });
3136
+ const destination = path25.join(getOutputDir(), `gemini_omni_${Date.now()}.mp4`);
3137
+ if (result.video.kind === "inline") {
3138
+ fs21.writeFileSync(destination, Buffer.from(result.video.data, "base64"));
3139
+ } else {
3140
+ const generated = await waitForGeminiFile(result.video.uri);
3141
+ await downloadGeminiFile(selectGeminiDownloadUri(generated, result.video.uri), destination);
3142
+ }
3143
+ const metadataPath = `${destination}.json`;
3144
+ fs21.writeFileSync(
3145
+ metadataPath,
3146
+ `${JSON.stringify(buildOmniVideoMetadata(opts, result.interactionId), null, 2)}
3147
+ `
3148
+ );
3149
+ if (opts.output) {
3150
+ fs21.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
3151
+ fs21.copyFileSync(destination, opts.output);
3152
+ fs21.copyFileSync(metadataPath, `${opts.output}.json`);
3153
+ }
3154
+ logger.success(`Saved video: ${destination}`);
3155
+ logger.success(`Saved metadata: ${metadataPath}`);
3156
+ if (opts.output) logger.success(`Copied to: ${opts.output}`);
3157
+ console.log(`Interaction ID: ${result.interactionId}`);
3158
+ });
3159
+ }
3160
+ function buildOmniVideoMetadata(opts, interactionId) {
3161
+ return {
3162
+ provider: "gemini",
3163
+ model: "gemini-omni-flash-preview",
3164
+ prompt: opts.prompt,
3165
+ inputFiles: [...opts.image ?? [], ...opts.video ? [opts.video] : []].map(
3166
+ (filePath) => path25.basename(filePath)
3167
+ ),
3168
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3169
+ interactionId
3170
+ };
3171
+ }
3172
+ function validateInputs(opts) {
3173
+ if (opts.video && opts.image?.length) {
3174
+ throw new ValidationError("Use either --video or --image guidance, not both.");
3175
+ }
3176
+ if (opts.video) {
3177
+ requireLocalFile(opts.video, "video");
3178
+ if (!getMimeType(opts.video).startsWith("video/")) {
3179
+ throw new ValidationError(`Unsupported video file: ${opts.video}`);
3180
+ }
3181
+ }
3182
+ for (const imagePath of opts.image ?? []) {
3183
+ requireLocalFile(imagePath, "image");
3184
+ if (!getMimeType(imagePath).startsWith("image/")) {
3185
+ throw new ValidationError(`Unsupported image file: ${imagePath}`);
3186
+ }
3187
+ }
3188
+ }
3189
+ function requireLocalFile(filePath, label) {
3190
+ if (/^https?:\/\//i.test(filePath)) throw new ValidationError(`${label} URLs are not supported.`);
3191
+ if (!fs21.existsSync(filePath)) throw new ValidationError(`${label} file not found: ${filePath}`);
3192
+ }
3193
+ function readImage(filePath) {
3194
+ return { data: fs21.readFileSync(filePath).toString("base64"), mimeType: getMimeType(filePath) };
3195
+ }
3196
+ function parseTimeout(value) {
3197
+ const timeout = Number.parseInt(value, 10);
3198
+ if (!Number.isFinite(timeout) || timeout <= 0)
3199
+ throw new ValidationError("--upload-timeout must be positive.");
3200
+ return timeout;
3201
+ }
3202
+
3203
+ // src/providers/gemini/commands/transcribe.ts
3204
+ init_errors();
3205
+ import fs22 from "fs";
2990
3206
  function registerTranscribeCommand2(parent) {
2991
3207
  parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
2992
3208
  async (opts) => {
2993
3209
  const logger = createLogger({ verbose: opts.verbose ?? false });
2994
3210
  const model = opts.model ?? getDefaultModel("transcribe");
2995
3211
  for (const f of opts.files) {
2996
- if (!fs21.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
3212
+ if (!fs22.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
2997
3213
  }
2998
3214
  const results = [];
2999
3215
  for (let i = 0; i < opts.files.length; i++) {
@@ -3046,13 +3262,14 @@ function registerGeminiCommands(program2) {
3046
3262
  registerGeminiImageToImageCommand(gemini);
3047
3263
  registerGenerateVideoCommand(gemini);
3048
3264
  registerGeminiImageToVideoCommand(gemini);
3265
+ registerOmniVideoCommand(gemini);
3049
3266
  registerGeminiGenerateSpeechCommand(gemini);
3050
3267
  }
3051
3268
 
3052
3269
  // src/providers/leonardo/commands/generate.ts
3053
3270
  init_http_client();
3054
- import fs22 from "fs";
3055
- import path25 from "path";
3271
+ import fs23 from "fs";
3272
+ import path26 from "path";
3056
3273
 
3057
3274
  // src/providers/leonardo/client.ts
3058
3275
  init_errors();
@@ -3126,11 +3343,11 @@ var LeonardoClient = class {
3126
3343
  }
3127
3344
  apiKey;
3128
3345
  baseUrl;
3129
- async request(path41, opts = {}) {
3346
+ async request(path46, opts = {}) {
3130
3347
  const base = this.baseUrl.replace(/\/$/, "");
3131
- const v2Override = path41.startsWith("v2:");
3348
+ const v2Override = path46.startsWith("v2:");
3132
3349
  const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
3133
- const finalPath = v2Override ? path41.slice(3) : path41;
3350
+ const finalPath = v2Override ? path46.slice(3) : path46;
3134
3351
  const url = new URL(finalBase + finalPath);
3135
3352
  if (opts.query) {
3136
3353
  for (const [k, v] of Object.entries(opts.query)) {
@@ -3149,14 +3366,14 @@ var LeonardoClient = class {
3149
3366
  timeoutMs: opts.timeoutMs ?? 12e4
3150
3367
  });
3151
3368
  }
3152
- get(path41, query, logger) {
3153
- return this.request(path41, { method: "GET", query, logger });
3369
+ get(path46, query, logger) {
3370
+ return this.request(path46, { method: "GET", query, logger });
3154
3371
  }
3155
- post(path41, body, logger) {
3156
- return this.request(path41, { method: "POST", body, logger });
3372
+ post(path46, body, logger) {
3373
+ return this.request(path46, { method: "POST", body, logger });
3157
3374
  }
3158
- delete(path41, logger) {
3159
- return this.request(path41, { method: "DELETE", logger });
3375
+ delete(path46, logger) {
3376
+ return this.request(path46, { method: "DELETE", logger });
3160
3377
  }
3161
3378
  };
3162
3379
  function createLeonardoClient() {
@@ -3167,7 +3384,7 @@ function createLeonardoClient() {
3167
3384
  function extFromUrl(url, fallback = ".png") {
3168
3385
  try {
3169
3386
  const u = new URL(url);
3170
- const e = path25.extname(u.pathname);
3387
+ const e = path26.extname(u.pathname);
3171
3388
  return e.length > 0 ? e : fallback;
3172
3389
  } catch {
3173
3390
  return fallback;
@@ -3252,7 +3469,7 @@ function registerLeonardoGenerateCommand(parent) {
3252
3469
  if (!img) continue;
3253
3470
  const ext = extFromUrl(img.url);
3254
3471
  const filename = autoFilename(generationId, i + 1, ext);
3255
- const filepath = path25.join(outDir, filename);
3472
+ const filepath = path26.join(outDir, filename);
3256
3473
  try {
3257
3474
  await downloadFile(img.url, filepath);
3258
3475
  saved.push(filepath);
@@ -3262,9 +3479,9 @@ function registerLeonardoGenerateCommand(parent) {
3262
3479
  }
3263
3480
  }
3264
3481
  if (opts.output && saved[0]) {
3265
- const dest = path25.resolve(opts.output);
3266
- fs22.mkdirSync(path25.dirname(dest), { recursive: true });
3267
- fs22.copyFileSync(saved[0], dest);
3482
+ const dest = path26.resolve(opts.output);
3483
+ fs23.mkdirSync(path26.dirname(dest), { recursive: true });
3484
+ fs23.copyFileSync(saved[0], dest);
3268
3485
  logger.success(`Copied to ${dest}`);
3269
3486
  }
3270
3487
  console.log(`
@@ -3276,11 +3493,11 @@ Generated ${saved.length} image(s):`);
3276
3493
 
3277
3494
  // src/providers/leonardo/commands/image-to-image.ts
3278
3495
  init_http_client();
3279
- import fs23 from "fs";
3280
- import path26 from "path";
3496
+ import fs24 from "fs";
3497
+ import path27 from "path";
3281
3498
  function extFromUrl2(url, fallback = ".png") {
3282
3499
  try {
3283
- return path26.extname(new URL(url).pathname) || fallback;
3500
+ return path27.extname(new URL(url).pathname) || fallback;
3284
3501
  } catch {
3285
3502
  return fallback;
3286
3503
  }
@@ -3342,7 +3559,7 @@ function registerLeonardoImageToImageCommand(parent) {
3342
3559
  if (!img) continue;
3343
3560
  const ext = extFromUrl2(img.url);
3344
3561
  const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
3345
- const filepath = path26.join(outDir, filename);
3562
+ const filepath = path27.join(outDir, filename);
3346
3563
  try {
3347
3564
  await downloadFile(img.url, filepath);
3348
3565
  saved.push(filepath);
@@ -3352,9 +3569,9 @@ function registerLeonardoImageToImageCommand(parent) {
3352
3569
  }
3353
3570
  }
3354
3571
  if (opts.output && saved[0]) {
3355
- const dest = path26.resolve(opts.output);
3356
- fs23.mkdirSync(path26.dirname(dest), { recursive: true });
3357
- fs23.copyFileSync(saved[0], dest);
3572
+ const dest = path27.resolve(opts.output);
3573
+ fs24.mkdirSync(path27.dirname(dest), { recursive: true });
3574
+ fs24.copyFileSync(saved[0], dest);
3358
3575
  logger.success(`Copied to ${dest}`);
3359
3576
  }
3360
3577
  console.log(`
@@ -3366,7 +3583,7 @@ Generated ${saved.length} image(s):`);
3366
3583
 
3367
3584
  // src/providers/leonardo/commands/video.ts
3368
3585
  init_http_client();
3369
- import path27 from "path";
3586
+ import path28 from "path";
3370
3587
  function v2DimsFor(resolution) {
3371
3588
  if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
3372
3589
  if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
@@ -3477,7 +3694,7 @@ async function pollAndDownload(client, jobId, opts) {
3477
3694
  logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
3478
3695
  process.exit(1);
3479
3696
  }
3480
- const outPath = opts.output ?? path27.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
3697
+ const outPath = opts.output ?? path28.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
3481
3698
  await downloadFile(videoUrl, outPath);
3482
3699
  logger.success(`Saved ${outPath}`);
3483
3700
  await maybeDownloadThumb(gen, outPath, {
@@ -3584,7 +3801,7 @@ function registerLeonardoModelsCommand(parent) {
3584
3801
 
3585
3802
  // src/providers/leonardo/commands/status.ts
3586
3803
  init_http_client();
3587
- import path28 from "path";
3804
+ import path29 from "path";
3588
3805
  var VIDEO_KEYS = ["motionMP4URL", "url"];
3589
3806
  var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
3590
3807
  var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
@@ -3649,7 +3866,7 @@ function registerLeonardoStatusCommand(parent) {
3649
3866
  const outDir = getOutputDir();
3650
3867
  let idx = 0;
3651
3868
  for (const url of videos) {
3652
- const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path28.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
3869
+ const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
3653
3870
  await downloadFile(url, dest);
3654
3871
  logger.success(`Saved ${dest}`);
3655
3872
  await maybeDownloadThumb(gen, dest, {
@@ -3662,7 +3879,7 @@ function registerLeonardoStatusCommand(parent) {
3662
3879
  for (const url of images) {
3663
3880
  const m = url.match(IMAGE_EXT_RE2);
3664
3881
  const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
3665
- const dest = path28.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
3882
+ const dest = path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
3666
3883
  await downloadFile(url, dest);
3667
3884
  logger.success(`Saved ${dest}`);
3668
3885
  idx++;
@@ -3750,19 +3967,19 @@ function requireMinimaxKey() {
3750
3967
  }
3751
3968
  return key;
3752
3969
  }
3753
- function authHeaders3(apiKey) {
3970
+ function authHeaders2(apiKey) {
3754
3971
  return {
3755
3972
  Authorization: `Bearer ${apiKey}`,
3756
3973
  "Content-Type": "application/json"
3757
3974
  };
3758
3975
  }
3759
- async function apiPost(endpoint, payload, apiKey, opts = {}) {
3760
- const url = `${MINIMAX_BASE_URL}/${endpoint}`;
3976
+ async function apiPost(endpoint2, payload, apiKey, opts = {}) {
3977
+ const url = `${MINIMAX_BASE_URL}/${endpoint2}`;
3761
3978
  opts.logger?.debug(`POST ${url}`);
3762
3979
  const data = await httpJson({
3763
3980
  url,
3764
3981
  method: "POST",
3765
- headers: authHeaders3(apiKey),
3982
+ headers: authHeaders2(apiKey),
3766
3983
  body: payload,
3767
3984
  timeoutMs: opts.timeoutMs ?? 12e4
3768
3985
  });
@@ -3775,10 +3992,10 @@ async function apiPost(endpoint, payload, apiKey, opts = {}) {
3775
3992
  }
3776
3993
  return data;
3777
3994
  }
3778
- async function apiGet2(endpoint, params, apiKey) {
3779
- const url = new URL(`${MINIMAX_BASE_URL}/${endpoint}`);
3995
+ async function apiGet2(endpoint2, params, apiKey) {
3996
+ const url = new URL(`${MINIMAX_BASE_URL}/${endpoint2}`);
3780
3997
  for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
3781
- return httpJson({ url: url.toString(), headers: authHeaders3(apiKey) });
3998
+ return httpJson({ url: url.toString(), headers: authHeaders2(apiKey) });
3782
3999
  }
3783
4000
  async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
3784
4001
  const { intervalMs = 1e4, timeoutMs = 6e5, logger } = opts;
@@ -3794,7 +4011,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
3794
4011
  if (status === "Failed" || status === "Error") {
3795
4012
  throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
3796
4013
  }
3797
- await sleep4(intervalMs);
4014
+ await sleep5(intervalMs);
3798
4015
  elapsed += intervalMs;
3799
4016
  }
3800
4017
  throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
@@ -3813,14 +4030,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
3813
4030
  await downloadFile(downloadUrl, dest);
3814
4031
  return dest;
3815
4032
  }
3816
- function sleep4(ms) {
4033
+ function sleep5(ms) {
3817
4034
  return new Promise((r) => setTimeout(r, ms));
3818
4035
  }
3819
4036
 
3820
4037
  // src/providers/minimax/generators/music.ts
3821
4038
  init_http_client();
3822
- import fs24 from "fs";
3823
- import path29 from "path";
4039
+ import fs25 from "fs";
4040
+ import path30 from "path";
3824
4041
  async function generateMinimaxMusic(opts) {
3825
4042
  const {
3826
4043
  apiKey,
@@ -3865,14 +4082,14 @@ async function generateMinimaxMusic(opts) {
3865
4082
  }
3866
4083
  const durationMs = resp.extra_info?.music_duration ?? 0;
3867
4084
  const outDir = getOutputDir();
3868
- const dest = path29.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
4085
+ const dest = path30.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
3869
4086
  try {
3870
4087
  if (audioData.startsWith("http")) {
3871
4088
  const bytes = await fetchBytes(audioData);
3872
- fs24.writeFileSync(dest, bytes);
4089
+ fs25.writeFileSync(dest, bytes);
3873
4090
  } else {
3874
4091
  const audioBytes = Buffer.from(audioData, "hex");
3875
- fs24.writeFileSync(dest, audioBytes);
4092
+ fs25.writeFileSync(dest, audioBytes);
3876
4093
  }
3877
4094
  } catch (e) {
3878
4095
  return {
@@ -3883,8 +4100,8 @@ async function generateMinimaxMusic(opts) {
3883
4100
  const durSec = durationMs / 1e3;
3884
4101
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
3885
4102
  if (output) {
3886
- fs24.mkdirSync(path29.dirname(path29.resolve(output)), { recursive: true });
3887
- fs24.copyFileSync(dest, output);
4103
+ fs25.mkdirSync(path30.dirname(path30.resolve(output)), { recursive: true });
4104
+ fs25.copyFileSync(dest, output);
3888
4105
  }
3889
4106
  return { status: "success", generatedAudio: dest, durationMs, model };
3890
4107
  }
@@ -3958,8 +4175,8 @@ Generated music: ${result.generatedAudio}`);
3958
4175
  init_errors();
3959
4176
 
3960
4177
  // src/providers/minimax/generators/speech.ts
3961
- import fs25 from "fs";
3962
- import path30 from "path";
4178
+ import fs26 from "fs";
4179
+ import path31 from "path";
3963
4180
  async function generateMinimaxSpeech(opts) {
3964
4181
  const {
3965
4182
  apiKey,
@@ -4006,12 +4223,12 @@ async function generateMinimaxSpeech(opts) {
4006
4223
  const audioBytes = Buffer.from(audioHex, "hex");
4007
4224
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
4008
4225
  const outDir = getOutputDir();
4009
- const dest = path30.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
4010
- fs25.writeFileSync(dest, audioBytes);
4226
+ const dest = path31.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
4227
+ fs26.writeFileSync(dest, audioBytes);
4011
4228
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
4012
4229
  if (output) {
4013
- fs25.mkdirSync(path30.dirname(path30.resolve(output)), { recursive: true });
4014
- fs25.copyFileSync(dest, output);
4230
+ fs26.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
4231
+ fs26.copyFileSync(dest, output);
4015
4232
  }
4016
4233
  return { status: "success", generatedAudio: dest, model };
4017
4234
  }
@@ -4058,8 +4275,8 @@ Generated audio: ${result.generatedAudio}`);
4058
4275
  init_errors();
4059
4276
 
4060
4277
  // src/providers/minimax/generators/video.ts
4061
- import fs26 from "fs";
4062
- import path31 from "path";
4278
+ import fs27 from "fs";
4279
+ import path32 from "path";
4063
4280
  async function generateMinimaxVideo(opts) {
4064
4281
  const {
4065
4282
  apiKey,
@@ -4105,18 +4322,18 @@ async function generateMinimaxVideo(opts) {
4105
4322
  return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
4106
4323
  }
4107
4324
  const outDir = getOutputDir();
4108
- const dest = path31.join(outDir, `minimax_video_${Date.now()}.mp4`);
4325
+ const dest = path32.join(outDir, `minimax_video_${Date.now()}.mp4`);
4109
4326
  try {
4110
4327
  await downloadMinimaxFile(fileId, apiKey, dest, logger);
4111
4328
  } catch (e) {
4112
4329
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
4113
4330
  }
4114
4331
  const elapsed = (Date.now() - start) / 1e3;
4115
- const stat = fs26.statSync(dest);
4332
+ const stat = fs27.statSync(dest);
4116
4333
  const fileSizeMb = stat.size / (1024 * 1024);
4117
4334
  if (output) {
4118
- fs26.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
4119
- fs26.copyFileSync(dest, output);
4335
+ fs27.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
4336
+ fs27.copyFileSync(dest, output);
4120
4337
  }
4121
4338
  await maybeDownloadThumb(pollResult, dest, {
4122
4339
  skip: thumb === false,
@@ -4179,8 +4396,8 @@ Generated video: ${result.generatedVideo}`);
4179
4396
  // src/providers/minimax/generators/image.ts
4180
4397
  init_errors();
4181
4398
  init_http_client();
4182
- import fs27 from "fs";
4183
- import path32 from "path";
4399
+ import fs28 from "fs";
4400
+ import path33 from "path";
4184
4401
  async function generateMinimaxImage(opts) {
4185
4402
  const {
4186
4403
  apiKey,
@@ -4218,8 +4435,8 @@ async function generateMinimaxImage(opts) {
4218
4435
  try {
4219
4436
  const bytes = await fetchBytes(url);
4220
4437
  const fname = `minimax_image_${Date.now()}_${i}.png`;
4221
- const dest = path32.join(outDir, fname);
4222
- fs27.writeFileSync(dest, bytes);
4438
+ const dest = path33.join(outDir, fname);
4439
+ fs28.writeFileSync(dest, bytes);
4223
4440
  savedFiles.push(dest);
4224
4441
  logger?.success(`Saved: ${dest}`);
4225
4442
  } catch (e) {
@@ -4230,8 +4447,8 @@ async function generateMinimaxImage(opts) {
4230
4447
  }
4231
4448
  }
4232
4449
  if (output && savedFiles[0]) {
4233
- fs27.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
4234
- fs27.copyFileSync(savedFiles[0], output);
4450
+ fs28.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
4451
+ fs28.copyFileSync(savedFiles[0], output);
4235
4452
  }
4236
4453
  return { status: "success", generatedImages: savedFiles, model };
4237
4454
  }
@@ -4267,8 +4484,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
4267
4484
  // src/providers/minimax/commands/image-to-image.ts
4268
4485
  init_errors();
4269
4486
  init_http_client();
4270
- import fs28 from "fs";
4271
- import path33 from "path";
4487
+ import fs29 from "fs";
4488
+ import path34 from "path";
4272
4489
  function registerMinimaxImageToImageCommand(parent) {
4273
4490
  parent.command("image-to-image").alias("i2i").description(
4274
4491
  "MiniMax character/subject preservation. CAVEAT: NOT free-form image editing \u2014 the --ref image is used only as a character/face reference for a NEW scene described by --prompt."
@@ -4323,8 +4540,8 @@ function registerMinimaxImageToImageCommand(parent) {
4323
4540
  if (!url) continue;
4324
4541
  try {
4325
4542
  const bytes = await fetchBytes(url);
4326
- const dest = path33.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
4327
- fs28.writeFileSync(dest, bytes);
4543
+ const dest = path34.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
4544
+ fs29.writeFileSync(dest, bytes);
4328
4545
  saved.push(dest);
4329
4546
  logger.success(`Saved: ${dest}`);
4330
4547
  } catch (e) {
@@ -4335,8 +4552,8 @@ function registerMinimaxImageToImageCommand(parent) {
4335
4552
  }
4336
4553
  }
4337
4554
  if (opts.output && saved[0]) {
4338
- fs28.mkdirSync(path33.dirname(path33.resolve(opts.output)), { recursive: true });
4339
- fs28.copyFileSync(saved[0], opts.output);
4555
+ fs29.mkdirSync(path34.dirname(path34.resolve(opts.output)), { recursive: true });
4556
+ fs29.copyFileSync(saved[0], opts.output);
4340
4557
  logger.success(`Copied to: ${opts.output}`);
4341
4558
  }
4342
4559
  console.log(`
@@ -4356,9 +4573,636 @@ function registerMinimaxCommands(program2) {
4356
4573
  registerMinimaxGenerateMusicCommand(minimax);
4357
4574
  }
4358
4575
 
4576
+ // src/providers/openai/commands/generate-speech.ts
4577
+ init_errors();
4578
+
4579
+ // src/providers/openai/models.ts
4580
+ init_errors();
4581
+ var OPENAI_BASE_URL = "https://api.openai.com/v1";
4582
+ var DEFAULT_OPENAI_IMAGE_MODEL = "gpt-image-2";
4583
+ var DEFAULT_OPENAI_TTS_MODEL = "gpt-4o-mini-tts";
4584
+ var DEFAULT_OPENAI_STT_MODEL = "gpt-4o-transcribe";
4585
+ var OPENAI_IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp"];
4586
+ var OPENAI_IMAGE_QUALITIES = ["low", "medium", "high"];
4587
+ var OPENAI_IMAGE_SIZES = ["1024x1024", "1024x1536", "1536x1024", "auto"];
4588
+ var OPENAI_TTS_OUTPUT_FORMATS = ["mp3", "opus", "aac", "flac", "wav", "pcm"];
4589
+ var OPENAI_TTS_VOICES = [
4590
+ "alloy",
4591
+ "ash",
4592
+ "ballad",
4593
+ "cedar",
4594
+ "coral",
4595
+ "echo",
4596
+ "fable",
4597
+ "marin",
4598
+ "nova",
4599
+ "onyx",
4600
+ "sage",
4601
+ "shimmer",
4602
+ "verse"
4603
+ ];
4604
+ var OPENAI_TRANSCRIPT_FORMATS = ["json", "text", "diarized_json"];
4605
+ function isOneOf(value, values) {
4606
+ return values.includes(value);
4607
+ }
4608
+ function extFromImageFormat(format) {
4609
+ if (format === "jpeg") return "jpg";
4610
+ return format;
4611
+ }
4612
+ function extFromAudioFormat(format) {
4613
+ return format;
4614
+ }
4615
+ function parseImageFormat(value) {
4616
+ if (!isOneOf(value, OPENAI_IMAGE_OUTPUT_FORMATS)) {
4617
+ throw new ValidationError(`--format must be one of: ${OPENAI_IMAGE_OUTPUT_FORMATS.join(", ")}`);
4618
+ }
4619
+ return value;
4620
+ }
4621
+ function parseImageQuality(value) {
4622
+ if (!isOneOf(value, OPENAI_IMAGE_QUALITIES)) {
4623
+ throw new ValidationError(`--quality must be one of: ${OPENAI_IMAGE_QUALITIES.join(", ")}`);
4624
+ }
4625
+ return value;
4626
+ }
4627
+ function parseImageSize(value) {
4628
+ if (!isOneOf(value, OPENAI_IMAGE_SIZES)) {
4629
+ throw new ValidationError(`--size must be one of: ${OPENAI_IMAGE_SIZES.join(", ")}`);
4630
+ }
4631
+ return value;
4632
+ }
4633
+ function parseDriver(value) {
4634
+ if (!isOneOf(value, ["api", "codex", "auto"])) {
4635
+ throw new ValidationError("--driver must be one of: api, codex, auto");
4636
+ }
4637
+ return value;
4638
+ }
4639
+ function parseNumImages(value) {
4640
+ if (!/^\d+$/.test(value)) {
4641
+ throw new ValidationError("--num-images must be an integer from 1 to 10");
4642
+ }
4643
+ const parsed = Number.parseInt(value, 10);
4644
+ if (parsed < 1 || parsed > 10) {
4645
+ throw new ValidationError("--num-images must be an integer from 1 to 10");
4646
+ }
4647
+ return parsed;
4648
+ }
4649
+ function parseTtsFormat(value) {
4650
+ if (!isOneOf(value, OPENAI_TTS_OUTPUT_FORMATS)) {
4651
+ throw new ValidationError(
4652
+ `--output-format must be one of: ${OPENAI_TTS_OUTPUT_FORMATS.join(", ")}`
4653
+ );
4654
+ }
4655
+ return value;
4656
+ }
4657
+ function validateTtsVoice(value) {
4658
+ if (!isOneOf(value, OPENAI_TTS_VOICES)) {
4659
+ throw new ValidationError(`--voice must be one of: ${OPENAI_TTS_VOICES.join(", ")}`);
4660
+ }
4661
+ }
4662
+ function parseTranscriptFormat(value) {
4663
+ if (!isOneOf(value, OPENAI_TRANSCRIPT_FORMATS)) {
4664
+ throw new ValidationError(`--format must be one of: ${OPENAI_TRANSCRIPT_FORMATS.join(", ")}`);
4665
+ }
4666
+ return value;
4667
+ }
4668
+ function resolveTranscriptionRequestFormat(cliFormat, model) {
4669
+ if (cliFormat === "diarized_json") {
4670
+ if (model !== "gpt-4o-transcribe-diarize") {
4671
+ throw new ValidationError("--format diarized_json requires gpt-4o-transcribe-diarize");
4672
+ }
4673
+ return "diarized_json";
4674
+ }
4675
+ return "json";
4676
+ }
4677
+ function validateKnownSpeakerPairing(names = [], refs = []) {
4678
+ if (names.length !== refs.length) {
4679
+ throw new ValidationError(
4680
+ "--known-speaker-name and --known-speaker-reference must have equal counts"
4681
+ );
4682
+ }
4683
+ if (names.length > 4) {
4684
+ throw new ValidationError("OpenAI diarization supports up to 4 known speakers");
4685
+ }
4686
+ }
4687
+
4688
+ // src/providers/openai/openai-audio.ts
4689
+ init_http_client();
4690
+ import fs31 from "fs";
4691
+ import path36 from "path";
4692
+
4693
+ // src/providers/openai/client.ts
4694
+ import fs30 from "fs";
4695
+ import path35 from "path";
4696
+ init_errors();
4697
+ init_http_client();
4698
+ var DEFAULT_TIMEOUT_MS3 = 24e4;
4699
+ function requireOpenAIKey() {
4700
+ const key = resolveKey("OPENAI_API_KEY");
4701
+ if (!key) {
4702
+ throw new ConfigError(
4703
+ "OPENAI_API_KEY is not set. Get one at https://platform.openai.com/api-keys"
4704
+ );
4705
+ }
4706
+ return key;
4707
+ }
4708
+ function endpoint(pathname) {
4709
+ return `${OPENAI_BASE_URL}/${pathname.replace(/^\//, "")}`;
4710
+ }
4711
+ function authHeaders3(apiKey, extra) {
4712
+ return { Authorization: `Bearer ${apiKey}`, ...extra ?? {} };
4713
+ }
4714
+ function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
4715
+ return httpJson({
4716
+ url: endpoint(pathname),
4717
+ method: "POST",
4718
+ headers: authHeaders3(apiKey, { "Content-Type": "application/json" }),
4719
+ body,
4720
+ timeoutMs
4721
+ });
4722
+ }
4723
+ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
4724
+ const url = endpoint(pathname);
4725
+ let res;
4726
+ try {
4727
+ res = await globalThis.fetch(url, {
4728
+ method: "POST",
4729
+ headers: authHeaders3(apiKey),
4730
+ body: form,
4731
+ signal: AbortSignal.timeout(timeoutMs)
4732
+ });
4733
+ } catch (cause) {
4734
+ throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
4735
+ }
4736
+ if (!res.ok) {
4737
+ const text = await res.text().catch(() => "");
4738
+ throw new HttpError(res.status, text.slice(0, 500), url);
4739
+ }
4740
+ return res.json();
4741
+ }
4742
+ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
4743
+ const url = endpoint(pathname);
4744
+ let res;
4745
+ try {
4746
+ res = await globalThis.fetch(url, {
4747
+ method: "POST",
4748
+ headers: authHeaders3(apiKey, { "Content-Type": "application/json" }),
4749
+ body: JSON.stringify(body),
4750
+ signal: AbortSignal.timeout(timeoutMs)
4751
+ });
4752
+ } catch (cause) {
4753
+ throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
4754
+ }
4755
+ if (!res.ok) {
4756
+ const text = await res.text().catch(() => "");
4757
+ throw new HttpError(res.status, text.slice(0, 500), url);
4758
+ }
4759
+ return Buffer.from(await res.arrayBuffer());
4760
+ }
4761
+ function fileFromPath(filePath) {
4762
+ const abs = path35.resolve(filePath);
4763
+ if (!fs30.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
4764
+ const bytes = fs30.readFileSync(abs);
4765
+ return new File([new Uint8Array(bytes)], path35.basename(abs));
4766
+ }
4767
+
4768
+ // src/providers/openai/openai-audio.ts
4769
+ function buildSpeechBody(opts) {
4770
+ const body = {
4771
+ model: opts.model ?? DEFAULT_OPENAI_TTS_MODEL,
4772
+ input: opts.text,
4773
+ voice: opts.voice,
4774
+ response_format: opts.outputFormat
4775
+ };
4776
+ if (opts.instructions) body.instructions = opts.instructions;
4777
+ return body;
4778
+ }
4779
+ async function buildTranscriptionForm(opts) {
4780
+ const model = opts.model ?? DEFAULT_OPENAI_STT_MODEL;
4781
+ const form = new FormData();
4782
+ form.append("file", opts.input);
4783
+ form.append("model", model);
4784
+ form.append("response_format", resolveTranscriptionRequestFormat(opts.cliFormat, model));
4785
+ if (opts.language) form.append("language", opts.language);
4786
+ if (opts.chunkingStrategy) form.append("chunking_strategy", opts.chunkingStrategy);
4787
+ const names = opts.knownSpeakerNames ?? [];
4788
+ const refs = opts.knownSpeakerReferences ?? [];
4789
+ validateKnownSpeakerPairing(names, refs);
4790
+ for (const name of names) form.append("known_speaker_names[]", name);
4791
+ for (const ref of refs) {
4792
+ form.append("known_speaker_references[]", await resolveAudioReferenceDataUrl(ref));
4793
+ }
4794
+ return form;
4795
+ }
4796
+ async function resolveAudioReferenceDataUrl(ref) {
4797
+ const resolved = await resolveAudioInput(ref);
4798
+ if (resolved.kind === "data") return resolved.dataUrl;
4799
+ const bytes = await fetchBytes(resolved.url);
4800
+ return `data:${audioMimeFromReference(resolved.url)};base64,${bytes.toString("base64")}`;
4801
+ }
4802
+ function audioMimeFromReference(ref) {
4803
+ const pathname = new URL(ref).pathname;
4804
+ const ext = path36.extname(pathname).toLowerCase();
4805
+ switch (ext) {
4806
+ case ".mp3":
4807
+ return "audio/mpeg";
4808
+ case ".wav":
4809
+ return "audio/wav";
4810
+ case ".m4a":
4811
+ return "audio/mp4";
4812
+ case ".flac":
4813
+ return "audio/flac";
4814
+ case ".ogg":
4815
+ return "audio/ogg";
4816
+ default:
4817
+ return "application/octet-stream";
4818
+ }
4819
+ }
4820
+ async function generateOpenAISpeech(opts) {
4821
+ const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
4822
+ const ext = extFromAudioFormat(opts.outputFormat);
4823
+ const dest = path36.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
4824
+ fs31.writeFileSync(dest, bytes);
4825
+ opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
4826
+ if (opts.output) {
4827
+ fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
4828
+ fs31.copyFileSync(dest, opts.output);
4829
+ }
4830
+ return dest;
4831
+ }
4832
+ async function transcribeOpenAIAudio(opts) {
4833
+ const form = await buildTranscriptionForm({
4834
+ input: fileFromPath(opts.inputPath),
4835
+ model: opts.model,
4836
+ cliFormat: opts.format,
4837
+ language: opts.language,
4838
+ chunkingStrategy: opts.chunkingStrategy ?? (opts.model === "gpt-4o-transcribe-diarize" || opts.format === "diarized_json" ? "auto" : void 0),
4839
+ knownSpeakerNames: opts.knownSpeakerNames,
4840
+ knownSpeakerReferences: opts.knownSpeakerReferences
4841
+ });
4842
+ const data = await postOpenAIForm(
4843
+ "/audio/transcriptions",
4844
+ form,
4845
+ requireOpenAIKey(),
4846
+ 6e5
4847
+ );
4848
+ const text = typeof data.text === "string" ? data.text : void 0;
4849
+ const ext = opts.format === "text" ? "txt" : "json";
4850
+ const dest = opts.output ? path36.resolve(opts.output) : path36.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
4851
+ fs31.mkdirSync(path36.dirname(dest), { recursive: true });
4852
+ fs31.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
4853
+ opts.logger?.success(`Saved transcript: ${dest}`);
4854
+ return { path: dest, text };
4855
+ }
4856
+
4857
+ // src/providers/openai/commands/generate-speech.ts
4858
+ function registerOpenAIGenerateSpeechCommand(parent) {
4859
+ 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) => {
4860
+ const text = opts.text ?? opts.prompt;
4861
+ if (!text) throw new ValidationError("Either --text or --prompt is required");
4862
+ validateTtsVoice(opts.voice);
4863
+ const logger = createLogger({ verbose: opts.verbose ?? false });
4864
+ const dest = await generateOpenAISpeech({
4865
+ text,
4866
+ model: opts.model,
4867
+ voice: opts.voice,
4868
+ outputFormat: parseTtsFormat(opts.outputFormat),
4869
+ instructions: opts.instructions,
4870
+ output: opts.output,
4871
+ logger
4872
+ });
4873
+ console.log(`
4874
+ Generated audio: ${dest}`);
4875
+ });
4876
+ }
4877
+
4878
+ // src/providers/openai/codex-image-driver.ts
4879
+ import fs32 from "fs";
4880
+ import os2 from "os";
4881
+ import path37 from "path";
4882
+ import { execa } from "execa";
4883
+ init_errors();
4884
+ var defaultRunner = async (cmd, args, opts) => {
4885
+ const res = await execa(cmd, args, {
4886
+ cwd: opts?.cwd,
4887
+ env: opts?.env,
4888
+ reject: false,
4889
+ timeout: opts?.timeoutMs
4890
+ });
4891
+ return { stdout: res.stdout, stderr: res.stderr, exitCode: res.exitCode ?? 0 };
4892
+ };
4893
+ async function checkCodexAuthenticated(runner = defaultRunner) {
4894
+ try {
4895
+ const res = await runner("codex", ["login", "status"], { timeoutMs: 15e3 });
4896
+ return res.exitCode === 0 && /Logged in using ChatGPT/i.test(`${res.stdout}
4897
+ ${res.stderr}`);
4898
+ } catch {
4899
+ return false;
4900
+ }
4901
+ }
4902
+ async function resolveImageDriver(opts) {
4903
+ const hasApiKey = opts.hasApiKey ?? Boolean(resolveKey("OPENAI_API_KEY"));
4904
+ const codexAuthCheck = opts.codexAuthCheck ?? (() => checkCodexAuthenticated());
4905
+ if (opts.requested === "api") {
4906
+ if (!hasApiKey) throw new ConfigError("OPENAI_API_KEY is required for --driver api");
4907
+ return "api";
4908
+ }
4909
+ if (opts.requested === "codex") {
4910
+ if (!await codexAuthCheck()) throw new ConfigError("Codex driver requires `codex login`");
4911
+ return "codex";
4912
+ }
4913
+ if (hasApiKey) return "api";
4914
+ if (await codexAuthCheck()) return "codex";
4915
+ throw new ConfigError(
4916
+ "Set OPENAI_API_KEY or run `codex login` for the experimental image driver"
4917
+ );
4918
+ }
4919
+ function sanitizeCodexEnv() {
4920
+ const keep = ["PATH", "HOME", "CODEX_HOME", "TMPDIR", "TEMP", "LANG", "LC_ALL"];
4921
+ const env = {};
4922
+ for (const key of keep) {
4923
+ if (process.env[key]) env[key] = process.env[key];
4924
+ }
4925
+ return env;
4926
+ }
4927
+ async function runCodexImageGeneration(opts) {
4928
+ const runner = opts.runner ?? defaultRunner;
4929
+ const ext = extFromImageFormat(opts.outputFormat);
4930
+ const dest = opts.output ? path37.resolve(opts.output) : path37.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
4931
+ fs32.mkdirSync(path37.dirname(dest), { recursive: true });
4932
+ const tmp = fs32.mkdtempSync(path37.join(os2.tmpdir(), "multix-codex-image-"));
4933
+ const schema = path37.join(tmp, "schema.json");
4934
+ const result = path37.join(tmp, "result.json");
4935
+ fs32.writeFileSync(
4936
+ schema,
4937
+ JSON.stringify({
4938
+ type: "object",
4939
+ properties: { status: { type: "string" }, path: { type: "string" } },
4940
+ required: ["status", "path"],
4941
+ additionalProperties: false
4942
+ })
4943
+ );
4944
+ const args = [
4945
+ "exec",
4946
+ "--ephemeral",
4947
+ "--skip-git-repo-check",
4948
+ "--cd",
4949
+ tmp,
4950
+ "--output-schema",
4951
+ schema,
4952
+ "--output-last-message",
4953
+ result
4954
+ ];
4955
+ for (const ref of opts.refs ?? []) args.push("--image", ref);
4956
+ args.push(buildCodexPrompt(opts.prompt, dest));
4957
+ const res = await runner("codex", args, {
4958
+ cwd: tmp,
4959
+ env: sanitizeCodexEnv(),
4960
+ timeoutMs: 6e5
4961
+ });
4962
+ if (res.exitCode !== 0) {
4963
+ throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
4964
+ }
4965
+ if (!fs32.existsSync(dest) || fs32.statSync(dest).size === 0) {
4966
+ throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
4967
+ }
4968
+ return { path: dest };
4969
+ }
4970
+ function buildCodexPrompt(userPrompt, outputPath) {
4971
+ return [
4972
+ `Create exactly one image at ${outputPath}.`,
4973
+ "Do not modify any repository files.",
4974
+ "Use the attached reference images if present.",
4975
+ `Image request: ${userPrompt}`,
4976
+ `Reply with JSON only: {"status":"ok","path":"${outputPath}"}.`
4977
+ ].join(" ");
4978
+ }
4979
+
4980
+ // src/providers/openai/openai-image.ts
4981
+ init_errors();
4982
+ init_http_client();
4983
+ import fs33 from "fs";
4984
+ import path38 from "path";
4985
+ function buildImageGenerationBody(opts) {
4986
+ return {
4987
+ model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
4988
+ prompt: opts.prompt,
4989
+ size: opts.size ?? "1024x1024",
4990
+ quality: opts.quality ?? "medium",
4991
+ output_format: opts.outputFormat,
4992
+ n: opts.numImages ?? 1
4993
+ };
4994
+ }
4995
+ async function buildImageEditForm(opts) {
4996
+ const form = new FormData();
4997
+ form.append("model", opts.model);
4998
+ form.append("prompt", opts.prompt);
4999
+ form.append("size", opts.size);
5000
+ form.append("quality", opts.quality);
5001
+ form.append("output_format", opts.outputFormat);
5002
+ for (const ref of opts.refs) {
5003
+ form.append("image[]", new File([new Uint8Array(ref.bytes)], ref.filename, { type: ref.mime }));
5004
+ }
5005
+ return form;
5006
+ }
5007
+ function saveOpenAIImages(opts) {
5008
+ const items = opts.data ?? [];
5009
+ if (items.length === 0) throw new ProviderError("No images in OpenAI response", "openai");
5010
+ const outDir = getOutputDir();
5011
+ const ext = extFromImageFormat(opts.format);
5012
+ const ts = Date.now();
5013
+ const saved = [];
5014
+ for (let i = 0; i < items.length; i++) {
5015
+ const b64 = items[i]?.b64_json;
5016
+ if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
5017
+ const dest = path38.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
5018
+ fs33.writeFileSync(dest, Buffer.from(b64, "base64"));
5019
+ saved.push(dest);
5020
+ opts.logger?.success(`Saved: ${dest}`);
5021
+ }
5022
+ if (opts.output && saved[0]) {
5023
+ fs33.mkdirSync(path38.dirname(path38.resolve(opts.output)), { recursive: true });
5024
+ fs33.copyFileSync(saved[0], opts.output);
5025
+ opts.logger?.success(`Copied to: ${opts.output}`);
5026
+ }
5027
+ return saved;
5028
+ }
5029
+ async function resolveImageRefBytes(ref) {
5030
+ if (isHttpUrl(ref)) {
5031
+ const bytes2 = await fetchBytes(ref);
5032
+ const pathname = new URL(ref).pathname;
5033
+ const filename = path38.basename(pathname) || "reference.png";
5034
+ return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
5035
+ }
5036
+ const file = fileFromPath(ref);
5037
+ const bytes = Buffer.from(await file.arrayBuffer());
5038
+ return { filename: file.name, bytes, mime: file.type || sniffImageMime2(bytes, file.name) };
5039
+ }
5040
+ async function generateOpenAIImage(opts) {
5041
+ const data = await postOpenAIJson(
5042
+ "/images/generations",
5043
+ buildImageGenerationBody(opts),
5044
+ requireOpenAIKey()
5045
+ );
5046
+ return saveOpenAIImages({
5047
+ data: data.data,
5048
+ task: "image",
5049
+ format: opts.outputFormat,
5050
+ output: opts.output,
5051
+ logger: opts.logger
5052
+ });
5053
+ }
5054
+ async function editOpenAIImage(opts) {
5055
+ const refs = await Promise.all(opts.refs.map((ref) => resolveImageRefBytes(ref)));
5056
+ const form = await buildImageEditForm({
5057
+ prompt: opts.prompt,
5058
+ refs,
5059
+ model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
5060
+ size: opts.size ?? "1024x1024",
5061
+ quality: opts.quality ?? "medium",
5062
+ outputFormat: opts.outputFormat
5063
+ });
5064
+ const data = await postOpenAIForm("/images/edits", form, requireOpenAIKey());
5065
+ return saveOpenAIImages({
5066
+ data: data.data,
5067
+ task: "image-edit",
5068
+ format: opts.outputFormat,
5069
+ output: opts.output,
5070
+ logger: opts.logger
5071
+ });
5072
+ }
5073
+ function sniffImageMime2(buf, name) {
5074
+ if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
5075
+ if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
5076
+ if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
5077
+ return "image/webp";
5078
+ const ext = path38.extname(name).toLowerCase();
5079
+ if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
5080
+ if (ext === ".webp") return "image/webp";
5081
+ return "image/png";
5082
+ }
5083
+
5084
+ // src/providers/openai/commands/generate.ts
5085
+ function registerOpenAIGenerateCommand(parent) {
5086
+ 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(
5087
+ "-m, --model <id>",
5088
+ "Image model",
5089
+ resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
5090
+ ).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) => {
5091
+ const logger = createLogger({ verbose: opts.verbose ?? false });
5092
+ const outputFormat = parseImageFormat(opts.format);
5093
+ const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
5094
+ const saved = driver === "codex" ? [
5095
+ (await runCodexImageGeneration({
5096
+ prompt: opts.prompt,
5097
+ outputFormat,
5098
+ output: opts.output
5099
+ })).path
5100
+ ] : await generateOpenAIImage({
5101
+ prompt: opts.prompt,
5102
+ model: opts.model,
5103
+ size: parseImageSize(opts.size),
5104
+ quality: parseImageQuality(opts.quality),
5105
+ outputFormat,
5106
+ numImages: parseNumImages(opts.numImages),
5107
+ output: opts.output,
5108
+ logger
5109
+ });
5110
+ console.log(`
5111
+ Generated ${saved.length} image(s):`);
5112
+ for (const file of saved) console.log(` ${file}`);
5113
+ });
5114
+ }
5115
+
5116
+ // src/providers/openai/commands/image-to-image.ts
5117
+ init_errors();
5118
+ function registerOpenAIImageToImageCommand(parent) {
5119
+ 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(
5120
+ "--ref <path>",
5121
+ "Reference image path or URL; repeatable",
5122
+ collect5,
5123
+ []
5124
+ ).option("--driver <driver>", "api|codex|auto", "auto").option(
5125
+ "-m, --model <id>",
5126
+ "Image model",
5127
+ resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
5128
+ ).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) => {
5129
+ if (!opts.ref || opts.ref.length === 0) {
5130
+ throw new ValidationError("At least one --ref is required for image-to-image.");
5131
+ }
5132
+ const logger = createLogger({ verbose: opts.verbose ?? false });
5133
+ const outputFormat = parseImageFormat(opts.format);
5134
+ const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
5135
+ const saved = driver === "codex" ? [
5136
+ (await runCodexImageGeneration({
5137
+ prompt: opts.prompt,
5138
+ refs: opts.ref,
5139
+ outputFormat,
5140
+ output: opts.output
5141
+ })).path
5142
+ ] : await editOpenAIImage({
5143
+ prompt: opts.prompt,
5144
+ refs: opts.ref,
5145
+ model: opts.model,
5146
+ size: parseImageSize(opts.size),
5147
+ quality: parseImageQuality(opts.quality),
5148
+ outputFormat,
5149
+ output: opts.output,
5150
+ logger
5151
+ });
5152
+ console.log(`
5153
+ Generated ${saved.length} image(s):`);
5154
+ for (const file of saved) console.log(` ${file}`);
5155
+ });
5156
+ }
5157
+ function collect5(value, prev) {
5158
+ return [...prev, value];
5159
+ }
5160
+
5161
+ // src/providers/openai/commands/transcribe.ts
5162
+ function registerOpenAITranscribeCommand(parent) {
5163
+ 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(
5164
+ "--known-speaker-name <name>",
5165
+ "Known speaker name; repeatable",
5166
+ collect6,
5167
+ []
5168
+ ).option(
5169
+ "--known-speaker-reference <path>",
5170
+ "2-10s reference audio; repeatable",
5171
+ collect6,
5172
+ []
5173
+ ).option("--output <path>", "Save transcript to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
5174
+ const logger = createLogger({ verbose: opts.verbose ?? false });
5175
+ const result = await transcribeOpenAIAudio({
5176
+ inputPath: opts.input,
5177
+ model: opts.model,
5178
+ format: parseTranscriptFormat(opts.format),
5179
+ language: opts.language,
5180
+ chunkingStrategy: opts.chunkingStrategy,
5181
+ knownSpeakerNames: opts.knownSpeakerName,
5182
+ knownSpeakerReferences: opts.knownSpeakerReference,
5183
+ output: opts.output,
5184
+ logger
5185
+ });
5186
+ if (opts.format === "text" && result.text) console.log(`
5187
+ ${result.text.trim()}`);
5188
+ });
5189
+ }
5190
+ function collect6(value, prev) {
5191
+ return [...prev, value];
5192
+ }
5193
+
5194
+ // src/providers/openai/commands/index.ts
5195
+ function registerOpenAICommands(program2) {
5196
+ const openai = program2.command("openai").description("OpenAI image generation/editing, TTS, STT, and experimental Codex image driver");
5197
+ registerOpenAIGenerateCommand(openai);
5198
+ registerOpenAIImageToImageCommand(openai);
5199
+ registerOpenAIGenerateSpeechCommand(openai);
5200
+ registerOpenAITranscribeCommand(openai);
5201
+ }
5202
+
4359
5203
  // src/providers/openrouter/client.ts
4360
- import fs29 from "fs";
4361
- import path34 from "path";
5204
+ import fs34 from "fs";
5205
+ import path39 from "path";
4362
5206
  init_errors();
4363
5207
  init_http_client();
4364
5208
 
@@ -4523,8 +5367,8 @@ async function generateOpenRouterImage(opts) {
4523
5367
  if (!url) continue;
4524
5368
  try {
4525
5369
  const bytes = await fetchBytes(url);
4526
- const dest = path34.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
4527
- fs29.writeFileSync(dest, bytes);
5370
+ const dest = path39.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
5371
+ fs34.writeFileSync(dest, bytes);
4528
5372
  savedFiles.push(dest);
4529
5373
  logger?.success(`Saved: ${dest}`);
4530
5374
  } catch (e) {
@@ -4535,8 +5379,8 @@ async function generateOpenRouterImage(opts) {
4535
5379
  }
4536
5380
  }
4537
5381
  if (output && savedFiles[0]) {
4538
- fs29.mkdirSync(path34.dirname(path34.resolve(output)), { recursive: true });
4539
- fs29.writeFileSync(output, fs29.readFileSync(savedFiles[0]));
5382
+ fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
5383
+ fs34.writeFileSync(output, fs34.readFileSync(savedFiles[0]));
4540
5384
  }
4541
5385
  return { status: "success", generatedImages: savedFiles, model: usedModel };
4542
5386
  }
@@ -4571,8 +5415,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
4571
5415
  }
4572
5416
 
4573
5417
  // src/providers/openrouter/commands/image-to-image.ts
4574
- import fs30 from "fs";
4575
- import path35 from "path";
5418
+ import fs35 from "fs";
5419
+ import path40 from "path";
4576
5420
  init_errors();
4577
5421
  init_http_client();
4578
5422
  var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
@@ -4582,7 +5426,7 @@ function registerOpenRouterImageToImageCommand(parent) {
4582
5426
  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
5427
  "--ref <path>",
4584
5428
  "Reference image (URL or local path); repeatable (most models accept 1)",
4585
- collect5,
5429
+ collect7,
4586
5430
  []
4587
5431
  ).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
5432
  async (opts) => {
@@ -4623,7 +5467,7 @@ Generated ${saved.length} image(s):`);
4623
5467
  }
4624
5468
  );
4625
5469
  }
4626
- function collect5(value, prev) {
5470
+ function collect7(value, prev) {
4627
5471
  return [...prev, value];
4628
5472
  }
4629
5473
  function parseStrength(raw) {
@@ -4662,14 +5506,14 @@ async function runImageToImage(opts) {
4662
5506
  const url = parsed.urls[i];
4663
5507
  if (!url) continue;
4664
5508
  const bytes = await fetchBytes(url);
4665
- const dest = path35.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
4666
- fs30.writeFileSync(dest, bytes);
5509
+ const dest = path40.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
5510
+ fs35.writeFileSync(dest, bytes);
4667
5511
  saved.push(dest);
4668
5512
  opts.logger.success(`Saved: ${dest}`);
4669
5513
  }
4670
5514
  if (opts.output && saved[0]) {
4671
- fs30.mkdirSync(path35.dirname(path35.resolve(opts.output)), { recursive: true });
4672
- fs30.copyFileSync(saved[0], opts.output);
5515
+ fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
5516
+ fs35.copyFileSync(saved[0], opts.output);
4673
5517
  opts.logger.success(`Copied to: ${opts.output}`);
4674
5518
  }
4675
5519
  return saved;
@@ -4715,13 +5559,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
4715
5559
  }
4716
5560
 
4717
5561
  // src/providers/openrouter/commands/video-status.ts
4718
- import fs31 from "fs";
4719
- import path36 from "path";
5562
+ import fs36 from "fs";
5563
+ import path41 from "path";
4720
5564
  async function downloadOpenRouterVideo(opts) {
4721
5565
  const { jobId, logger, source } = opts;
4722
5566
  const apiKey = requireOpenRouterKey();
4723
5567
  const url = videoContentUrl(jobId);
4724
- const dest = path36.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
5568
+ const dest = path41.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
4725
5569
  const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
4726
5570
  if (!r.ok) {
4727
5571
  logger.error(`Download HTTP ${r.status}`);
@@ -4731,14 +5575,14 @@ async function downloadOpenRouterVideo(opts) {
4731
5575
  logger.error("Empty response body");
4732
5576
  process.exit(1);
4733
5577
  }
4734
- fs31.mkdirSync(path36.dirname(dest), { recursive: true });
4735
- const ws = fs31.createWriteStream(dest);
5578
+ fs36.mkdirSync(path41.dirname(dest), { recursive: true });
5579
+ const ws = fs36.createWriteStream(dest);
4736
5580
  const { pipeline: pipeline2 } = await import("stream/promises");
4737
5581
  await pipeline2(r.body, ws);
4738
5582
  logger.success(`Saved ${dest}`);
4739
5583
  if (opts.output) {
4740
- fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
4741
- fs31.copyFileSync(dest, opts.output);
5584
+ fs36.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
5585
+ fs36.copyFileSync(dest, opts.output);
4742
5586
  logger.success(`Copied to ${opts.output}`);
4743
5587
  }
4744
5588
  await maybeDownloadThumb(source, dest, {
@@ -4898,10 +5742,10 @@ function registerOpenRouterCommands(program2) {
4898
5742
  }
4899
5743
 
4900
5744
  // src/commands/check-helpers/binary-check.ts
4901
- import { execa } from "execa";
5745
+ import { execa as execa2 } from "execa";
4902
5746
  async function checkBinary(binary) {
4903
5747
  try {
4904
- const result = await execa(binary, ["--version"], { reject: true, all: true });
5748
+ const result = await execa2(binary, ["--version"], { reject: true, all: true });
4905
5749
  const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
4906
5750
  return { available: true, version, binary };
4907
5751
  } catch (e) {
@@ -4982,6 +5826,12 @@ var PROVIDERS = [
4982
5826
  optional: true,
4983
5827
  link: "https://openrouter.ai/settings/keys"
4984
5828
  },
5829
+ {
5830
+ name: "OpenAI",
5831
+ envPrimary: "OPENAI_API_KEY",
5832
+ optional: true,
5833
+ link: "https://platform.openai.com/api-keys"
5834
+ },
4985
5835
  {
4986
5836
  name: "MiniMax",
4987
5837
  envPrimary: "MINIMAX_API_KEY",
@@ -5017,6 +5867,10 @@ function resolveProviderKey(p) {
5017
5867
  }
5018
5868
  return {};
5019
5869
  }
5870
+ async function checkCodexImageReadiness(hasAnyProviderKey, verbose = false, checker = checkCodexAuthenticated) {
5871
+ if (hasAnyProviderKey && !verbose) return false;
5872
+ return checker();
5873
+ }
5020
5874
  function registerCheckCommand(program2) {
5021
5875
  program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
5022
5876
  const logger = createLogger({ verbose: opts.verbose ?? false });
@@ -5053,10 +5907,25 @@ function registerCheckCommand(program2) {
5053
5907
  logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
5054
5908
  }
5055
5909
  }
5910
+ const shouldCheckCodex = !anyKey || opts.verbose;
5911
+ let codexReady = false;
5912
+ if (shouldCheckCodex) {
5913
+ logger.header("Codex Image Driver");
5914
+ logger.info("Checking Codex login status...");
5915
+ codexReady = await checkCodexImageReadiness(anyKey, opts.verbose ?? false);
5916
+ if (codexReady) {
5917
+ logger.success("Codex authenticated \u2014 experimental OpenAI image driver available");
5918
+ } else {
5919
+ logger.info("Codex image driver unavailable (run `codex login` to enable it)");
5920
+ }
5921
+ }
5056
5922
  if (!anyKey) {
5057
- logger.header("No Provider Keys Configured");
5058
- console.log(SETUP_HINTS);
5059
- process.exit(1);
5923
+ if (!codexReady) {
5924
+ logger.header("No Provider Keys Configured");
5925
+ console.log(SETUP_HINTS);
5926
+ process.exit(1);
5927
+ }
5928
+ anyKey = true;
5060
5929
  }
5061
5930
  const geminiKey = resolved.get("Gemini")?.key;
5062
5931
  if (geminiKey) {
@@ -5079,9 +5948,11 @@ function registerCheckCommand(program2) {
5079
5948
  console.log(
5080
5949
  " Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
5081
5950
  );
5951
+ } else if (resolved.get("OpenAI")?.key) {
5952
+ console.log(" OpenAI API setup available (images, TTS, STT)");
5082
5953
  } 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");
5954
+ console.log(" Image generation via configured providers or experimental Codex driver");
5955
+ console.log(" Add API keys for full provider features");
5085
5956
  }
5086
5957
  } else {
5087
5958
  logger.error("One or more checks failed \u2014 see above");
@@ -5092,8 +5963,8 @@ function registerCheckCommand(program2) {
5092
5963
 
5093
5964
  // src/commands/doc/convert.ts
5094
5965
  init_errors();
5095
- import fs32 from "fs";
5096
- import path37 from "path";
5966
+ import fs37 from "fs";
5967
+ import path42 from "path";
5097
5968
 
5098
5969
  // src/commands/doc/default-prompt.ts
5099
5970
  var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
@@ -5114,18 +5985,18 @@ async function convertDocuments(opts) {
5114
5985
  const model = opts.model ?? DOC_MODEL_DEFAULT;
5115
5986
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
5116
5987
  for (const f of inputs) {
5117
- if (!fs32.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
5988
+ if (!fs37.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
5118
5989
  }
5119
5990
  let outputPath;
5120
5991
  if (output) {
5121
- outputPath = path37.resolve(output);
5992
+ outputPath = path42.resolve(output);
5122
5993
  } 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`);
5994
+ const base = path42.basename(inputs[0], path42.extname(inputs[0]));
5995
+ outputPath = path42.join(getOutputDir(), `${base}-extraction.md`);
5125
5996
  } else {
5126
- outputPath = path37.join(getOutputDir(), "document-extraction.md");
5997
+ outputPath = path42.join(getOutputDir(), "document-extraction.md");
5127
5998
  }
5128
- fs32.mkdirSync(path37.dirname(outputPath), { recursive: true });
5999
+ fs37.mkdirSync(path42.dirname(outputPath), { recursive: true });
5129
6000
  const results = [];
5130
6001
  for (let i = 0; i < inputs.length; i++) {
5131
6002
  const filePath = inputs[i];
@@ -5157,7 +6028,7 @@ async function convertDocuments(opts) {
5157
6028
  if (attempt < 2) {
5158
6029
  const wait = 2 ** attempt * 1e3;
5159
6030
  logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
5160
- await sleep5(wait);
6031
+ await sleep6(wait);
5161
6032
  }
5162
6033
  }
5163
6034
  }
@@ -5176,7 +6047,7 @@ async function convertDocuments(opts) {
5176
6047
  `);
5177
6048
  for (const r of results) {
5178
6049
  lines.push(`
5179
- ## ${path37.basename(r.file)}
6050
+ ## ${path42.basename(r.file)}
5180
6051
  `);
5181
6052
  if (r.status === "success" && r.markdown) {
5182
6053
  lines.push(`${r.markdown}
@@ -5189,7 +6060,7 @@ async function convertDocuments(opts) {
5189
6060
  }
5190
6061
  lines.push("\n---\n");
5191
6062
  }
5192
- fs32.writeFileSync(outputPath, lines.join("\n"), "utf8");
6063
+ fs37.writeFileSync(outputPath, lines.join("\n"), "utf8");
5193
6064
  const succeeded = results.filter((r) => r.status === "success").length;
5194
6065
  console.log(`
5195
6066
  ${"=".repeat(50)}`);
@@ -5205,7 +6076,7 @@ ${"=".repeat(50)}`);
5205
6076
  const failed = results.filter((r) => r.status === "error").length;
5206
6077
  if (failed > 0) process.exit(1);
5207
6078
  }
5208
- function sleep5(ms) {
6079
+ function sleep6(ms) {
5209
6080
  return new Promise((r) => setTimeout(r, ms));
5210
6081
  }
5211
6082
 
@@ -5235,12 +6106,12 @@ function registerDocCommands(program2) {
5235
6106
 
5236
6107
  // src/commands/media/index.ts
5237
6108
  init_errors();
5238
- import fs35 from "fs";
5239
- import path40 from "path";
6109
+ import fs40 from "fs";
6110
+ import path45 from "path";
5240
6111
 
5241
6112
  // src/commands/media/batch.ts
5242
- import fs33 from "fs";
5243
- import path38 from "path";
6113
+ import fs38 from "fs";
6114
+ import path43 from "path";
5244
6115
 
5245
6116
  // src/commands/media/detect-type.ts
5246
6117
  var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
@@ -5259,10 +6130,10 @@ init_errors();
5259
6130
 
5260
6131
  // src/commands/media/ffmpeg-runner.ts
5261
6132
  init_errors();
5262
- import { execa as execa2 } from "execa";
6133
+ import { execa as execa3 } from "execa";
5263
6134
  async function assertFfmpeg() {
5264
6135
  try {
5265
- await execa2("ffmpeg", ["-version"], { reject: true });
6136
+ await execa3("ffmpeg", ["-version"], { reject: true });
5266
6137
  } catch (e) {
5267
6138
  if (e.code === "ENOENT") {
5268
6139
  throw new ValidationError(
@@ -5275,7 +6146,7 @@ async function runFfmpeg(args, opts = {}) {
5275
6146
  const { verbose, logger } = opts;
5276
6147
  logger?.debug(`ffmpeg ${args.join(" ")}`);
5277
6148
  try {
5278
- const result = await execa2("ffmpeg", args, { reject: true, all: true });
6149
+ const result = await execa3("ffmpeg", args, { reject: true, all: true });
5279
6150
  if (verbose && result.stderr) logger?.debug(result.stderr);
5280
6151
  } catch (e) {
5281
6152
  const err2 = e;
@@ -5285,7 +6156,7 @@ async function runFfmpeg(args, opts = {}) {
5285
6156
  }
5286
6157
  async function getMediaInfo(filePath) {
5287
6158
  try {
5288
- const result = await execa2(
6159
+ const result = await execa3(
5289
6160
  "ffprobe",
5290
6161
  ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath],
5291
6162
  { reject: true }
@@ -5348,10 +6219,10 @@ async function optimizeAudio(opts) {
5348
6219
 
5349
6220
  // src/commands/media/magick-runner.ts
5350
6221
  init_errors();
5351
- import { execa as execa3 } from "execa";
6222
+ import { execa as execa4 } from "execa";
5352
6223
  async function assertMagick() {
5353
6224
  try {
5354
- await execa3("magick", ["-version"], { reject: true });
6225
+ await execa4("magick", ["-version"], { reject: true });
5355
6226
  } catch (e) {
5356
6227
  if (e.code === "ENOENT") {
5357
6228
  throw new ValidationError(
@@ -5364,7 +6235,7 @@ async function runMagick(args, opts = {}) {
5364
6235
  const { verbose, logger } = opts;
5365
6236
  logger?.debug(`magick ${args.join(" ")}`);
5366
6237
  try {
5367
- const result = await execa3("magick", args, { reject: true, all: true });
6238
+ const result = await execa4("magick", args, { reject: true, all: true });
5368
6239
  if (verbose && result.stderr) logger?.debug(result.stderr);
5369
6240
  } catch (e) {
5370
6241
  const err2 = e;
@@ -5436,10 +6307,10 @@ async function batchOptimize(opts) {
5436
6307
  verbose,
5437
6308
  logger
5438
6309
  } = opts;
5439
- fs33.mkdirSync(outputDir, { recursive: true });
5440
- const entries = fs33.readdirSync(inputDir);
6310
+ fs38.mkdirSync(outputDir, { recursive: true });
6311
+ const entries = fs38.readdirSync(inputDir);
5441
6312
  const mediaFiles = entries.filter((f) => {
5442
- const ext = path38.extname(f).toLowerCase();
6313
+ const ext = path43.extname(f).toLowerCase();
5443
6314
  return ALL_EXTS.has(ext);
5444
6315
  });
5445
6316
  if (mediaFiles.length === 0) {
@@ -5450,9 +6321,9 @@ async function batchOptimize(opts) {
5450
6321
  let succeeded = 0;
5451
6322
  const errors = [];
5452
6323
  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();
6324
+ const inputPath = path43.join(inputDir, file);
6325
+ const outputPath = path43.join(outputDir, file);
6326
+ const ext = path43.extname(file).toLowerCase();
5456
6327
  const kind = extToKind(ext);
5457
6328
  logger?.debug(`Processing: ${file}`);
5458
6329
  try {
@@ -5489,8 +6360,8 @@ async function batchOptimize(opts) {
5489
6360
 
5490
6361
  // src/commands/media/split-video.ts
5491
6362
  init_errors();
5492
- import fs34 from "fs";
5493
- import path39 from "path";
6363
+ import fs39 from "fs";
6364
+ import path44 from "path";
5494
6365
  async function splitVideo(opts) {
5495
6366
  await assertFfmpeg();
5496
6367
  const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
@@ -5501,10 +6372,10 @@ async function splitVideo(opts) {
5501
6372
  logger?.info("Video is short enough \u2014 no splitting needed");
5502
6373
  return [input];
5503
6374
  }
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}`);
6375
+ fs39.mkdirSync(outputDir, { recursive: true });
6376
+ const ext = path44.extname(input) || ".mp4";
6377
+ const baseName = path44.basename(input, ext);
6378
+ const outputPattern = path44.join(outputDir, `${baseName}_chunk_%03d${ext}`);
5508
6379
  logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
5509
6380
  const args = [
5510
6381
  "-i",
@@ -5523,8 +6394,8 @@ async function splitVideo(opts) {
5523
6394
  await runFfmpeg(args, { verbose, logger });
5524
6395
  const files = [];
5525
6396
  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);
6397
+ const fname = path44.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
6398
+ if (fs39.existsSync(fname)) files.push(fname);
5528
6399
  }
5529
6400
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
5530
6401
  return files;
@@ -5538,9 +6409,9 @@ function registerMediaCommands(program2) {
5538
6409
  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
6410
  async (opts) => {
5540
6411
  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();
6412
+ if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
6413
+ fs40.mkdirSync(path45.dirname(path45.resolve(opts.output)), { recursive: true });
6414
+ const ext = path45.extname(opts.input).toLowerCase();
5544
6415
  const kind = extToKind(ext);
5545
6416
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
5546
6417
  const quality = Number.parseInt(opts.quality, 10);
@@ -5580,7 +6451,7 @@ function registerMediaCommands(program2) {
5580
6451
  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
6452
  async (opts) => {
5582
6453
  const logger = createLogger({ verbose: opts.verbose ?? false });
5583
- if (!fs35.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
6454
+ if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
5584
6455
  const chunks = await splitVideo({
5585
6456
  input: opts.input,
5586
6457
  outputDir: opts.outputDir,
@@ -5596,7 +6467,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
5596
6467
  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
6468
  async (opts) => {
5598
6469
  const logger = createLogger({ verbose: opts.verbose ?? false });
5599
- if (!fs35.existsSync(opts.inputDir))
6470
+ if (!fs40.existsSync(opts.inputDir))
5600
6471
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
5601
6472
  const result = await batchOptimize({
5602
6473
  inputDir: opts.inputDir,
@@ -5622,6 +6493,7 @@ function registerCommands(program2) {
5622
6493
  registerCheckCommand(program2);
5623
6494
  registerGeminiCommands(program2);
5624
6495
  registerMinimaxCommands(program2);
6496
+ registerOpenAICommands(program2);
5625
6497
  registerOpenRouterCommands(program2);
5626
6498
  registerLeonardoCommands(program2);
5627
6499
  registerBytePlusCommands(program2);