@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/CHANGELOG.md +19 -0
- package/README.md +50 -3
- package/dist/cli.js +2242 -279
- package/package.json +5 -2
- package/skill/SKILL.md +31 -2
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
|
|
866
|
-
if (!
|
|
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}${
|
|
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
|
|
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(
|
|
2185
|
+
async request(path57, opts = {}) {
|
|
2186
2186
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
2187
|
-
const url = new URL(base +
|
|
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(
|
|
2206
|
-
return this.request(
|
|
2205
|
+
get(path57, query, logger) {
|
|
2206
|
+
return this.request(path57, { method: "GET", query, logger });
|
|
2207
2207
|
}
|
|
2208
|
-
post(
|
|
2209
|
-
return this.request(
|
|
2208
|
+
post(path57, body, logger) {
|
|
2209
|
+
return this.request(path57, { method: "POST", body, logger });
|
|
2210
2210
|
}
|
|
2211
|
-
delete(
|
|
2212
|
-
return this.request(
|
|
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:
|
|
2682
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
2683
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
2684
|
-
return toRef(kind, await resolveAudioInput(
|
|
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(
|
|
4111
|
+
async request(path57, opts = {}) {
|
|
4112
4112
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
4113
|
-
const suffix =
|
|
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(
|
|
4131
|
-
return this.request(
|
|
4130
|
+
get(path57, logger) {
|
|
4131
|
+
return this.request(path57, { method: "GET", logger });
|
|
4132
4132
|
}
|
|
4133
|
-
post(
|
|
4134
|
-
return this.request(
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
5945
|
-
|
|
5946
|
-
const
|
|
5947
|
-
|
|
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/
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
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
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
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
|
|
5969
|
-
import
|
|
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(
|
|
7971
|
+
async request(path57, opts = {}) {
|
|
6043
7972
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
6044
|
-
const v2Override =
|
|
7973
|
+
const v2Override = path57.startsWith("v2:");
|
|
6045
7974
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
6046
|
-
const finalPath = v2Override ?
|
|
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(
|
|
6066
|
-
return this.request(
|
|
7994
|
+
get(path57, query, logger) {
|
|
7995
|
+
return this.request(path57, { method: "GET", query, logger });
|
|
6067
7996
|
}
|
|
6068
|
-
post(
|
|
6069
|
-
return this.request(
|
|
7997
|
+
post(path57, body, logger) {
|
|
7998
|
+
return this.request(path57, { method: "POST", body, logger });
|
|
6070
7999
|
}
|
|
6071
|
-
delete(
|
|
6072
|
-
return this.request(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
6179
|
-
|
|
6180
|
-
|
|
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
|
|
6193
|
-
import
|
|
8121
|
+
import fs31 from "fs";
|
|
8122
|
+
import path36 from "path";
|
|
6194
8123
|
function extFromUrl3(url, fallback = ".png") {
|
|
6195
8124
|
try {
|
|
6196
|
-
return
|
|
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 =
|
|
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 =
|
|
6269
|
-
|
|
6270
|
-
|
|
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
|
|
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 ??
|
|
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
|
|
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 :
|
|
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 =
|
|
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
|
|
6735
|
-
import
|
|
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 =
|
|
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
|
-
|
|
8713
|
+
fs32.writeFileSync(dest, bytes);
|
|
6785
8714
|
} else {
|
|
6786
8715
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
6787
|
-
|
|
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
|
-
|
|
6799
|
-
|
|
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
|
|
6874
|
-
import
|
|
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 =
|
|
6922
|
-
|
|
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
|
-
|
|
6926
|
-
|
|
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
|
|
6974
|
-
import
|
|
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 =
|
|
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 =
|
|
8956
|
+
const stat = fs34.statSync(dest);
|
|
7028
8957
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
7029
8958
|
if (output) {
|
|
7030
|
-
|
|
7031
|
-
|
|
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
|
|
7094
|
-
import
|
|
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 =
|
|
7133
|
-
|
|
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
|
-
|
|
7145
|
-
|
|
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
|
|
7185
|
-
import
|
|
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 =
|
|
7241
|
-
|
|
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
|
-
|
|
7253
|
-
|
|
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
|
|
7388
|
-
import
|
|
9316
|
+
import fs38 from "fs";
|
|
9317
|
+
import path45 from "path";
|
|
7389
9318
|
|
|
7390
9319
|
// src/providers/openai/client.ts
|
|
7391
|
-
import
|
|
7392
|
-
import
|
|
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 =
|
|
7459
|
-
if (!
|
|
7460
|
-
const bytes =
|
|
7461
|
-
return new File([new Uint8Array(bytes)],
|
|
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 =
|
|
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 =
|
|
7520
|
-
|
|
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
|
-
|
|
7524
|
-
|
|
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 ?
|
|
7547
|
-
|
|
7548
|
-
|
|
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
|
|
9504
|
+
import fs39 from "fs";
|
|
7576
9505
|
import os3 from "os";
|
|
7577
|
-
import
|
|
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 ?
|
|
7627
|
-
|
|
7628
|
-
const tmp =
|
|
7629
|
-
const schema =
|
|
7630
|
-
const result =
|
|
7631
|
-
|
|
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 (!
|
|
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
|
|
7679
|
-
import
|
|
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 =
|
|
7713
|
-
|
|
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
|
-
|
|
7719
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
9806
|
+
collect7,
|
|
7878
9807
|
[]
|
|
7879
9808
|
).option(
|
|
7880
9809
|
"--known-speaker-reference <path>",
|
|
7881
9810
|
"2-10s reference audio; repeatable",
|
|
7882
|
-
|
|
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
|
|
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
|
|
7918
|
-
import
|
|
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 =
|
|
8083
|
-
|
|
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
|
-
|
|
8095
|
-
|
|
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
|
|
8137
|
-
import
|
|
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
|
-
|
|
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
|
|
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 =
|
|
8230
|
-
|
|
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
|
-
|
|
8236
|
-
|
|
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
|
|
8282
|
-
import
|
|
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 =
|
|
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
|
-
|
|
8298
|
-
const ws =
|
|
8299
|
-
const { pipeline:
|
|
8300
|
-
await
|
|
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
|
-
|
|
8304
|
-
|
|
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 [
|
|
8498
|
-
const [owner, name, ...extra] = (
|
|
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
|
|
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 ??
|
|
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(
|
|
10574
|
+
raw(path57, method, body, headers2, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
8646
10575
|
return fetchJson({
|
|
8647
|
-
url: `${this.baseUrl}${
|
|
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
|
|
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(
|
|
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
|
|
8831
|
-
import
|
|
8832
|
-
import
|
|
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
|
|
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,
|
|
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:
|
|
8850
|
-
else if (value.length > 0) result.texts.push(
|
|
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(
|
|
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(
|
|
10791
|
+
if (joined.length > 0) result.texts.push(path57 ? `${path57}: ${joined}` : joined);
|
|
8863
10792
|
return;
|
|
8864
10793
|
}
|
|
8865
|
-
value.forEach((item, i) => visit(item, `${
|
|
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,
|
|
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
|
|
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
|
|
8994
|
-
const output =
|
|
8995
|
-
if (isDirTarget(opts.output)) return
|
|
8996
|
-
const givenExt =
|
|
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 =
|
|
10932
|
+
const fileStream = fs44.createWriteStream(dest);
|
|
9004
10933
|
try {
|
|
9005
|
-
await
|
|
10934
|
+
await pipeline3(
|
|
9006
10935
|
Readable.fromWeb(response.body),
|
|
9007
10936
|
fileStream
|
|
9008
10937
|
);
|
|
9009
10938
|
} catch (err2) {
|
|
9010
|
-
if (!fileStream.closed) await
|
|
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
|
-
|
|
9028
|
-
|
|
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
|
-
|
|
10973
|
+
fs44.mkdirSync(path51.dirname(dest), { recursive: true });
|
|
9045
10974
|
try {
|
|
9046
10975
|
await writeResponse(response, dest);
|
|
9047
10976
|
} catch (err2) {
|
|
9048
|
-
|
|
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
|
|
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
|
|
11025
|
+
return parsePositiveInt2(value, "--prefer-wait", 60);
|
|
9097
11026
|
}
|
|
9098
|
-
function
|
|
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: ${
|
|
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: ${
|
|
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 =
|
|
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:
|
|
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
|
|
9227
|
-
import
|
|
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 =
|
|
11164
|
+
const filePath = path52.resolve(value);
|
|
9236
11165
|
let stat;
|
|
9237
11166
|
try {
|
|
9238
|
-
stat =
|
|
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 =
|
|
11178
|
+
const filename = path52.basename(filePath);
|
|
9250
11179
|
const contentType = mimeFromFilename(filename);
|
|
9251
|
-
const bytes =
|
|
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
|
|
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
|
-
|
|
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:
|
|
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: ${
|
|
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:
|
|
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
|
|
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) =>
|
|
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
|
-
|
|
9674
|
-
import
|
|
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 {
|
|
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
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
}
|
|
9702
|
-
|
|
9703
|
-
|
|
9704
|
-
|
|
9705
|
-
|
|
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
|
-
|
|
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
|
|
9711
|
-
|
|
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:
|
|
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
|
-
## ${
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
9829
|
-
import
|
|
11790
|
+
import fs50 from "fs";
|
|
11791
|
+
import path56 from "path";
|
|
9830
11792
|
|
|
9831
11793
|
// src/commands/media/batch.ts
|
|
9832
|
-
import
|
|
9833
|
-
import
|
|
11794
|
+
import fs48 from "fs";
|
|
11795
|
+
import path54 from "path";
|
|
9834
11796
|
|
|
9835
11797
|
// src/commands/media/detect-type.ts
|
|
9836
|
-
var
|
|
9837
|
-
var
|
|
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 (
|
|
9842
|
-
if (
|
|
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([...
|
|
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
|
-
|
|
10030
|
-
const entries =
|
|
11991
|
+
fs48.mkdirSync(outputDir, { recursive: true });
|
|
11992
|
+
const entries = fs48.readdirSync(inputDir);
|
|
10031
11993
|
const mediaFiles = entries.filter((f) => {
|
|
10032
|
-
const ext =
|
|
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 =
|
|
10044
|
-
const outputPath =
|
|
10045
|
-
const ext =
|
|
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
|
|
10083
|
-
import
|
|
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
|
-
|
|
10095
|
-
const ext =
|
|
10096
|
-
const baseName =
|
|
10097
|
-
const outputPattern =
|
|
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 =
|
|
10117
|
-
if (
|
|
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 (!
|
|
10132
|
-
|
|
10133
|
-
const ext =
|
|
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 (!
|
|
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 (!
|
|
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) => {
|