@mrgoonie/multix 0.12.0 → 0.14.0

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
@@ -862,13 +862,13 @@ var AtlasCloudClient = class {
862
862
  }
863
863
  /** Submit a generation job. `body` holds `model` plus the model's own fields, flattened. */
864
864
  async submit(kind, body) {
865
- const path54 = SUBMIT_PATHS[kind];
866
- if (!path54) throw new ValidationError(`Unknown Atlas Cloud media type: ${String(kind)}`);
865
+ const path57 = SUBMIT_PATHS[kind];
866
+ if (!path57) throw new ValidationError(`Unknown Atlas Cloud media type: ${String(kind)}`);
867
867
  if (typeof body.model !== "string" || !body.model.trim()) {
868
868
  throw new ValidationError("Atlas Cloud request body needs a non-empty `model`");
869
869
  }
870
870
  const env = await fetchJson({
871
- url: `${this.baseUrl}${path54}`,
871
+ url: `${this.baseUrl}${path57}`,
872
872
  method: "POST",
873
873
  headers: this.authHeaders(),
874
874
  body,
@@ -1428,14 +1428,14 @@ function detectThumbUrl(obj) {
1428
1428
  }
1429
1429
  return null;
1430
1430
  }
1431
- async function downloadThumbBeside(thumbUrl, videoPath, logger) {
1431
+ async function downloadThumbBeside(thumbUrl, videoPath, logger, download = downloadFile) {
1432
1432
  const match = thumbUrl.match(IMAGE_EXT_RE);
1433
1433
  const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
1434
1434
  const dir = path6.dirname(videoPath);
1435
1435
  const base = path6.basename(videoPath, path6.extname(videoPath));
1436
1436
  const dest = path6.join(dir, `${base}_thumb.${ext}`);
1437
1437
  try {
1438
- await downloadFile(thumbUrl, dest);
1438
+ await download(thumbUrl, dest);
1439
1439
  logger?.success(`Saved thumb ${dest}`);
1440
1440
  return dest;
1441
1441
  } catch (e) {
@@ -1450,7 +1450,7 @@ async function maybeDownloadThumb(source, videoPath, opts = {}) {
1450
1450
  opts.logger?.debug("No thumbnail URL detected in provider response");
1451
1451
  return null;
1452
1452
  }
1453
- const saved = await downloadThumbBeside(url, videoPath, opts.logger);
1453
+ const saved = await downloadThumbBeside(url, videoPath, opts.logger, opts.download);
1454
1454
  if (saved && opts.copyTo) {
1455
1455
  const copyExt = path6.extname(saved);
1456
1456
  const copyBase = path6.basename(opts.copyTo, path6.extname(opts.copyTo));
@@ -2182,9 +2182,9 @@ var BytePlusClient = class {
2182
2182
  }
2183
2183
  apiKey;
2184
2184
  baseUrl;
2185
- async request(path54, opts = {}) {
2185
+ async request(path57, opts = {}) {
2186
2186
  const base = this.baseUrl.replace(/\/$/, "");
2187
- const url = new URL(base + path54);
2187
+ const url = new URL(base + path57);
2188
2188
  if (opts.query) {
2189
2189
  for (const [k, v] of Object.entries(opts.query)) {
2190
2190
  if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
@@ -2202,14 +2202,14 @@ var BytePlusClient = class {
2202
2202
  timeoutMs: opts.timeoutMs ?? 12e4
2203
2203
  });
2204
2204
  }
2205
- get(path54, query, logger) {
2206
- return this.request(path54, { method: "GET", query, logger });
2205
+ get(path57, query, logger) {
2206
+ return this.request(path57, { method: "GET", query, logger });
2207
2207
  }
2208
- post(path54, body, logger) {
2209
- return this.request(path54, { method: "POST", body, logger });
2208
+ post(path57, body, logger) {
2209
+ return this.request(path57, { method: "POST", body, logger });
2210
2210
  }
2211
- delete(path54, logger) {
2212
- return this.request(path54, { method: "DELETE", logger });
2211
+ delete(path57, logger) {
2212
+ return this.request(path57, { method: "DELETE", logger });
2213
2213
  }
2214
2214
  };
2215
2215
  function createBytePlusClient() {
@@ -2678,10 +2678,10 @@ function toRef(kind, resolved, role) {
2678
2678
  return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
2679
2679
  }
2680
2680
  async function resolveRefSpec(kind, spec, logger) {
2681
- const { path: path54, role } = parseRefSpec(spec);
2682
- if (kind === "image") return toRef(kind, await resolveImageInput(path54, logger), role);
2683
- if (kind === "video") return toRef(kind, await resolveVideoInput(path54, logger), role);
2684
- return toRef(kind, await resolveAudioInput(path54, logger), role);
2681
+ const { path: path57, role } = parseRefSpec(spec);
2682
+ if (kind === "image") return toRef(kind, await resolveImageInput(path57, logger), role);
2683
+ if (kind === "video") return toRef(kind, await resolveVideoInput(path57, logger), role);
2684
+ return toRef(kind, await resolveAudioInput(path57, logger), role);
2685
2685
  }
2686
2686
  async function resolveRefSpecs(specs, logger) {
2687
2687
  const out = [];
@@ -4108,9 +4108,9 @@ var FalClient = class {
4108
4108
  apiKey;
4109
4109
  baseUrl;
4110
4110
  /** `path` is appended verbatim to the base queue URL, e.g. "/fal-ai/flux/schnell". */
4111
- async request(path54, opts = {}) {
4111
+ async request(path57, opts = {}) {
4112
4112
  const base = this.baseUrl.replace(/\/$/, "");
4113
- const suffix = path54.startsWith("/") ? path54 : `/${path54}`;
4113
+ const suffix = path57.startsWith("/") ? path57 : `/${path57}`;
4114
4114
  return this.requestAbsolute(base + suffix, opts);
4115
4115
  }
4116
4116
  /** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
@@ -4127,11 +4127,11 @@ var FalClient = class {
4127
4127
  timeoutMs: opts.timeoutMs ?? 12e4
4128
4128
  });
4129
4129
  }
4130
- get(path54, logger) {
4131
- return this.request(path54, { method: "GET", logger });
4130
+ get(path57, logger) {
4131
+ return this.request(path57, { method: "GET", logger });
4132
4132
  }
4133
- post(path54, body, logger) {
4134
- return this.request(path54, { method: "POST", body, logger });
4133
+ post(path57, body, logger) {
4134
+ return this.request(path57, { method: "POST", body, logger });
4135
4135
  }
4136
4136
  getAbsolute(url, logger) {
4137
4137
  return this.requestAbsolute(url, { method: "GET", logger });
@@ -4611,11 +4611,15 @@ function trackTempDir(dir) {
4611
4611
  for (const s of SIGNALS) process.once(s, onSignal);
4612
4612
  return dispose;
4613
4613
  }
4614
+ var DEFAULT_FLAG = "--files";
4615
+ var DEFAULT_EXAMPLE = "cat clip.mp3 | multix gemini transcribe --files -";
4616
+ var DEFAULT_EXT_EXAMPLE = "mp3";
4614
4617
  async function stdinToTempFile(opts = {}) {
4615
4618
  const input = opts.input ?? process.stdin;
4619
+ const flag = opts.flag ?? DEFAULT_FLAG;
4616
4620
  if (input.isTTY) {
4617
4621
  throw new ValidationError(
4618
- "`--files -` reads piped input, but stdin is a terminal. Example: cat clip.mp3 | multix gemini transcribe --files -"
4622
+ `\`${flag} -\` reads piped input, but stdin is a terminal. Example: ${opts.example ?? DEFAULT_EXAMPLE}`
4619
4623
  );
4620
4624
  }
4621
4625
  const forcedExt = opts.ext ? normalizeExt(opts.ext) : void 0;
@@ -4624,7 +4628,8 @@ async function stdinToTempFile(opts = {}) {
4624
4628
  const cleanup = trackTempDir(dir);
4625
4629
  const rawPath = path23.join(dir, "input");
4626
4630
  const hint = setTimeout(() => {
4627
- process.stderr.write("Waiting for data on stdin (`--files -`)... press Ctrl+C to cancel\n");
4631
+ process.stderr.write(`Waiting for data on stdin (\`${flag} -\`)... press Ctrl+C to cancel
4632
+ `);
4628
4633
  }, WAIT_HINT_MS);
4629
4634
  hint.unref?.();
4630
4635
  let head = Buffer.alloc(0);
@@ -4651,7 +4656,7 @@ async function stdinToTempFile(opts = {}) {
4651
4656
  clearTimeout(hint);
4652
4657
  fs20.closeSync(fd);
4653
4658
  }
4654
- if (size === 0) throw new ValidationError("`--files -` was given but stdin was empty");
4659
+ if (size === 0) throw new ValidationError(`\`${flag} -\` was given but stdin was empty`);
4655
4660
  const detectedExt = forcedExt ?? detectExtension(head);
4656
4661
  const finalPath = path23.join(dir, `stdin${detectedExt ?? ".bin"}`);
4657
4662
  fs20.renameSync(rawPath, finalPath);
@@ -4665,10 +4670,11 @@ function labelError(message, input) {
4665
4670
  return input.path === input.label ? message : message.split(input.path).join(input.label);
4666
4671
  }
4667
4672
  async function resolveFileInputs(files, opts = {}) {
4673
+ const flag = opts.flag ?? DEFAULT_FLAG;
4668
4674
  const stdinCount = files.filter((f) => f === STDIN_ARG).length;
4669
- if (stdinCount > 1) throw new ValidationError("`-` (stdin) can only be passed once to --files");
4675
+ if (stdinCount > 1) throw new ValidationError(`\`-\` (stdin) can only be passed once to ${flag}`);
4670
4676
  if (opts.stdinExt && stdinCount === 0) {
4671
- throw new ValidationError("--stdin-ext only applies when --files includes `-`");
4677
+ throw new ValidationError(`--stdin-ext only applies when ${flag} includes \`-\``);
4672
4678
  }
4673
4679
  for (const f of files) {
4674
4680
  if (f !== STDIN_ARG && !fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
@@ -4680,12 +4686,14 @@ async function resolveFileInputs(files, opts = {}) {
4680
4686
  const stdin = await stdinToTempFile({
4681
4687
  input: opts.input,
4682
4688
  ext: opts.stdinExt,
4683
- maxBytes: opts.maxBytes
4689
+ maxBytes: opts.maxBytes,
4690
+ flag: opts.flag,
4691
+ example: opts.example
4684
4692
  });
4685
4693
  if (!stdin.detected) {
4686
4694
  stdin.cleanup();
4687
4695
  throw new ValidationError(
4688
- "Could not detect the type of piped stdin; pass --stdin-ext (e.g. --stdin-ext mp3)"
4696
+ `Could not detect the type of piped stdin; pass --stdin-ext (e.g. --stdin-ext ${opts.extExample ?? DEFAULT_EXT_EXAMPLE})`
4689
4697
  );
4690
4698
  }
4691
4699
  return {
@@ -5933,40 +5941,1961 @@ function registerTranscribeCommand2(parent) {
5933
5941
  response: extractText(resp)
5934
5942
  });
5935
5943
  } catch (e) {
5936
- const msg = labelError(
5937
- e instanceof Error ? e.message : String(e),
5938
- inputs[i]
5939
- );
5940
- logger.error(`Failed: ${label} \u2014 ${msg}`);
5941
- results.push({ file: label, status: "error", error: msg });
5944
+ const msg = labelError(
5945
+ e instanceof Error ? e.message : String(e),
5946
+ inputs[i]
5947
+ );
5948
+ logger.error(`Failed: ${label} \u2014 ${msg}`);
5949
+ results.push({ file: label, status: "error", error: msg });
5950
+ }
5951
+ }
5952
+ formatResults(results, opts.format, opts.output);
5953
+ cleanup();
5954
+ const failed = results.filter((r) => r.status === "error").length;
5955
+ if (failed > 0) process.exit(1);
5956
+ }
5957
+ );
5958
+ }
5959
+
5960
+ // src/providers/gemini/commands/index.ts
5961
+ function registerGeminiCommands(program2) {
5962
+ const gemini = program2.command("gemini").description(
5963
+ "Gemini: analyze, transcribe, extract data, generate images (Imagen/Nano Banana), video (Veo), and speech (Flash TTS)"
5964
+ );
5965
+ registerAnalyzeCommand(gemini);
5966
+ registerTranscribeCommand2(gemini);
5967
+ registerExtractCommand(gemini);
5968
+ registerGenerateCommand(gemini);
5969
+ registerGeminiImageToImageCommand(gemini);
5970
+ registerGenerateVideoCommand(gemini);
5971
+ registerGeminiImageToVideoCommand(gemini);
5972
+ registerGeminiGenerateSpeechCommand(gemini);
5973
+ }
5974
+
5975
+ // src/providers/heygen/models.ts
5976
+ var DEFAULT_HEYGEN_BASE_URL = "https://api.heygen.com";
5977
+ var HEYGEN_API_SETTINGS_URL = "https://app.heygen.com/developers/api";
5978
+ var HEYGEN_USAGE_URL = "https://app.heygen.com/developers/usage";
5979
+ var AVATAR_VIDEO_RESOLUTIONS = ["4k", "1080p", "720p"];
5980
+ var AVATAR_VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "4:5", "5:4", "1:1", "auto"];
5981
+ var AVATAR_ENGINES = ["avatar_iv", "avatar_v", "avatar_iii"];
5982
+ var EXPRESSIVENESS_LEVELS = ["high", "medium", "low"];
5983
+ var AVATAR_TYPES = ["studio_avatar", "digital_twin", "photo_avatar"];
5984
+ var OWNERSHIP_VALUES = ["public", "private"];
5985
+ var AGENT_ORIENTATIONS = ["landscape", "portrait"];
5986
+ var QUALITY_MODES = ["speed", "precision"];
5987
+ var SPEECH_ENGINES = ["starfish", "orca", "elevenlabs"];
5988
+ var HEYGEN_VIDEO_MODEL = "heygen-video-1";
5989
+ var MODEL_VIDEO_MODES = ["text_to_video", "image_to_video", "reference_to_video"];
5990
+ var MODEL_VIDEO_RESOLUTIONS = ["480p", "768p", "1080p", "2k"];
5991
+ var MODEL_VIDEO_ASPECT_RATIOS = [
5992
+ "21:9",
5993
+ "16:9",
5994
+ "4:3",
5995
+ "1:1",
5996
+ "3:4",
5997
+ "9:16",
5998
+ "adaptive"
5999
+ ];
6000
+ var MODEL_VIDEO_PROMPT_ENHANCEMENT = ["turbo", "quality", "disabled"];
6001
+ var LIMITS = {
6002
+ agentPromptMax: 1e4,
6003
+ agentFilesMax: 20,
6004
+ scriptMax: 5e3,
6005
+ speechTextMax: 5e3,
6006
+ /** POST /v3/assets single-request upload cap (32 MB). */
6007
+ assetUploadMaxBytes: 32 * 1024 * 1024,
6008
+ modelVideoPromptMax: 32e3,
6009
+ modelVideoMinDuration: 5,
6010
+ modelVideoMaxDuration: 15,
6011
+ modelVideoReferenceImagesMax: 9,
6012
+ modelVideoReferenceVideosMax: 3,
6013
+ modelVideoReferenceAudioMax: 3,
6014
+ /** Combined cap across reference images, videos and audio. */
6015
+ modelVideoReferenceTotalMax: 12,
6016
+ avatarListMax: 50,
6017
+ voiceListMax: 100,
6018
+ stylesListMax: 100
6019
+ };
6020
+ var ASSET_MIME_BY_EXT = {
6021
+ png: "image/png",
6022
+ jpg: "image/jpeg",
6023
+ jpeg: "image/jpeg",
6024
+ mp4: "video/mp4",
6025
+ webm: "video/webm",
6026
+ mp3: "audio/mpeg",
6027
+ wav: "audio/wav",
6028
+ pdf: "application/pdf",
6029
+ srt: "application/x-subrip"
6030
+ };
6031
+ var JOB_KINDS = [
6032
+ "agent",
6033
+ "video",
6034
+ "translation",
6035
+ "lipsync",
6036
+ "model-video"
6037
+ ];
6038
+ var HEYGEN_OPERATIONS = {
6039
+ "video-agent": {
6040
+ description: "POST /v3/video-agents (prompt-to-video session)",
6041
+ path: "/v3/video-agents",
6042
+ kind: "agent",
6043
+ idField: "session_id",
6044
+ idempotent: false
6045
+ },
6046
+ video: {
6047
+ description: "POST /v3/videos (avatar | image | cinematic_avatar | studio)",
6048
+ path: "/v3/videos",
6049
+ kind: "video",
6050
+ idField: "video_id",
6051
+ idempotent: true
6052
+ },
6053
+ "video-translation": {
6054
+ description: "POST /v3/video-translations",
6055
+ path: "/v3/video-translations",
6056
+ kind: "translation",
6057
+ idsField: "video_translation_ids",
6058
+ idempotent: true
6059
+ },
6060
+ lipsync: {
6061
+ description: "POST /v3/lipsyncs",
6062
+ path: "/v3/lipsyncs",
6063
+ kind: "lipsync",
6064
+ idField: "lipsync_id",
6065
+ idempotent: true
6066
+ },
6067
+ "model-video": {
6068
+ description: "POST /v3/models/videos (HeyGen Video model)",
6069
+ path: "/v3/models/videos",
6070
+ kind: "model-video",
6071
+ idField: "video_id",
6072
+ idempotent: true
6073
+ },
6074
+ speech: {
6075
+ description: "POST /v3/voices/speech (synchronous text-to-speech)",
6076
+ path: "/v3/voices/speech",
6077
+ kind: null,
6078
+ idempotent: false
6079
+ }
6080
+ };
6081
+
6082
+ // src/providers/heygen/node-helpers.ts
6083
+ import { once as once2 } from "events";
6084
+ import fs29 from "fs";
6085
+ import path32 from "path";
6086
+ import { pipeline as pipeline2 } from "stream/promises";
6087
+ init_errors();
6088
+ init_redact();
6089
+
6090
+ // src/providers/heygen/client.ts
6091
+ init_errors();
6092
+ init_redact();
6093
+
6094
+ // src/providers/heygen/heygen-errors.ts
6095
+ init_errors();
6096
+ var HeygenApiError = class extends HttpError {
6097
+ constructor(status, snippet, url, retryAfterMs, apiCode, apiMessage, param) {
6098
+ super(status, snippet, url, retryAfterMs);
6099
+ this.apiCode = apiCode;
6100
+ this.apiMessage = apiMessage;
6101
+ this.param = param;
6102
+ this.name = "HeygenApiError";
6103
+ this.message = describeHeygenError(status, apiCode, apiMessage, param, retryAfterMs);
6104
+ }
6105
+ apiCode;
6106
+ apiMessage;
6107
+ param;
6108
+ };
6109
+ var HeygenJobFailedError = class extends ProviderError {
6110
+ constructor(message, failureCode) {
6111
+ super(message, "heygen");
6112
+ this.failureCode = failureCode;
6113
+ this.name = "HeygenJobFailedError";
6114
+ }
6115
+ failureCode;
6116
+ };
6117
+ function parseHeygenErrorBody(snippet) {
6118
+ try {
6119
+ const parsed = JSON.parse(snippet);
6120
+ if (parsed && typeof parsed.error === "object" && parsed.error !== null) return parsed.error;
6121
+ if (parsed && typeof parsed.error === "string") return { message: parsed.error };
6122
+ } catch {
6123
+ }
6124
+ const field = (name) => snippet.match(new RegExp(`"${name}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`))?.[1];
6125
+ const out = {};
6126
+ const code = field("code");
6127
+ const message = field("message");
6128
+ const param = field("param");
6129
+ if (code) out.code = code;
6130
+ if (message) out.message = message;
6131
+ if (param) out.param = param;
6132
+ return out;
6133
+ }
6134
+ function toHeygenApiError(error) {
6135
+ if (error instanceof HeygenApiError) return error;
6136
+ const body = error.status === 0 ? {} : parseHeygenErrorBody(error.snippet);
6137
+ return new HeygenApiError(
6138
+ error.status,
6139
+ error.snippet,
6140
+ error.url,
6141
+ error.retryAfterMs,
6142
+ body.code,
6143
+ body.message ?? (error.status === 0 ? error.snippet : void 0),
6144
+ body.param ?? void 0
6145
+ );
6146
+ }
6147
+ function hintFor(status, code) {
6148
+ if (status === 0) return "Network error reaching HeyGen \u2014 check connectivity and retry.";
6149
+ if (status === 401)
6150
+ return `Check HEYGEN_API_KEY (sent as the X-Api-Key header) or create a new key at ${HEYGEN_API_SETTINGS_URL}.`;
6151
+ if (status === 402)
6152
+ return `Add API credits or upgrade the API plan at ${HEYGEN_USAGE_URL}. HeyGen API usage is billed separately from web/app subscriptions.`;
6153
+ if (status === 403) {
6154
+ if (code === "insufficient_api_key_scope")
6155
+ return "The API key lacks the scope this operation needs \u2014 create a key with the required scopes.";
6156
+ return "The API key, workspace, or plan is not entitled to this feature (some avatar and digital-twin features require Enterprise access).";
6157
+ }
6158
+ if (status === 404) return "Check the ID. For `heygen status`, make sure --type matches the ID.";
6159
+ if (status === 409 && code === "request_in_progress")
6160
+ return "An identical request is still being processed \u2014 wait and retry.";
6161
+ if (status === 409) return "The resource is not ready or conflicts with existing state.";
6162
+ if (status === 400 || status === 422) return "Fix the request parameters and retry.";
6163
+ if (status === 429)
6164
+ return code === "quota_exceeded" ? `Usage quota exceeded \u2014 see ${HEYGEN_USAGE_URL}.` : "Rate or concurrency limit reached \u2014 wait and retry, or reduce parallel jobs.";
6165
+ if (status >= 500) return "HeyGen server error \u2014 retry later.";
6166
+ return "";
6167
+ }
6168
+ function describeHeygenError(status, code, message, param, retryAfterMs) {
6169
+ const label = status === 0 ? "HeyGen request failed" : `HeyGen API error ${status}`;
6170
+ const codePart = code ? ` (${code})` : "";
6171
+ const msgPart = message ? `: ${message}` : "";
6172
+ const paramPart = param ? ` (param: ${param})` : "";
6173
+ const retryPart = retryAfterMs !== void 0 ? ` Retry after ${Math.ceil(retryAfterMs / 1e3)}s.` : "";
6174
+ const hint = hintFor(status, code);
6175
+ const base = `${label}${codePart}${msgPart}${paramPart}`;
6176
+ const sentence = /[.!?]$/.test(base) ? base : `${base}.`;
6177
+ return `${sentence}${retryPart}${hint ? ` ${hint}` : ""}`;
6178
+ }
6179
+
6180
+ // src/providers/heygen/client.ts
6181
+ var MAX_RETRY_WAIT_MS = 6e4;
6182
+ var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6183
+ function seg(id, label) {
6184
+ if (typeof id !== "string") throw new ValidationError(`${label} must be a string`);
6185
+ const trimmed = id.trim();
6186
+ if (!trimmed) throw new ValidationError(`${label} must not be empty`);
6187
+ if (trimmed === "." || trimmed === "..") {
6188
+ throw new ValidationError(`Invalid ${label}: "${trimmed}"`);
6189
+ }
6190
+ return encodeURIComponent(trimmed);
6191
+ }
6192
+ function describePayload(data) {
6193
+ const text = JSON.stringify(
6194
+ data,
6195
+ (_k, v) => typeof v === "string" && /^https?:\/\//i.test(v) ? redactUrl(v) : v
6196
+ );
6197
+ return (text ?? String(data)).slice(0, 500);
6198
+ }
6199
+ function requireJobId(data, field, path57) {
6200
+ const value = data?.[field];
6201
+ const ok2 = Array.isArray(value) ? value.length > 0 && value.every((v) => typeof v === "string" && v.length > 0) : typeof value === "string" && value.length > 0;
6202
+ if (!ok2) {
6203
+ throw new ProviderError(
6204
+ `HeyGen ${path57} returned no ${field}; a job may still have been created, so check the HeyGen dashboard. Response: ${describePayload(data)}`,
6205
+ "heygen"
6206
+ );
6207
+ }
6208
+ return data;
6209
+ }
6210
+ var HeygenClient = class {
6211
+ apiKey;
6212
+ baseUrl;
6213
+ fetchImpl;
6214
+ timeoutMs;
6215
+ maxAttempts;
6216
+ sleep;
6217
+ newIdempotencyKey;
6218
+ constructor(opts) {
6219
+ if (!opts.apiKey?.trim()) throw new ValidationError("HeyGen apiKey is required");
6220
+ this.apiKey = opts.apiKey.trim();
6221
+ this.baseUrl = (opts.baseUrl ?? DEFAULT_HEYGEN_BASE_URL).replace(/\/+$/, "");
6222
+ this.fetchImpl = opts.fetch;
6223
+ this.timeoutMs = opts.timeoutMs ?? 12e4;
6224
+ this.maxAttempts = Math.max(1, opts.maxAttempts ?? 3);
6225
+ this.sleep = opts.sleep ?? defaultSleep2;
6226
+ this.newIdempotencyKey = opts.newIdempotencyKey ?? (() => crypto.randomUUID());
6227
+ }
6228
+ /** Build an absolute URL for a v3 path plus query parameters. */
6229
+ url(path57, query) {
6230
+ const suffix = path57.startsWith("/") ? path57 : `/${path57}`;
6231
+ const params = new URLSearchParams();
6232
+ for (const [k, v] of Object.entries(query ?? {})) {
6233
+ if (v !== void 0 && v !== "") params.set(k, String(v));
6234
+ }
6235
+ const qs = params.toString();
6236
+ return `${this.baseUrl}${suffix}${qs ? `?${qs}` : ""}`;
6237
+ }
6238
+ headers(idempotencyKey) {
6239
+ return {
6240
+ Accept: "application/json",
6241
+ "X-Api-Key": this.apiKey,
6242
+ ...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
6243
+ };
6244
+ }
6245
+ /**
6246
+ * Send a request and return the raw parsed JSON (envelope included).
6247
+ * Retries are bounded and only where safe: a 429 carrying Retry-After was
6248
+ * rejected before processing; 5xx/network failures and 409
6249
+ * `request_in_progress` are retried only when an Idempotency-Key makes the
6250
+ * replay safe. GET requests are left to the caller's polling loop.
6251
+ */
6252
+ async send(path57, opts = {}) {
6253
+ const method = opts.method ?? "GET";
6254
+ const key = typeof opts.idempotencyKey === "string" ? opts.idempotencyKey : opts.idempotencyKey ? this.newIdempotencyKey() : void 0;
6255
+ const url = this.url(path57, opts.query);
6256
+ for (let attempt = 1; ; attempt++) {
6257
+ try {
6258
+ return await fetchJson({
6259
+ url,
6260
+ method,
6261
+ headers: this.headers(key),
6262
+ body: opts.body,
6263
+ timeoutMs: Math.max(1, Math.min(this.timeoutMs, opts.timeoutMs ?? this.timeoutMs)),
6264
+ fetch: this.fetchImpl
6265
+ });
6266
+ } catch (error) {
6267
+ const apiError = error instanceof HttpError ? toHeygenApiError(error) : error;
6268
+ if (method === "GET" || attempt >= this.maxAttempts || !(apiError instanceof HttpError)) {
6269
+ throw apiError;
6270
+ }
6271
+ const wait = this.retryDelay(apiError, key, attempt);
6272
+ if (wait === void 0) throw apiError;
6273
+ await this.sleep(wait);
6274
+ }
6275
+ }
6276
+ }
6277
+ retryDelay(error, key, attempt) {
6278
+ const code = error instanceof HeygenApiError ? error.apiCode : void 0;
6279
+ if (error.status === 429 && error.retryAfterMs !== void 0 && code !== "quota_exceeded") {
6280
+ return Math.min(error.retryAfterMs, MAX_RETRY_WAIT_MS);
6281
+ }
6282
+ if (!key) return void 0;
6283
+ if (error.status === 0 || error.status >= 500 || code === "request_in_progress") {
6284
+ return Math.min(1e3 * 2 ** (attempt - 1), MAX_RETRY_WAIT_MS);
6285
+ }
6286
+ return void 0;
6287
+ }
6288
+ /** Send and unwrap `{ data }`. A 2xx body carrying `error` is treated as a failure. */
6289
+ async request(path57, opts = {}) {
6290
+ const json = await this.send(path57, opts);
6291
+ return unwrapData(json, path57);
6292
+ }
6293
+ async page(path57, query) {
6294
+ const json = await this.send(path57, {
6295
+ query
6296
+ });
6297
+ const items = unwrapData(json, path57);
6298
+ if (!Array.isArray(items)) {
6299
+ throw new ProviderError(
6300
+ `Unexpected HeyGen response from ${path57}: data is not a list`,
6301
+ "heygen"
6302
+ );
6303
+ }
6304
+ const next = json.next_token;
6305
+ return {
6306
+ items,
6307
+ hasMore: json.has_more === true,
6308
+ nextToken: typeof next === "string" && next ? next : null
6309
+ };
6310
+ }
6311
+ // ── Account ───────────────────────────────────────────────────────────
6312
+ getCurrentUser() {
6313
+ return this.request("/v3/users/me");
6314
+ }
6315
+ // ── Video Agent ───────────────────────────────────────────────────────
6316
+ /** POST /v3/video-agents. No Idempotency-Key: the endpoint does not document one. */
6317
+ async createVideoAgent(body) {
6318
+ const path57 = "/v3/video-agents";
6319
+ const data = await this.request(path57, { method: "POST", body });
6320
+ return requireJobId(data, "session_id", path57);
6321
+ }
6322
+ async getVideoAgentSession(sessionId, opts = {}) {
6323
+ return this.request(`/v3/video-agents/${seg(sessionId, "session id")}`, opts);
6324
+ }
6325
+ listVideoAgentStyles(query = {}) {
6326
+ return this.page("/v3/video-agents/styles", { ...query });
6327
+ }
6328
+ // ── Videos ────────────────────────────────────────────────────────────
6329
+ async createVideo(body, idempotencyKey = true) {
6330
+ const path57 = "/v3/videos";
6331
+ const data = await this.request(path57, {
6332
+ method: "POST",
6333
+ body,
6334
+ idempotencyKey
6335
+ });
6336
+ return requireJobId(data, "video_id", path57);
6337
+ }
6338
+ async getVideo(videoId, opts = {}) {
6339
+ return this.request(`/v3/videos/${seg(videoId, "video id")}`, opts);
6340
+ }
6341
+ /** POST /v3/templates/{id}. Returns the created video's `VideoDetail`. */
6342
+ async generateFromTemplate(templateId, body, idempotencyKey = true) {
6343
+ const path57 = `/v3/templates/${seg(templateId, "template id")}`;
6344
+ const data = await this.request(path57, {
6345
+ method: "POST",
6346
+ body,
6347
+ idempotencyKey
6348
+ });
6349
+ return requireJobId(data, "id", path57);
6350
+ }
6351
+ // ── HeyGen Video model ────────────────────────────────────────────────
6352
+ async createModelVideo(body, idempotencyKey = true) {
6353
+ const path57 = "/v3/models/videos";
6354
+ const data = await this.request(path57, {
6355
+ method: "POST",
6356
+ body,
6357
+ idempotencyKey
6358
+ });
6359
+ return requireJobId(data, "video_id", path57);
6360
+ }
6361
+ async getModelVideo(videoId, opts = {}) {
6362
+ return this.request(`/v3/models/videos/${seg(videoId, "video id")}`, opts);
6363
+ }
6364
+ // ── Catalog ───────────────────────────────────────────────────────────
6365
+ listAvatarGroups(query = {}) {
6366
+ return this.page("/v3/avatars", { ...query });
6367
+ }
6368
+ /** Looks are what video generation needs: a look `id` is the `avatar_id`. */
6369
+ listAvatarLooks(query = {}) {
6370
+ return this.page("/v3/avatars/looks", { ...query });
6371
+ }
6372
+ listVoices(query = {}) {
6373
+ return this.page("/v3/voices", { ...query });
6374
+ }
6375
+ // ── Speech ────────────────────────────────────────────────────────────
6376
+ createSpeech(body) {
6377
+ return this.request("/v3/voices/speech", { method: "POST", body });
6378
+ }
6379
+ // ── Translation & lipsync ─────────────────────────────────────────────
6380
+ async createVideoTranslation(body, idempotencyKey = true) {
6381
+ const path57 = "/v3/video-translations";
6382
+ const data = await this.request(path57, {
6383
+ method: "POST",
6384
+ body,
6385
+ idempotencyKey
6386
+ });
6387
+ return requireJobId(data, "video_translation_ids", path57);
6388
+ }
6389
+ async getVideoTranslation(id, opts = {}) {
6390
+ return this.request(`/v3/video-translations/${seg(id, "video translation id")}`, opts);
6391
+ }
6392
+ async listTranslationLanguages() {
6393
+ const data = await this.request("/v3/video-translations/languages");
6394
+ if (!Array.isArray(data.languages)) {
6395
+ throw new ProviderError("Unexpected HeyGen response: languages is not a list", "heygen");
6396
+ }
6397
+ return data.languages.filter((l) => typeof l === "string");
6398
+ }
6399
+ async createLipsync(body, idempotencyKey = true) {
6400
+ const path57 = "/v3/lipsyncs";
6401
+ const data = await this.request(path57, {
6402
+ method: "POST",
6403
+ body,
6404
+ idempotencyKey
6405
+ });
6406
+ return requireJobId(data, "lipsync_id", path57);
6407
+ }
6408
+ async getLipsync(id, opts = {}) {
6409
+ return this.request(`/v3/lipsyncs/${seg(id, "lipsync id")}`, opts);
6410
+ }
6411
+ // ── Assets ────────────────────────────────────────────────────────────
6412
+ /**
6413
+ * POST /v3/assets (multipart, max 32 MB). Accepts bytes or a Blob so it works
6414
+ * without a filesystem; for remote files prefer passing `{type:"url"}` inputs.
6415
+ */
6416
+ async uploadAsset(input) {
6417
+ const blob = input.data instanceof Blob ? input.data : new Blob([input.data], { type: input.mimeType });
6418
+ if (blob.size === 0) throw new ValidationError(`Cannot upload empty file ${input.filename}`);
6419
+ if (blob.size > LIMITS.assetUploadMaxBytes) {
6420
+ throw new ValidationError(
6421
+ `${input.filename} is ${(blob.size / 1024 / 1024).toFixed(1)} MB; POST /v3/assets accepts at most 32 MB. Host the file at a public HTTPS URL and pass the URL instead.`
6422
+ );
6423
+ }
6424
+ const typed = blob.type ? blob : new Blob([blob], { type: input.mimeType });
6425
+ const key = this.newIdempotencyKey();
6426
+ const url = this.url("/v3/assets");
6427
+ const doFetch = this.fetchImpl ?? ((u, init) => globalThis.fetch(u, init));
6428
+ for (let attempt = 1; ; attempt++) {
6429
+ const form = new FormData();
6430
+ form.append("file", typed, input.filename);
6431
+ let response;
6432
+ try {
6433
+ response = await doFetch(url, {
6434
+ method: "POST",
6435
+ headers: this.headers(key),
6436
+ body: form,
6437
+ signal: AbortSignal.timeout(this.timeoutMs)
6438
+ });
6439
+ } catch (cause) {
6440
+ const msg = cause instanceof Error ? cause.message : String(cause);
6441
+ const err3 = toHeygenApiError(new HttpError(0, msg, url));
6442
+ if (attempt >= this.maxAttempts) throw err3;
6443
+ await this.sleep(this.retryDelay(err3, key, attempt) ?? 1e3);
6444
+ continue;
6445
+ }
6446
+ if (response.ok) {
6447
+ let json;
6448
+ try {
6449
+ json = await response.json();
6450
+ } catch {
6451
+ throw new ProviderError(
6452
+ `HeyGen /v3/assets returned a non-JSON response (HTTP ${response.status}) for ${input.filename}`,
6453
+ "heygen"
6454
+ );
6455
+ }
6456
+ const data = unwrapData(json, "/v3/assets");
6457
+ return requireJobId(data, "asset_id", "/v3/assets");
6458
+ }
6459
+ const err2 = toHeygenApiError(await httpErrorFromResponse(response, url));
6460
+ const wait = attempt < this.maxAttempts ? this.retryDelay(err2, key, attempt) : void 0;
6461
+ if (wait === void 0) throw err2;
6462
+ await this.sleep(wait);
6463
+ }
6464
+ }
6465
+ };
6466
+ function unwrapData(json, path57) {
6467
+ if (json === null || typeof json !== "object") {
6468
+ throw new ProviderError(`Unexpected HeyGen response from ${path57}: not a JSON object`, "heygen");
6469
+ }
6470
+ const record = json;
6471
+ const error = record.error;
6472
+ if (error && typeof error === "object") {
6473
+ const e = error;
6474
+ throw new ProviderError(
6475
+ `HeyGen API error${e.code ? ` (${e.code})` : ""}: ${e.message ?? "unknown error"}`,
6476
+ "heygen"
6477
+ );
6478
+ }
6479
+ if (!("data" in record) || record.data === null || record.data === void 0) {
6480
+ throw new ProviderError(`Unexpected HeyGen response from ${path57}: missing data`, "heygen");
6481
+ }
6482
+ return record.data;
6483
+ }
6484
+
6485
+ // src/providers/heygen/jobs.ts
6486
+ var str = (v) => typeof v === "string" && v ? v : void 0;
6487
+ function videoState(video, kind = "video", id = video.id, opts = {}) {
6488
+ const done = video.status === "completed" || video.status === "failed";
6489
+ const captioned = str(video.captioned_video_url);
6490
+ const state = {
6491
+ kind,
6492
+ id,
6493
+ status: video.status,
6494
+ done,
6495
+ succeeded: video.status === "completed",
6496
+ videoId: video.id,
6497
+ outputUrl: opts.burnCaptions ? captioned : str(video.video_url) ?? captioned,
6498
+ captionedVideoUrl: captioned,
6499
+ thumbnailUrl: str(video.thumbnail_url),
6500
+ captionUrl: str(video.subtitle_url),
6501
+ failureCode: str(video.failure_code),
6502
+ failureMessage: str(video.failure_message),
6503
+ raw: { video }
6504
+ };
6505
+ if (opts.burnCaptions && state.succeeded && !captioned) {
6506
+ return {
6507
+ ...state,
6508
+ succeeded: false,
6509
+ failureCode: "captioned_video_missing",
6510
+ failureMessage: "Burned-in captions were requested but HeyGen returned no captioned_video_url. The uncaptioned render is available via `heygen status --raw`."
6511
+ };
6512
+ }
6513
+ return state;
6514
+ }
6515
+ function translationState(t) {
6516
+ const audioOnly = !str(t.video_url) && !!str(t.audio_url);
6517
+ return {
6518
+ kind: "translation",
6519
+ id: t.id,
6520
+ status: t.status,
6521
+ done: t.status === "completed" || t.status === "failed",
6522
+ succeeded: t.status === "completed",
6523
+ outputUrl: str(t.video_url) ?? str(t.audio_url),
6524
+ ...audioOnly ? { outputIsAudio: true } : {},
6525
+ captionUrl: str(t.srt_caption_url) ?? str(t.vtt_caption_url),
6526
+ outputLanguage: str(t.output_language),
6527
+ failureMessage: str(t.failure_message),
6528
+ raw: { translation: t }
6529
+ };
6530
+ }
6531
+ function lipsyncState(l) {
6532
+ return {
6533
+ kind: "lipsync",
6534
+ id: l.id,
6535
+ status: l.status,
6536
+ done: l.status === "completed" || l.status === "failed",
6537
+ succeeded: l.status === "completed",
6538
+ outputUrl: str(l.video_url),
6539
+ captionUrl: str(l.caption_url),
6540
+ failureMessage: str(l.failure_message),
6541
+ raw: { lipsync: l }
6542
+ };
6543
+ }
6544
+ function modelVideoState(v) {
6545
+ const done = v.status === "completed" || v.status === "failed" || v.status === "cancelled";
6546
+ return {
6547
+ kind: "model-video",
6548
+ id: v.video_id,
6549
+ status: v.status,
6550
+ done,
6551
+ succeeded: v.status === "completed",
6552
+ videoId: v.video_id,
6553
+ outputUrl: str(v.video_url),
6554
+ failureCode: str(v.failure_code),
6555
+ failureMessage: str(v.failure_message),
6556
+ raw: { video: v }
6557
+ };
6558
+ }
6559
+ function agentState(session, video) {
6560
+ const base = {
6561
+ kind: "agent",
6562
+ id: session.session_id,
6563
+ sessionId: session.session_id,
6564
+ sessionStatus: session.status,
6565
+ progress: session.progress
6566
+ };
6567
+ const sessionError = session.error ?? void 0;
6568
+ if (video) {
6569
+ const v = videoState(video, "agent", session.session_id);
6570
+ if (!v.done && session.status === "failed" && sessionError?.code !== "context_limit_exceeded") {
6571
+ return {
6572
+ ...v,
6573
+ ...base,
6574
+ status: "failed",
6575
+ done: true,
6576
+ succeeded: false,
6577
+ failureCode: sessionError?.code,
6578
+ failureMessage: sessionError?.message ?? "Video Agent session failed",
6579
+ raw: { session, video }
6580
+ };
6581
+ }
6582
+ if (v.done && !v.succeeded && session.status === "waiting_for_input") {
6583
+ return {
6584
+ ...v,
6585
+ ...base,
6586
+ failureCode: v.failureCode ?? "waiting_for_input",
6587
+ failureMessage: v.failureMessage ?? "Video Agent paused for input; continue the session in the HeyGen app or start a new prompt.",
6588
+ raw: { session, video }
6589
+ };
6590
+ }
6591
+ return { ...v, ...base, raw: { session, video } };
6592
+ }
6593
+ const pending2 = { ...base, status: session.status, raw: { session } };
6594
+ if (session.status === "failed") {
6595
+ return {
6596
+ ...pending2,
6597
+ done: true,
6598
+ succeeded: false,
6599
+ failureCode: sessionError?.code,
6600
+ failureMessage: sessionError?.message ?? "Video Agent session failed"
6601
+ };
6602
+ }
6603
+ if (session.status === "waiting_for_input") {
6604
+ return {
6605
+ ...pending2,
6606
+ done: true,
6607
+ succeeded: false,
6608
+ failureCode: "waiting_for_input",
6609
+ failureMessage: "Video Agent paused for input before rendering; continue the session in the HeyGen app or start a new prompt."
6610
+ };
6611
+ }
6612
+ if (session.status === "completed") {
6613
+ return {
6614
+ ...pending2,
6615
+ done: true,
6616
+ succeeded: false,
6617
+ failureCode: "no_video",
6618
+ failureMessage: "Video Agent session completed without a video_id."
6619
+ };
6620
+ }
6621
+ return { ...pending2, done: false, succeeded: false };
6622
+ }
6623
+ function createJobStatusReader(client, kind, id, opts = {}) {
6624
+ return async (ctx) => {
6625
+ const call = { timeoutMs: ctx?.remainingMs };
6626
+ switch (kind) {
6627
+ case "video":
6628
+ return videoState(await client.getVideo(id, call), "video", id, opts);
6629
+ case "translation":
6630
+ return translationState(await client.getVideoTranslation(id, call));
6631
+ case "lipsync":
6632
+ return lipsyncState(await client.getLipsync(id, call));
6633
+ case "model-video":
6634
+ return modelVideoState(await client.getModelVideo(id, call));
6635
+ case "agent": {
6636
+ const session = await client.getVideoAgentSession(id, call);
6637
+ if (!session.video_id) return agentState(session);
6638
+ return agentState(session, await client.getVideo(session.video_id, call));
6639
+ }
6640
+ }
6641
+ };
6642
+ }
6643
+ function getJobState(client, kind, id, opts = {}) {
6644
+ return createJobStatusReader(client, kind, id, opts)();
6645
+ }
6646
+ var DEFAULT_POLL_INTERVAL_MS = 1e4;
6647
+ var STAGGER_STEP_MS = 500;
6648
+ var STAGGER_MAX_MS = 5e3;
6649
+ function staggerOffsetMs(index, intervalMs) {
6650
+ if (!Number.isFinite(index) || index <= 0) return 0;
6651
+ const step = Math.min(STAGGER_STEP_MS, Math.max(0, intervalMs));
6652
+ return Math.min(index * step, STAGGER_MAX_MS);
6653
+ }
6654
+ var defaultSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6655
+ async function waitForJob(client, kind, id, opts) {
6656
+ const { burnCaptions, startDelayMs = 0, ...poll2 } = opts;
6657
+ let pendingDelay = Math.min(Math.max(0, startDelayMs), Math.floor(poll2.timeoutMs / 2));
6658
+ const read = createJobStatusReader(client, kind, id, { burnCaptions });
6659
+ return pollUntil({
6660
+ intervalMs: DEFAULT_POLL_INTERVAL_MS,
6661
+ maxIntervalMs: 3e4,
6662
+ ...poll2,
6663
+ fetchStatus: async (ctx) => {
6664
+ const delay = pendingDelay;
6665
+ if (delay <= 0) return read(ctx);
6666
+ pendingDelay = 0;
6667
+ await (poll2.sleep ?? defaultSleep3)(delay);
6668
+ return read({ remainingMs: Math.max(1, ctx.remainingMs - delay) });
6669
+ },
6670
+ isTerminal: (s) => s.done
6671
+ });
6672
+ }
6673
+ function assertJobSucceeded(state) {
6674
+ if (state.succeeded) return;
6675
+ const label = state.kind === "agent" ? `Video Agent session ${state.id}` : `${state.kind} ${state.id}`;
6676
+ const code = state.failureCode ? ` (${state.failureCode})` : "";
6677
+ const msg = state.failureMessage ? `: ${state.failureMessage}` : "";
6678
+ throw new HeygenJobFailedError(
6679
+ `HeyGen ${label} ended with status "${state.status}"${code}${msg}`,
6680
+ state.failureCode
6681
+ );
6682
+ }
6683
+ function shellArg(value) {
6684
+ return /^[A-Za-z0-9_-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
6685
+ }
6686
+ function resumeCommand2(kind, id, opts = {}) {
6687
+ const captions = opts.burnCaptions ? " --burn-captions" : "";
6688
+ return `multix heygen status ${shellArg(id)} --type ${kind} --wait --download${captions}`;
6689
+ }
6690
+
6691
+ // src/providers/heygen/request-builders.ts
6692
+ init_errors();
6693
+ init_redact();
6694
+ function oneOf(value, allowed, flag) {
6695
+ if (value === void 0) return void 0;
6696
+ if (!allowed.includes(value)) {
6697
+ throw new ValidationError(`${flag} must be one of: ${allowed.join(", ")} (got "${value}")`);
6698
+ }
6699
+ return value;
6700
+ }
6701
+ function numberInRange(value, flag, min, max, integer = false) {
6702
+ if (value === void 0 || value === "") return void 0;
6703
+ const n = typeof value === "number" ? value : Number(value);
6704
+ if (!Number.isFinite(n) || n < min || n > max || integer && !Number.isInteger(n)) {
6705
+ throw new ValidationError(
6706
+ `${flag} must be ${integer ? "an integer" : "a number"} between ${min} and ${max} (got "${value}")`
6707
+ );
6708
+ }
6709
+ return n;
6710
+ }
6711
+ function requireHttps(url, flag) {
6712
+ if (!/^https:\/\//i.test(url)) throw new ValidationError(`${flag} must be an https:// URL`);
6713
+ return url;
6714
+ }
6715
+ function isHttpsUrl(url) {
6716
+ try {
6717
+ return new URL(url).protocol === "https:";
6718
+ } catch {
6719
+ return false;
6720
+ }
6721
+ }
6722
+ var LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
6723
+ function checkBaseUrl(raw, variable) {
6724
+ const value = raw.trim();
6725
+ let url;
6726
+ try {
6727
+ url = new URL(value);
6728
+ } catch {
6729
+ throw new ConfigError(`${variable} must be an absolute https:// URL`);
6730
+ }
6731
+ const loopbackHttp = url.protocol === "http:" && LOOPBACK_HOSTS.has(url.hostname);
6732
+ if (url.protocol !== "https:" && !loopbackHttp) {
6733
+ throw new ConfigError(
6734
+ `${variable} must use https:// (got ${redactUrl(value)}); plain http:// is only allowed for localhost, 127.0.0.1, or [::1]`
6735
+ );
6736
+ }
6737
+ return value;
6738
+ }
6739
+ function classifyMediaRef(value) {
6740
+ const v = value.trim();
6741
+ if (!v) throw new ValidationError("Media reference must not be empty");
6742
+ if (/^https?:\/\//i.test(v)) return { kind: "url", url: requireHttps(v, "Media URL") };
6743
+ const asset = v.match(/^asset(?:_id)?:(.+)$/i);
6744
+ if (asset?.[1]) return { kind: "asset", assetId: asset[1].trim() };
6745
+ return { kind: "path", path: v };
6746
+ }
6747
+ function splitLanguages(raw) {
6748
+ const out = [];
6749
+ let depth = 0;
6750
+ let current = "";
6751
+ for (const ch of raw) {
6752
+ if (ch === "(") depth++;
6753
+ if (ch === ")") depth = Math.max(0, depth - 1);
6754
+ if (ch === "," && depth === 0) {
6755
+ out.push(current);
6756
+ current = "";
6757
+ } else {
6758
+ current += ch;
6759
+ }
6760
+ }
6761
+ out.push(current);
6762
+ const langs = out.map((s) => s.trim()).filter(Boolean);
6763
+ if (langs.length === 0) throw new ValidationError("--languages must list at least one language");
6764
+ return [...new Set(langs)];
6765
+ }
6766
+ function checkCallback(url) {
6767
+ return url === void 0 ? void 0 : requireHttps(url, "--callback-url");
6768
+ }
6769
+ function buildVideoAgentRequest(input) {
6770
+ const prompt = input.prompt?.trim() ?? "";
6771
+ if (prompt.length < 1 || prompt.length > LIMITS.agentPromptMax) {
6772
+ throw new ValidationError(`Prompt must be 1-${LIMITS.agentPromptMax} characters`);
6773
+ }
6774
+ const files = input.files ?? [];
6775
+ if (files.length > LIMITS.agentFilesMax) {
6776
+ throw new ValidationError(`Video Agent accepts at most ${LIMITS.agentFilesMax} files`);
6777
+ }
6778
+ const body = { prompt, mode: "generate" };
6779
+ if (input.avatarId) body.avatar_id = input.avatarId;
6780
+ if (input.voiceId) body.voice_id = input.voiceId;
6781
+ if (input.styleId) body.style_id = input.styleId;
6782
+ if (input.brandKitId) body.brand_kit_id = input.brandKitId;
6783
+ if (input.brandGlossaryId) body.brand_glossary_id = input.brandGlossaryId;
6784
+ const orientation = oneOf(input.orientation, AGENT_ORIENTATIONS, "--orientation");
6785
+ if (orientation) body.orientation = orientation;
6786
+ if (files.length > 0) body.files = files;
6787
+ const callbackUrl = checkCallback(input.callbackUrl);
6788
+ if (callbackUrl) body.callback_url = callbackUrl;
6789
+ if (input.callbackId) body.callback_id = input.callbackId;
6790
+ return body;
6791
+ }
6792
+ function buildAvatarVideoRequest(input) {
6793
+ if (!input.avatarId?.trim()) throw new ValidationError("--avatar <look-id> is required");
6794
+ const script = input.script?.trim();
6795
+ if (script && input.audio) {
6796
+ throw new ValidationError("Provide either a script or --audio, not both");
6797
+ }
6798
+ if (!script && !input.audio) {
6799
+ throw new ValidationError("Provide a script argument or --audio <url|asset:id|file>");
6800
+ }
6801
+ if (script && script.length > LIMITS.scriptMax) {
6802
+ throw new ValidationError(`Script must be at most ${LIMITS.scriptMax} characters`);
6803
+ }
6804
+ const body = { type: "avatar", avatar_id: input.avatarId.trim() };
6805
+ if (script) {
6806
+ body.script = script;
6807
+ if (input.voiceId) body.voice_id = input.voiceId;
6808
+ } else if (input.audio) {
6809
+ if (input.voiceId) throw new ValidationError("--voice applies to scripts, not --audio");
6810
+ if (input.audio.type === "url") body.audio_url = input.audio.url;
6811
+ else if (input.audio.type === "asset_id") body.audio_asset_id = input.audio.asset_id;
6812
+ else throw new ValidationError("--audio must be a URL, asset id, or uploaded file");
6813
+ }
6814
+ const resolution = oneOf(input.resolution, AVATAR_VIDEO_RESOLUTIONS, "--resolution");
6815
+ if (resolution) body.resolution = resolution;
6816
+ const aspect = oneOf(input.aspectRatio, AVATAR_VIDEO_ASPECT_RATIOS, "--aspect-ratio");
6817
+ if (aspect) body.aspect_ratio = aspect;
6818
+ const fit = oneOf(input.fit, ["contain", "cover"], "--fit");
6819
+ if (fit) body.fit = fit;
6820
+ const outputFormat = oneOf(input.outputFormat, ["mp4", "webm"], "--output-format");
6821
+ if (outputFormat) body.output_format = outputFormat;
6822
+ if (input.backgroundColor && input.backgroundImage) {
6823
+ throw new ValidationError("Use either --background-color or --background-image, not both");
6824
+ }
6825
+ if (outputFormat === "webm" && (input.backgroundColor || input.backgroundImage)) {
6826
+ throw new ValidationError(
6827
+ "--output-format webm renders a transparent background; drop --background-*"
6828
+ );
6829
+ }
6830
+ if (input.backgroundColor) {
6831
+ if (!/^#[0-9a-f]{6}$/i.test(input.backgroundColor)) {
6832
+ throw new ValidationError("--background-color must be a hex color like #112233");
6833
+ }
6834
+ body.background = { type: "color", value: input.backgroundColor };
6835
+ } else if (input.backgroundImage) {
6836
+ const bg = input.backgroundImage;
6837
+ if (bg.type === "url") body.background = { type: "image", url: bg.url };
6838
+ else if (bg.type === "asset_id") body.background = { type: "image", asset_id: bg.asset_id };
6839
+ }
6840
+ if (input.removeBackground) body.remove_background = true;
6841
+ if (input.captions || input.burnCaptions) {
6842
+ body.caption = {
6843
+ file_format: "srt",
6844
+ ...input.burnCaptions ? { style: "default" } : {}
6845
+ };
6846
+ }
6847
+ const speed = numberInRange(input.speed, "--speed", 0.5, 1.5);
6848
+ const pitch = numberInRange(input.pitch, "--pitch", -50, 50);
6849
+ const volume = numberInRange(input.volume, "--volume", 0, 1);
6850
+ if (speed !== void 0 || pitch !== void 0 || volume !== void 0 || input.locale) {
6851
+ if (!script) throw new ValidationError("Voice settings apply only to scripted videos");
6852
+ body.voice_settings = {
6853
+ ...speed !== void 0 ? { speed } : {},
6854
+ ...pitch !== void 0 ? { pitch } : {},
6855
+ ...volume !== void 0 ? { volume } : {},
6856
+ ...input.locale ? { locale: input.locale } : {}
6857
+ };
6858
+ }
6859
+ const engine = oneOf(input.engine, AVATAR_ENGINES, "--engine");
6860
+ if (input.referenceLookId && engine !== "avatar_v") {
6861
+ throw new ValidationError("--reference-look requires --engine avatar_v");
6862
+ }
6863
+ if (engine) {
6864
+ body.engine = {
6865
+ type: engine,
6866
+ ...input.referenceLookId ? { reference_look_id: input.referenceLookId } : {}
6867
+ };
6868
+ }
6869
+ if (input.motionPrompt) {
6870
+ if (engine === "avatar_iii") {
6871
+ throw new ValidationError("--motion-prompt is not supported with --engine avatar_iii");
6872
+ }
6873
+ body.motion_prompt = input.motionPrompt;
6874
+ }
6875
+ const expressiveness = oneOf(input.expressiveness, EXPRESSIVENESS_LEVELS, "--expressiveness");
6876
+ if (expressiveness) {
6877
+ if (engine === "avatar_v" || engine === "avatar_iii") {
6878
+ throw new ValidationError("--expressiveness is Avatar IV only (photo avatars)");
6879
+ }
6880
+ body.expressiveness = expressiveness;
6881
+ }
6882
+ if (resolution === "4k" && engine !== "avatar_iii") {
6883
+ throw new ValidationError(
6884
+ "--resolution 4k requires --engine avatar_iii (digital twins and studio avatars)"
6885
+ );
6886
+ }
6887
+ if (input.title) body.title = input.title;
6888
+ if (input.folderId) body.folder_id = input.folderId;
6889
+ if (input.brandGlossaryId) body.brand_glossary_id = input.brandGlossaryId;
6890
+ const callbackUrl = checkCallback(input.callbackUrl);
6891
+ if (callbackUrl) body.callback_url = callbackUrl;
6892
+ if (input.callbackId) body.callback_id = input.callbackId;
6893
+ return body;
6894
+ }
6895
+ function buildSpeechRequest(input) {
6896
+ const text = input.text ?? "";
6897
+ if (text.trim().length < 1 || text.length > LIMITS.speechTextMax) {
6898
+ throw new ValidationError(`Text must be 1-${LIMITS.speechTextMax} characters`);
6899
+ }
6900
+ if (!input.voiceId?.trim()) throw new ValidationError("--voice <voice-id> is required");
6901
+ const body = { text, voice_id: input.voiceId.trim() };
6902
+ if (input.ssml) body.input_type = "ssml";
6903
+ const speed = numberInRange(input.speed, "--speed", 0.5, 2);
6904
+ if (speed !== void 0) body.speed = speed;
6905
+ if (input.language) body.language = input.language;
6906
+ if (input.locale) body.locale = input.locale;
6907
+ const engine = oneOf(input.engine, SPEECH_ENGINES, "--engine");
6908
+ if (engine) body.engine = engine;
6909
+ return body;
6910
+ }
6911
+ function buildModelVideoRequest(input) {
6912
+ const prompt = input.prompt?.trim() ?? "";
6913
+ if (prompt.length < 1 || prompt.length > LIMITS.modelVideoPromptMax) {
6914
+ throw new ValidationError(`Prompt must be 1-${LIMITS.modelVideoPromptMax} characters`);
6915
+ }
6916
+ const refImages = input.referenceImages ?? [];
6917
+ const refVideos = input.referenceVideos ?? [];
6918
+ const refAudio = input.referenceAudio ?? [];
6919
+ const hasRefs = refImages.length + refVideos.length + refAudio.length > 0;
6920
+ const inferred = input.image ? "image_to_video" : hasRefs ? "reference_to_video" : "text_to_video";
6921
+ const mode = oneOf(input.mode, MODEL_VIDEO_MODES, "--mode") ?? inferred;
6922
+ if (mode === "image_to_video" && !input.image) {
6923
+ throw new ValidationError("image_to_video requires --image");
6924
+ }
6925
+ if (mode !== "image_to_video" && input.image) {
6926
+ throw new ValidationError("--image is only valid with --mode image_to_video");
6927
+ }
6928
+ if (mode === "reference_to_video" && refImages.length + refVideos.length === 0) {
6929
+ throw new ValidationError(
6930
+ "reference_to_video needs at least one --reference-image or --reference-video"
6931
+ );
6932
+ }
6933
+ if (mode !== "reference_to_video" && hasRefs) {
6934
+ throw new ValidationError("--reference-* inputs are only valid with --mode reference_to_video");
6935
+ }
6936
+ if (refImages.length > LIMITS.modelVideoReferenceImagesMax) {
6937
+ throw new ValidationError(`At most ${LIMITS.modelVideoReferenceImagesMax} reference images`);
6938
+ }
6939
+ if (refVideos.length > LIMITS.modelVideoReferenceVideosMax) {
6940
+ throw new ValidationError(`At most ${LIMITS.modelVideoReferenceVideosMax} reference videos`);
6941
+ }
6942
+ if (refAudio.length > LIMITS.modelVideoReferenceAudioMax) {
6943
+ throw new ValidationError(
6944
+ `At most ${LIMITS.modelVideoReferenceAudioMax} reference audio files`
6945
+ );
6946
+ }
6947
+ const refTotal = refImages.length + refVideos.length + refAudio.length;
6948
+ if (refTotal > LIMITS.modelVideoReferenceTotalMax) {
6949
+ throw new ValidationError(
6950
+ `At most ${LIMITS.modelVideoReferenceTotalMax} reference files in total (got ${refTotal})`
6951
+ );
6952
+ }
6953
+ const body = { model: HEYGEN_VIDEO_MODEL, mode, prompt };
6954
+ const duration = numberInRange(
6955
+ input.duration,
6956
+ "--duration",
6957
+ LIMITS.modelVideoMinDuration,
6958
+ LIMITS.modelVideoMaxDuration,
6959
+ true
6960
+ );
6961
+ if (duration !== void 0) body.duration = duration;
6962
+ const resolution = oneOf(input.resolution, MODEL_VIDEO_RESOLUTIONS, "--resolution");
6963
+ if (resolution) body.resolution = resolution;
6964
+ const aspect = oneOf(input.aspectRatio, MODEL_VIDEO_ASPECT_RATIOS, "--aspect-ratio");
6965
+ if (aspect === "adaptive" && mode !== "reference_to_video") {
6966
+ throw new ValidationError("--aspect-ratio adaptive is only valid for reference_to_video");
6967
+ }
6968
+ if (aspect && mode === "image_to_video") {
6969
+ throw new ValidationError("image_to_video follows the first frame; drop --aspect-ratio");
6970
+ }
6971
+ if (aspect) body.aspect_ratio = aspect;
6972
+ const enhancement = oneOf(
6973
+ input.promptEnhancement,
6974
+ MODEL_VIDEO_PROMPT_ENHANCEMENT,
6975
+ "--prompt-enhancement"
6976
+ );
6977
+ if (enhancement) body.prompt_enhancement = enhancement;
6978
+ const seed = numberInRange(input.seed, "--seed", 0, 4294967295, true);
6979
+ if (seed !== void 0) body.seed = seed;
6980
+ if (input.image) body.image = input.image;
6981
+ if (refImages.length) body.reference_images = refImages;
6982
+ if (refVideos.length) body.reference_videos = refVideos;
6983
+ if (refAudio.length) body.reference_audio = refAudio;
6984
+ const callbackUrl = checkCallback(input.callbackUrl);
6985
+ if (callbackUrl) body.callback_url = callbackUrl;
6986
+ if (input.callbackId) body.callback_id = input.callbackId;
6987
+ return body;
6988
+ }
6989
+ function qualityMode(value) {
6990
+ return oneOf(value, QUALITY_MODES, "--mode");
6991
+ }
6992
+
6993
+ // src/providers/heygen/node-helpers.ts
6994
+ function requireHeygenKey() {
6995
+ const key = resolveKey("HEYGEN_API_KEY")?.trim();
6996
+ if (!key) {
6997
+ throw new ConfigError(
6998
+ `HEYGEN_API_KEY is not set. Create an API key at ${HEYGEN_API_SETTINGS_URL} (API usage is billed separately from HeyGen web plans).`
6999
+ );
7000
+ }
7001
+ return key;
7002
+ }
7003
+ function createHeygenClientFromEnv() {
7004
+ const apiKey = requireHeygenKey();
7005
+ const override = resolveKey("HEYGEN_BASE_URL")?.trim();
7006
+ return new HeygenClient({
7007
+ apiKey,
7008
+ baseUrl: override ? checkBaseUrl(override, "HEYGEN_BASE_URL") : DEFAULT_HEYGEN_BASE_URL
7009
+ });
7010
+ }
7011
+ async function uploadLocalFile(client, filePath, logger) {
7012
+ const abs = path32.resolve(filePath);
7013
+ let stat;
7014
+ try {
7015
+ stat = fs29.statSync(abs);
7016
+ } catch {
7017
+ throw new ValidationError(
7018
+ `File not found: ${filePath}. Use an https:// URL, asset:<asset_id>, or an existing file.`
7019
+ );
7020
+ }
7021
+ if (!stat.isFile()) throw new ValidationError(`Not a file: ${filePath}`);
7022
+ const ext = path32.extname(abs).slice(1).toLowerCase();
7023
+ const mimeType = ASSET_MIME_BY_EXT[ext];
7024
+ if (!mimeType) {
7025
+ throw new ValidationError(
7026
+ `Unsupported file type ".${ext}" for HeyGen upload. Supported: ${Object.keys(ASSET_MIME_BY_EXT).join(", ")}`
7027
+ );
7028
+ }
7029
+ if (stat.size > LIMITS.assetUploadMaxBytes) {
7030
+ throw new ValidationError(
7031
+ `${filePath} is ${(stat.size / 1024 / 1024).toFixed(1)} MB; HeyGen asset upload accepts at most 32 MB. Host it at a public https:// URL instead.`
7032
+ );
7033
+ }
7034
+ logger.info(`Uploading ${path32.basename(abs)} to HeyGen assets...`);
7035
+ const res = await client.uploadAsset({
7036
+ data: new Uint8Array(fs29.readFileSync(abs)),
7037
+ filename: path32.basename(abs),
7038
+ mimeType
7039
+ });
7040
+ logger.debug(`Uploaded asset ${res.asset_id} (${res.mime_type}, ${res.size_bytes} bytes)`);
7041
+ return { type: "asset_id", asset_id: res.asset_id };
7042
+ }
7043
+ async function resolveMediaInput(client, value, logger) {
7044
+ const ref = classifyMediaRef(value);
7045
+ if (ref.kind === "url") return { type: "url", url: ref.url };
7046
+ if (ref.kind === "asset") return { type: "asset_id", asset_id: ref.assetId };
7047
+ return uploadLocalFile(client, ref.path, logger);
7048
+ }
7049
+ async function resolveMediaInputs(client, values, logger) {
7050
+ const out = [];
7051
+ for (const v of values ?? []) out.push(await resolveMediaInput(client, v, logger));
7052
+ return out;
7053
+ }
7054
+ function collect5(value, previous = []) {
7055
+ return [...previous, value];
7056
+ }
7057
+ function parsePositiveInt(raw, flag, fallback) {
7058
+ if (raw === void 0) return fallback;
7059
+ const n = Number(raw);
7060
+ if (!Number.isInteger(n) || n <= 0)
7061
+ throw new ValidationError(`${flag} must be a positive integer`);
7062
+ return n;
7063
+ }
7064
+ var DEFAULT_WAIT_TIMEOUT_MS = 18e5;
7065
+ function addWaitOptions(cmd, defaultTimeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
7066
+ return cmd.option("--async", "Return job IDs and a resume command immediately (no polling)").option("--wait-timeout <ms>", "Max time to wait for completion", String(defaultTimeoutMs)).option(
7067
+ "--poll-interval <ms>",
7068
+ "Initial poll interval (backs off to 30s)",
7069
+ String(DEFAULT_POLL_INTERVAL_MS)
7070
+ ).option("--no-download", "Do not download outputs; print output URLs instead").option(
7071
+ "--output <path>",
7072
+ "Output file or directory. A path ending in / or \\, an existing directory, or a path without an extension is a directory; with several outputs, a media file name gets a per-language suffix (dub.mp4 -> dub-Spanish.mp4)"
7073
+ ).option("--captions", "Also download the caption/subtitle file when one is available").option("--no-thumb", "Skip downloading the video thumbnail").option("--json", "Print a JSON summary instead of human-readable output").option("-v, --verbose", "Verbose logging");
7074
+ }
7075
+ var VIDEO_EXTS = /* @__PURE__ */ new Set(["mp4", "webm", "mov"]);
7076
+ var AUDIO_EXTS = /* @__PURE__ */ new Set(["mp3", "wav", "m4a", "aac"]);
7077
+ var MEDIA_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS, ...AUDIO_EXTS]);
7078
+ var OUTPUT_EXTS = /* @__PURE__ */ new Set([...MEDIA_EXTS, "srt", "vtt"]);
7079
+ function inferExtension(url, fallback) {
7080
+ try {
7081
+ const ext = path32.extname(new URL(url).pathname).slice(1).toLowerCase();
7082
+ return OUTPUT_EXTS.has(ext) ? ext : fallback;
7083
+ } catch {
7084
+ return fallback;
7085
+ }
7086
+ }
7087
+ function safeSlug(value) {
7088
+ return value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "out";
7089
+ }
7090
+ var MAX_REDIRECTS = 5;
7091
+ var isRedirect = (status) => [301, 302, 303, 307, 308].includes(status);
7092
+ async function downloadHttpsOnly(url, dest, timeoutMs) {
7093
+ const signal = AbortSignal.timeout(timeoutMs);
7094
+ let current = url;
7095
+ for (let hop = 0; ; hop++) {
7096
+ if (!isHttpsUrl(current)) {
7097
+ throw new HttpError(0, "refusing to follow a redirect to a non-https URL", current);
7098
+ }
7099
+ let response;
7100
+ try {
7101
+ response = await globalThis.fetch(current, { signal, redirect: "manual" });
7102
+ } catch (cause) {
7103
+ throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), current);
7104
+ }
7105
+ if (response.url && !isHttpsUrl(response.url)) {
7106
+ await response.body?.cancel();
7107
+ throw new HttpError(0, "redirected to a non-https URL", response.url);
7108
+ }
7109
+ const location = response.headers.get("location");
7110
+ if (isRedirect(response.status) && location) {
7111
+ await response.body?.cancel();
7112
+ if (hop >= MAX_REDIRECTS) throw new HttpError(0, "too many redirects", url);
7113
+ current = new URL(location, current).href;
7114
+ continue;
7115
+ }
7116
+ if (!response.ok) throw new HttpError(response.status, "(download failed)", current);
7117
+ if (!response.body) throw new HttpError(0, "Response body is null", current);
7118
+ fs29.mkdirSync(path32.dirname(dest), { recursive: true });
7119
+ const fileStream = fs29.createWriteStream(dest);
7120
+ try {
7121
+ await pipeline2(response.body, fileStream);
7122
+ } catch (err2) {
7123
+ if (!fileStream.closed) await once2(fileStream, "close");
7124
+ throw err2;
7125
+ }
7126
+ return;
7127
+ }
7128
+ }
7129
+ async function downloadOutput(url, dest, resumeHint) {
7130
+ if (!url) {
7131
+ throw new ValidationError(`HeyGen reported success but returned no output URL. ${resumeHint}`);
7132
+ }
7133
+ if (!isHttpsUrl(url)) {
7134
+ throw new ValidationError(
7135
+ `Refusing to download HeyGen output from a non-https URL (${redactUrl(url)})`
7136
+ );
7137
+ }
7138
+ const tmp = `${dest}.${process.pid}-${Date.now()}.part`;
7139
+ try {
7140
+ await downloadHttpsOnly(url, tmp, 6e5);
7141
+ fs29.renameSync(tmp, dest);
7142
+ } catch (e) {
7143
+ fs29.rmSync(tmp, { force: true });
7144
+ const status = e instanceof HttpError ? e.status : 0;
7145
+ const expired = status === 403 || status === 404 || status === 410;
7146
+ const reason = e instanceof Error ? e.message : String(e);
7147
+ throw new ValidationError(
7148
+ `Failed to download HeyGen output from ${redactUrl(url)}: ${reason}.${expired ? " The signed URL may have expired." : ""} ${resumeHint}`
7149
+ );
7150
+ }
7151
+ return dest;
7152
+ }
7153
+ function defaultOutputPath(state, ext) {
7154
+ const lang = state.outputLanguage ? `-${safeSlug(state.outputLanguage)}` : "";
7155
+ return path32.join(getOutputDir(), `heygen-${state.kind}${lang}-${safeSlug(state.id)}.${ext}`);
7156
+ }
7157
+ function statOrUndefined(p) {
7158
+ try {
7159
+ return fs29.statSync(p);
7160
+ } catch {
7161
+ return void 0;
7162
+ }
7163
+ }
7164
+ function isDirectoryOutput2(output, target) {
7165
+ if (/[\\/]$/.test(output)) return true;
7166
+ const stat = statOrUndefined(target);
7167
+ if (stat) return stat.isDirectory();
7168
+ return path32.extname(target) === "";
7169
+ }
7170
+ function withMediaExt(file, ext) {
7171
+ const given = path32.extname(file).slice(1).toLowerCase();
7172
+ const mismatch = VIDEO_EXTS.has(given) && AUDIO_EXTS.has(ext) || AUDIO_EXTS.has(given) && VIDEO_EXTS.has(ext);
7173
+ return mismatch ? `${file.slice(0, -(given.length + 1))}.${ext}` : file;
7174
+ }
7175
+ function outputPathFor(state, ext, output, count) {
7176
+ if (!output) return defaultOutputPath(state, ext);
7177
+ const target = path32.resolve(output);
7178
+ const givenExt = path32.extname(target);
7179
+ const isTemplate = count > 1 && MEDIA_EXTS.has(givenExt.slice(1).toLowerCase());
7180
+ if (isDirectoryOutput2(output, target) || count > 1 && !isTemplate) {
7181
+ fs29.mkdirSync(target, { recursive: true });
7182
+ return path32.join(target, path32.basename(defaultOutputPath(state, ext)));
7183
+ }
7184
+ if (!isTemplate) return withMediaExt(target, ext);
7185
+ const base = path32.basename(target, givenExt);
7186
+ const suffix = safeSlug(state.outputLanguage ?? state.id);
7187
+ return withMediaExt(path32.join(path32.dirname(target), `${base}-${suffix}${givenExt}`), ext);
7188
+ }
7189
+ function claimPath(dest, state, used) {
7190
+ const ext = path32.extname(dest);
7191
+ const stem = dest.slice(0, dest.length - ext.length);
7192
+ let candidate = dest;
7193
+ for (let n = 1; used.has(candidate.toLowerCase()); n++) {
7194
+ const id = safeSlug(state.id);
7195
+ candidate = n === 1 ? `${stem}-${id}${ext}` : `${stem}-${id}-${n}${ext}`;
7196
+ }
7197
+ used.add(candidate.toLowerCase());
7198
+ return candidate;
7199
+ }
7200
+ function captionPathFor(dest, capExt) {
7201
+ const stem = path32.join(path32.dirname(dest), path32.basename(dest, path32.extname(dest)));
7202
+ const capDest = `${stem}.${capExt}`;
7203
+ return capDest.toLowerCase() === dest.toLowerCase() ? `${stem}.captions.${capExt}` : capDest;
7204
+ }
7205
+ var downloadThumbHttpsOnly = (url, dest) => downloadHttpsOnly(url, dest, 12e4);
7206
+ function summarizeState(state) {
7207
+ const out = {
7208
+ kind: state.kind,
7209
+ id: state.id,
7210
+ status: state.status
7211
+ };
7212
+ if (state.sessionId) out.session_id = state.sessionId;
7213
+ if (state.sessionStatus) out.session_status = state.sessionStatus;
7214
+ if (state.videoId) out.video_id = state.videoId;
7215
+ if (state.progress !== void 0) out.progress = state.progress;
7216
+ if (state.outputLanguage) out.output_language = state.outputLanguage;
7217
+ if (state.outputUrl) out.output_url = redactUrl(state.outputUrl);
7218
+ if (state.captionedVideoUrl) out.captioned_video_url = redactUrl(state.captionedVideoUrl);
7219
+ if (state.failureCode) out.failure_code = state.failureCode;
7220
+ if (state.failureMessage) out.failure_message = state.failureMessage;
7221
+ return out;
7222
+ }
7223
+ async function deliverOne(state, opts, count, logger, files, usedPaths) {
7224
+ const hint = `Refresh with: ${resumeCommand2(state.kind, state.id, opts)}`;
7225
+ if (opts.download === false) {
7226
+ if (!state.outputUrl) {
7227
+ throw new ValidationError(`No output URL for ${state.kind} ${state.id}. ${hint}`);
7228
+ }
7229
+ if (!opts.json) console.log(state.outputUrl);
7230
+ const urls = {
7231
+ output_url: state.outputUrl,
7232
+ ...state.captionUrl ? { caption_url: state.captionUrl } : {}
7233
+ };
7234
+ return { state, files, urls };
7235
+ }
7236
+ const ext = inferExtension(state.outputUrl ?? "", state.outputIsAudio ? "mp3" : "mp4");
7237
+ const dest = claimPath(outputPathFor(state, ext, opts.output, count), state, usedPaths);
7238
+ await downloadOutput(state.outputUrl, dest, hint);
7239
+ logger.success(`Saved ${dest}`);
7240
+ files.push(dest);
7241
+ if (opts.captions && state.captionUrl) {
7242
+ const capExt = inferExtension(state.captionUrl, "srt");
7243
+ const capDest = claimPath(captionPathFor(dest, capExt), state, usedPaths);
7244
+ await downloadOutput(state.captionUrl, capDest, hint);
7245
+ logger.success(`Saved ${capDest}`);
7246
+ files.push(capDest);
7247
+ } else if (opts.captions) {
7248
+ logger.warn(`No caption file available for ${state.kind} ${state.id}`);
7249
+ }
7250
+ if ((state.kind === "video" || state.kind === "agent") && opts.thumb !== false) {
7251
+ const thumbUrl = detectThumbUrl(state.raw);
7252
+ if (thumbUrl && !isHttpsUrl(thumbUrl)) {
7253
+ logger.warn(`Skipping thumbnail from a non-https URL (${redactUrl(thumbUrl)})`);
7254
+ } else {
7255
+ const thumb = await maybeDownloadThumb(state.raw, dest, {
7256
+ logger,
7257
+ download: downloadThumbHttpsOnly
7258
+ });
7259
+ if (thumb) files.push(thumb);
7260
+ }
7261
+ }
7262
+ return { state, files };
7263
+ }
7264
+ async function deliverOutputs(states, opts, logger) {
7265
+ const finished = [];
7266
+ const failures = [];
7267
+ const usedPaths = /* @__PURE__ */ new Set();
7268
+ for (const state of states) {
7269
+ const files = [];
7270
+ try {
7271
+ finished.push(await deliverOne(state, opts, states.length, logger, files, usedPaths));
7272
+ } catch (e) {
7273
+ failures.push(`${state.kind} ${state.id}: ${e instanceof Error ? e.message : String(e)}`);
7274
+ if (files.length > 0) finished.push({ state, files });
7275
+ }
7276
+ }
7277
+ return { finished, failures };
7278
+ }
7279
+ function printFinished(results, opts) {
7280
+ if (opts.json) {
7281
+ const jobs = results.map((r) => ({ ...summarizeState(r.state), ...r.urls, files: r.files }));
7282
+ if (jobs.length > 0) console.log(JSON.stringify(jobs.length === 1 ? jobs[0] : jobs, null, 2));
7283
+ return;
7284
+ }
7285
+ for (const r of results) {
7286
+ for (const f of r.files) console.log(`Saved: ${f}`);
7287
+ }
7288
+ }
7289
+ function printSubmitted(kind, ids, opts) {
7290
+ if (opts.json) {
7291
+ const jobs = ids.map((id) => ({
7292
+ kind,
7293
+ id,
7294
+ status: "submitted",
7295
+ resume: resumeCommand2(kind, id, opts)
7296
+ }));
7297
+ console.log(JSON.stringify(jobs.length === 1 ? jobs[0] : jobs, null, 2));
7298
+ return;
7299
+ }
7300
+ for (const id of ids) {
7301
+ console.log(`Submitted ${kind} job: ${id}`);
7302
+ console.log(` Resume with: ${resumeCommand2(kind, id, opts)}`);
7303
+ }
7304
+ }
7305
+ async function completeJobs(client, kind, ids, opts, logger) {
7306
+ if (!Array.isArray(ids) || ids.length === 0 || ids.some((id) => typeof id !== "string" || !id)) {
7307
+ throw new ValidationError(`HeyGen returned no usable ${kind} job ID`);
7308
+ }
7309
+ if (opts.async) {
7310
+ printSubmitted(kind, ids, opts);
7311
+ return [];
7312
+ }
7313
+ const timeoutMs = parsePositiveInt(opts.waitTimeout, "--wait-timeout", DEFAULT_WAIT_TIMEOUT_MS);
7314
+ const intervalMs = parsePositiveInt(
7315
+ opts.pollInterval,
7316
+ "--poll-interval",
7317
+ DEFAULT_POLL_INTERVAL_MS
7318
+ );
7319
+ logger.info(
7320
+ `Waiting for ${ids.length > 1 ? `${ids.length} ${kind} jobs` : `${kind} ${ids[0]}`}...`
7321
+ );
7322
+ const settled = await Promise.allSettled(
7323
+ ids.map(
7324
+ (id, index) => waitForJob(client, kind, id, {
7325
+ timeoutMs,
7326
+ intervalMs,
7327
+ startDelayMs: staggerOffsetMs(index, intervalMs),
7328
+ maxIntervalMs: Math.max(intervalMs, 3e4),
7329
+ burnCaptions: opts.burnCaptions,
7330
+ resumeHint: `Resume with: ${resumeCommand2(kind, id, opts)}`,
7331
+ onPoll: (s, attempt) => logger.debug(
7332
+ `${kind} ${id} poll #${attempt}: ${s.status}${s.videoId ? ` (video ${s.videoId})` : ""}`
7333
+ )
7334
+ })
7335
+ )
7336
+ );
7337
+ const states = [];
7338
+ const failures = [];
7339
+ for (const [i, r] of settled.entries()) {
7340
+ if (r.status === "fulfilled") {
7341
+ if (r.value.succeeded) states.push(r.value);
7342
+ else {
7343
+ try {
7344
+ assertJobSucceeded(r.value);
7345
+ } catch (e) {
7346
+ failures.push(e instanceof Error ? e.message : String(e));
5942
7347
  }
5943
7348
  }
5944
- formatResults(results, opts.format, opts.output);
5945
- cleanup();
5946
- const failed = results.filter((r) => r.status === "error").length;
5947
- if (failed > 0) process.exit(1);
7349
+ } else {
7350
+ const err2 = r.reason;
7351
+ const msg = err2 instanceof Error ? err2.message : String(err2);
7352
+ failures.push(err2 instanceof PollTimeoutError ? msg : `${kind} ${ids[i]}: ${msg}`);
7353
+ }
7354
+ }
7355
+ const delivery = states.length > 0 ? await deliverOutputs(states, opts, logger) : { finished: [], failures: [] };
7356
+ failures.push(...delivery.failures);
7357
+ printFinished(delivery.finished, opts);
7358
+ if (failures.length > 0) {
7359
+ const total = ids.length;
7360
+ const ok2 = total - failures.length;
7361
+ const header = total > 1 ? `${ok2} of ${total} ${kind} jobs delivered; ${failures.length} failed:
7362
+ ` : "";
7363
+ throw new HeygenJobFailedError(`${header}${failures.join("\n")}`);
7364
+ }
7365
+ return delivery.finished;
7366
+ }
7367
+
7368
+ // src/providers/heygen/commands/catalog.ts
7369
+ function pageLimit(raw, max) {
7370
+ return numberInRange(raw, "--limit", 1, max, true);
7371
+ }
7372
+ function printTable(page, columns) {
7373
+ if (page.items.length === 0) {
7374
+ console.log("No results.");
7375
+ } else {
7376
+ const cells = page.items.map(
7377
+ (row) => columns.map(([, get]) => {
7378
+ const v = get(row);
7379
+ return Array.isArray(v) ? v.join(",") : v === null || v === void 0 ? "-" : String(v);
7380
+ })
7381
+ );
7382
+ const widths = columns.map(
7383
+ ([h], i) => Math.min(48, Math.max(h.length, ...cells.map((c2) => (c2[i] ?? "").length)))
7384
+ );
7385
+ const fmt = (vals) => vals.map((v, i) => v.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
7386
+ console.log(fmt(columns.map(([h]) => h)));
7387
+ for (const c2 of cells) console.log(fmt(c2));
7388
+ }
7389
+ if (page.hasMore && page.nextToken) console.log(`
7390
+ More results: --cursor ${page.nextToken}`);
7391
+ }
7392
+ function printPage(page, json, columns) {
7393
+ if (json) {
7394
+ console.log(
7395
+ JSON.stringify(
7396
+ { data: page.items, has_more: page.hasMore, next_token: page.nextToken },
7397
+ null,
7398
+ 2
7399
+ )
7400
+ );
7401
+ } else {
7402
+ printTable(page, columns);
7403
+ }
7404
+ }
7405
+ function registerHeygenCatalogCommands(parent) {
7406
+ parent.command("avatars").description(
7407
+ "List avatar looks (the look ID is the --avatar for videos); --groups lists characters instead"
7408
+ ).option("--groups", "List avatar groups (characters) instead of looks").option("--group <group-id>", "Only looks in this avatar group").option("--type <t>", `Look type: ${AVATAR_TYPES.join(" | ")}`).option("--ownership <o>", "public (presets) | private (yours)").option("--limit <n>", `Page size (1-${LIMITS.avatarListMax})`).option("--cursor <token>", "Next-page cursor from a previous call").option("--json", "Print JSON").action(
7409
+ async (opts) => {
7410
+ const ownership = oneOf(opts.ownership, OWNERSHIP_VALUES, "--ownership");
7411
+ const avatarType = oneOf(opts.type, AVATAR_TYPES, "--type");
7412
+ const limit = pageLimit(opts.limit, LIMITS.avatarListMax);
7413
+ const client = createHeygenClientFromEnv();
7414
+ if (opts.groups) {
7415
+ const page2 = await client.listAvatarGroups({ ownership, limit, token: opts.cursor });
7416
+ printPage(page2, opts.json, [
7417
+ ["GROUP_ID", (g) => g.id],
7418
+ ["NAME", (g) => g.name],
7419
+ ["LOOKS", (g) => g.looks_count],
7420
+ ["GENDER", (g) => g.gender],
7421
+ ["DEFAULT_VOICE", (g) => g.default_voice_id]
7422
+ ]);
7423
+ if (!opts.json)
7424
+ console.log("\nUse `heygen avatars --group <GROUP_ID>` to list its looks.");
7425
+ return;
7426
+ }
7427
+ const page = await client.listAvatarLooks({
7428
+ group_id: opts.group,
7429
+ avatar_type: avatarType,
7430
+ ownership,
7431
+ limit,
7432
+ token: opts.cursor
7433
+ });
7434
+ printPage(page, opts.json, [
7435
+ ["LOOK_ID", (l) => l.id],
7436
+ ["NAME", (l) => l.name],
7437
+ ["TYPE", (l) => l.avatar_type],
7438
+ ["GROUP_ID", (l) => l.group_id],
7439
+ ["ENGINES", (l) => l.supported_api_engines],
7440
+ ["DEFAULT_VOICE", (l) => l.default_voice_id]
7441
+ ]);
7442
+ }
7443
+ );
7444
+ parent.command("voices").description("List voices (voice_id for `heygen video`, `video-agent`, and `tts`)").option("--language <name>", "Filter by language, e.g. English").option("--gender <g>", "male | female").option("--engine <e>", "Filter by engine, e.g. starfish | orca | elevenlabs").option("--type <t>", "public (default) | private (your cloned voices)").option("--limit <n>", `Page size (1-${LIMITS.voiceListMax})`).option("--cursor <token>", "Next-page cursor from a previous call").option("--json", "Print JSON").action(
7445
+ async (opts) => {
7446
+ const type = oneOf(opts.type, OWNERSHIP_VALUES, "--type");
7447
+ const limit = pageLimit(opts.limit, LIMITS.voiceListMax);
7448
+ const client = createHeygenClientFromEnv();
7449
+ const page = await client.listVoices({
7450
+ language: opts.language,
7451
+ gender: opts.gender,
7452
+ engine: opts.engine,
7453
+ type,
7454
+ limit,
7455
+ token: opts.cursor
7456
+ });
7457
+ printPage(page, opts.json, [
7458
+ ["VOICE_ID", (v) => v.voice_id],
7459
+ ["NAME", (v) => v.name],
7460
+ ["LANGUAGE", (v) => v.language],
7461
+ ["GENDER", (v) => v.gender],
7462
+ ["ENGINES", (v) => v.available_engines]
7463
+ ]);
5948
7464
  }
5949
7465
  );
7466
+ parent.command("styles").description("List Video Agent visual styles (style_id for `heygen video-agent --style`)").option("--tag <tag>", "Filter by tag, e.g. cinematic").option("--limit <n>", `Page size (1-${LIMITS.stylesListMax})`).option("--cursor <token>", "Next-page cursor from a previous call").option("--json", "Print JSON").action(async (opts) => {
7467
+ const limit = pageLimit(opts.limit, LIMITS.stylesListMax);
7468
+ const client = createHeygenClientFromEnv();
7469
+ const page = await client.listVideoAgentStyles({ tag: opts.tag, limit, token: opts.cursor });
7470
+ printPage(page, opts.json, [
7471
+ ["STYLE_ID", (s) => s.style_id],
7472
+ ["NAME", (s) => s.name],
7473
+ ["TAGS", (s) => s.tags],
7474
+ ["ASPECT", (s) => s.aspect_ratio]
7475
+ ]);
7476
+ });
7477
+ parent.command("languages").description("List supported video translation target languages").option("--json", "Print JSON").action(async (opts) => {
7478
+ const client = createHeygenClientFromEnv();
7479
+ const languages = await client.listTranslationLanguages();
7480
+ if (opts.json) console.log(JSON.stringify(languages, null, 2));
7481
+ else for (const l of languages) console.log(l);
7482
+ });
7483
+ parent.command("me").description("Show the HeyGen account for the API key (billing type, API credits/balance)").option("--json", "Print JSON").action(async (opts) => {
7484
+ const client = createHeygenClientFromEnv();
7485
+ const me = await client.getCurrentUser();
7486
+ if (opts.json) {
7487
+ console.log(JSON.stringify(me, null, 2));
7488
+ return;
7489
+ }
7490
+ const name = [me.first_name, me.last_name].filter(Boolean).join(" ") || me.username || "-";
7491
+ console.log(`User: ${name}`);
7492
+ if (me.billing_type) console.log(`Billing type: ${me.billing_type}`);
7493
+ for (const key of ["wallet", "subscription", "usage_based"]) {
7494
+ if (me[key] !== void 0 && me[key] !== null) {
7495
+ console.log(`${key}: ${JSON.stringify(me[key])}`);
7496
+ }
7497
+ }
7498
+ });
5950
7499
  }
5951
7500
 
5952
- // src/providers/gemini/commands/index.ts
5953
- function registerGeminiCommands(program2) {
5954
- const gemini = program2.command("gemini").description(
5955
- "Gemini: analyze, transcribe, extract data, generate images (Imagen/Nano Banana), video (Veo), and speech (Flash TTS)"
7501
+ // src/providers/heygen/commands/lipsync.ts
7502
+ init_errors();
7503
+ function registerHeygenLipsyncCommand(parent) {
7504
+ const cmd = parent.command("lipsync").description(
7505
+ "Lip-sync an existing video to new audio (refs: https:// URL, asset:<id>, or file)"
7506
+ ).requiredOption("--video <ref>", "Source video").requiredOption("--audio <ref>", "Replacement audio").option("--mode <m>", "speed (default) | precision").option("--keep-format", "Preserve source resolution/bitrate").option("--no-dynamic-duration", "Disable dynamic duration adjustment").option("--disable-music", "Remove the background music track").option("--enhance-speech", "Enhance speech quality").option("--start <s>", "Start time in seconds").option("--end <s>", "End time in seconds").option("--title <text>", "Job title").option("--folder <id>", "Destination folder ID").option("--callback-url <url>", "HTTPS webhook URL notified on completion").option("--callback-id <id>", "Caller ID echoed in the webhook payload");
7507
+ addWaitOptions(cmd).action(async (opts) => {
7508
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7509
+ const mode = qualityMode(opts.mode);
7510
+ const start = numberInRange(opts.start, "--start", 0, Number.MAX_SAFE_INTEGER);
7511
+ const end = numberInRange(opts.end, "--end", 0, Number.MAX_SAFE_INTEGER);
7512
+ if (start !== void 0 && end !== void 0 && end <= start) {
7513
+ throw new ValidationError("--end must be greater than --start");
7514
+ }
7515
+ const callbackUrl = checkCallback(opts.callbackUrl);
7516
+ const client = createHeygenClientFromEnv();
7517
+ const body = {
7518
+ video: await resolveMediaInput(client, opts.video, logger),
7519
+ audio: await resolveMediaInput(client, opts.audio, logger)
7520
+ };
7521
+ if (mode) body.mode = mode;
7522
+ if (opts.keepFormat) body.keep_the_same_format = true;
7523
+ if (opts.dynamicDuration === false) body.enable_dynamic_duration = false;
7524
+ if (opts.disableMusic) body.disable_music_track = true;
7525
+ if (opts.enhanceSpeech) body.enable_speech_enhancement = true;
7526
+ if (start !== void 0) body.start_time = start;
7527
+ if (end !== void 0) body.end_time = end;
7528
+ if (opts.title) body.title = opts.title;
7529
+ if (opts.folder) body.folder_id = opts.folder;
7530
+ if (callbackUrl) body.callback_url = callbackUrl;
7531
+ if (opts.callbackId) body.callback_id = opts.callbackId;
7532
+ logger.info("Submitting lipsync...");
7533
+ const created = await client.createLipsync(body);
7534
+ await completeJobs(client, "lipsync", [created.lipsync_id], opts, logger);
7535
+ });
7536
+ }
7537
+
7538
+ // src/providers/heygen/commands/model-video.ts
7539
+ var PENDING = { type: "asset_id", asset_id: "pending" };
7540
+ var pending = (refs) => (refs ?? []).map(() => PENDING);
7541
+ function registerHeygenModelVideoCommand(parent) {
7542
+ const cmd = parent.command("model-video <prompt>").description(
7543
+ "HeyGen Video model (heygen-video-1): 5-15 s scene with audio from a prompt (not an avatar video)"
7544
+ ).option("--mode <m>", "text_to_video | image_to_video | reference_to_video (inferred)").option("--image <ref>", "First frame for image_to_video: https:// URL, asset:<id>, or file").option("--reference-image <ref>", "Reference image (repeatable, max 9)", collect5).option("--reference-video <ref>", "Reference video (repeatable, max 3)", collect5).option("--reference-audio <ref>", "Reference audio (repeatable, max 3)", collect5).option("--duration <s>", "Clip length 5-15 seconds").option("--resolution <r>", "480p | 768p | 1080p | 2k").option("--aspect-ratio <r>", "21:9 | 16:9 | 4:3 | 1:1 | 3:4 | 9:16 | adaptive").option("--prompt-enhancement <p>", "turbo | quality | disabled").option("--seed <n>", "Seed for repeatable output").option("--callback-url <url>", "HTTPS webhook URL notified on completion").option("--callback-id <id>", "Caller ID echoed in the webhook payload");
7545
+ addWaitOptions(cmd).action(async (prompt, opts) => {
7546
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7547
+ const input = {
7548
+ prompt,
7549
+ mode: opts.mode,
7550
+ duration: opts.duration,
7551
+ resolution: opts.resolution,
7552
+ aspectRatio: opts.aspectRatio,
7553
+ promptEnhancement: opts.promptEnhancement,
7554
+ seed: opts.seed,
7555
+ callbackUrl: opts.callbackUrl,
7556
+ callbackId: opts.callbackId
7557
+ };
7558
+ buildModelVideoRequest({
7559
+ ...input,
7560
+ image: opts.image ? PENDING : void 0,
7561
+ referenceImages: pending(opts.referenceImage),
7562
+ referenceVideos: pending(opts.referenceVideo),
7563
+ referenceAudio: pending(opts.referenceAudio)
7564
+ });
7565
+ const client = createHeygenClientFromEnv();
7566
+ if (opts.image) input.image = await resolveMediaInput(client, opts.image, logger);
7567
+ input.referenceImages = await resolveMediaInputs(client, opts.referenceImage, logger);
7568
+ input.referenceVideos = await resolveMediaInputs(client, opts.referenceVideo, logger);
7569
+ input.referenceAudio = await resolveMediaInputs(client, opts.referenceAudio, logger);
7570
+ const body = buildModelVideoRequest(input);
7571
+ logger.info(`Submitting HeyGen Video (${body.mode})...`);
7572
+ const created = await client.createModelVideo(body);
7573
+ await completeJobs(client, "model-video", [created.video_id], opts, logger);
7574
+ });
7575
+ }
7576
+
7577
+ // src/providers/heygen/commands/run.ts
7578
+ init_errors();
7579
+ import path33 from "path";
7580
+ function extractJobIds(operation, data) {
7581
+ const op = HEYGEN_OPERATIONS[operation];
7582
+ if (!op) return [];
7583
+ if (op.idsField) {
7584
+ const ids = data[op.idsField];
7585
+ return Array.isArray(ids) ? ids.filter((x) => typeof x === "string" && !!x) : [];
7586
+ }
7587
+ const id = op.idField ? data[op.idField] : void 0;
7588
+ return typeof id === "string" && id ? [id] : [];
7589
+ }
7590
+ function registerHeygenRunCommand(parent) {
7591
+ const names = Object.keys(HEYGEN_OPERATIONS);
7592
+ const cmd = parent.command("run <operation>").description(`Send a raw v3 payload to a documented create operation: ${names.join(", ")}`).requiredOption("--input <json>", "Request body, inline JSON or @path/to/payload.json");
7593
+ addWaitOptions(cmd).action(async (operation, opts) => {
7594
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7595
+ const op = HEYGEN_OPERATIONS[operation];
7596
+ if (!op) {
7597
+ throw new ValidationError(
7598
+ `Unknown operation "${operation}". Supported: ${names.map((n) => `${n} (${HEYGEN_OPERATIONS[n]?.description})`).join("; ")}`
7599
+ );
7600
+ }
7601
+ const body = parseJsonInput(opts.input);
7602
+ const client = createHeygenClientFromEnv();
7603
+ logger.info(`Calling ${op.description}...`);
7604
+ const json = await client.send(op.path, {
7605
+ method: "POST",
7606
+ body,
7607
+ idempotencyKey: op.idempotent
7608
+ });
7609
+ const data = unwrapData(json, op.path);
7610
+ if (op.kind === null) {
7611
+ const audioUrl = typeof data.audio_url === "string" ? data.audio_url : void 0;
7612
+ if (opts.download === false || !audioUrl) {
7613
+ console.log(JSON.stringify(data, null, 2));
7614
+ return;
7615
+ }
7616
+ const ext = inferExtension(audioUrl, "mp3");
7617
+ const dest = opts.output ? path33.resolve(opts.output) : path33.join(getOutputDir(), `heygen-speech-${Date.now()}.${ext}`);
7618
+ await downloadOutput(audioUrl, dest, "Re-run the command to generate a fresh URL.");
7619
+ console.log(`Saved: ${dest}`);
7620
+ return;
7621
+ }
7622
+ const ids = extractJobIds(operation, data);
7623
+ if (ids.length === 0) {
7624
+ throw new ValidationError(
7625
+ `HeyGen response for ${operation} had no job ID: ${JSON.stringify(data).slice(0, 300)}`
7626
+ );
7627
+ }
7628
+ await completeJobs(client, op.kind, ids, opts, logger);
7629
+ });
7630
+ }
7631
+
7632
+ // src/providers/heygen/commands/status.ts
7633
+ function registerHeygenStatusCommand(parent) {
7634
+ parent.command("status <id>").description("Check (or wait for and download) a HeyGen job by ID").option(
7635
+ "--type <type>",
7636
+ "agent (session ID) | video | translation | lipsync | model-video",
7637
+ "video"
7638
+ ).option("--wait", "Poll until the job finishes").option("--download", "Download the output when complete (implies --wait)").option("--wait-timeout <ms>", "Max time to wait", "1800000").option(
7639
+ "--poll-interval <ms>",
7640
+ "Initial poll interval (backs off to 30s)",
7641
+ String(DEFAULT_POLL_INTERVAL_MS)
7642
+ ).option("--output <path>", "Output file path (with --download)").option("--captions", "Also download the caption/subtitle file when available").option(
7643
+ "--burn-captions",
7644
+ "Video: use the burned-in caption render (captioned_video_url); fails if absent"
7645
+ ).option("--no-thumb", "Skip downloading the video thumbnail").option("--json", "Print a JSON summary").option("--raw", "Print the raw API payload (includes signed URLs) instead of the summary").option("-v, --verbose", "Verbose logging").action(async (id, opts) => {
7646
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7647
+ const kind = oneOf(opts.type, JOB_KINDS, "--type") ?? "video";
7648
+ const client = createHeygenClientFromEnv();
7649
+ if (opts.download) {
7650
+ await completeJobs(client, kind, [id], { ...opts, async: false, download: true }, logger);
7651
+ return;
7652
+ }
7653
+ const read = { burnCaptions: opts.burnCaptions };
7654
+ const state = opts.wait ? await waitForJob(client, kind, id, {
7655
+ ...read,
7656
+ timeoutMs: parsePositiveInt(
7657
+ opts.waitTimeout,
7658
+ "--wait-timeout",
7659
+ DEFAULT_WAIT_TIMEOUT_MS
7660
+ ),
7661
+ intervalMs: parsePositiveInt(
7662
+ opts.pollInterval,
7663
+ "--poll-interval",
7664
+ DEFAULT_POLL_INTERVAL_MS
7665
+ ),
7666
+ resumeHint: `Resume with: ${resumeCommand2(kind, id, read)}`
7667
+ }) : await getJobState(client, kind, id, read);
7668
+ console.log(JSON.stringify(opts.raw ? state.raw : summarizeState(state), null, 2));
7669
+ if (state.done) assertJobSucceeded(state);
7670
+ else if (!opts.json) logger.info(`Still ${state.status}. Re-run with --wait to poll.`);
7671
+ });
7672
+ }
7673
+
7674
+ // src/providers/heygen/commands/template.ts
7675
+ init_errors();
7676
+ function templateBody(input, title) {
7677
+ const body = "variables" in input ? { ...input } : { variables: input };
7678
+ const vars = body.variables;
7679
+ if (vars === null || typeof vars !== "object" || Array.isArray(vars)) {
7680
+ throw new ValidationError("Template input must contain a `variables` object");
7681
+ }
7682
+ if (title) body.title = title;
7683
+ return body;
7684
+ }
7685
+ function registerHeygenTemplateCommand(parent) {
7686
+ const cmd = parent.command("template <template-id>").description("Generate a video from a HeyGen Studio template by filling its variables").requiredOption(
7687
+ "--input <json>",
7688
+ "Variables map or full request body, inline JSON or @path/to/vars.json"
7689
+ ).option("--title <text>", "Video title");
7690
+ addWaitOptions(cmd).action(async (templateId, opts) => {
7691
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7692
+ const body = templateBody(parseJsonInput(opts.input), opts.title);
7693
+ const client = createHeygenClientFromEnv();
7694
+ logger.info(`Generating from template ${templateId}...`);
7695
+ const video = await client.generateFromTemplate(templateId, body);
7696
+ await completeJobs(client, "video", [video.id], opts, logger);
7697
+ });
7698
+ }
7699
+
7700
+ // src/providers/heygen/commands/translate.ts
7701
+ init_errors();
7702
+ function registerHeygenTranslateCommand(parent) {
7703
+ const cmd = parent.command("translate <source>").description(
7704
+ "Translate a video (voice clone + lip-sync). Source: https:// URL, asset:<id>, or local file"
7705
+ ).requiredOption(
7706
+ "--languages <list>",
7707
+ 'Comma-separated target language names from `heygen languages`, e.g. "Spanish (Spain),English"'
7708
+ ).option("--mode <m>", "speed (default) | precision (higher lip-sync quality)").option("--input-language <code>", "Source language (auto-detected when omitted)").option("--audio-only", "Translate audio only and keep the original video frames").option("--audio <ref>", "Custom dubbing audio: https:// URL, asset:<id>, or file").option("--speakers <n>", "Number of speakers (improves separation)").option("--keep-format", "Preserve source resolution/bitrate").option("--no-dynamic-duration", "Disable dynamic duration adjustment").option("--disable-music", "Remove the background music track").option("--enhance-speech", "Enhance speech quality").option("--start <s>", "Start time in seconds (partial translation)").option("--end <s>", "End time in seconds (partial translation)").option("--brand-glossary <id>", "Brand glossary ID (term translations)").option("--title <text>", "Job title").option("--folder <id>", "Destination folder ID").option("--callback-url <url>", "HTTPS webhook URL notified on completion").option("--callback-id <id>", "Caller ID echoed in the webhook payload");
7709
+ addWaitOptions(cmd).action(async (source, opts) => {
7710
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7711
+ const languages = splitLanguages(opts.languages);
7712
+ const mode = qualityMode(opts.mode);
7713
+ const speakers = numberInRange(opts.speakers, "--speakers", 1, 100, true);
7714
+ const start = numberInRange(opts.start, "--start", 0, Number.MAX_SAFE_INTEGER);
7715
+ const end = numberInRange(opts.end, "--end", 0, Number.MAX_SAFE_INTEGER);
7716
+ if (start !== void 0 && end !== void 0 && end <= start) {
7717
+ throw new ValidationError("--end must be greater than --start");
7718
+ }
7719
+ const callbackUrl = checkCallback(opts.callbackUrl);
7720
+ const client = createHeygenClientFromEnv();
7721
+ const video = await resolveMediaInput(client, source, logger);
7722
+ const audio = opts.audio ? await resolveMediaInput(client, opts.audio, logger) : void 0;
7723
+ const body = { video, output_languages: languages };
7724
+ if (audio) body.audio = audio;
7725
+ if (mode) body.mode = mode;
7726
+ if (opts.inputLanguage) body.input_language = opts.inputLanguage;
7727
+ if (opts.audioOnly) body.translate_audio_only = true;
7728
+ if (speakers !== void 0) body.speaker_num = speakers;
7729
+ if (opts.keepFormat) body.keep_the_same_format = true;
7730
+ if (opts.dynamicDuration === false) body.enable_dynamic_duration = false;
7731
+ if (opts.disableMusic) body.disable_music_track = true;
7732
+ if (opts.enhanceSpeech) body.enable_speech_enhancement = true;
7733
+ if (start !== void 0) body.start_time = start;
7734
+ if (end !== void 0) body.end_time = end;
7735
+ if (opts.brandGlossary) body.brand_glossary_id = opts.brandGlossary;
7736
+ if (opts.title) body.title = opts.title;
7737
+ if (opts.folder) body.folder_id = opts.folder;
7738
+ if (callbackUrl) body.callback_url = callbackUrl;
7739
+ if (opts.callbackId) body.callback_id = opts.callbackId;
7740
+ logger.info(`Submitting translation into ${languages.length} language(s)...`);
7741
+ const created = await client.createVideoTranslation(body);
7742
+ await completeJobs(client, "translation", created.video_translation_ids, opts, logger);
7743
+ });
7744
+ }
7745
+
7746
+ // src/providers/heygen/commands/tts.ts
7747
+ import path34 from "path";
7748
+ init_redact();
7749
+ function registerHeygenTtsCommand(parent) {
7750
+ parent.command("tts <text>").description("Text-to-speech with a HeyGen catalog voice (1-5000 characters)").requiredOption("--voice <voice-id>", "Voice ID from `heygen voices`").option("--ssml", "Treat the text as SSML").option("--speed <n>", "Speed multiplier 0.5-2.0").option("--language <code>", "Base language code, e.g. en (auto-detected when omitted)").option("--locale <tag>", "Locale tag for Starfish voices, e.g. zh-HK").option("--engine <e>", "starfish | orca | elevenlabs (defaults to the voice's engine)").option("--output <path>", "Output audio path").option("--no-download", "Print the audio URL instead of downloading").option("--json", "Print a JSON summary").option("-v, --verbose", "Verbose logging").action(async (text, opts) => {
7751
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7752
+ const body = buildSpeechRequest({
7753
+ text,
7754
+ voiceId: opts.voice,
7755
+ ssml: opts.ssml,
7756
+ speed: opts.speed,
7757
+ language: opts.language,
7758
+ locale: opts.locale,
7759
+ engine: opts.engine
7760
+ });
7761
+ const client = createHeygenClientFromEnv();
7762
+ logger.info("Generating speech...");
7763
+ const res = await client.createSpeech(body);
7764
+ const summary = {
7765
+ duration: res.duration,
7766
+ engine: res.engine,
7767
+ request_id: res.request_id ?? null,
7768
+ audio_url: redactUrl(res.audio_url)
7769
+ };
7770
+ if (opts.download === false) {
7771
+ if (opts.json)
7772
+ console.log(JSON.stringify({ ...summary, audio_url: res.audio_url }, null, 2));
7773
+ else console.log(res.audio_url);
7774
+ return;
7775
+ }
7776
+ const ext = inferExtension(res.audio_url, "mp3");
7777
+ const name = `heygen-tts-${res.request_id ?? Date.now()}.${ext}`.replace(/[^\w.-]+/g, "-");
7778
+ const dest = opts.output ? path34.resolve(opts.output) : path34.join(getOutputDir(), name);
7779
+ await downloadOutput(res.audio_url, dest, "Re-run the command to generate a fresh URL.");
7780
+ if (opts.json) console.log(JSON.stringify({ ...summary, file: dest }, null, 2));
7781
+ else console.log(`Saved: ${dest} (${res.duration}s, ${res.engine})`);
7782
+ });
7783
+ }
7784
+
7785
+ // src/providers/heygen/commands/video-agent.ts
7786
+ function registerHeygenVideoAgentCommand(parent) {
7787
+ const cmd = parent.command("video-agent <prompt>").description(
7788
+ "Prompt-to-video with HeyGen Video Agent (agent writes the script and picks avatar/voice/scenes)"
7789
+ ).option("--avatar <look-id>", "Avatar look ID to feature (see `heygen avatars`)").option("--voice <voice-id>", "Narration voice ID (see `heygen voices`)").option("--style <style-id>", "Visual style ID (see `heygen styles`)").option("--brand-kit <id>", "Brand kit ID (colors, fonts, logos)").option("--brand-glossary <id>", "Brand glossary ID (pronunciations)").option("--orientation <o>", "landscape | portrait (auto-detected when omitted)").option(
7790
+ "--file <ref>",
7791
+ "Attachment: https:// URL, asset:<asset_id>, or local file to upload (repeatable, max 20)",
7792
+ collect5
7793
+ ).option("--callback-url <url>", "HTTPS webhook URL notified on completion/failure").option("--callback-id <id>", "Caller ID echoed in the webhook payload");
7794
+ addWaitOptions(cmd).action(async (prompt, opts) => {
7795
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7796
+ buildVideoAgentRequest({
7797
+ prompt,
7798
+ orientation: opts.orientation,
7799
+ callbackUrl: opts.callbackUrl
7800
+ });
7801
+ const client = createHeygenClientFromEnv();
7802
+ const files = await resolveMediaInputs(client, opts.file, logger);
7803
+ const body = buildVideoAgentRequest({
7804
+ prompt,
7805
+ avatarId: opts.avatar,
7806
+ voiceId: opts.voice,
7807
+ styleId: opts.style,
7808
+ brandKitId: opts.brandKit,
7809
+ brandGlossaryId: opts.brandGlossary,
7810
+ orientation: opts.orientation,
7811
+ files,
7812
+ callbackUrl: opts.callbackUrl,
7813
+ callbackId: opts.callbackId
7814
+ });
7815
+ logger.info("Creating Video Agent session...");
7816
+ const created = await client.createVideoAgent(body);
7817
+ logger.debug(`session ${created.session_id} status=${created.status}`);
7818
+ await completeJobs(client, "agent", [created.session_id], opts, logger);
7819
+ });
7820
+ }
7821
+
7822
+ // src/providers/heygen/commands/video.ts
7823
+ function toInput(script, opts) {
7824
+ return {
7825
+ avatarId: opts.avatar,
7826
+ script,
7827
+ voiceId: opts.voice,
7828
+ resolution: opts.resolution,
7829
+ aspectRatio: opts.aspectRatio,
7830
+ engine: opts.engine,
7831
+ referenceLookId: opts.referenceLook,
7832
+ motionPrompt: opts.motionPrompt,
7833
+ expressiveness: opts.expressiveness,
7834
+ fit: opts.fit,
7835
+ backgroundColor: opts.backgroundColor,
7836
+ removeBackground: opts.removeBackground,
7837
+ outputFormat: opts.outputFormat,
7838
+ captions: opts.captions,
7839
+ burnCaptions: opts.burnCaptions,
7840
+ speed: opts.speed,
7841
+ pitch: opts.pitch,
7842
+ volume: opts.volume,
7843
+ locale: opts.locale,
7844
+ title: opts.title,
7845
+ folderId: opts.folder,
7846
+ brandGlossaryId: opts.brandGlossary,
7847
+ callbackUrl: opts.callbackUrl,
7848
+ callbackId: opts.callbackId
7849
+ };
7850
+ }
7851
+ var PENDING_UPLOAD = { type: "asset_id", asset_id: "pending" };
7852
+ function registerHeygenVideoCommand(parent) {
7853
+ const cmd = parent.command("video [script]").description(
7854
+ "Scripted avatar video: your avatar look + voice speak your exact script (or lip-sync --audio)"
7855
+ ).requiredOption("--avatar <look-id>", "Avatar look ID (from `heygen avatars`, not a group ID)").option("--voice <voice-id>", "Voice ID (defaults to the look's default voice)").option(
7856
+ "--audio <ref>",
7857
+ "Recorded audio instead of a script: https:// URL, asset:<id>, or file"
7858
+ ).option("--resolution <r>", "4k | 1080p | 720p (4k: Avatar III digital twins/studio avatars)").option("--aspect-ratio <r>", "16:9 | 9:16 | 4:5 | 5:4 | 1:1 | auto", "auto").option("--engine <e>", "avatar_iv (default) | avatar_v | avatar_iii").option("--reference-look <look-id>", "Avatar V animation reference (digital_twin look)").option("--motion-prompt <text>", "Body motion/gesture prompt (photo avatars, or Avatar V)").option("--expressiveness <level>", "high | medium | low (photo avatars, Avatar IV)").option("--fit <f>", "contain | cover").option("--background-color <hex>", "Solid background color, e.g. #ffffff").option("--background-image <ref>", "Background image: https:// URL, asset:<id>, or file").option("--remove-background", "Remove the avatar background (matting-enabled avatars)").option("--output-format <f>", "mp4 (default) | webm (transparent background)").option("--burn-captions", "Burn captions into the video (subtitle file is always produced)").option("--speed <n>", "Voice speed 0.5-1.5").option("--pitch <n>", "Voice pitch -50..50 semitones").option("--volume <n>", "Voice volume 0-1").option("--locale <code>", "Voice locale hint, e.g. en-US").option("--title <text>", "Video title in the HeyGen dashboard").option("--folder <id>", "Destination folder ID").option("--brand-glossary <id>", "Brand glossary ID (pronunciations)").option("--callback-url <url>", "HTTPS webhook URL notified on completion/failure").option("--callback-id <id>", "Caller ID echoed in the webhook payload");
7859
+ addWaitOptions(cmd).action(async (script, opts) => {
7860
+ const logger = createLogger({ verbose: opts.verbose ?? false });
7861
+ const input = toInput(script, opts);
7862
+ buildAvatarVideoRequest({
7863
+ ...input,
7864
+ audio: opts.audio ? PENDING_UPLOAD : void 0,
7865
+ backgroundImage: opts.backgroundImage ? PENDING_UPLOAD : void 0
7866
+ });
7867
+ const client = createHeygenClientFromEnv();
7868
+ if (opts.audio) input.audio = await resolveMediaInput(client, opts.audio, logger);
7869
+ if (opts.backgroundImage) {
7870
+ input.backgroundImage = await resolveMediaInput(client, opts.backgroundImage, logger);
7871
+ }
7872
+ const body = buildAvatarVideoRequest(input);
7873
+ logger.info("Submitting avatar video...");
7874
+ const created = await client.createVideo(body);
7875
+ await completeJobs(client, "video", [created.video_id], opts, logger);
7876
+ });
7877
+ }
7878
+
7879
+ // src/providers/heygen/commands/index.ts
7880
+ function registerHeygenCommands(program2) {
7881
+ const heygen = program2.command("heygen").description(
7882
+ "HeyGen v3: Video Agent prompt-to-video, scripted avatar videos, translation, lipsync, TTS"
5956
7883
  );
5957
- registerAnalyzeCommand(gemini);
5958
- registerTranscribeCommand2(gemini);
5959
- registerExtractCommand(gemini);
5960
- registerGenerateCommand(gemini);
5961
- registerGeminiImageToImageCommand(gemini);
5962
- registerGenerateVideoCommand(gemini);
5963
- registerGeminiImageToVideoCommand(gemini);
5964
- registerGeminiGenerateSpeechCommand(gemini);
7884
+ registerHeygenVideoAgentCommand(heygen);
7885
+ registerHeygenVideoCommand(heygen);
7886
+ registerHeygenStatusCommand(heygen);
7887
+ registerHeygenCatalogCommands(heygen);
7888
+ registerHeygenTranslateCommand(heygen);
7889
+ registerHeygenLipsyncCommand(heygen);
7890
+ registerHeygenTtsCommand(heygen);
7891
+ registerHeygenTemplateCommand(heygen);
7892
+ registerHeygenModelVideoCommand(heygen);
7893
+ registerHeygenRunCommand(heygen);
5965
7894
  }
5966
7895
 
5967
7896
  // src/providers/leonardo/commands/generate.ts
5968
- import fs29 from "fs";
5969
- import path32 from "path";
7897
+ import fs30 from "fs";
7898
+ import path35 from "path";
5970
7899
 
5971
7900
  // src/providers/leonardo/client.ts
5972
7901
  init_errors();
@@ -6039,11 +7968,11 @@ var LeonardoClient = class {
6039
7968
  }
6040
7969
  apiKey;
6041
7970
  baseUrl;
6042
- async request(path54, opts = {}) {
7971
+ async request(path57, opts = {}) {
6043
7972
  const base = this.baseUrl.replace(/\/$/, "");
6044
- const v2Override = path54.startsWith("v2:");
7973
+ const v2Override = path57.startsWith("v2:");
6045
7974
  const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
6046
- const finalPath = v2Override ? path54.slice(3) : path54;
7975
+ const finalPath = v2Override ? path57.slice(3) : path57;
6047
7976
  const url = new URL(finalBase + finalPath);
6048
7977
  if (opts.query) {
6049
7978
  for (const [k, v] of Object.entries(opts.query)) {
@@ -6062,14 +7991,14 @@ var LeonardoClient = class {
6062
7991
  timeoutMs: opts.timeoutMs ?? 12e4
6063
7992
  });
6064
7993
  }
6065
- get(path54, query, logger) {
6066
- return this.request(path54, { method: "GET", query, logger });
7994
+ get(path57, query, logger) {
7995
+ return this.request(path57, { method: "GET", query, logger });
6067
7996
  }
6068
- post(path54, body, logger) {
6069
- return this.request(path54, { method: "POST", body, logger });
7997
+ post(path57, body, logger) {
7998
+ return this.request(path57, { method: "POST", body, logger });
6070
7999
  }
6071
- delete(path54, logger) {
6072
- return this.request(path54, { method: "DELETE", logger });
8000
+ delete(path57, logger) {
8001
+ return this.request(path57, { method: "DELETE", logger });
6073
8002
  }
6074
8003
  };
6075
8004
  function createLeonardoClient() {
@@ -6080,7 +8009,7 @@ function createLeonardoClient() {
6080
8009
  function extFromUrl2(url, fallback = ".png") {
6081
8010
  try {
6082
8011
  const u = new URL(url);
6083
- const e = path32.extname(u.pathname);
8012
+ const e = path35.extname(u.pathname);
6084
8013
  return e.length > 0 ? e : fallback;
6085
8014
  } catch {
6086
8015
  return fallback;
@@ -6165,7 +8094,7 @@ function registerLeonardoGenerateCommand(parent) {
6165
8094
  if (!img) continue;
6166
8095
  const ext = extFromUrl2(img.url);
6167
8096
  const filename = autoFilename(generationId, i + 1, ext);
6168
- const filepath = path32.join(outDir, filename);
8097
+ const filepath = path35.join(outDir, filename);
6169
8098
  try {
6170
8099
  await downloadFile(img.url, filepath);
6171
8100
  saved.push(filepath);
@@ -6175,9 +8104,9 @@ function registerLeonardoGenerateCommand(parent) {
6175
8104
  }
6176
8105
  }
6177
8106
  if (opts.output && saved[0]) {
6178
- const dest = path32.resolve(opts.output);
6179
- fs29.mkdirSync(path32.dirname(dest), { recursive: true });
6180
- fs29.copyFileSync(saved[0], dest);
8107
+ const dest = path35.resolve(opts.output);
8108
+ fs30.mkdirSync(path35.dirname(dest), { recursive: true });
8109
+ fs30.copyFileSync(saved[0], dest);
6181
8110
  logger.success(`Copied to ${dest}`);
6182
8111
  }
6183
8112
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -6189,11 +8118,11 @@ Generated ${finalImages.length} image(s):`);
6189
8118
  }
6190
8119
 
6191
8120
  // src/providers/leonardo/commands/image-to-image.ts
6192
- import fs30 from "fs";
6193
- import path33 from "path";
8121
+ import fs31 from "fs";
8122
+ import path36 from "path";
6194
8123
  function extFromUrl3(url, fallback = ".png") {
6195
8124
  try {
6196
- return path33.extname(new URL(url).pathname) || fallback;
8125
+ return path36.extname(new URL(url).pathname) || fallback;
6197
8126
  } catch {
6198
8127
  return fallback;
6199
8128
  }
@@ -6255,7 +8184,7 @@ function registerLeonardoImageToImageCommand(parent) {
6255
8184
  if (!img) continue;
6256
8185
  const ext = extFromUrl3(img.url);
6257
8186
  const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
6258
- const filepath = path33.join(outDir, filename);
8187
+ const filepath = path36.join(outDir, filename);
6259
8188
  try {
6260
8189
  await downloadFile(img.url, filepath);
6261
8190
  saved.push(filepath);
@@ -6265,9 +8194,9 @@ function registerLeonardoImageToImageCommand(parent) {
6265
8194
  }
6266
8195
  }
6267
8196
  if (opts.output && saved[0]) {
6268
- const dest = path33.resolve(opts.output);
6269
- fs30.mkdirSync(path33.dirname(dest), { recursive: true });
6270
- fs30.copyFileSync(saved[0], dest);
8197
+ const dest = path36.resolve(opts.output);
8198
+ fs31.mkdirSync(path36.dirname(dest), { recursive: true });
8199
+ fs31.copyFileSync(saved[0], dest);
6271
8200
  logger.success(`Copied to ${dest}`);
6272
8201
  }
6273
8202
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -6279,7 +8208,7 @@ Generated ${finalImages.length} image(s):`);
6279
8208
  }
6280
8209
 
6281
8210
  // src/providers/leonardo/commands/video.ts
6282
- import path34 from "path";
8211
+ import path37 from "path";
6283
8212
  function v2DimsFor(resolution) {
6284
8213
  if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
6285
8214
  if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
@@ -6390,7 +8319,7 @@ async function pollAndDownload(client, jobId, opts) {
6390
8319
  logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
6391
8320
  process.exit(1);
6392
8321
  }
6393
- const outPath = opts.output ?? path34.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
8322
+ const outPath = opts.output ?? path37.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
6394
8323
  await downloadFile(videoUrl2, outPath);
6395
8324
  logger.success(`Saved ${outPath}`);
6396
8325
  await maybeDownloadThumb(gen, outPath, {
@@ -6496,7 +8425,7 @@ function registerLeonardoModelsCommand(parent) {
6496
8425
  }
6497
8426
 
6498
8427
  // src/providers/leonardo/commands/status.ts
6499
- import path35 from "path";
8428
+ import path38 from "path";
6500
8429
  var VIDEO_KEYS = ["motionMP4URL", "url"];
6501
8430
  var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
6502
8431
  var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
@@ -6561,7 +8490,7 @@ function registerLeonardoStatusCommand(parent) {
6561
8490
  const outDir = getOutputDir();
6562
8491
  let idx = 0;
6563
8492
  for (const url of videos) {
6564
- const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path35.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
8493
+ const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path38.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
6565
8494
  await downloadFile(url, dest);
6566
8495
  logger.success(`Saved ${dest}`);
6567
8496
  await maybeDownloadThumb(gen, dest, {
@@ -6574,7 +8503,7 @@ function registerLeonardoStatusCommand(parent) {
6574
8503
  for (const url of images) {
6575
8504
  const m = url.match(IMAGE_EXT_RE2);
6576
8505
  const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
6577
- const dest = path35.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
8506
+ const dest = path38.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
6578
8507
  await downloadFile(url, dest);
6579
8508
  logger.success(`Saved ${dest}`);
6580
8509
  idx++;
@@ -6731,8 +8660,8 @@ function sleep3(ms) {
6731
8660
  }
6732
8661
 
6733
8662
  // src/providers/minimax/generators/music.ts
6734
- import fs31 from "fs";
6735
- import path36 from "path";
8663
+ import fs32 from "fs";
8664
+ import path39 from "path";
6736
8665
  async function generateMinimaxMusic(opts) {
6737
8666
  const {
6738
8667
  apiKey,
@@ -6777,14 +8706,14 @@ async function generateMinimaxMusic(opts) {
6777
8706
  }
6778
8707
  const durationMs = resp.extra_info?.music_duration ?? 0;
6779
8708
  const outDir = getOutputDir();
6780
- const dest = path36.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
8709
+ const dest = path39.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
6781
8710
  try {
6782
8711
  if (audioData.startsWith("http")) {
6783
8712
  const bytes = await fetchBytes(audioData);
6784
- fs31.writeFileSync(dest, bytes);
8713
+ fs32.writeFileSync(dest, bytes);
6785
8714
  } else {
6786
8715
  const audioBytes = Buffer.from(audioData, "hex");
6787
- fs31.writeFileSync(dest, audioBytes);
8716
+ fs32.writeFileSync(dest, audioBytes);
6788
8717
  }
6789
8718
  } catch (e) {
6790
8719
  return {
@@ -6795,8 +8724,8 @@ async function generateMinimaxMusic(opts) {
6795
8724
  const durSec = durationMs / 1e3;
6796
8725
  logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
6797
8726
  if (output) {
6798
- fs31.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
6799
- fs31.copyFileSync(dest, output);
8727
+ fs32.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
8728
+ fs32.copyFileSync(dest, output);
6800
8729
  }
6801
8730
  return { status: "success", generatedAudio: dest, durationMs, model };
6802
8731
  }
@@ -6870,8 +8799,8 @@ Generated music: ${result.generatedAudio}`);
6870
8799
  init_errors();
6871
8800
 
6872
8801
  // src/providers/minimax/generators/speech.ts
6873
- import fs32 from "fs";
6874
- import path37 from "path";
8802
+ import fs33 from "fs";
8803
+ import path40 from "path";
6875
8804
  async function generateMinimaxSpeech(opts) {
6876
8805
  const {
6877
8806
  apiKey,
@@ -6918,12 +8847,12 @@ async function generateMinimaxSpeech(opts) {
6918
8847
  const audioBytes = Buffer.from(audioHex, "hex");
6919
8848
  const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
6920
8849
  const outDir = getOutputDir();
6921
- const dest = path37.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
6922
- fs32.writeFileSync(dest, audioBytes);
8850
+ const dest = path40.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
8851
+ fs33.writeFileSync(dest, audioBytes);
6923
8852
  logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
6924
8853
  if (output) {
6925
- fs32.mkdirSync(path37.dirname(path37.resolve(output)), { recursive: true });
6926
- fs32.copyFileSync(dest, output);
8854
+ fs33.mkdirSync(path40.dirname(path40.resolve(output)), { recursive: true });
8855
+ fs33.copyFileSync(dest, output);
6927
8856
  }
6928
8857
  return { status: "success", generatedAudio: dest, model };
6929
8858
  }
@@ -6970,8 +8899,8 @@ Generated audio: ${result.generatedAudio}`);
6970
8899
  init_errors();
6971
8900
 
6972
8901
  // src/providers/minimax/generators/video.ts
6973
- import fs33 from "fs";
6974
- import path38 from "path";
8902
+ import fs34 from "fs";
8903
+ import path41 from "path";
6975
8904
  async function generateMinimaxVideo(opts) {
6976
8905
  const {
6977
8906
  apiKey,
@@ -7017,18 +8946,18 @@ async function generateMinimaxVideo(opts) {
7017
8946
  return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
7018
8947
  }
7019
8948
  const outDir = getOutputDir();
7020
- const dest = path38.join(outDir, `minimax_video_${Date.now()}.mp4`);
8949
+ const dest = path41.join(outDir, `minimax_video_${Date.now()}.mp4`);
7021
8950
  try {
7022
8951
  await downloadMinimaxFile(fileId, apiKey, dest, logger);
7023
8952
  } catch (e) {
7024
8953
  return { status: "error", error: e instanceof Error ? e.message : String(e) };
7025
8954
  }
7026
8955
  const elapsed = (Date.now() - start) / 1e3;
7027
- const stat = fs33.statSync(dest);
8956
+ const stat = fs34.statSync(dest);
7028
8957
  const fileSizeMb = stat.size / (1024 * 1024);
7029
8958
  if (output) {
7030
- fs33.mkdirSync(path38.dirname(path38.resolve(output)), { recursive: true });
7031
- fs33.copyFileSync(dest, output);
8959
+ fs34.mkdirSync(path41.dirname(path41.resolve(output)), { recursive: true });
8960
+ fs34.copyFileSync(dest, output);
7032
8961
  }
7033
8962
  await maybeDownloadThumb(pollResult, dest, {
7034
8963
  skip: thumb === false,
@@ -7090,8 +9019,8 @@ Generated video: ${result.generatedVideo}`);
7090
9019
 
7091
9020
  // src/providers/minimax/generators/image.ts
7092
9021
  init_errors();
7093
- import fs34 from "fs";
7094
- import path39 from "path";
9022
+ import fs35 from "fs";
9023
+ import path42 from "path";
7095
9024
  async function generateMinimaxImage(opts) {
7096
9025
  const {
7097
9026
  apiKey,
@@ -7129,8 +9058,8 @@ async function generateMinimaxImage(opts) {
7129
9058
  try {
7130
9059
  const bytes = await fetchBytes(url);
7131
9060
  const fname = `minimax_image_${Date.now()}_${i}.png`;
7132
- const dest = path39.join(outDir, fname);
7133
- fs34.writeFileSync(dest, bytes);
9061
+ const dest = path42.join(outDir, fname);
9062
+ fs35.writeFileSync(dest, bytes);
7134
9063
  savedFiles.push(dest);
7135
9064
  logger?.success(`Saved: ${dest}`);
7136
9065
  } catch (e) {
@@ -7141,8 +9070,8 @@ async function generateMinimaxImage(opts) {
7141
9070
  }
7142
9071
  }
7143
9072
  if (output && savedFiles[0]) {
7144
- fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
7145
- fs34.copyFileSync(savedFiles[0], output);
9073
+ fs35.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
9074
+ fs35.copyFileSync(savedFiles[0], output);
7146
9075
  }
7147
9076
  return { status: "success", generatedImages: savedFiles, model };
7148
9077
  }
@@ -7181,8 +9110,8 @@ Generated ${finalImages.length} image(s):`);
7181
9110
 
7182
9111
  // src/providers/minimax/commands/image-to-image.ts
7183
9112
  init_errors();
7184
- import fs35 from "fs";
7185
- import path40 from "path";
9113
+ import fs36 from "fs";
9114
+ import path43 from "path";
7186
9115
  function registerMinimaxImageToImageCommand(parent) {
7187
9116
  parent.command("image-to-image").alias("i2i").description(
7188
9117
  "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."
@@ -7237,8 +9166,8 @@ function registerMinimaxImageToImageCommand(parent) {
7237
9166
  if (!url) continue;
7238
9167
  try {
7239
9168
  const bytes = await fetchBytes(url);
7240
- const dest = path40.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
7241
- fs35.writeFileSync(dest, bytes);
9169
+ const dest = path43.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
9170
+ fs36.writeFileSync(dest, bytes);
7242
9171
  saved.push(dest);
7243
9172
  logger.success(`Saved: ${dest}`);
7244
9173
  } catch (e) {
@@ -7249,8 +9178,8 @@ function registerMinimaxImageToImageCommand(parent) {
7249
9178
  }
7250
9179
  }
7251
9180
  if (opts.output && saved[0]) {
7252
- fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
7253
- fs35.copyFileSync(saved[0], opts.output);
9181
+ fs36.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
9182
+ fs36.copyFileSync(saved[0], opts.output);
7254
9183
  logger.success(`Copied to: ${opts.output}`);
7255
9184
  }
7256
9185
  const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
@@ -7384,12 +9313,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
7384
9313
  }
7385
9314
 
7386
9315
  // src/providers/openai/openai-audio.ts
7387
- import fs37 from "fs";
7388
- import path42 from "path";
9316
+ import fs38 from "fs";
9317
+ import path45 from "path";
7389
9318
 
7390
9319
  // src/providers/openai/client.ts
7391
- import fs36 from "fs";
7392
- import path41 from "path";
9320
+ import fs37 from "fs";
9321
+ import path44 from "path";
7393
9322
  init_errors();
7394
9323
  var DEFAULT_TIMEOUT_MS3 = 24e4;
7395
9324
  function requireOpenAIKey() {
@@ -7455,10 +9384,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
7455
9384
  return Buffer.from(await res.arrayBuffer());
7456
9385
  }
7457
9386
  function fileFromPath(filePath) {
7458
- const abs = path41.resolve(filePath);
7459
- if (!fs36.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
7460
- const bytes = fs36.readFileSync(abs);
7461
- return new File([new Uint8Array(bytes)], path41.basename(abs));
9387
+ const abs = path44.resolve(filePath);
9388
+ if (!fs37.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
9389
+ const bytes = fs37.readFileSync(abs);
9390
+ return new File([new Uint8Array(bytes)], path44.basename(abs));
7462
9391
  }
7463
9392
 
7464
9393
  // src/providers/openai/openai-audio.ts
@@ -7497,7 +9426,7 @@ async function resolveAudioReferenceDataUrl(ref) {
7497
9426
  }
7498
9427
  function audioMimeFromReference(ref) {
7499
9428
  const pathname = new URL(ref).pathname;
7500
- const ext = path42.extname(pathname).toLowerCase();
9429
+ const ext = path45.extname(pathname).toLowerCase();
7501
9430
  switch (ext) {
7502
9431
  case ".mp3":
7503
9432
  return "audio/mpeg";
@@ -7516,12 +9445,12 @@ function audioMimeFromReference(ref) {
7516
9445
  async function generateOpenAISpeech(opts) {
7517
9446
  const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
7518
9447
  const ext = extFromAudioFormat(opts.outputFormat);
7519
- const dest = path42.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
7520
- fs37.writeFileSync(dest, bytes);
9448
+ const dest = path45.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
9449
+ fs38.writeFileSync(dest, bytes);
7521
9450
  opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
7522
9451
  if (opts.output) {
7523
- fs37.mkdirSync(path42.dirname(path42.resolve(opts.output)), { recursive: true });
7524
- fs37.copyFileSync(dest, opts.output);
9452
+ fs38.mkdirSync(path45.dirname(path45.resolve(opts.output)), { recursive: true });
9453
+ fs38.copyFileSync(dest, opts.output);
7525
9454
  }
7526
9455
  return dest;
7527
9456
  }
@@ -7543,9 +9472,9 @@ async function transcribeOpenAIAudio(opts) {
7543
9472
  );
7544
9473
  const text = typeof data.text === "string" ? data.text : void 0;
7545
9474
  const ext = opts.format === "text" ? "txt" : "json";
7546
- const dest = opts.output ? path42.resolve(opts.output) : path42.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
7547
- fs37.mkdirSync(path42.dirname(dest), { recursive: true });
7548
- fs37.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
9475
+ const dest = opts.output ? path45.resolve(opts.output) : path45.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
9476
+ fs38.mkdirSync(path45.dirname(dest), { recursive: true });
9477
+ fs38.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
7549
9478
  opts.logger?.success(`Saved transcript: ${dest}`);
7550
9479
  return { path: dest, text };
7551
9480
  }
@@ -7572,9 +9501,9 @@ Generated audio: ${dest}`);
7572
9501
  }
7573
9502
 
7574
9503
  // src/providers/openai/codex-image-driver.ts
7575
- import fs38 from "fs";
9504
+ import fs39 from "fs";
7576
9505
  import os3 from "os";
7577
- import path43 from "path";
9506
+ import path46 from "path";
7578
9507
  import { execa } from "execa";
7579
9508
  init_errors();
7580
9509
  var defaultRunner = async (cmd, args, opts) => {
@@ -7623,12 +9552,12 @@ function sanitizeCodexEnv() {
7623
9552
  async function runCodexImageGeneration(opts) {
7624
9553
  const runner = opts.runner ?? defaultRunner;
7625
9554
  const ext = extFromImageFormat(opts.outputFormat);
7626
- const dest = opts.output ? path43.resolve(opts.output) : path43.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
7627
- fs38.mkdirSync(path43.dirname(dest), { recursive: true });
7628
- const tmp = fs38.mkdtempSync(path43.join(os3.tmpdir(), "multix-codex-image-"));
7629
- const schema = path43.join(tmp, "schema.json");
7630
- const result = path43.join(tmp, "result.json");
7631
- fs38.writeFileSync(
9555
+ const dest = opts.output ? path46.resolve(opts.output) : path46.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
9556
+ fs39.mkdirSync(path46.dirname(dest), { recursive: true });
9557
+ const tmp = fs39.mkdtempSync(path46.join(os3.tmpdir(), "multix-codex-image-"));
9558
+ const schema = path46.join(tmp, "schema.json");
9559
+ const result = path46.join(tmp, "result.json");
9560
+ fs39.writeFileSync(
7632
9561
  schema,
7633
9562
  JSON.stringify({
7634
9563
  type: "object",
@@ -7658,7 +9587,7 @@ async function runCodexImageGeneration(opts) {
7658
9587
  if (res.exitCode !== 0) {
7659
9588
  throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
7660
9589
  }
7661
- if (!fs38.existsSync(dest) || fs38.statSync(dest).size === 0) {
9590
+ if (!fs39.existsSync(dest) || fs39.statSync(dest).size === 0) {
7662
9591
  throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
7663
9592
  }
7664
9593
  return { path: dest };
@@ -7675,8 +9604,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
7675
9604
 
7676
9605
  // src/providers/openai/openai-image.ts
7677
9606
  init_errors();
7678
- import fs39 from "fs";
7679
- import path44 from "path";
9607
+ import fs40 from "fs";
9608
+ import path47 from "path";
7680
9609
  function buildImageGenerationBody(opts) {
7681
9610
  return {
7682
9611
  model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
@@ -7709,14 +9638,14 @@ function saveOpenAIImages(opts) {
7709
9638
  for (let i = 0; i < items.length; i++) {
7710
9639
  const b64 = items[i]?.b64_json;
7711
9640
  if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
7712
- const dest = path44.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
7713
- fs39.writeFileSync(dest, Buffer.from(b64, "base64"));
9641
+ const dest = path47.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
9642
+ fs40.writeFileSync(dest, Buffer.from(b64, "base64"));
7714
9643
  saved.push(dest);
7715
9644
  opts.logger?.success(`Saved: ${dest}`);
7716
9645
  }
7717
9646
  if (opts.output && saved[0]) {
7718
- fs39.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
7719
- fs39.copyFileSync(saved[0], opts.output);
9647
+ fs40.mkdirSync(path47.dirname(path47.resolve(opts.output)), { recursive: true });
9648
+ fs40.copyFileSync(saved[0], opts.output);
7720
9649
  opts.logger?.success(`Copied to: ${opts.output}`);
7721
9650
  }
7722
9651
  return saved;
@@ -7725,7 +9654,7 @@ async function resolveImageRefBytes(ref) {
7725
9654
  if (isHttpUrl(ref)) {
7726
9655
  const bytes2 = await fetchBytes(ref);
7727
9656
  const pathname = new URL(ref).pathname;
7728
- const filename = path44.basename(pathname) || "reference.png";
9657
+ const filename = path47.basename(pathname) || "reference.png";
7729
9658
  return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
7730
9659
  }
7731
9660
  const file = fileFromPath(ref);
@@ -7770,7 +9699,7 @@ function sniffImageMime2(buf, name) {
7770
9699
  if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
7771
9700
  if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
7772
9701
  return "image/webp";
7773
- const ext = path44.extname(name).toLowerCase();
9702
+ const ext = path47.extname(name).toLowerCase();
7774
9703
  if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
7775
9704
  if (ext === ".webp") return "image/webp";
7776
9705
  return "image/png";
@@ -7822,7 +9751,7 @@ function registerOpenAIImageToImageCommand(parent) {
7822
9751
  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(
7823
9752
  "--ref <path>",
7824
9753
  "Reference image path or URL; repeatable",
7825
- collect5,
9754
+ collect6,
7826
9755
  []
7827
9756
  ).option("--driver <driver>", "api|codex|auto", "auto").option(
7828
9757
  "-m, --model <id>",
@@ -7865,7 +9794,7 @@ Generated ${finalImages.length} image(s):`);
7865
9794
  for (const f of finalImages) console.log(` ${f}`);
7866
9795
  });
7867
9796
  }
7868
- function collect5(value, prev) {
9797
+ function collect6(value, prev) {
7869
9798
  return [...prev, value];
7870
9799
  }
7871
9800
 
@@ -7874,12 +9803,12 @@ function registerOpenAITranscribeCommand(parent) {
7874
9803
  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(
7875
9804
  "--known-speaker-name <name>",
7876
9805
  "Known speaker name; repeatable",
7877
- collect6,
9806
+ collect7,
7878
9807
  []
7879
9808
  ).option(
7880
9809
  "--known-speaker-reference <path>",
7881
9810
  "2-10s reference audio; repeatable",
7882
- collect6,
9811
+ collect7,
7883
9812
  []
7884
9813
  ).option("--output <path>", "Save transcript to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
7885
9814
  const logger = createLogger({ verbose: opts.verbose ?? false });
@@ -7898,7 +9827,7 @@ function registerOpenAITranscribeCommand(parent) {
7898
9827
  ${result.text.trim()}`);
7899
9828
  });
7900
9829
  }
7901
- function collect6(value, prev) {
9830
+ function collect7(value, prev) {
7902
9831
  return [...prev, value];
7903
9832
  }
7904
9833
 
@@ -7914,8 +9843,8 @@ function registerOpenAICommands(program2) {
7914
9843
  }
7915
9844
 
7916
9845
  // src/providers/openrouter/client.ts
7917
- import fs40 from "fs";
7918
- import path45 from "path";
9846
+ import fs41 from "fs";
9847
+ import path48 from "path";
7919
9848
  init_errors();
7920
9849
 
7921
9850
  // src/providers/openrouter/payload.ts
@@ -8079,8 +10008,8 @@ async function generateOpenRouterImage(opts) {
8079
10008
  if (!url) continue;
8080
10009
  try {
8081
10010
  const bytes = await fetchBytes(url);
8082
- const dest = path45.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
8083
- fs40.writeFileSync(dest, bytes);
10011
+ const dest = path48.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
10012
+ fs41.writeFileSync(dest, bytes);
8084
10013
  savedFiles.push(dest);
8085
10014
  logger?.success(`Saved: ${dest}`);
8086
10015
  } catch (e) {
@@ -8091,8 +10020,8 @@ async function generateOpenRouterImage(opts) {
8091
10020
  }
8092
10021
  }
8093
10022
  if (output && savedFiles[0]) {
8094
- fs40.mkdirSync(path45.dirname(path45.resolve(output)), { recursive: true });
8095
- fs40.writeFileSync(output, fs40.readFileSync(savedFiles[0]));
10023
+ fs41.mkdirSync(path48.dirname(path48.resolve(output)), { recursive: true });
10024
+ fs41.writeFileSync(output, fs41.readFileSync(savedFiles[0]));
8096
10025
  }
8097
10026
  return { status: "success", generatedImages: savedFiles, model: usedModel };
8098
10027
  }
@@ -8133,8 +10062,8 @@ Generated ${finalImages.length} image(s):`);
8133
10062
  }
8134
10063
 
8135
10064
  // src/providers/openrouter/commands/image-to-image.ts
8136
- import fs41 from "fs";
8137
- import path46 from "path";
10065
+ import fs42 from "fs";
10066
+ import path49 from "path";
8138
10067
  init_errors();
8139
10068
  var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
8140
10069
  var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
@@ -8145,7 +10074,7 @@ function registerOpenRouterImageToImageCommand(parent) {
8145
10074
  ).requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
8146
10075
  "--ref <path>",
8147
10076
  "Reference image (URL or local path); repeatable (most models accept 1)",
8148
- collect7,
10077
+ collect8,
8149
10078
  []
8150
10079
  ).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").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
8151
10080
  async (opts) => {
@@ -8187,7 +10116,7 @@ Generated ${finalImages.length} image(s):`);
8187
10116
  }
8188
10117
  );
8189
10118
  }
8190
- function collect7(value, prev) {
10119
+ function collect8(value, prev) {
8191
10120
  return [...prev, value];
8192
10121
  }
8193
10122
  function parseStrength(raw) {
@@ -8226,14 +10155,14 @@ async function runImageToImage(opts) {
8226
10155
  const url = parsed.urls[i];
8227
10156
  if (!url) continue;
8228
10157
  const bytes = await fetchBytes(url);
8229
- const dest = path46.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
8230
- fs41.writeFileSync(dest, bytes);
10158
+ const dest = path49.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
10159
+ fs42.writeFileSync(dest, bytes);
8231
10160
  saved.push(dest);
8232
10161
  opts.logger.success(`Saved: ${dest}`);
8233
10162
  }
8234
10163
  if (opts.output && saved[0]) {
8235
- fs41.mkdirSync(path46.dirname(path46.resolve(opts.output)), { recursive: true });
8236
- fs41.copyFileSync(saved[0], opts.output);
10164
+ fs42.mkdirSync(path49.dirname(path49.resolve(opts.output)), { recursive: true });
10165
+ fs42.copyFileSync(saved[0], opts.output);
8237
10166
  opts.logger.success(`Copied to: ${opts.output}`);
8238
10167
  }
8239
10168
  return saved;
@@ -8278,13 +10207,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
8278
10207
  }
8279
10208
 
8280
10209
  // src/providers/openrouter/commands/video-status.ts
8281
- import fs42 from "fs";
8282
- import path47 from "path";
10210
+ import fs43 from "fs";
10211
+ import path50 from "path";
8283
10212
  async function downloadOpenRouterVideo(opts) {
8284
10213
  const { jobId, logger, source } = opts;
8285
10214
  const apiKey = requireOpenRouterKey();
8286
10215
  const url = videoContentUrl(jobId);
8287
- const dest = path47.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
10216
+ const dest = path50.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
8288
10217
  const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
8289
10218
  if (!r.ok) {
8290
10219
  logger.error(`Download HTTP ${r.status}`);
@@ -8294,14 +10223,14 @@ async function downloadOpenRouterVideo(opts) {
8294
10223
  logger.error("Empty response body");
8295
10224
  process.exit(1);
8296
10225
  }
8297
- fs42.mkdirSync(path47.dirname(dest), { recursive: true });
8298
- const ws = fs42.createWriteStream(dest);
8299
- const { pipeline: pipeline3 } = await import("stream/promises");
8300
- await pipeline3(r.body, ws);
10226
+ fs43.mkdirSync(path50.dirname(dest), { recursive: true });
10227
+ const ws = fs43.createWriteStream(dest);
10228
+ const { pipeline: pipeline4 } = await import("stream/promises");
10229
+ await pipeline4(r.body, ws);
8301
10230
  logger.success(`Saved ${dest}`);
8302
10231
  if (opts.output) {
8303
- fs42.mkdirSync(path47.dirname(path47.resolve(opts.output)), { recursive: true });
8304
- fs42.copyFileSync(dest, opts.output);
10232
+ fs43.mkdirSync(path50.dirname(path50.resolve(opts.output)), { recursive: true });
10233
+ fs43.copyFileSync(dest, opts.output);
8305
10234
  logger.success(`Copied to ${opts.output}`);
8306
10235
  }
8307
10236
  await maybeDownloadThumb(source, dest, {
@@ -8494,8 +10423,8 @@ function parseModelRef(ref, explicitVersion) {
8494
10423
  }
8495
10424
  return { kind: "version", version: trimmed };
8496
10425
  }
8497
- const [path54, inlineVersion, ...rest] = trimmed.split(":");
8498
- const [owner, name, ...extra] = (path54 ?? "").split("/");
10426
+ const [path57, inlineVersion, ...rest] = trimmed.split(":");
10427
+ const [owner, name, ...extra] = (path57 ?? "").split("/");
8499
10428
  if (rest.length > 0 || extra.length > 0 || !owner || !name) {
8500
10429
  throw new ValidationError(
8501
10430
  `Invalid Replicate model "${ref}" \u2014 expected owner/name, owner/name:version, or a 64-char version id`
@@ -8625,7 +10554,7 @@ function assertSucceeded(prediction2) {
8625
10554
  PROVIDER
8626
10555
  );
8627
10556
  }
8628
- var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
10557
+ var defaultSleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8629
10558
  var ReplicateClient = class {
8630
10559
  apiKey;
8631
10560
  baseUrl;
@@ -8636,15 +10565,15 @@ var ReplicateClient = class {
8636
10565
  this.apiKey = opts.apiKey;
8637
10566
  this.baseUrl = (opts.baseUrl || DEFAULT_REPLICATE_BASE_URL).replace(/\/+$/, "");
8638
10567
  this.doFetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
8639
- this.sleep = opts.sleep ?? defaultSleep2;
10568
+ this.sleep = opts.sleep ?? defaultSleep4;
8640
10569
  }
8641
10570
  headers(extra = {}) {
8642
10571
  return { accept: "application/json", authorization: `Bearer ${this.apiKey}`, ...extra };
8643
10572
  }
8644
10573
  /** Raw request: throws HttpError so polling can classify transient failures. */
8645
- raw(path54, method, body, headers2, timeoutMs = REQUEST_TIMEOUT_MS) {
10574
+ raw(path57, method, body, headers2, timeoutMs = REQUEST_TIMEOUT_MS) {
8646
10575
  return fetchJson({
8647
- url: `${this.baseUrl}${path54}`,
10576
+ url: `${this.baseUrl}${path57}`,
8648
10577
  method,
8649
10578
  headers: this.headers(headers2),
8650
10579
  body,
@@ -8656,7 +10585,7 @@ var ReplicateClient = class {
8656
10585
  async createPrediction(opts) {
8657
10586
  const { model, input } = opts;
8658
10587
  const isOfficial = model.kind === "official";
8659
- const path54 = isOfficial ? `/models/${model.owner}/${model.name}/predictions` : "/predictions";
10588
+ const path57 = isOfficial ? `/models/${model.owner}/${model.name}/predictions` : "/predictions";
8660
10589
  const body = isOfficial ? { input } : { version: model.version, input };
8661
10590
  const subject = isOfficial ? `model ${model.owner}/${model.name}` : `model version ${model.version}`;
8662
10591
  const headers2 = {};
@@ -8668,7 +10597,7 @@ var ReplicateClient = class {
8668
10597
  }
8669
10598
  for (let attempt = 0; ; attempt++) {
8670
10599
  try {
8671
- const res = await this.raw(path54, "POST", body, headers2, timeoutMs);
10600
+ const res = await this.raw(path57, "POST", body, headers2, timeoutMs);
8672
10601
  return validatePrediction(res);
8673
10602
  } catch (error) {
8674
10603
  if (error instanceof HttpError && error.status === 429 && attempt < CREATE_MAX_RETRIES) {
@@ -8827,11 +10756,11 @@ init_errors();
8827
10756
  // src/providers/replicate/node-outputs.ts
8828
10757
  init_errors();
8829
10758
  init_redact();
8830
- import { once as once2 } from "events";
8831
- import fs43 from "fs";
8832
- import path48 from "path";
10759
+ import { once as once3 } from "events";
10760
+ import fs44 from "fs";
10761
+ import path51 from "path";
8833
10762
  import { Readable } from "stream";
8834
- import { pipeline as pipeline2 } from "stream/promises";
10763
+ import { pipeline as pipeline3 } from "stream/promises";
8835
10764
 
8836
10765
  // src/providers/replicate/output.ts
8837
10766
  init_redact();
@@ -8843,15 +10772,15 @@ function isMediaRef(value) {
8843
10772
  function extractOutput(output) {
8844
10773
  const result = { media: [], texts: [] };
8845
10774
  const seen = /* @__PURE__ */ new Set();
8846
- const visit = (value, path54) => {
10775
+ const visit = (value, path57) => {
8847
10776
  if (value === null || value === void 0) return;
8848
10777
  if (typeof value === "string") {
8849
- if (isMediaRef(value)) result.media.push({ url: value, path: path54 || "output" });
8850
- else if (value.length > 0) result.texts.push(path54 ? `${path54}: ${value}` : value);
10778
+ if (isMediaRef(value)) result.media.push({ url: value, path: path57 || "output" });
10779
+ else if (value.length > 0) result.texts.push(path57 ? `${path57}: ${value}` : value);
8851
10780
  return;
8852
10781
  }
8853
10782
  if (typeof value !== "object") {
8854
- result.texts.push(path54 ? `${path54}: ${String(value)}` : String(value));
10783
+ result.texts.push(path57 ? `${path57}: ${String(value)}` : String(value));
8855
10784
  return;
8856
10785
  }
8857
10786
  if (seen.has(value)) return;
@@ -8859,14 +10788,14 @@ function extractOutput(output) {
8859
10788
  if (Array.isArray(value)) {
8860
10789
  if (value.length > 0 && value.every((v) => typeof v === "string" && !isMediaRef(v))) {
8861
10790
  const joined = value.join("");
8862
- if (joined.length > 0) result.texts.push(path54 ? `${path54}: ${joined}` : joined);
10791
+ if (joined.length > 0) result.texts.push(path57 ? `${path57}: ${joined}` : joined);
8863
10792
  return;
8864
10793
  }
8865
- value.forEach((item, i) => visit(item, `${path54}[${i}]`));
10794
+ value.forEach((item, i) => visit(item, `${path57}[${i}]`));
8866
10795
  return;
8867
10796
  }
8868
10797
  for (const [key, val] of Object.entries(value)) {
8869
- visit(val, path54 ? `${path54}.${key}` : key);
10798
+ visit(val, path57 ? `${path57}.${key}` : key);
8870
10799
  }
8871
10800
  };
8872
10801
  visit(output, "");
@@ -8984,30 +10913,30 @@ var DOWNLOAD_TIMEOUT_MS = 3e5;
8984
10913
  function isDirTarget(output) {
8985
10914
  if (/[\\/]$/.test(output)) return true;
8986
10915
  try {
8987
- return fs43.statSync(output).isDirectory();
10916
+ return fs44.statSync(output).isDirectory();
8988
10917
  } catch {
8989
10918
  return false;
8990
10919
  }
8991
10920
  }
8992
10921
  function resolveDestination(predictionId, index, count, ext, opts) {
8993
- if (!opts.output) return path48.join(opts.outDir, outputFilename(predictionId, index, ext));
8994
- const output = path48.resolve(opts.output);
8995
- if (isDirTarget(opts.output)) return path48.join(output, outputFilename(predictionId, index, ext));
8996
- const givenExt = path48.extname(output);
10922
+ if (!opts.output) return path51.join(opts.outDir, outputFilename(predictionId, index, ext));
10923
+ const output = path51.resolve(opts.output);
10924
+ if (isDirTarget(opts.output)) return path51.join(output, outputFilename(predictionId, index, ext));
10925
+ const givenExt = path51.extname(output);
8997
10926
  if (count === 1) return givenExt ? output : `${output}${ext}`;
8998
10927
  const base = givenExt ? output.slice(0, -givenExt.length) : output;
8999
10928
  return `${base}-${String(index + 1).padStart(2, "0")}${ext}`;
9000
10929
  }
9001
10930
  async function writeResponse(response, dest) {
9002
10931
  if (!response.body) throw new HttpError(0, "Response body is null", response.url);
9003
- const fileStream = fs43.createWriteStream(dest);
10932
+ const fileStream = fs44.createWriteStream(dest);
9004
10933
  try {
9005
- await pipeline2(
10934
+ await pipeline3(
9006
10935
  Readable.fromWeb(response.body),
9007
10936
  fileStream
9008
10937
  );
9009
10938
  } catch (err2) {
9010
- if (!fileStream.closed) await once2(fileStream, "close");
10939
+ if (!fileStream.closed) await once3(fileStream, "close");
9011
10940
  throw err2;
9012
10941
  }
9013
10942
  }
@@ -9024,8 +10953,8 @@ async function saveOne(item, predictionId, index, count, opts) {
9024
10953
  extFromUrl4(item.url) ?? ".bin",
9025
10954
  opts
9026
10955
  );
9027
- fs43.mkdirSync(path48.dirname(dest2), { recursive: true });
9028
- fs43.writeFileSync(dest2, bytes);
10956
+ fs44.mkdirSync(path51.dirname(dest2), { recursive: true });
10957
+ fs44.writeFileSync(dest2, bytes);
9029
10958
  return dest2;
9030
10959
  }
9031
10960
  const doFetch = opts.fetch ?? ((url, init) => globalThis.fetch(url, init));
@@ -9041,11 +10970,11 @@ async function saveOne(item, predictionId, index, count, opts) {
9041
10970
  }
9042
10971
  const ext = extFromUrl4(item.url) ?? extFromContentType(response.headers.get("content-type")) ?? ".bin";
9043
10972
  const dest = resolveDestination(predictionId, index, count, ext, opts);
9044
- fs43.mkdirSync(path48.dirname(dest), { recursive: true });
10973
+ fs44.mkdirSync(path51.dirname(dest), { recursive: true });
9045
10974
  try {
9046
10975
  await writeResponse(response, dest);
9047
10976
  } catch (err2) {
9048
- fs43.rmSync(dest, { force: true });
10977
+ fs44.rmSync(dest, { force: true });
9049
10978
  throw err2;
9050
10979
  }
9051
10980
  return dest;
@@ -9081,7 +11010,7 @@ function addFlowOptions(cmd, defaultTimeoutMs) {
9081
11010
  "Ask Replicate to hold the create request open (1-60s, default 60); keeps polling if still running"
9082
11011
  ).option("--wait-timeout <ms>", "Polling timeout in milliseconds", String(defaultTimeoutMs)).option("--download", "Download media outputs (default)").option("--no-download", "Print output URLs instead of downloading").option("--output <path>", "Output file path, or a directory for multiple outputs").option("-v, --verbose", "Verbose logging");
9083
11012
  }
9084
- function parsePositiveInt(raw, flag, max) {
11013
+ function parsePositiveInt2(raw, flag, max) {
9085
11014
  const n = Number(raw);
9086
11015
  if (!Number.isInteger(n) || n <= 0 || max !== void 0 && n > max) {
9087
11016
  throw new ValidationError(
@@ -9093,15 +11022,15 @@ function parsePositiveInt(raw, flag, max) {
9093
11022
  function parsePreferWait(value) {
9094
11023
  if (value === void 0 || value === false) return void 0;
9095
11024
  if (value === true) return 60;
9096
- return parsePositiveInt(value, "--prefer-wait", 60);
11025
+ return parsePositiveInt2(value, "--prefer-wait", 60);
9097
11026
  }
9098
- function resumeCommand2(id) {
11027
+ function resumeCommand3(id) {
9099
11028
  return `multix replicate status ${id} --wait --download`;
9100
11029
  }
9101
11030
  function printAsyncHandle(prediction2) {
9102
11031
  console.log(`Prediction submitted: ${prediction2.id} (status: ${prediction2.status})`);
9103
11032
  console.log(` Check: multix replicate status ${prediction2.id}`);
9104
- console.log(` Resume: ${resumeCommand2(prediction2.id)}`);
11033
+ console.log(` Resume: ${resumeCommand3(prediction2.id)}`);
9105
11034
  console.log(` Cancel: multix replicate cancel ${prediction2.id}`);
9106
11035
  }
9107
11036
  async function reportOutputs(prediction2, opts) {
@@ -9135,7 +11064,7 @@ async function completePrediction(client, prediction2, opts) {
9135
11064
  }
9136
11065
  const final = await client.waitForPrediction(prediction2, {
9137
11066
  timeoutMs: opts.waitTimeoutMs,
9138
- resumeHint: `Resume with: ${resumeCommand2(id)}`,
11067
+ resumeHint: `Resume with: ${resumeCommand3(id)}`,
9139
11068
  onPoll: (p, attempt) => {
9140
11069
  if (p.status !== lastStatus) opts.logger.debug(`attempt ${attempt}: ${p.status}`);
9141
11070
  lastStatus = p.status;
@@ -9152,7 +11081,7 @@ async function executePrediction2(params) {
9152
11081
  if (opts.async && opts.preferWait) {
9153
11082
  throw new ValidationError("--async and --prefer-wait are mutually exclusive");
9154
11083
  }
9155
- const waitTimeoutMs = parsePositiveInt(opts.waitTimeout, "--wait-timeout");
11084
+ const waitTimeoutMs = parsePositiveInt2(opts.waitTimeout, "--wait-timeout");
9156
11085
  const preferWaitSeconds = parsePreferWait(opts.preferWait);
9157
11086
  logger.info(`Submitting to Replicate ${describeModelRef(model)}...`);
9158
11087
  const prediction2 = await client.createPrediction({ model, input, preferWaitSeconds });
@@ -9186,7 +11115,7 @@ function buildImageInput(prompt, opts) {
9186
11115
  ...extra,
9187
11116
  prompt,
9188
11117
  ...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
9189
- ...opts.numOutputs ? { num_outputs: parsePositiveInt(opts.numOutputs, "--num-outputs", 10) } : {},
11118
+ ...opts.numOutputs ? { num_outputs: parsePositiveInt2(opts.numOutputs, "--num-outputs", 10) } : {},
9190
11119
  ...opts.seed !== void 0 ? { seed: parseSeed(opts.seed) } : {},
9191
11120
  ...opts.outputFormat ? { output_format: opts.outputFormat } : {}
9192
11121
  };
@@ -9223,8 +11152,8 @@ function registerReplicateImageCommand(parent) {
9223
11152
 
9224
11153
  // src/providers/replicate/node-input-files.ts
9225
11154
  init_errors();
9226
- import fs44 from "fs";
9227
- import path49 from "path";
11155
+ import fs45 from "fs";
11156
+ import path52 from "path";
9228
11157
  var DATA_URI_MAX_BYTES = 256 * 1024;
9229
11158
  var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
9230
11159
  function formatMb2(bytes) {
@@ -9232,10 +11161,10 @@ function formatMb2(bytes) {
9232
11161
  }
9233
11162
  async function resolveFileInput(value, client, logger) {
9234
11163
  if (isMediaRef(value)) return value;
9235
- const filePath = path49.resolve(value);
11164
+ const filePath = path52.resolve(value);
9236
11165
  let stat;
9237
11166
  try {
9238
- stat = fs44.statSync(filePath);
11167
+ stat = fs45.statSync(filePath);
9239
11168
  } catch {
9240
11169
  throw new ValidationError(`Input file not found: ${value} (pass a local path or an https URL)`);
9241
11170
  }
@@ -9246,9 +11175,9 @@ async function resolveFileInput(value, client, logger) {
9246
11175
  `Input file ${value} is ${formatMb2(stat.size)}; Replicate accepts uploads under 100MB. Host it and pass an https URL instead.`
9247
11176
  );
9248
11177
  }
9249
- const filename = path49.basename(filePath);
11178
+ const filename = path52.basename(filePath);
9250
11179
  const contentType = mimeFromFilename(filename);
9251
- const bytes = fs44.readFileSync(filePath);
11180
+ const bytes = fs45.readFileSync(filePath);
9252
11181
  if (stat.size <= DATA_URI_MAX_BYTES) {
9253
11182
  logger.debug(`Inlining ${filename} (${stat.size} bytes) as a data URI`);
9254
11183
  return `data:${contentType};base64,${bytes.toString("base64")}`;
@@ -9275,14 +11204,14 @@ function parseFileAssignments(values) {
9275
11204
  }
9276
11205
 
9277
11206
  // src/providers/replicate/commands/run.ts
9278
- var collect8 = (value, previous = []) => [...previous, value];
11207
+ var collect9 = (value, previous = []) => [...previous, value];
9279
11208
  function registerReplicateRunCommand(parent) {
9280
11209
  const cmd = parent.command("run <model>").description(
9281
11210
  "Run any Replicate model: owner/name (official), owner/name:version, or a version id"
9282
11211
  ).requiredOption("--input <json>", "Model input as JSON, or @path/to/input.json").option("--version <id>", "Explicit model version id (uses the versioned endpoint)").option(
9283
11212
  "--file <key=path>",
9284
11213
  "Set input <key> to a local file or URL (uploaded if needed); repeatable",
9285
- collect8
11214
+ collect9
9286
11215
  );
9287
11216
  addFlowOptions(cmd, 6e5).action(
9288
11217
  async (model, opts) => {
@@ -9306,7 +11235,7 @@ function registerReplicateStatusCommand(parent) {
9306
11235
  const client = createReplicateClient();
9307
11236
  if (opts.wait) {
9308
11237
  await completePrediction(client, predictionId, {
9309
- waitTimeoutMs: parsePositiveInt(opts.waitTimeout, "--wait-timeout"),
11238
+ waitTimeoutMs: parsePositiveInt2(opts.waitTimeout, "--wait-timeout"),
9310
11239
  logger,
9311
11240
  download: Boolean(opts.download),
9312
11241
  output: opts.output
@@ -9320,7 +11249,7 @@ function registerReplicateStatusCommand(parent) {
9320
11249
  }
9321
11250
  console.log(JSON.stringify(summarizePrediction(prediction2), null, 2));
9322
11251
  if (!isTerminalStatus2(prediction2.status)) {
9323
- logger.info(`Still running. Resume with: ${resumeCommand2(prediction2.id)}`);
11252
+ logger.info(`Still running. Resume with: ${resumeCommand3(prediction2.id)}`);
9324
11253
  } else if (prediction2.status !== "succeeded") {
9325
11254
  throw new ReplicatePredictionError(prediction2);
9326
11255
  }
@@ -9346,7 +11275,7 @@ function buildVideoInput(prompt, opts) {
9346
11275
  return {
9347
11276
  ...extra,
9348
11277
  prompt,
9349
- ...opts.duration ? { duration: parsePositiveInt(opts.duration, "--duration", 60) } : {},
11278
+ ...opts.duration ? { duration: parsePositiveInt2(opts.duration, "--duration", 60) } : {},
9350
11279
  ...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
9351
11280
  ...opts.resolution ? { resolution: opts.resolution } : {},
9352
11281
  ...opts.seed !== void 0 ? { seed: parseSeed(opts.seed) } : {}
@@ -9375,7 +11304,7 @@ function registerReplicateCommands(program2) {
9375
11304
  }
9376
11305
 
9377
11306
  // src/commands/check.ts
9378
- import fs45 from "fs";
11307
+ import fs46 from "fs";
9379
11308
  import os4 from "os";
9380
11309
 
9381
11310
  // src/commands/check-helpers/binary-check.ts
@@ -9434,6 +11363,7 @@ To configure multix, set at least one provider API key:
9434
11363
  fal.ai : https://fal.ai/dashboard/keys
9435
11364
  Atlas Cloud : https://www.atlascloud.ai/console/api-keys
9436
11365
  Replicate : https://replicate.com/account/api-tokens
11366
+ HeyGen : https://app.heygen.com/developers/api
9437
11367
 
9438
11368
  Option A \u2014 user global config (recommended):
9439
11369
  echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
@@ -9515,6 +11445,12 @@ var PROVIDERS = [
9515
11445
  envFallback: "REPLICATE_API_KEY",
9516
11446
  optional: true,
9517
11447
  link: "https://replicate.com/account/api-tokens"
11448
+ },
11449
+ {
11450
+ name: "HeyGen",
11451
+ envPrimary: "HEYGEN_API_KEY",
11452
+ optional: true,
11453
+ link: "https://app.heygen.com/developers/api"
9518
11454
  }
9519
11455
  ];
9520
11456
  function resolveProviderKey(p) {
@@ -9566,7 +11502,7 @@ function registerCheckCommand(program2) {
9566
11502
  }
9567
11503
  if (!legacyClaudeEnvEnabled()) {
9568
11504
  const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os4.homedir();
9569
- const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs45.existsSync(f));
11505
+ const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs46.existsSync(f));
9570
11506
  if (legacy.length > 0) {
9571
11507
  logger.info(
9572
11508
  `Found ${legacy.length} legacy .claude env file(s); set MULTIX_LEGACY_CLAUDE_ENV=1 to load them`
@@ -9670,9 +11606,8 @@ function registerCheckCommand(program2) {
9670
11606
  }
9671
11607
 
9672
11608
  // src/commands/doc/convert.ts
9673
- init_errors();
9674
- import fs46 from "fs";
9675
- import path50 from "path";
11609
+ import fs47 from "fs";
11610
+ import path53 from "path";
9676
11611
 
9677
11612
  // src/commands/doc/default-prompt.ts
9678
11613
  var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
@@ -9688,27 +11623,45 @@ Requirements:
9688
11623
  Output only the markdown content without any preamble or explanation.`;
9689
11624
 
9690
11625
  // src/commands/doc/convert.ts
11626
+ function autoNameBase(input) {
11627
+ if (input === STDIN_ARG) return "stdin";
11628
+ return path53.basename(input, path53.extname(input));
11629
+ }
9691
11630
  async function convertDocuments(opts) {
9692
- const { inputs, output, autoName = false, verbose, logger } = opts;
11631
+ const { output, autoName = false, verbose, logger } = opts;
9693
11632
  const model = opts.model ?? DOC_MODEL_DEFAULT;
9694
11633
  const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
9695
- for (const f of inputs) {
9696
- if (!fs46.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
9697
- }
9698
- let outputPath;
9699
- if (output) {
9700
- outputPath = path50.resolve(output);
9701
- } else if (autoName && inputs.length === 1 && inputs[0]) {
9702
- const base = path50.basename(inputs[0], path50.extname(inputs[0]));
9703
- outputPath = path50.join(getOutputDir(), `${base}-extraction.md`);
9704
- } else {
9705
- outputPath = path50.join(getOutputDir(), "document-extraction.md");
11634
+ const { inputs, cleanup } = await resolveFileInputs(opts.inputs, {
11635
+ stdinExt: opts.stdinExt,
11636
+ input: opts.stdin,
11637
+ flag: "--input",
11638
+ example: "cat report.pdf | multix doc convert --input -",
11639
+ extExample: "pdf"
11640
+ });
11641
+ let failed;
11642
+ try {
11643
+ let outputPath;
11644
+ if (output) {
11645
+ outputPath = path53.resolve(output);
11646
+ } else if (autoName && opts.inputs.length === 1 && opts.inputs[0]) {
11647
+ outputPath = path53.join(getOutputDir(), `${autoNameBase(opts.inputs[0])}-extraction.md`);
11648
+ } else {
11649
+ outputPath = path53.join(getOutputDir(), "document-extraction.md");
11650
+ }
11651
+ fs47.mkdirSync(path53.dirname(outputPath), { recursive: true });
11652
+ failed = await convertResolved(inputs, { outputPath, model, prompt, verbose, logger });
11653
+ } finally {
11654
+ cleanup();
9706
11655
  }
9707
- fs46.mkdirSync(path50.dirname(outputPath), { recursive: true });
11656
+ if (failed > 0) process.exit(1);
11657
+ }
11658
+ async function convertResolved(inputs, ctx) {
11659
+ const { outputPath, model, prompt, verbose, logger } = ctx;
9708
11660
  const results = [];
9709
11661
  for (let i = 0; i < inputs.length; i++) {
9710
- const filePath = inputs[i];
9711
- logger?.debug(`[${i + 1}/${inputs.length}] Converting: ${filePath}`);
11662
+ const input = inputs[i];
11663
+ const filePath = input.path;
11664
+ logger?.debug(`[${i + 1}/${inputs.length}] Converting: ${input.label}`);
9712
11665
  let markdown;
9713
11666
  let errorMsg;
9714
11667
  for (let attempt = 0; attempt < 3; attempt++) {
@@ -9740,7 +11693,7 @@ async function convertDocuments(opts) {
9740
11693
  markdown = extractText(resp);
9741
11694
  break;
9742
11695
  } catch (e) {
9743
- errorMsg = e instanceof Error ? e.message : String(e);
11696
+ errorMsg = labelError(e instanceof Error ? e.message : String(e), input);
9744
11697
  if (isGeminiRateLimitError(e)) break;
9745
11698
  if (attempt < 2) {
9746
11699
  const wait = 2 ** attempt * 1e3;
@@ -9749,8 +11702,11 @@ async function convertDocuments(opts) {
9749
11702
  }
9750
11703
  }
9751
11704
  }
11705
+ if (markdown === void 0) {
11706
+ logger?.error(`Failed: ${input.label} \u2014 ${errorMsg}`);
11707
+ }
9752
11708
  results.push({
9753
- file: filePath,
11709
+ file: input.label,
9754
11710
  status: markdown !== void 0 ? "success" : "error",
9755
11711
  markdown,
9756
11712
  error: errorMsg
@@ -9764,7 +11720,7 @@ async function convertDocuments(opts) {
9764
11720
  `);
9765
11721
  for (const r of results) {
9766
11722
  lines.push(`
9767
- ## ${path50.basename(r.file)}
11723
+ ## ${path53.basename(r.file).replace(/[<>]/g, "\\$&")}
9768
11724
  `);
9769
11725
  if (r.status === "success" && r.markdown) {
9770
11726
  lines.push(`${r.markdown}
@@ -9777,7 +11733,7 @@ async function convertDocuments(opts) {
9777
11733
  }
9778
11734
  lines.push("\n---\n");
9779
11735
  }
9780
- fs46.writeFileSync(outputPath, lines.join("\n"), "utf8");
11736
+ fs47.writeFileSync(outputPath, lines.join("\n"), "utf8");
9781
11737
  const succeeded = results.filter((r) => r.status === "success").length;
9782
11738
  console.log(`
9783
11739
  ${"=".repeat(50)}`);
@@ -9790,8 +11746,7 @@ ${"=".repeat(50)}`);
9790
11746
  console.log(` [${r.status}] ${r.file}`);
9791
11747
  }
9792
11748
  }
9793
- const failed = results.filter((r) => r.status === "error").length;
9794
- if (failed > 0) process.exit(1);
11749
+ return results.length - succeeded;
9795
11750
  }
9796
11751
  function sleep4(ms) {
9797
11752
  return new Promise((r) => setTimeout(r, ms));
@@ -9804,14 +11759,21 @@ function registerDocCommands(program2) {
9804
11759
  );
9805
11760
  doc.command("convert").description("Convert documents to Markdown using Gemini Files API").requiredOption(
9806
11761
  "--input <files...>",
9807
- "Input file(s) to convert",
11762
+ "Input file(s) to convert (use - to read piped stdin)",
9808
11763
  (val, prev) => [...prev, val],
9809
11764
  []
9810
- ).alias("-i").option("--output <path>", "Output markdown file path").option("--auto-name", "Derive output filename from input basename (single file only)").option("--model <id>", `Gemini model to use (default: ${DOC_MODEL_DEFAULT})`).option("--prompt <text>", "Custom conversion prompt").option("-v, --verbose", "Verbose logging").action(
11765
+ ).alias("-i").option(
11766
+ "--stdin-ext <ext>",
11767
+ "Extension for piped stdin when auto-detection fails (e.g. pdf, txt)"
11768
+ ).option("--output <path>", "Output markdown file path").option(
11769
+ "--auto-name",
11770
+ "Derive output filename from input basename (single file only; stdin becomes stdin-extraction.md)"
11771
+ ).option("--model <id>", `Gemini model to use (default: ${DOC_MODEL_DEFAULT})`).option("--prompt <text>", "Custom conversion prompt").option("-v, --verbose", "Verbose logging").action(
9811
11772
  async (opts) => {
9812
11773
  const logger = createLogger({ verbose: opts.verbose ?? false });
9813
11774
  await convertDocuments({
9814
11775
  inputs: opts.input,
11776
+ stdinExt: opts.stdinExt,
9815
11777
  output: opts.output,
9816
11778
  autoName: opts.autoName,
9817
11779
  model: opts.model,
@@ -9825,21 +11787,21 @@ function registerDocCommands(program2) {
9825
11787
 
9826
11788
  // src/commands/media/index.ts
9827
11789
  init_errors();
9828
- import fs49 from "fs";
9829
- import path53 from "path";
11790
+ import fs50 from "fs";
11791
+ import path56 from "path";
9830
11792
 
9831
11793
  // src/commands/media/batch.ts
9832
- import fs47 from "fs";
9833
- import path51 from "path";
11794
+ import fs48 from "fs";
11795
+ import path54 from "path";
9834
11796
 
9835
11797
  // src/commands/media/detect-type.ts
9836
- var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
9837
- var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg", ".aiff"]);
11798
+ var VIDEO_EXTS2 = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
11799
+ var AUDIO_EXTS2 = /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".flac", ".aac", ".ogg", ".aiff"]);
9838
11800
  var IMAGE_EXTS2 = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".heic", ".heif", ".gif", ".bmp"]);
9839
11801
  function extToKind(ext) {
9840
11802
  const lower = ext.toLowerCase();
9841
- if (VIDEO_EXTS.has(lower)) return "video";
9842
- if (AUDIO_EXTS.has(lower)) return "audio";
11803
+ if (VIDEO_EXTS2.has(lower)) return "video";
11804
+ if (AUDIO_EXTS2.has(lower)) return "audio";
9843
11805
  if (IMAGE_EXTS2.has(lower)) return "image";
9844
11806
  return void 0;
9845
11807
  }
@@ -10015,7 +11977,7 @@ async function optimizeVideo(opts) {
10015
11977
  }
10016
11978
 
10017
11979
  // src/commands/media/batch.ts
10018
- var ALL_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS, ...AUDIO_EXTS, ...IMAGE_EXTS2]);
11980
+ var ALL_EXTS = /* @__PURE__ */ new Set([...VIDEO_EXTS2, ...AUDIO_EXTS2, ...IMAGE_EXTS2]);
10019
11981
  async function batchOptimize(opts) {
10020
11982
  const {
10021
11983
  inputDir,
@@ -10026,10 +11988,10 @@ async function batchOptimize(opts) {
10026
11988
  verbose,
10027
11989
  logger
10028
11990
  } = opts;
10029
- fs47.mkdirSync(outputDir, { recursive: true });
10030
- const entries = fs47.readdirSync(inputDir);
11991
+ fs48.mkdirSync(outputDir, { recursive: true });
11992
+ const entries = fs48.readdirSync(inputDir);
10031
11993
  const mediaFiles = entries.filter((f) => {
10032
- const ext = path51.extname(f).toLowerCase();
11994
+ const ext = path54.extname(f).toLowerCase();
10033
11995
  return ALL_EXTS.has(ext);
10034
11996
  });
10035
11997
  if (mediaFiles.length === 0) {
@@ -10040,9 +12002,9 @@ async function batchOptimize(opts) {
10040
12002
  let succeeded = 0;
10041
12003
  const errors = [];
10042
12004
  for (const file of mediaFiles) {
10043
- const inputPath = path51.join(inputDir, file);
10044
- const outputPath = path51.join(outputDir, file);
10045
- const ext = path51.extname(file).toLowerCase();
12005
+ const inputPath = path54.join(inputDir, file);
12006
+ const outputPath = path54.join(outputDir, file);
12007
+ const ext = path54.extname(file).toLowerCase();
10046
12008
  const kind = extToKind(ext);
10047
12009
  logger?.debug(`Processing: ${file}`);
10048
12010
  try {
@@ -10079,8 +12041,8 @@ async function batchOptimize(opts) {
10079
12041
 
10080
12042
  // src/commands/media/split-video.ts
10081
12043
  init_errors();
10082
- import fs48 from "fs";
10083
- import path52 from "path";
12044
+ import fs49 from "fs";
12045
+ import path55 from "path";
10084
12046
  async function splitVideo(opts) {
10085
12047
  await assertFfmpeg();
10086
12048
  const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
@@ -10091,10 +12053,10 @@ async function splitVideo(opts) {
10091
12053
  logger?.info("Video is short enough \u2014 no splitting needed");
10092
12054
  return [input];
10093
12055
  }
10094
- fs48.mkdirSync(outputDir, { recursive: true });
10095
- const ext = path52.extname(input) || ".mp4";
10096
- const baseName = path52.basename(input, ext);
10097
- const outputPattern = path52.join(outputDir, `${baseName}_chunk_%03d${ext}`);
12056
+ fs49.mkdirSync(outputDir, { recursive: true });
12057
+ const ext = path55.extname(input) || ".mp4";
12058
+ const baseName = path55.basename(input, ext);
12059
+ const outputPattern = path55.join(outputDir, `${baseName}_chunk_%03d${ext}`);
10098
12060
  logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
10099
12061
  const args = [
10100
12062
  "-i",
@@ -10113,8 +12075,8 @@ async function splitVideo(opts) {
10113
12075
  await runFfmpeg(args, { verbose, logger });
10114
12076
  const files = [];
10115
12077
  for (let i = 0; i < numChunks + 1; i++) {
10116
- const fname = path52.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
10117
- if (fs48.existsSync(fname)) files.push(fname);
12078
+ const fname = path55.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
12079
+ if (fs49.existsSync(fname)) files.push(fname);
10118
12080
  }
10119
12081
  logger?.success(`Created ${files.length} chunks in ${outputDir}`);
10120
12082
  return files;
@@ -10128,9 +12090,9 @@ function registerMediaCommands(program2) {
10128
12090
  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(
10129
12091
  async (opts) => {
10130
12092
  const logger = createLogger({ verbose: opts.verbose ?? false });
10131
- if (!fs49.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
10132
- fs49.mkdirSync(path53.dirname(path53.resolve(opts.output)), { recursive: true });
10133
- const ext = path53.extname(opts.input).toLowerCase();
12093
+ if (!fs50.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
12094
+ fs50.mkdirSync(path56.dirname(path56.resolve(opts.output)), { recursive: true });
12095
+ const ext = path56.extname(opts.input).toLowerCase();
10134
12096
  const kind = extToKind(ext);
10135
12097
  if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
10136
12098
  const quality = Number.parseInt(opts.quality, 10);
@@ -10170,7 +12132,7 @@ function registerMediaCommands(program2) {
10170
12132
  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(
10171
12133
  async (opts) => {
10172
12134
  const logger = createLogger({ verbose: opts.verbose ?? false });
10173
- if (!fs49.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
12135
+ if (!fs50.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
10174
12136
  const chunks = await splitVideo({
10175
12137
  input: opts.input,
10176
12138
  outputDir: opts.outputDir,
@@ -10186,7 +12148,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
10186
12148
  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(
10187
12149
  async (opts) => {
10188
12150
  const logger = createLogger({ verbose: opts.verbose ?? false });
10189
- if (!fs49.existsSync(opts.inputDir))
12151
+ if (!fs50.existsSync(opts.inputDir))
10190
12152
  throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
10191
12153
  const result = await batchOptimize({
10192
12154
  inputDir: opts.inputDir,
@@ -10368,6 +12330,7 @@ function registerCommands(program2) {
10368
12330
  registerFalCommands(program2);
10369
12331
  registerAtlasCloudCommands(program2);
10370
12332
  registerReplicateCommands(program2);
12333
+ registerHeygenCommands(program2);
10371
12334
  registerMediaCommands(program2);
10372
12335
  registerDocCommands(program2);
10373
12336
  }
@@ -10381,7 +12344,7 @@ loadEnv();
10381
12344
  var require3 = createRequire2(import.meta.url);
10382
12345
  var pkg = require3("../package.json");
10383
12346
  var program = new Command().name("multix").description(
10384
- "AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, ffmpeg, and ImageMagick"
12347
+ "AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, and ImageMagick"
10385
12348
  ).version(pkg.version).enablePositionalOptions();
10386
12349
  registerCommands(program);
10387
12350
  program.parseAsync(process.argv).catch((err2) => {