@mrgoonie/multix 0.12.0 → 0.13.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 +7 -0
- package/README.md +44 -1
- package/dist/cli.js +2011 -220
- package/package.json +5 -2
- package/skill/SKILL.md +30 -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,
|
|
@@ -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 });
|
|
@@ -5964,9 +5964,1792 @@ function registerGeminiCommands(program2) {
|
|
|
5964
5964
|
registerGeminiGenerateSpeechCommand(gemini);
|
|
5965
5965
|
}
|
|
5966
5966
|
|
|
5967
|
+
// src/providers/heygen/models.ts
|
|
5968
|
+
var DEFAULT_HEYGEN_BASE_URL = "https://api.heygen.com";
|
|
5969
|
+
var HEYGEN_API_SETTINGS_URL = "https://app.heygen.com/developers/api";
|
|
5970
|
+
var HEYGEN_USAGE_URL = "https://app.heygen.com/developers/usage";
|
|
5971
|
+
var AVATAR_VIDEO_RESOLUTIONS = ["4k", "1080p", "720p"];
|
|
5972
|
+
var AVATAR_VIDEO_ASPECT_RATIOS = ["16:9", "9:16", "4:5", "5:4", "1:1", "auto"];
|
|
5973
|
+
var AVATAR_ENGINES = ["avatar_iv", "avatar_v", "avatar_iii"];
|
|
5974
|
+
var EXPRESSIVENESS_LEVELS = ["high", "medium", "low"];
|
|
5975
|
+
var AVATAR_TYPES = ["studio_avatar", "digital_twin", "photo_avatar"];
|
|
5976
|
+
var OWNERSHIP_VALUES = ["public", "private"];
|
|
5977
|
+
var AGENT_ORIENTATIONS = ["landscape", "portrait"];
|
|
5978
|
+
var QUALITY_MODES = ["speed", "precision"];
|
|
5979
|
+
var SPEECH_ENGINES = ["starfish", "orca", "elevenlabs"];
|
|
5980
|
+
var HEYGEN_VIDEO_MODEL = "heygen-video-1";
|
|
5981
|
+
var MODEL_VIDEO_MODES = ["text_to_video", "image_to_video", "reference_to_video"];
|
|
5982
|
+
var MODEL_VIDEO_RESOLUTIONS = ["480p", "768p", "1080p", "2k"];
|
|
5983
|
+
var MODEL_VIDEO_ASPECT_RATIOS = [
|
|
5984
|
+
"21:9",
|
|
5985
|
+
"16:9",
|
|
5986
|
+
"4:3",
|
|
5987
|
+
"1:1",
|
|
5988
|
+
"3:4",
|
|
5989
|
+
"9:16",
|
|
5990
|
+
"adaptive"
|
|
5991
|
+
];
|
|
5992
|
+
var MODEL_VIDEO_PROMPT_ENHANCEMENT = ["turbo", "quality", "disabled"];
|
|
5993
|
+
var LIMITS = {
|
|
5994
|
+
agentPromptMax: 1e4,
|
|
5995
|
+
agentFilesMax: 20,
|
|
5996
|
+
scriptMax: 5e3,
|
|
5997
|
+
speechTextMax: 5e3,
|
|
5998
|
+
/** POST /v3/assets single-request upload cap (32 MB). */
|
|
5999
|
+
assetUploadMaxBytes: 32 * 1024 * 1024,
|
|
6000
|
+
modelVideoPromptMax: 32e3,
|
|
6001
|
+
modelVideoMinDuration: 5,
|
|
6002
|
+
modelVideoMaxDuration: 15,
|
|
6003
|
+
modelVideoReferenceImagesMax: 9,
|
|
6004
|
+
modelVideoReferenceVideosMax: 3,
|
|
6005
|
+
modelVideoReferenceAudioMax: 3,
|
|
6006
|
+
/** Combined cap across reference images, videos and audio. */
|
|
6007
|
+
modelVideoReferenceTotalMax: 12,
|
|
6008
|
+
avatarListMax: 50,
|
|
6009
|
+
voiceListMax: 100,
|
|
6010
|
+
stylesListMax: 100
|
|
6011
|
+
};
|
|
6012
|
+
var ASSET_MIME_BY_EXT = {
|
|
6013
|
+
png: "image/png",
|
|
6014
|
+
jpg: "image/jpeg",
|
|
6015
|
+
jpeg: "image/jpeg",
|
|
6016
|
+
mp4: "video/mp4",
|
|
6017
|
+
webm: "video/webm",
|
|
6018
|
+
mp3: "audio/mpeg",
|
|
6019
|
+
wav: "audio/wav",
|
|
6020
|
+
pdf: "application/pdf",
|
|
6021
|
+
srt: "application/x-subrip"
|
|
6022
|
+
};
|
|
6023
|
+
var JOB_KINDS = [
|
|
6024
|
+
"agent",
|
|
6025
|
+
"video",
|
|
6026
|
+
"translation",
|
|
6027
|
+
"lipsync",
|
|
6028
|
+
"model-video"
|
|
6029
|
+
];
|
|
6030
|
+
var HEYGEN_OPERATIONS = {
|
|
6031
|
+
"video-agent": {
|
|
6032
|
+
description: "POST /v3/video-agents (prompt-to-video session)",
|
|
6033
|
+
path: "/v3/video-agents",
|
|
6034
|
+
kind: "agent",
|
|
6035
|
+
idField: "session_id",
|
|
6036
|
+
idempotent: false
|
|
6037
|
+
},
|
|
6038
|
+
video: {
|
|
6039
|
+
description: "POST /v3/videos (avatar | image | cinematic_avatar | studio)",
|
|
6040
|
+
path: "/v3/videos",
|
|
6041
|
+
kind: "video",
|
|
6042
|
+
idField: "video_id",
|
|
6043
|
+
idempotent: true
|
|
6044
|
+
},
|
|
6045
|
+
"video-translation": {
|
|
6046
|
+
description: "POST /v3/video-translations",
|
|
6047
|
+
path: "/v3/video-translations",
|
|
6048
|
+
kind: "translation",
|
|
6049
|
+
idsField: "video_translation_ids",
|
|
6050
|
+
idempotent: true
|
|
6051
|
+
},
|
|
6052
|
+
lipsync: {
|
|
6053
|
+
description: "POST /v3/lipsyncs",
|
|
6054
|
+
path: "/v3/lipsyncs",
|
|
6055
|
+
kind: "lipsync",
|
|
6056
|
+
idField: "lipsync_id",
|
|
6057
|
+
idempotent: true
|
|
6058
|
+
},
|
|
6059
|
+
"model-video": {
|
|
6060
|
+
description: "POST /v3/models/videos (HeyGen Video model)",
|
|
6061
|
+
path: "/v3/models/videos",
|
|
6062
|
+
kind: "model-video",
|
|
6063
|
+
idField: "video_id",
|
|
6064
|
+
idempotent: true
|
|
6065
|
+
},
|
|
6066
|
+
speech: {
|
|
6067
|
+
description: "POST /v3/voices/speech (synchronous text-to-speech)",
|
|
6068
|
+
path: "/v3/voices/speech",
|
|
6069
|
+
kind: null,
|
|
6070
|
+
idempotent: false
|
|
6071
|
+
}
|
|
6072
|
+
};
|
|
6073
|
+
|
|
6074
|
+
// src/providers/heygen/node-helpers.ts
|
|
6075
|
+
import fs29 from "fs";
|
|
6076
|
+
import path32 from "path";
|
|
6077
|
+
init_errors();
|
|
6078
|
+
init_redact();
|
|
6079
|
+
|
|
6080
|
+
// src/providers/heygen/client.ts
|
|
6081
|
+
init_errors();
|
|
6082
|
+
init_redact();
|
|
6083
|
+
|
|
6084
|
+
// src/providers/heygen/heygen-errors.ts
|
|
6085
|
+
init_errors();
|
|
6086
|
+
var HeygenApiError = class extends HttpError {
|
|
6087
|
+
constructor(status, snippet, url, retryAfterMs, apiCode, apiMessage, param) {
|
|
6088
|
+
super(status, snippet, url, retryAfterMs);
|
|
6089
|
+
this.apiCode = apiCode;
|
|
6090
|
+
this.apiMessage = apiMessage;
|
|
6091
|
+
this.param = param;
|
|
6092
|
+
this.name = "HeygenApiError";
|
|
6093
|
+
this.message = describeHeygenError(status, apiCode, apiMessage, param, retryAfterMs);
|
|
6094
|
+
}
|
|
6095
|
+
apiCode;
|
|
6096
|
+
apiMessage;
|
|
6097
|
+
param;
|
|
6098
|
+
};
|
|
6099
|
+
var HeygenJobFailedError = class extends ProviderError {
|
|
6100
|
+
constructor(message, failureCode) {
|
|
6101
|
+
super(message, "heygen");
|
|
6102
|
+
this.failureCode = failureCode;
|
|
6103
|
+
this.name = "HeygenJobFailedError";
|
|
6104
|
+
}
|
|
6105
|
+
failureCode;
|
|
6106
|
+
};
|
|
6107
|
+
function parseHeygenErrorBody(snippet) {
|
|
6108
|
+
try {
|
|
6109
|
+
const parsed = JSON.parse(snippet);
|
|
6110
|
+
if (parsed && typeof parsed.error === "object" && parsed.error !== null) return parsed.error;
|
|
6111
|
+
if (parsed && typeof parsed.error === "string") return { message: parsed.error };
|
|
6112
|
+
} catch {
|
|
6113
|
+
}
|
|
6114
|
+
const field = (name) => snippet.match(new RegExp(`"${name}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`))?.[1];
|
|
6115
|
+
const out = {};
|
|
6116
|
+
const code = field("code");
|
|
6117
|
+
const message = field("message");
|
|
6118
|
+
const param = field("param");
|
|
6119
|
+
if (code) out.code = code;
|
|
6120
|
+
if (message) out.message = message;
|
|
6121
|
+
if (param) out.param = param;
|
|
6122
|
+
return out;
|
|
6123
|
+
}
|
|
6124
|
+
function toHeygenApiError(error) {
|
|
6125
|
+
if (error instanceof HeygenApiError) return error;
|
|
6126
|
+
const body = error.status === 0 ? {} : parseHeygenErrorBody(error.snippet);
|
|
6127
|
+
return new HeygenApiError(
|
|
6128
|
+
error.status,
|
|
6129
|
+
error.snippet,
|
|
6130
|
+
error.url,
|
|
6131
|
+
error.retryAfterMs,
|
|
6132
|
+
body.code,
|
|
6133
|
+
body.message ?? (error.status === 0 ? error.snippet : void 0),
|
|
6134
|
+
body.param ?? void 0
|
|
6135
|
+
);
|
|
6136
|
+
}
|
|
6137
|
+
function hintFor(status, code) {
|
|
6138
|
+
if (status === 0) return "Network error reaching HeyGen \u2014 check connectivity and retry.";
|
|
6139
|
+
if (status === 401)
|
|
6140
|
+
return `Check HEYGEN_API_KEY (sent as the X-Api-Key header) or create a new key at ${HEYGEN_API_SETTINGS_URL}.`;
|
|
6141
|
+
if (status === 402)
|
|
6142
|
+
return `Add API credits or upgrade the API plan at ${HEYGEN_USAGE_URL}. HeyGen API usage is billed separately from web/app subscriptions.`;
|
|
6143
|
+
if (status === 403) {
|
|
6144
|
+
if (code === "insufficient_api_key_scope")
|
|
6145
|
+
return "The API key lacks the scope this operation needs \u2014 create a key with the required scopes.";
|
|
6146
|
+
return "The API key, workspace, or plan is not entitled to this feature (some avatar and digital-twin features require Enterprise access).";
|
|
6147
|
+
}
|
|
6148
|
+
if (status === 404) return "Check the ID. For `heygen status`, make sure --type matches the ID.";
|
|
6149
|
+
if (status === 409 && code === "request_in_progress")
|
|
6150
|
+
return "An identical request is still being processed \u2014 wait and retry.";
|
|
6151
|
+
if (status === 409) return "The resource is not ready or conflicts with existing state.";
|
|
6152
|
+
if (status === 400 || status === 422) return "Fix the request parameters and retry.";
|
|
6153
|
+
if (status === 429)
|
|
6154
|
+
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.";
|
|
6155
|
+
if (status >= 500) return "HeyGen server error \u2014 retry later.";
|
|
6156
|
+
return "";
|
|
6157
|
+
}
|
|
6158
|
+
function describeHeygenError(status, code, message, param, retryAfterMs) {
|
|
6159
|
+
const label = status === 0 ? "HeyGen request failed" : `HeyGen API error ${status}`;
|
|
6160
|
+
const codePart = code ? ` (${code})` : "";
|
|
6161
|
+
const msgPart = message ? `: ${message}` : "";
|
|
6162
|
+
const paramPart = param ? ` (param: ${param})` : "";
|
|
6163
|
+
const retryPart = retryAfterMs !== void 0 ? ` Retry after ${Math.ceil(retryAfterMs / 1e3)}s.` : "";
|
|
6164
|
+
const hint = hintFor(status, code);
|
|
6165
|
+
const base = `${label}${codePart}${msgPart}${paramPart}`;
|
|
6166
|
+
const sentence = /[.!?]$/.test(base) ? base : `${base}.`;
|
|
6167
|
+
return `${sentence}${retryPart}${hint ? ` ${hint}` : ""}`;
|
|
6168
|
+
}
|
|
6169
|
+
|
|
6170
|
+
// src/providers/heygen/client.ts
|
|
6171
|
+
var MAX_RETRY_WAIT_MS = 6e4;
|
|
6172
|
+
var defaultSleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6173
|
+
function seg(id, label) {
|
|
6174
|
+
if (typeof id !== "string") throw new ValidationError(`${label} must be a string`);
|
|
6175
|
+
const trimmed = id.trim();
|
|
6176
|
+
if (!trimmed) throw new ValidationError(`${label} must not be empty`);
|
|
6177
|
+
if (trimmed === "." || trimmed === "..") {
|
|
6178
|
+
throw new ValidationError(`Invalid ${label}: "${trimmed}"`);
|
|
6179
|
+
}
|
|
6180
|
+
return encodeURIComponent(trimmed);
|
|
6181
|
+
}
|
|
6182
|
+
function describePayload(data) {
|
|
6183
|
+
const text = JSON.stringify(
|
|
6184
|
+
data,
|
|
6185
|
+
(_k, v) => typeof v === "string" && /^https?:\/\//i.test(v) ? redactUrl(v) : v
|
|
6186
|
+
);
|
|
6187
|
+
return (text ?? String(data)).slice(0, 500);
|
|
6188
|
+
}
|
|
6189
|
+
function requireJobId(data, field, path57) {
|
|
6190
|
+
const value = data?.[field];
|
|
6191
|
+
const ok2 = Array.isArray(value) ? value.length > 0 && value.every((v) => typeof v === "string" && v.length > 0) : typeof value === "string" && value.length > 0;
|
|
6192
|
+
if (!ok2) {
|
|
6193
|
+
throw new ProviderError(
|
|
6194
|
+
`HeyGen ${path57} returned no ${field}; a job may still have been created, so check the HeyGen dashboard. Response: ${describePayload(data)}`,
|
|
6195
|
+
"heygen"
|
|
6196
|
+
);
|
|
6197
|
+
}
|
|
6198
|
+
return data;
|
|
6199
|
+
}
|
|
6200
|
+
var HeygenClient = class {
|
|
6201
|
+
apiKey;
|
|
6202
|
+
baseUrl;
|
|
6203
|
+
fetchImpl;
|
|
6204
|
+
timeoutMs;
|
|
6205
|
+
maxAttempts;
|
|
6206
|
+
sleep;
|
|
6207
|
+
newIdempotencyKey;
|
|
6208
|
+
constructor(opts) {
|
|
6209
|
+
if (!opts.apiKey?.trim()) throw new ValidationError("HeyGen apiKey is required");
|
|
6210
|
+
this.apiKey = opts.apiKey.trim();
|
|
6211
|
+
this.baseUrl = (opts.baseUrl ?? DEFAULT_HEYGEN_BASE_URL).replace(/\/+$/, "");
|
|
6212
|
+
this.fetchImpl = opts.fetch;
|
|
6213
|
+
this.timeoutMs = opts.timeoutMs ?? 12e4;
|
|
6214
|
+
this.maxAttempts = Math.max(1, opts.maxAttempts ?? 3);
|
|
6215
|
+
this.sleep = opts.sleep ?? defaultSleep2;
|
|
6216
|
+
this.newIdempotencyKey = opts.newIdempotencyKey ?? (() => crypto.randomUUID());
|
|
6217
|
+
}
|
|
6218
|
+
/** Build an absolute URL for a v3 path plus query parameters. */
|
|
6219
|
+
url(path57, query) {
|
|
6220
|
+
const suffix = path57.startsWith("/") ? path57 : `/${path57}`;
|
|
6221
|
+
const params = new URLSearchParams();
|
|
6222
|
+
for (const [k, v] of Object.entries(query ?? {})) {
|
|
6223
|
+
if (v !== void 0 && v !== "") params.set(k, String(v));
|
|
6224
|
+
}
|
|
6225
|
+
const qs = params.toString();
|
|
6226
|
+
return `${this.baseUrl}${suffix}${qs ? `?${qs}` : ""}`;
|
|
6227
|
+
}
|
|
6228
|
+
headers(idempotencyKey) {
|
|
6229
|
+
return {
|
|
6230
|
+
Accept: "application/json",
|
|
6231
|
+
"X-Api-Key": this.apiKey,
|
|
6232
|
+
...idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}
|
|
6233
|
+
};
|
|
6234
|
+
}
|
|
6235
|
+
/**
|
|
6236
|
+
* Send a request and return the raw parsed JSON (envelope included).
|
|
6237
|
+
* Retries are bounded and only where safe: a 429 carrying Retry-After was
|
|
6238
|
+
* rejected before processing; 5xx/network failures and 409
|
|
6239
|
+
* `request_in_progress` are retried only when an Idempotency-Key makes the
|
|
6240
|
+
* replay safe. GET requests are left to the caller's polling loop.
|
|
6241
|
+
*/
|
|
6242
|
+
async send(path57, opts = {}) {
|
|
6243
|
+
const method = opts.method ?? "GET";
|
|
6244
|
+
const key = typeof opts.idempotencyKey === "string" ? opts.idempotencyKey : opts.idempotencyKey ? this.newIdempotencyKey() : void 0;
|
|
6245
|
+
const url = this.url(path57, opts.query);
|
|
6246
|
+
for (let attempt = 1; ; attempt++) {
|
|
6247
|
+
try {
|
|
6248
|
+
return await fetchJson({
|
|
6249
|
+
url,
|
|
6250
|
+
method,
|
|
6251
|
+
headers: this.headers(key),
|
|
6252
|
+
body: opts.body,
|
|
6253
|
+
timeoutMs: Math.max(1, Math.min(this.timeoutMs, opts.timeoutMs ?? this.timeoutMs)),
|
|
6254
|
+
fetch: this.fetchImpl
|
|
6255
|
+
});
|
|
6256
|
+
} catch (error) {
|
|
6257
|
+
const apiError = error instanceof HttpError ? toHeygenApiError(error) : error;
|
|
6258
|
+
if (method === "GET" || attempt >= this.maxAttempts || !(apiError instanceof HttpError)) {
|
|
6259
|
+
throw apiError;
|
|
6260
|
+
}
|
|
6261
|
+
const wait = this.retryDelay(apiError, key, attempt);
|
|
6262
|
+
if (wait === void 0) throw apiError;
|
|
6263
|
+
await this.sleep(wait);
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
retryDelay(error, key, attempt) {
|
|
6268
|
+
const code = error instanceof HeygenApiError ? error.apiCode : void 0;
|
|
6269
|
+
if (error.status === 429 && error.retryAfterMs !== void 0 && code !== "quota_exceeded") {
|
|
6270
|
+
return Math.min(error.retryAfterMs, MAX_RETRY_WAIT_MS);
|
|
6271
|
+
}
|
|
6272
|
+
if (!key) return void 0;
|
|
6273
|
+
if (error.status === 0 || error.status >= 500 || code === "request_in_progress") {
|
|
6274
|
+
return Math.min(1e3 * 2 ** (attempt - 1), MAX_RETRY_WAIT_MS);
|
|
6275
|
+
}
|
|
6276
|
+
return void 0;
|
|
6277
|
+
}
|
|
6278
|
+
/** Send and unwrap `{ data }`. A 2xx body carrying `error` is treated as a failure. */
|
|
6279
|
+
async request(path57, opts = {}) {
|
|
6280
|
+
const json = await this.send(path57, opts);
|
|
6281
|
+
return unwrapData(json, path57);
|
|
6282
|
+
}
|
|
6283
|
+
async page(path57, query) {
|
|
6284
|
+
const json = await this.send(path57, {
|
|
6285
|
+
query
|
|
6286
|
+
});
|
|
6287
|
+
const items = unwrapData(json, path57);
|
|
6288
|
+
if (!Array.isArray(items)) {
|
|
6289
|
+
throw new ProviderError(
|
|
6290
|
+
`Unexpected HeyGen response from ${path57}: data is not a list`,
|
|
6291
|
+
"heygen"
|
|
6292
|
+
);
|
|
6293
|
+
}
|
|
6294
|
+
const next = json.next_token;
|
|
6295
|
+
return {
|
|
6296
|
+
items,
|
|
6297
|
+
hasMore: json.has_more === true,
|
|
6298
|
+
nextToken: typeof next === "string" && next ? next : null
|
|
6299
|
+
};
|
|
6300
|
+
}
|
|
6301
|
+
// ── Account ───────────────────────────────────────────────────────────
|
|
6302
|
+
getCurrentUser() {
|
|
6303
|
+
return this.request("/v3/users/me");
|
|
6304
|
+
}
|
|
6305
|
+
// ── Video Agent ───────────────────────────────────────────────────────
|
|
6306
|
+
/** POST /v3/video-agents. No Idempotency-Key: the endpoint does not document one. */
|
|
6307
|
+
async createVideoAgent(body) {
|
|
6308
|
+
const path57 = "/v3/video-agents";
|
|
6309
|
+
const data = await this.request(path57, { method: "POST", body });
|
|
6310
|
+
return requireJobId(data, "session_id", path57);
|
|
6311
|
+
}
|
|
6312
|
+
async getVideoAgentSession(sessionId, opts = {}) {
|
|
6313
|
+
return this.request(`/v3/video-agents/${seg(sessionId, "session id")}`, opts);
|
|
6314
|
+
}
|
|
6315
|
+
listVideoAgentStyles(query = {}) {
|
|
6316
|
+
return this.page("/v3/video-agents/styles", { ...query });
|
|
6317
|
+
}
|
|
6318
|
+
// ── Videos ────────────────────────────────────────────────────────────
|
|
6319
|
+
async createVideo(body, idempotencyKey = true) {
|
|
6320
|
+
const path57 = "/v3/videos";
|
|
6321
|
+
const data = await this.request(path57, {
|
|
6322
|
+
method: "POST",
|
|
6323
|
+
body,
|
|
6324
|
+
idempotencyKey
|
|
6325
|
+
});
|
|
6326
|
+
return requireJobId(data, "video_id", path57);
|
|
6327
|
+
}
|
|
6328
|
+
async getVideo(videoId, opts = {}) {
|
|
6329
|
+
return this.request(`/v3/videos/${seg(videoId, "video id")}`, opts);
|
|
6330
|
+
}
|
|
6331
|
+
/** POST /v3/templates/{id}. Returns the created video's `VideoDetail`. */
|
|
6332
|
+
async generateFromTemplate(templateId, body, idempotencyKey = true) {
|
|
6333
|
+
const path57 = `/v3/templates/${seg(templateId, "template id")}`;
|
|
6334
|
+
const data = await this.request(path57, {
|
|
6335
|
+
method: "POST",
|
|
6336
|
+
body,
|
|
6337
|
+
idempotencyKey
|
|
6338
|
+
});
|
|
6339
|
+
return requireJobId(data, "id", path57);
|
|
6340
|
+
}
|
|
6341
|
+
// ── HeyGen Video model ────────────────────────────────────────────────
|
|
6342
|
+
async createModelVideo(body, idempotencyKey = true) {
|
|
6343
|
+
const path57 = "/v3/models/videos";
|
|
6344
|
+
const data = await this.request(path57, {
|
|
6345
|
+
method: "POST",
|
|
6346
|
+
body,
|
|
6347
|
+
idempotencyKey
|
|
6348
|
+
});
|
|
6349
|
+
return requireJobId(data, "video_id", path57);
|
|
6350
|
+
}
|
|
6351
|
+
async getModelVideo(videoId, opts = {}) {
|
|
6352
|
+
return this.request(`/v3/models/videos/${seg(videoId, "video id")}`, opts);
|
|
6353
|
+
}
|
|
6354
|
+
// ── Catalog ───────────────────────────────────────────────────────────
|
|
6355
|
+
listAvatarGroups(query = {}) {
|
|
6356
|
+
return this.page("/v3/avatars", { ...query });
|
|
6357
|
+
}
|
|
6358
|
+
/** Looks are what video generation needs: a look `id` is the `avatar_id`. */
|
|
6359
|
+
listAvatarLooks(query = {}) {
|
|
6360
|
+
return this.page("/v3/avatars/looks", { ...query });
|
|
6361
|
+
}
|
|
6362
|
+
listVoices(query = {}) {
|
|
6363
|
+
return this.page("/v3/voices", { ...query });
|
|
6364
|
+
}
|
|
6365
|
+
// ── Speech ────────────────────────────────────────────────────────────
|
|
6366
|
+
createSpeech(body) {
|
|
6367
|
+
return this.request("/v3/voices/speech", { method: "POST", body });
|
|
6368
|
+
}
|
|
6369
|
+
// ── Translation & lipsync ─────────────────────────────────────────────
|
|
6370
|
+
async createVideoTranslation(body, idempotencyKey = true) {
|
|
6371
|
+
const path57 = "/v3/video-translations";
|
|
6372
|
+
const data = await this.request(path57, {
|
|
6373
|
+
method: "POST",
|
|
6374
|
+
body,
|
|
6375
|
+
idempotencyKey
|
|
6376
|
+
});
|
|
6377
|
+
return requireJobId(data, "video_translation_ids", path57);
|
|
6378
|
+
}
|
|
6379
|
+
async getVideoTranslation(id, opts = {}) {
|
|
6380
|
+
return this.request(`/v3/video-translations/${seg(id, "video translation id")}`, opts);
|
|
6381
|
+
}
|
|
6382
|
+
async listTranslationLanguages() {
|
|
6383
|
+
const data = await this.request("/v3/video-translations/languages");
|
|
6384
|
+
if (!Array.isArray(data.languages)) {
|
|
6385
|
+
throw new ProviderError("Unexpected HeyGen response: languages is not a list", "heygen");
|
|
6386
|
+
}
|
|
6387
|
+
return data.languages.filter((l) => typeof l === "string");
|
|
6388
|
+
}
|
|
6389
|
+
async createLipsync(body, idempotencyKey = true) {
|
|
6390
|
+
const path57 = "/v3/lipsyncs";
|
|
6391
|
+
const data = await this.request(path57, {
|
|
6392
|
+
method: "POST",
|
|
6393
|
+
body,
|
|
6394
|
+
idempotencyKey
|
|
6395
|
+
});
|
|
6396
|
+
return requireJobId(data, "lipsync_id", path57);
|
|
6397
|
+
}
|
|
6398
|
+
async getLipsync(id, opts = {}) {
|
|
6399
|
+
return this.request(`/v3/lipsyncs/${seg(id, "lipsync id")}`, opts);
|
|
6400
|
+
}
|
|
6401
|
+
// ── Assets ────────────────────────────────────────────────────────────
|
|
6402
|
+
/**
|
|
6403
|
+
* POST /v3/assets (multipart, max 32 MB). Accepts bytes or a Blob so it works
|
|
6404
|
+
* without a filesystem; for remote files prefer passing `{type:"url"}` inputs.
|
|
6405
|
+
*/
|
|
6406
|
+
async uploadAsset(input) {
|
|
6407
|
+
const blob = input.data instanceof Blob ? input.data : new Blob([input.data], { type: input.mimeType });
|
|
6408
|
+
if (blob.size === 0) throw new ValidationError(`Cannot upload empty file ${input.filename}`);
|
|
6409
|
+
if (blob.size > LIMITS.assetUploadMaxBytes) {
|
|
6410
|
+
throw new ValidationError(
|
|
6411
|
+
`${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.`
|
|
6412
|
+
);
|
|
6413
|
+
}
|
|
6414
|
+
const typed = blob.type ? blob : new Blob([blob], { type: input.mimeType });
|
|
6415
|
+
const key = this.newIdempotencyKey();
|
|
6416
|
+
const url = this.url("/v3/assets");
|
|
6417
|
+
const doFetch = this.fetchImpl ?? ((u, init) => globalThis.fetch(u, init));
|
|
6418
|
+
for (let attempt = 1; ; attempt++) {
|
|
6419
|
+
const form = new FormData();
|
|
6420
|
+
form.append("file", typed, input.filename);
|
|
6421
|
+
let response;
|
|
6422
|
+
try {
|
|
6423
|
+
response = await doFetch(url, {
|
|
6424
|
+
method: "POST",
|
|
6425
|
+
headers: this.headers(key),
|
|
6426
|
+
body: form,
|
|
6427
|
+
signal: AbortSignal.timeout(this.timeoutMs)
|
|
6428
|
+
});
|
|
6429
|
+
} catch (cause) {
|
|
6430
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
6431
|
+
const err3 = toHeygenApiError(new HttpError(0, msg, url));
|
|
6432
|
+
if (attempt >= this.maxAttempts) throw err3;
|
|
6433
|
+
await this.sleep(this.retryDelay(err3, key, attempt) ?? 1e3);
|
|
6434
|
+
continue;
|
|
6435
|
+
}
|
|
6436
|
+
if (response.ok) {
|
|
6437
|
+
let json;
|
|
6438
|
+
try {
|
|
6439
|
+
json = await response.json();
|
|
6440
|
+
} catch {
|
|
6441
|
+
throw new ProviderError(
|
|
6442
|
+
`HeyGen /v3/assets returned a non-JSON response (HTTP ${response.status}) for ${input.filename}`,
|
|
6443
|
+
"heygen"
|
|
6444
|
+
);
|
|
6445
|
+
}
|
|
6446
|
+
const data = unwrapData(json, "/v3/assets");
|
|
6447
|
+
return requireJobId(data, "asset_id", "/v3/assets");
|
|
6448
|
+
}
|
|
6449
|
+
const err2 = toHeygenApiError(await httpErrorFromResponse(response, url));
|
|
6450
|
+
const wait = attempt < this.maxAttempts ? this.retryDelay(err2, key, attempt) : void 0;
|
|
6451
|
+
if (wait === void 0) throw err2;
|
|
6452
|
+
await this.sleep(wait);
|
|
6453
|
+
}
|
|
6454
|
+
}
|
|
6455
|
+
};
|
|
6456
|
+
function unwrapData(json, path57) {
|
|
6457
|
+
if (json === null || typeof json !== "object") {
|
|
6458
|
+
throw new ProviderError(`Unexpected HeyGen response from ${path57}: not a JSON object`, "heygen");
|
|
6459
|
+
}
|
|
6460
|
+
const record = json;
|
|
6461
|
+
const error = record.error;
|
|
6462
|
+
if (error && typeof error === "object") {
|
|
6463
|
+
const e = error;
|
|
6464
|
+
throw new ProviderError(
|
|
6465
|
+
`HeyGen API error${e.code ? ` (${e.code})` : ""}: ${e.message ?? "unknown error"}`,
|
|
6466
|
+
"heygen"
|
|
6467
|
+
);
|
|
6468
|
+
}
|
|
6469
|
+
if (!("data" in record) || record.data === null || record.data === void 0) {
|
|
6470
|
+
throw new ProviderError(`Unexpected HeyGen response from ${path57}: missing data`, "heygen");
|
|
6471
|
+
}
|
|
6472
|
+
return record.data;
|
|
6473
|
+
}
|
|
6474
|
+
|
|
6475
|
+
// src/providers/heygen/jobs.ts
|
|
6476
|
+
var str = (v) => typeof v === "string" && v ? v : void 0;
|
|
6477
|
+
function videoState(video, kind = "video", id = video.id, opts = {}) {
|
|
6478
|
+
const done = video.status === "completed" || video.status === "failed";
|
|
6479
|
+
const captioned = str(video.captioned_video_url);
|
|
6480
|
+
const state = {
|
|
6481
|
+
kind,
|
|
6482
|
+
id,
|
|
6483
|
+
status: video.status,
|
|
6484
|
+
done,
|
|
6485
|
+
succeeded: video.status === "completed",
|
|
6486
|
+
videoId: video.id,
|
|
6487
|
+
outputUrl: opts.burnCaptions ? captioned : str(video.video_url) ?? captioned,
|
|
6488
|
+
captionedVideoUrl: captioned,
|
|
6489
|
+
thumbnailUrl: str(video.thumbnail_url),
|
|
6490
|
+
captionUrl: str(video.subtitle_url),
|
|
6491
|
+
failureCode: str(video.failure_code),
|
|
6492
|
+
failureMessage: str(video.failure_message),
|
|
6493
|
+
raw: { video }
|
|
6494
|
+
};
|
|
6495
|
+
if (opts.burnCaptions && state.succeeded && !captioned) {
|
|
6496
|
+
return {
|
|
6497
|
+
...state,
|
|
6498
|
+
succeeded: false,
|
|
6499
|
+
failureCode: "captioned_video_missing",
|
|
6500
|
+
failureMessage: "Burned-in captions were requested but HeyGen returned no captioned_video_url. The uncaptioned render is available via `heygen status --raw`."
|
|
6501
|
+
};
|
|
6502
|
+
}
|
|
6503
|
+
return state;
|
|
6504
|
+
}
|
|
6505
|
+
function translationState(t) {
|
|
6506
|
+
return {
|
|
6507
|
+
kind: "translation",
|
|
6508
|
+
id: t.id,
|
|
6509
|
+
status: t.status,
|
|
6510
|
+
done: t.status === "completed" || t.status === "failed",
|
|
6511
|
+
succeeded: t.status === "completed",
|
|
6512
|
+
outputUrl: str(t.video_url) ?? str(t.audio_url),
|
|
6513
|
+
captionUrl: str(t.srt_caption_url) ?? str(t.vtt_caption_url),
|
|
6514
|
+
outputLanguage: str(t.output_language),
|
|
6515
|
+
failureMessage: str(t.failure_message),
|
|
6516
|
+
raw: { translation: t }
|
|
6517
|
+
};
|
|
6518
|
+
}
|
|
6519
|
+
function lipsyncState(l) {
|
|
6520
|
+
return {
|
|
6521
|
+
kind: "lipsync",
|
|
6522
|
+
id: l.id,
|
|
6523
|
+
status: l.status,
|
|
6524
|
+
done: l.status === "completed" || l.status === "failed",
|
|
6525
|
+
succeeded: l.status === "completed",
|
|
6526
|
+
outputUrl: str(l.video_url),
|
|
6527
|
+
captionUrl: str(l.caption_url),
|
|
6528
|
+
failureMessage: str(l.failure_message),
|
|
6529
|
+
raw: { lipsync: l }
|
|
6530
|
+
};
|
|
6531
|
+
}
|
|
6532
|
+
function modelVideoState(v) {
|
|
6533
|
+
const done = v.status === "completed" || v.status === "failed" || v.status === "cancelled";
|
|
6534
|
+
return {
|
|
6535
|
+
kind: "model-video",
|
|
6536
|
+
id: v.video_id,
|
|
6537
|
+
status: v.status,
|
|
6538
|
+
done,
|
|
6539
|
+
succeeded: v.status === "completed",
|
|
6540
|
+
videoId: v.video_id,
|
|
6541
|
+
outputUrl: str(v.video_url),
|
|
6542
|
+
failureCode: str(v.failure_code),
|
|
6543
|
+
failureMessage: str(v.failure_message),
|
|
6544
|
+
raw: { video: v }
|
|
6545
|
+
};
|
|
6546
|
+
}
|
|
6547
|
+
function agentState(session, video) {
|
|
6548
|
+
const base = {
|
|
6549
|
+
kind: "agent",
|
|
6550
|
+
id: session.session_id,
|
|
6551
|
+
sessionId: session.session_id,
|
|
6552
|
+
sessionStatus: session.status,
|
|
6553
|
+
progress: session.progress
|
|
6554
|
+
};
|
|
6555
|
+
const sessionError = session.error ?? void 0;
|
|
6556
|
+
if (video) {
|
|
6557
|
+
const v = videoState(video, "agent", session.session_id);
|
|
6558
|
+
if (!v.done && session.status === "failed" && sessionError?.code !== "context_limit_exceeded") {
|
|
6559
|
+
return {
|
|
6560
|
+
...v,
|
|
6561
|
+
...base,
|
|
6562
|
+
status: "failed",
|
|
6563
|
+
done: true,
|
|
6564
|
+
succeeded: false,
|
|
6565
|
+
failureCode: sessionError?.code,
|
|
6566
|
+
failureMessage: sessionError?.message ?? "Video Agent session failed",
|
|
6567
|
+
raw: { session, video }
|
|
6568
|
+
};
|
|
6569
|
+
}
|
|
6570
|
+
if (v.done && !v.succeeded && session.status === "waiting_for_input") {
|
|
6571
|
+
return {
|
|
6572
|
+
...v,
|
|
6573
|
+
...base,
|
|
6574
|
+
failureCode: v.failureCode ?? "waiting_for_input",
|
|
6575
|
+
failureMessage: v.failureMessage ?? "Video Agent paused for input; continue the session in the HeyGen app or start a new prompt.",
|
|
6576
|
+
raw: { session, video }
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6579
|
+
return { ...v, ...base, raw: { session, video } };
|
|
6580
|
+
}
|
|
6581
|
+
const pending2 = { ...base, status: session.status, raw: { session } };
|
|
6582
|
+
if (session.status === "failed") {
|
|
6583
|
+
return {
|
|
6584
|
+
...pending2,
|
|
6585
|
+
done: true,
|
|
6586
|
+
succeeded: false,
|
|
6587
|
+
failureCode: sessionError?.code,
|
|
6588
|
+
failureMessage: sessionError?.message ?? "Video Agent session failed"
|
|
6589
|
+
};
|
|
6590
|
+
}
|
|
6591
|
+
if (session.status === "waiting_for_input") {
|
|
6592
|
+
return {
|
|
6593
|
+
...pending2,
|
|
6594
|
+
done: true,
|
|
6595
|
+
succeeded: false,
|
|
6596
|
+
failureCode: "waiting_for_input",
|
|
6597
|
+
failureMessage: "Video Agent paused for input before rendering; continue the session in the HeyGen app or start a new prompt."
|
|
6598
|
+
};
|
|
6599
|
+
}
|
|
6600
|
+
if (session.status === "completed") {
|
|
6601
|
+
return {
|
|
6602
|
+
...pending2,
|
|
6603
|
+
done: true,
|
|
6604
|
+
succeeded: false,
|
|
6605
|
+
failureCode: "no_video",
|
|
6606
|
+
failureMessage: "Video Agent session completed without a video_id."
|
|
6607
|
+
};
|
|
6608
|
+
}
|
|
6609
|
+
return { ...pending2, done: false, succeeded: false };
|
|
6610
|
+
}
|
|
6611
|
+
function createJobStatusReader(client, kind, id, opts = {}) {
|
|
6612
|
+
return async (ctx) => {
|
|
6613
|
+
const call = { timeoutMs: ctx?.remainingMs };
|
|
6614
|
+
switch (kind) {
|
|
6615
|
+
case "video":
|
|
6616
|
+
return videoState(await client.getVideo(id, call), "video", id, opts);
|
|
6617
|
+
case "translation":
|
|
6618
|
+
return translationState(await client.getVideoTranslation(id, call));
|
|
6619
|
+
case "lipsync":
|
|
6620
|
+
return lipsyncState(await client.getLipsync(id, call));
|
|
6621
|
+
case "model-video":
|
|
6622
|
+
return modelVideoState(await client.getModelVideo(id, call));
|
|
6623
|
+
case "agent": {
|
|
6624
|
+
const session = await client.getVideoAgentSession(id, call);
|
|
6625
|
+
if (!session.video_id) return agentState(session);
|
|
6626
|
+
return agentState(session, await client.getVideo(session.video_id, call));
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
6631
|
+
function getJobState(client, kind, id, opts = {}) {
|
|
6632
|
+
return createJobStatusReader(client, kind, id, opts)();
|
|
6633
|
+
}
|
|
6634
|
+
var DEFAULT_POLL_INTERVAL_MS = 1e4;
|
|
6635
|
+
function waitForJob(client, kind, id, opts) {
|
|
6636
|
+
const { burnCaptions, ...poll2 } = opts;
|
|
6637
|
+
return pollUntil({
|
|
6638
|
+
intervalMs: DEFAULT_POLL_INTERVAL_MS,
|
|
6639
|
+
maxIntervalMs: 3e4,
|
|
6640
|
+
...poll2,
|
|
6641
|
+
fetchStatus: createJobStatusReader(client, kind, id, { burnCaptions }),
|
|
6642
|
+
isTerminal: (s) => s.done
|
|
6643
|
+
});
|
|
6644
|
+
}
|
|
6645
|
+
function assertJobSucceeded(state) {
|
|
6646
|
+
if (state.succeeded) return;
|
|
6647
|
+
const label = state.kind === "agent" ? `Video Agent session ${state.id}` : `${state.kind} ${state.id}`;
|
|
6648
|
+
const code = state.failureCode ? ` (${state.failureCode})` : "";
|
|
6649
|
+
const msg = state.failureMessage ? `: ${state.failureMessage}` : "";
|
|
6650
|
+
throw new HeygenJobFailedError(
|
|
6651
|
+
`HeyGen ${label} ended with status "${state.status}"${code}${msg}`,
|
|
6652
|
+
state.failureCode
|
|
6653
|
+
);
|
|
6654
|
+
}
|
|
6655
|
+
function shellArg(value) {
|
|
6656
|
+
return /^[A-Za-z0-9_-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
|
|
6657
|
+
}
|
|
6658
|
+
function resumeCommand2(kind, id, opts = {}) {
|
|
6659
|
+
const captions = opts.burnCaptions ? " --burn-captions" : "";
|
|
6660
|
+
return `multix heygen status ${shellArg(id)} --type ${kind} --wait --download${captions}`;
|
|
6661
|
+
}
|
|
6662
|
+
|
|
6663
|
+
// src/providers/heygen/request-builders.ts
|
|
6664
|
+
init_errors();
|
|
6665
|
+
function oneOf(value, allowed, flag) {
|
|
6666
|
+
if (value === void 0) return void 0;
|
|
6667
|
+
if (!allowed.includes(value)) {
|
|
6668
|
+
throw new ValidationError(`${flag} must be one of: ${allowed.join(", ")} (got "${value}")`);
|
|
6669
|
+
}
|
|
6670
|
+
return value;
|
|
6671
|
+
}
|
|
6672
|
+
function numberInRange(value, flag, min, max, integer = false) {
|
|
6673
|
+
if (value === void 0 || value === "") return void 0;
|
|
6674
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
6675
|
+
if (!Number.isFinite(n) || n < min || n > max || integer && !Number.isInteger(n)) {
|
|
6676
|
+
throw new ValidationError(
|
|
6677
|
+
`${flag} must be ${integer ? "an integer" : "a number"} between ${min} and ${max} (got "${value}")`
|
|
6678
|
+
);
|
|
6679
|
+
}
|
|
6680
|
+
return n;
|
|
6681
|
+
}
|
|
6682
|
+
function requireHttps(url, flag) {
|
|
6683
|
+
if (!/^https:\/\//i.test(url)) throw new ValidationError(`${flag} must be an https:// URL`);
|
|
6684
|
+
return url;
|
|
6685
|
+
}
|
|
6686
|
+
function classifyMediaRef(value) {
|
|
6687
|
+
const v = value.trim();
|
|
6688
|
+
if (!v) throw new ValidationError("Media reference must not be empty");
|
|
6689
|
+
if (/^https?:\/\//i.test(v)) return { kind: "url", url: requireHttps(v, "Media URL") };
|
|
6690
|
+
const asset = v.match(/^asset(?:_id)?:(.+)$/i);
|
|
6691
|
+
if (asset?.[1]) return { kind: "asset", assetId: asset[1].trim() };
|
|
6692
|
+
return { kind: "path", path: v };
|
|
6693
|
+
}
|
|
6694
|
+
function splitLanguages(raw) {
|
|
6695
|
+
const out = [];
|
|
6696
|
+
let depth = 0;
|
|
6697
|
+
let current = "";
|
|
6698
|
+
for (const ch of raw) {
|
|
6699
|
+
if (ch === "(") depth++;
|
|
6700
|
+
if (ch === ")") depth = Math.max(0, depth - 1);
|
|
6701
|
+
if (ch === "," && depth === 0) {
|
|
6702
|
+
out.push(current);
|
|
6703
|
+
current = "";
|
|
6704
|
+
} else {
|
|
6705
|
+
current += ch;
|
|
6706
|
+
}
|
|
6707
|
+
}
|
|
6708
|
+
out.push(current);
|
|
6709
|
+
const langs = out.map((s) => s.trim()).filter(Boolean);
|
|
6710
|
+
if (langs.length === 0) throw new ValidationError("--languages must list at least one language");
|
|
6711
|
+
return [...new Set(langs)];
|
|
6712
|
+
}
|
|
6713
|
+
function checkCallback(url) {
|
|
6714
|
+
return url === void 0 ? void 0 : requireHttps(url, "--callback-url");
|
|
6715
|
+
}
|
|
6716
|
+
function buildVideoAgentRequest(input) {
|
|
6717
|
+
const prompt = input.prompt?.trim() ?? "";
|
|
6718
|
+
if (prompt.length < 1 || prompt.length > LIMITS.agentPromptMax) {
|
|
6719
|
+
throw new ValidationError(`Prompt must be 1-${LIMITS.agentPromptMax} characters`);
|
|
6720
|
+
}
|
|
6721
|
+
const files = input.files ?? [];
|
|
6722
|
+
if (files.length > LIMITS.agentFilesMax) {
|
|
6723
|
+
throw new ValidationError(`Video Agent accepts at most ${LIMITS.agentFilesMax} files`);
|
|
6724
|
+
}
|
|
6725
|
+
const body = { prompt, mode: "generate" };
|
|
6726
|
+
if (input.avatarId) body.avatar_id = input.avatarId;
|
|
6727
|
+
if (input.voiceId) body.voice_id = input.voiceId;
|
|
6728
|
+
if (input.styleId) body.style_id = input.styleId;
|
|
6729
|
+
if (input.brandKitId) body.brand_kit_id = input.brandKitId;
|
|
6730
|
+
if (input.brandGlossaryId) body.brand_glossary_id = input.brandGlossaryId;
|
|
6731
|
+
const orientation = oneOf(input.orientation, AGENT_ORIENTATIONS, "--orientation");
|
|
6732
|
+
if (orientation) body.orientation = orientation;
|
|
6733
|
+
if (files.length > 0) body.files = files;
|
|
6734
|
+
const callbackUrl = checkCallback(input.callbackUrl);
|
|
6735
|
+
if (callbackUrl) body.callback_url = callbackUrl;
|
|
6736
|
+
if (input.callbackId) body.callback_id = input.callbackId;
|
|
6737
|
+
return body;
|
|
6738
|
+
}
|
|
6739
|
+
function buildAvatarVideoRequest(input) {
|
|
6740
|
+
if (!input.avatarId?.trim()) throw new ValidationError("--avatar <look-id> is required");
|
|
6741
|
+
const script = input.script?.trim();
|
|
6742
|
+
if (script && input.audio) {
|
|
6743
|
+
throw new ValidationError("Provide either a script or --audio, not both");
|
|
6744
|
+
}
|
|
6745
|
+
if (!script && !input.audio) {
|
|
6746
|
+
throw new ValidationError("Provide a script argument or --audio <url|asset:id|file>");
|
|
6747
|
+
}
|
|
6748
|
+
if (script && script.length > LIMITS.scriptMax) {
|
|
6749
|
+
throw new ValidationError(`Script must be at most ${LIMITS.scriptMax} characters`);
|
|
6750
|
+
}
|
|
6751
|
+
const body = { type: "avatar", avatar_id: input.avatarId.trim() };
|
|
6752
|
+
if (script) {
|
|
6753
|
+
body.script = script;
|
|
6754
|
+
if (input.voiceId) body.voice_id = input.voiceId;
|
|
6755
|
+
} else if (input.audio) {
|
|
6756
|
+
if (input.voiceId) throw new ValidationError("--voice applies to scripts, not --audio");
|
|
6757
|
+
if (input.audio.type === "url") body.audio_url = input.audio.url;
|
|
6758
|
+
else if (input.audio.type === "asset_id") body.audio_asset_id = input.audio.asset_id;
|
|
6759
|
+
else throw new ValidationError("--audio must be a URL, asset id, or uploaded file");
|
|
6760
|
+
}
|
|
6761
|
+
const resolution = oneOf(input.resolution, AVATAR_VIDEO_RESOLUTIONS, "--resolution");
|
|
6762
|
+
if (resolution) body.resolution = resolution;
|
|
6763
|
+
const aspect = oneOf(input.aspectRatio, AVATAR_VIDEO_ASPECT_RATIOS, "--aspect-ratio");
|
|
6764
|
+
if (aspect) body.aspect_ratio = aspect;
|
|
6765
|
+
const fit = oneOf(input.fit, ["contain", "cover"], "--fit");
|
|
6766
|
+
if (fit) body.fit = fit;
|
|
6767
|
+
const outputFormat = oneOf(input.outputFormat, ["mp4", "webm"], "--output-format");
|
|
6768
|
+
if (outputFormat) body.output_format = outputFormat;
|
|
6769
|
+
if (input.backgroundColor && input.backgroundImage) {
|
|
6770
|
+
throw new ValidationError("Use either --background-color or --background-image, not both");
|
|
6771
|
+
}
|
|
6772
|
+
if (outputFormat === "webm" && (input.backgroundColor || input.backgroundImage)) {
|
|
6773
|
+
throw new ValidationError(
|
|
6774
|
+
"--output-format webm renders a transparent background; drop --background-*"
|
|
6775
|
+
);
|
|
6776
|
+
}
|
|
6777
|
+
if (input.backgroundColor) {
|
|
6778
|
+
if (!/^#[0-9a-f]{6}$/i.test(input.backgroundColor)) {
|
|
6779
|
+
throw new ValidationError("--background-color must be a hex color like #112233");
|
|
6780
|
+
}
|
|
6781
|
+
body.background = { type: "color", value: input.backgroundColor };
|
|
6782
|
+
} else if (input.backgroundImage) {
|
|
6783
|
+
const bg = input.backgroundImage;
|
|
6784
|
+
if (bg.type === "url") body.background = { type: "image", url: bg.url };
|
|
6785
|
+
else if (bg.type === "asset_id") body.background = { type: "image", asset_id: bg.asset_id };
|
|
6786
|
+
}
|
|
6787
|
+
if (input.removeBackground) body.remove_background = true;
|
|
6788
|
+
if (input.captions || input.burnCaptions) {
|
|
6789
|
+
body.caption = {
|
|
6790
|
+
file_format: "srt",
|
|
6791
|
+
...input.burnCaptions ? { style: "default" } : {}
|
|
6792
|
+
};
|
|
6793
|
+
}
|
|
6794
|
+
const speed = numberInRange(input.speed, "--speed", 0.5, 1.5);
|
|
6795
|
+
const pitch = numberInRange(input.pitch, "--pitch", -50, 50);
|
|
6796
|
+
const volume = numberInRange(input.volume, "--volume", 0, 1);
|
|
6797
|
+
if (speed !== void 0 || pitch !== void 0 || volume !== void 0 || input.locale) {
|
|
6798
|
+
if (!script) throw new ValidationError("Voice settings apply only to scripted videos");
|
|
6799
|
+
body.voice_settings = {
|
|
6800
|
+
...speed !== void 0 ? { speed } : {},
|
|
6801
|
+
...pitch !== void 0 ? { pitch } : {},
|
|
6802
|
+
...volume !== void 0 ? { volume } : {},
|
|
6803
|
+
...input.locale ? { locale: input.locale } : {}
|
|
6804
|
+
};
|
|
6805
|
+
}
|
|
6806
|
+
const engine = oneOf(input.engine, AVATAR_ENGINES, "--engine");
|
|
6807
|
+
if (input.referenceLookId && engine !== "avatar_v") {
|
|
6808
|
+
throw new ValidationError("--reference-look requires --engine avatar_v");
|
|
6809
|
+
}
|
|
6810
|
+
if (engine) {
|
|
6811
|
+
body.engine = {
|
|
6812
|
+
type: engine,
|
|
6813
|
+
...input.referenceLookId ? { reference_look_id: input.referenceLookId } : {}
|
|
6814
|
+
};
|
|
6815
|
+
}
|
|
6816
|
+
if (input.motionPrompt) {
|
|
6817
|
+
if (engine === "avatar_iii") {
|
|
6818
|
+
throw new ValidationError("--motion-prompt is not supported with --engine avatar_iii");
|
|
6819
|
+
}
|
|
6820
|
+
body.motion_prompt = input.motionPrompt;
|
|
6821
|
+
}
|
|
6822
|
+
const expressiveness = oneOf(input.expressiveness, EXPRESSIVENESS_LEVELS, "--expressiveness");
|
|
6823
|
+
if (expressiveness) {
|
|
6824
|
+
if (engine === "avatar_v" || engine === "avatar_iii") {
|
|
6825
|
+
throw new ValidationError("--expressiveness is Avatar IV only (photo avatars)");
|
|
6826
|
+
}
|
|
6827
|
+
body.expressiveness = expressiveness;
|
|
6828
|
+
}
|
|
6829
|
+
if (resolution === "4k" && engine !== "avatar_iii") {
|
|
6830
|
+
throw new ValidationError(
|
|
6831
|
+
"--resolution 4k requires --engine avatar_iii (digital twins and studio avatars)"
|
|
6832
|
+
);
|
|
6833
|
+
}
|
|
6834
|
+
if (input.title) body.title = input.title;
|
|
6835
|
+
if (input.folderId) body.folder_id = input.folderId;
|
|
6836
|
+
if (input.brandGlossaryId) body.brand_glossary_id = input.brandGlossaryId;
|
|
6837
|
+
const callbackUrl = checkCallback(input.callbackUrl);
|
|
6838
|
+
if (callbackUrl) body.callback_url = callbackUrl;
|
|
6839
|
+
if (input.callbackId) body.callback_id = input.callbackId;
|
|
6840
|
+
return body;
|
|
6841
|
+
}
|
|
6842
|
+
function buildSpeechRequest(input) {
|
|
6843
|
+
const text = input.text ?? "";
|
|
6844
|
+
if (text.trim().length < 1 || text.length > LIMITS.speechTextMax) {
|
|
6845
|
+
throw new ValidationError(`Text must be 1-${LIMITS.speechTextMax} characters`);
|
|
6846
|
+
}
|
|
6847
|
+
if (!input.voiceId?.trim()) throw new ValidationError("--voice <voice-id> is required");
|
|
6848
|
+
const body = { text, voice_id: input.voiceId.trim() };
|
|
6849
|
+
if (input.ssml) body.input_type = "ssml";
|
|
6850
|
+
const speed = numberInRange(input.speed, "--speed", 0.5, 2);
|
|
6851
|
+
if (speed !== void 0) body.speed = speed;
|
|
6852
|
+
if (input.language) body.language = input.language;
|
|
6853
|
+
if (input.locale) body.locale = input.locale;
|
|
6854
|
+
const engine = oneOf(input.engine, SPEECH_ENGINES, "--engine");
|
|
6855
|
+
if (engine) body.engine = engine;
|
|
6856
|
+
return body;
|
|
6857
|
+
}
|
|
6858
|
+
function buildModelVideoRequest(input) {
|
|
6859
|
+
const prompt = input.prompt?.trim() ?? "";
|
|
6860
|
+
if (prompt.length < 1 || prompt.length > LIMITS.modelVideoPromptMax) {
|
|
6861
|
+
throw new ValidationError(`Prompt must be 1-${LIMITS.modelVideoPromptMax} characters`);
|
|
6862
|
+
}
|
|
6863
|
+
const refImages = input.referenceImages ?? [];
|
|
6864
|
+
const refVideos = input.referenceVideos ?? [];
|
|
6865
|
+
const refAudio = input.referenceAudio ?? [];
|
|
6866
|
+
const hasRefs = refImages.length + refVideos.length + refAudio.length > 0;
|
|
6867
|
+
const inferred = input.image ? "image_to_video" : hasRefs ? "reference_to_video" : "text_to_video";
|
|
6868
|
+
const mode = oneOf(input.mode, MODEL_VIDEO_MODES, "--mode") ?? inferred;
|
|
6869
|
+
if (mode === "image_to_video" && !input.image) {
|
|
6870
|
+
throw new ValidationError("image_to_video requires --image");
|
|
6871
|
+
}
|
|
6872
|
+
if (mode !== "image_to_video" && input.image) {
|
|
6873
|
+
throw new ValidationError("--image is only valid with --mode image_to_video");
|
|
6874
|
+
}
|
|
6875
|
+
if (mode === "reference_to_video" && refImages.length + refVideos.length === 0) {
|
|
6876
|
+
throw new ValidationError(
|
|
6877
|
+
"reference_to_video needs at least one --reference-image or --reference-video"
|
|
6878
|
+
);
|
|
6879
|
+
}
|
|
6880
|
+
if (mode !== "reference_to_video" && hasRefs) {
|
|
6881
|
+
throw new ValidationError("--reference-* inputs are only valid with --mode reference_to_video");
|
|
6882
|
+
}
|
|
6883
|
+
if (refImages.length > LIMITS.modelVideoReferenceImagesMax) {
|
|
6884
|
+
throw new ValidationError(`At most ${LIMITS.modelVideoReferenceImagesMax} reference images`);
|
|
6885
|
+
}
|
|
6886
|
+
if (refVideos.length > LIMITS.modelVideoReferenceVideosMax) {
|
|
6887
|
+
throw new ValidationError(`At most ${LIMITS.modelVideoReferenceVideosMax} reference videos`);
|
|
6888
|
+
}
|
|
6889
|
+
if (refAudio.length > LIMITS.modelVideoReferenceAudioMax) {
|
|
6890
|
+
throw new ValidationError(
|
|
6891
|
+
`At most ${LIMITS.modelVideoReferenceAudioMax} reference audio files`
|
|
6892
|
+
);
|
|
6893
|
+
}
|
|
6894
|
+
const refTotal = refImages.length + refVideos.length + refAudio.length;
|
|
6895
|
+
if (refTotal > LIMITS.modelVideoReferenceTotalMax) {
|
|
6896
|
+
throw new ValidationError(
|
|
6897
|
+
`At most ${LIMITS.modelVideoReferenceTotalMax} reference files in total (got ${refTotal})`
|
|
6898
|
+
);
|
|
6899
|
+
}
|
|
6900
|
+
const body = { model: HEYGEN_VIDEO_MODEL, mode, prompt };
|
|
6901
|
+
const duration = numberInRange(
|
|
6902
|
+
input.duration,
|
|
6903
|
+
"--duration",
|
|
6904
|
+
LIMITS.modelVideoMinDuration,
|
|
6905
|
+
LIMITS.modelVideoMaxDuration,
|
|
6906
|
+
true
|
|
6907
|
+
);
|
|
6908
|
+
if (duration !== void 0) body.duration = duration;
|
|
6909
|
+
const resolution = oneOf(input.resolution, MODEL_VIDEO_RESOLUTIONS, "--resolution");
|
|
6910
|
+
if (resolution) body.resolution = resolution;
|
|
6911
|
+
const aspect = oneOf(input.aspectRatio, MODEL_VIDEO_ASPECT_RATIOS, "--aspect-ratio");
|
|
6912
|
+
if (aspect === "adaptive" && mode !== "reference_to_video") {
|
|
6913
|
+
throw new ValidationError("--aspect-ratio adaptive is only valid for reference_to_video");
|
|
6914
|
+
}
|
|
6915
|
+
if (aspect && mode === "image_to_video") {
|
|
6916
|
+
throw new ValidationError("image_to_video follows the first frame; drop --aspect-ratio");
|
|
6917
|
+
}
|
|
6918
|
+
if (aspect) body.aspect_ratio = aspect;
|
|
6919
|
+
const enhancement = oneOf(
|
|
6920
|
+
input.promptEnhancement,
|
|
6921
|
+
MODEL_VIDEO_PROMPT_ENHANCEMENT,
|
|
6922
|
+
"--prompt-enhancement"
|
|
6923
|
+
);
|
|
6924
|
+
if (enhancement) body.prompt_enhancement = enhancement;
|
|
6925
|
+
const seed = numberInRange(input.seed, "--seed", 0, 4294967295, true);
|
|
6926
|
+
if (seed !== void 0) body.seed = seed;
|
|
6927
|
+
if (input.image) body.image = input.image;
|
|
6928
|
+
if (refImages.length) body.reference_images = refImages;
|
|
6929
|
+
if (refVideos.length) body.reference_videos = refVideos;
|
|
6930
|
+
if (refAudio.length) body.reference_audio = refAudio;
|
|
6931
|
+
const callbackUrl = checkCallback(input.callbackUrl);
|
|
6932
|
+
if (callbackUrl) body.callback_url = callbackUrl;
|
|
6933
|
+
if (input.callbackId) body.callback_id = input.callbackId;
|
|
6934
|
+
return body;
|
|
6935
|
+
}
|
|
6936
|
+
function qualityMode(value) {
|
|
6937
|
+
return oneOf(value, QUALITY_MODES, "--mode");
|
|
6938
|
+
}
|
|
6939
|
+
|
|
6940
|
+
// src/providers/heygen/node-helpers.ts
|
|
6941
|
+
function requireHeygenKey() {
|
|
6942
|
+
const key = resolveKey("HEYGEN_API_KEY")?.trim();
|
|
6943
|
+
if (!key) {
|
|
6944
|
+
throw new ConfigError(
|
|
6945
|
+
`HEYGEN_API_KEY is not set. Create an API key at ${HEYGEN_API_SETTINGS_URL} (API usage is billed separately from HeyGen web plans).`
|
|
6946
|
+
);
|
|
6947
|
+
}
|
|
6948
|
+
return key;
|
|
6949
|
+
}
|
|
6950
|
+
function createHeygenClientFromEnv() {
|
|
6951
|
+
return new HeygenClient({
|
|
6952
|
+
apiKey: requireHeygenKey(),
|
|
6953
|
+
baseUrl: resolveKey("HEYGEN_BASE_URL") ?? DEFAULT_HEYGEN_BASE_URL
|
|
6954
|
+
});
|
|
6955
|
+
}
|
|
6956
|
+
async function uploadLocalFile(client, filePath, logger) {
|
|
6957
|
+
const abs = path32.resolve(filePath);
|
|
6958
|
+
let stat;
|
|
6959
|
+
try {
|
|
6960
|
+
stat = fs29.statSync(abs);
|
|
6961
|
+
} catch {
|
|
6962
|
+
throw new ValidationError(
|
|
6963
|
+
`File not found: ${filePath}. Use an https:// URL, asset:<asset_id>, or an existing file.`
|
|
6964
|
+
);
|
|
6965
|
+
}
|
|
6966
|
+
if (!stat.isFile()) throw new ValidationError(`Not a file: ${filePath}`);
|
|
6967
|
+
const ext = path32.extname(abs).slice(1).toLowerCase();
|
|
6968
|
+
const mimeType = ASSET_MIME_BY_EXT[ext];
|
|
6969
|
+
if (!mimeType) {
|
|
6970
|
+
throw new ValidationError(
|
|
6971
|
+
`Unsupported file type ".${ext}" for HeyGen upload. Supported: ${Object.keys(ASSET_MIME_BY_EXT).join(", ")}`
|
|
6972
|
+
);
|
|
6973
|
+
}
|
|
6974
|
+
if (stat.size > LIMITS.assetUploadMaxBytes) {
|
|
6975
|
+
throw new ValidationError(
|
|
6976
|
+
`${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.`
|
|
6977
|
+
);
|
|
6978
|
+
}
|
|
6979
|
+
logger.info(`Uploading ${path32.basename(abs)} to HeyGen assets...`);
|
|
6980
|
+
const res = await client.uploadAsset({
|
|
6981
|
+
data: new Uint8Array(fs29.readFileSync(abs)),
|
|
6982
|
+
filename: path32.basename(abs),
|
|
6983
|
+
mimeType
|
|
6984
|
+
});
|
|
6985
|
+
logger.debug(`Uploaded asset ${res.asset_id} (${res.mime_type}, ${res.size_bytes} bytes)`);
|
|
6986
|
+
return { type: "asset_id", asset_id: res.asset_id };
|
|
6987
|
+
}
|
|
6988
|
+
async function resolveMediaInput(client, value, logger) {
|
|
6989
|
+
const ref = classifyMediaRef(value);
|
|
6990
|
+
if (ref.kind === "url") return { type: "url", url: ref.url };
|
|
6991
|
+
if (ref.kind === "asset") return { type: "asset_id", asset_id: ref.assetId };
|
|
6992
|
+
return uploadLocalFile(client, ref.path, logger);
|
|
6993
|
+
}
|
|
6994
|
+
async function resolveMediaInputs(client, values, logger) {
|
|
6995
|
+
const out = [];
|
|
6996
|
+
for (const v of values ?? []) out.push(await resolveMediaInput(client, v, logger));
|
|
6997
|
+
return out;
|
|
6998
|
+
}
|
|
6999
|
+
function collect5(value, previous = []) {
|
|
7000
|
+
return [...previous, value];
|
|
7001
|
+
}
|
|
7002
|
+
function parsePositiveInt(raw, flag, fallback) {
|
|
7003
|
+
if (raw === void 0) return fallback;
|
|
7004
|
+
const n = Number(raw);
|
|
7005
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
7006
|
+
throw new ValidationError(`${flag} must be a positive integer`);
|
|
7007
|
+
return n;
|
|
7008
|
+
}
|
|
7009
|
+
var DEFAULT_WAIT_TIMEOUT_MS = 18e5;
|
|
7010
|
+
function addWaitOptions(cmd, defaultTimeoutMs = DEFAULT_WAIT_TIMEOUT_MS) {
|
|
7011
|
+
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(
|
|
7012
|
+
"--poll-interval <ms>",
|
|
7013
|
+
"Initial poll interval (backs off to 30s)",
|
|
7014
|
+
String(DEFAULT_POLL_INTERVAL_MS)
|
|
7015
|
+
).option("--no-download", "Do not download outputs; print output URLs instead").option("--output <path>", "Output file (or directory when several outputs are produced)").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");
|
|
7016
|
+
}
|
|
7017
|
+
var OUTPUT_EXTS = /* @__PURE__ */ new Set(["mp4", "webm", "mov", "mp3", "wav", "m4a", "aac", "srt", "vtt"]);
|
|
7018
|
+
function inferExtension(url, fallback) {
|
|
7019
|
+
try {
|
|
7020
|
+
const ext = path32.extname(new URL(url).pathname).slice(1).toLowerCase();
|
|
7021
|
+
return OUTPUT_EXTS.has(ext) ? ext : fallback;
|
|
7022
|
+
} catch {
|
|
7023
|
+
return fallback;
|
|
7024
|
+
}
|
|
7025
|
+
}
|
|
7026
|
+
function safeSlug(value) {
|
|
7027
|
+
return value.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "out";
|
|
7028
|
+
}
|
|
7029
|
+
async function downloadOutput(url, dest, resumeHint) {
|
|
7030
|
+
if (!url) {
|
|
7031
|
+
throw new ValidationError(`HeyGen reported success but returned no output URL. ${resumeHint}`);
|
|
7032
|
+
}
|
|
7033
|
+
if (!/^https?:\/\//i.test(url)) {
|
|
7034
|
+
throw new ValidationError(`HeyGen returned an invalid output URL (${redactUrl(url)})`);
|
|
7035
|
+
}
|
|
7036
|
+
const tmp = `${dest}.${process.pid}-${Date.now()}.part`;
|
|
7037
|
+
try {
|
|
7038
|
+
await downloadFile(url, tmp, 6e5);
|
|
7039
|
+
fs29.renameSync(tmp, dest);
|
|
7040
|
+
} catch (e) {
|
|
7041
|
+
fs29.rmSync(tmp, { force: true });
|
|
7042
|
+
const status = e instanceof HttpError ? e.status : 0;
|
|
7043
|
+
const expired = status === 403 || status === 404 || status === 410;
|
|
7044
|
+
const reason = e instanceof Error ? e.message : String(e);
|
|
7045
|
+
throw new ValidationError(
|
|
7046
|
+
`Failed to download HeyGen output from ${redactUrl(url)}: ${reason}.${expired ? " The signed URL may have expired." : ""} ${resumeHint}`
|
|
7047
|
+
);
|
|
7048
|
+
}
|
|
7049
|
+
return dest;
|
|
7050
|
+
}
|
|
7051
|
+
function defaultOutputPath(state, ext) {
|
|
7052
|
+
const lang = state.outputLanguage ? `-${safeSlug(state.outputLanguage)}` : "";
|
|
7053
|
+
return path32.join(getOutputDir(), `heygen-${state.kind}${lang}-${safeSlug(state.id)}.${ext}`);
|
|
7054
|
+
}
|
|
7055
|
+
function outputPathFor(state, ext, output, count) {
|
|
7056
|
+
if (!output) return defaultOutputPath(state, ext);
|
|
7057
|
+
if (count > 1) {
|
|
7058
|
+
const dir = path32.resolve(output);
|
|
7059
|
+
fs29.mkdirSync(dir, { recursive: true });
|
|
7060
|
+
return path32.join(dir, path32.basename(defaultOutputPath(state, ext)));
|
|
7061
|
+
}
|
|
7062
|
+
return path32.resolve(output);
|
|
7063
|
+
}
|
|
7064
|
+
function summarizeState(state) {
|
|
7065
|
+
const out = {
|
|
7066
|
+
kind: state.kind,
|
|
7067
|
+
id: state.id,
|
|
7068
|
+
status: state.status
|
|
7069
|
+
};
|
|
7070
|
+
if (state.sessionId) out.session_id = state.sessionId;
|
|
7071
|
+
if (state.sessionStatus) out.session_status = state.sessionStatus;
|
|
7072
|
+
if (state.videoId) out.video_id = state.videoId;
|
|
7073
|
+
if (state.progress !== void 0) out.progress = state.progress;
|
|
7074
|
+
if (state.outputLanguage) out.output_language = state.outputLanguage;
|
|
7075
|
+
if (state.outputUrl) out.output_url = redactUrl(state.outputUrl);
|
|
7076
|
+
if (state.captionedVideoUrl) out.captioned_video_url = redactUrl(state.captionedVideoUrl);
|
|
7077
|
+
if (state.failureCode) out.failure_code = state.failureCode;
|
|
7078
|
+
if (state.failureMessage) out.failure_message = state.failureMessage;
|
|
7079
|
+
return out;
|
|
7080
|
+
}
|
|
7081
|
+
async function deliverOne(state, opts, count, logger, files) {
|
|
7082
|
+
const hint = `Refresh with: ${resumeCommand2(state.kind, state.id, opts)}`;
|
|
7083
|
+
if (opts.download === false) {
|
|
7084
|
+
if (!state.outputUrl) {
|
|
7085
|
+
throw new ValidationError(`No output URL for ${state.kind} ${state.id}. ${hint}`);
|
|
7086
|
+
}
|
|
7087
|
+
if (!opts.json) console.log(state.outputUrl);
|
|
7088
|
+
const urls = {
|
|
7089
|
+
output_url: state.outputUrl,
|
|
7090
|
+
...state.captionUrl ? { caption_url: state.captionUrl } : {}
|
|
7091
|
+
};
|
|
7092
|
+
return { state, files, urls };
|
|
7093
|
+
}
|
|
7094
|
+
const ext = inferExtension(state.outputUrl ?? "", "mp4");
|
|
7095
|
+
const dest = outputPathFor(state, ext, opts.output, count);
|
|
7096
|
+
await downloadOutput(state.outputUrl, dest, hint);
|
|
7097
|
+
logger.success(`Saved ${dest}`);
|
|
7098
|
+
files.push(dest);
|
|
7099
|
+
if (opts.captions && state.captionUrl) {
|
|
7100
|
+
const capExt = inferExtension(state.captionUrl, "srt");
|
|
7101
|
+
const capDest = path32.join(
|
|
7102
|
+
path32.dirname(dest),
|
|
7103
|
+
`${path32.basename(dest, path32.extname(dest))}.${capExt}`
|
|
7104
|
+
);
|
|
7105
|
+
await downloadOutput(state.captionUrl, capDest, hint);
|
|
7106
|
+
logger.success(`Saved ${capDest}`);
|
|
7107
|
+
files.push(capDest);
|
|
7108
|
+
} else if (opts.captions) {
|
|
7109
|
+
logger.warn(`No caption file available for ${state.kind} ${state.id}`);
|
|
7110
|
+
}
|
|
7111
|
+
if (state.kind === "video" || state.kind === "agent") {
|
|
7112
|
+
const thumb = await maybeDownloadThumb(state.raw, dest, {
|
|
7113
|
+
skip: opts.thumb === false,
|
|
7114
|
+
logger
|
|
7115
|
+
});
|
|
7116
|
+
if (thumb) files.push(thumb);
|
|
7117
|
+
}
|
|
7118
|
+
return { state, files };
|
|
7119
|
+
}
|
|
7120
|
+
async function deliverOutputs(states, opts, logger) {
|
|
7121
|
+
const finished = [];
|
|
7122
|
+
const failures = [];
|
|
7123
|
+
for (const state of states) {
|
|
7124
|
+
const files = [];
|
|
7125
|
+
try {
|
|
7126
|
+
finished.push(await deliverOne(state, opts, states.length, logger, files));
|
|
7127
|
+
} catch (e) {
|
|
7128
|
+
failures.push(`${state.kind} ${state.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
7129
|
+
if (files.length > 0) finished.push({ state, files });
|
|
7130
|
+
}
|
|
7131
|
+
}
|
|
7132
|
+
return { finished, failures };
|
|
7133
|
+
}
|
|
7134
|
+
function printFinished(results, opts) {
|
|
7135
|
+
if (opts.json) {
|
|
7136
|
+
const jobs = results.map((r) => ({ ...summarizeState(r.state), ...r.urls, files: r.files }));
|
|
7137
|
+
if (jobs.length > 0) console.log(JSON.stringify(jobs.length === 1 ? jobs[0] : jobs, null, 2));
|
|
7138
|
+
return;
|
|
7139
|
+
}
|
|
7140
|
+
for (const r of results) {
|
|
7141
|
+
for (const f of r.files) console.log(`Saved: ${f}`);
|
|
7142
|
+
}
|
|
7143
|
+
}
|
|
7144
|
+
function printSubmitted(kind, ids, opts) {
|
|
7145
|
+
if (opts.json) {
|
|
7146
|
+
const jobs = ids.map((id) => ({
|
|
7147
|
+
kind,
|
|
7148
|
+
id,
|
|
7149
|
+
status: "submitted",
|
|
7150
|
+
resume: resumeCommand2(kind, id, opts)
|
|
7151
|
+
}));
|
|
7152
|
+
console.log(JSON.stringify(jobs.length === 1 ? jobs[0] : jobs, null, 2));
|
|
7153
|
+
return;
|
|
7154
|
+
}
|
|
7155
|
+
for (const id of ids) {
|
|
7156
|
+
console.log(`Submitted ${kind} job: ${id}`);
|
|
7157
|
+
console.log(` Resume with: ${resumeCommand2(kind, id, opts)}`);
|
|
7158
|
+
}
|
|
7159
|
+
}
|
|
7160
|
+
async function completeJobs(client, kind, ids, opts, logger) {
|
|
7161
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.some((id) => typeof id !== "string" || !id)) {
|
|
7162
|
+
throw new ValidationError(`HeyGen returned no usable ${kind} job ID`);
|
|
7163
|
+
}
|
|
7164
|
+
if (opts.async) {
|
|
7165
|
+
printSubmitted(kind, ids, opts);
|
|
7166
|
+
return [];
|
|
7167
|
+
}
|
|
7168
|
+
const timeoutMs = parsePositiveInt(opts.waitTimeout, "--wait-timeout", DEFAULT_WAIT_TIMEOUT_MS);
|
|
7169
|
+
const intervalMs = parsePositiveInt(
|
|
7170
|
+
opts.pollInterval,
|
|
7171
|
+
"--poll-interval",
|
|
7172
|
+
DEFAULT_POLL_INTERVAL_MS
|
|
7173
|
+
);
|
|
7174
|
+
logger.info(
|
|
7175
|
+
`Waiting for ${ids.length > 1 ? `${ids.length} ${kind} jobs` : `${kind} ${ids[0]}`}...`
|
|
7176
|
+
);
|
|
7177
|
+
const settled = await Promise.allSettled(
|
|
7178
|
+
ids.map(
|
|
7179
|
+
(id) => waitForJob(client, kind, id, {
|
|
7180
|
+
timeoutMs,
|
|
7181
|
+
intervalMs,
|
|
7182
|
+
maxIntervalMs: Math.max(intervalMs, 3e4),
|
|
7183
|
+
burnCaptions: opts.burnCaptions,
|
|
7184
|
+
resumeHint: `Resume with: ${resumeCommand2(kind, id, opts)}`,
|
|
7185
|
+
onPoll: (s, attempt) => logger.debug(
|
|
7186
|
+
`${kind} ${id} poll #${attempt}: ${s.status}${s.videoId ? ` (video ${s.videoId})` : ""}`
|
|
7187
|
+
)
|
|
7188
|
+
})
|
|
7189
|
+
)
|
|
7190
|
+
);
|
|
7191
|
+
const states = [];
|
|
7192
|
+
const failures = [];
|
|
7193
|
+
for (const [i, r] of settled.entries()) {
|
|
7194
|
+
if (r.status === "fulfilled") {
|
|
7195
|
+
if (r.value.succeeded) states.push(r.value);
|
|
7196
|
+
else {
|
|
7197
|
+
try {
|
|
7198
|
+
assertJobSucceeded(r.value);
|
|
7199
|
+
} catch (e) {
|
|
7200
|
+
failures.push(e instanceof Error ? e.message : String(e));
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
} else {
|
|
7204
|
+
const err2 = r.reason;
|
|
7205
|
+
const msg = err2 instanceof Error ? err2.message : String(err2);
|
|
7206
|
+
failures.push(err2 instanceof PollTimeoutError ? msg : `${kind} ${ids[i]}: ${msg}`);
|
|
7207
|
+
}
|
|
7208
|
+
}
|
|
7209
|
+
const delivery = states.length > 0 ? await deliverOutputs(states, opts, logger) : { finished: [], failures: [] };
|
|
7210
|
+
failures.push(...delivery.failures);
|
|
7211
|
+
printFinished(delivery.finished, opts);
|
|
7212
|
+
if (failures.length > 0) {
|
|
7213
|
+
const total = ids.length;
|
|
7214
|
+
const ok2 = total - failures.length;
|
|
7215
|
+
const header = total > 1 ? `${ok2} of ${total} ${kind} jobs delivered; ${failures.length} failed:
|
|
7216
|
+
` : "";
|
|
7217
|
+
throw new HeygenJobFailedError(`${header}${failures.join("\n")}`);
|
|
7218
|
+
}
|
|
7219
|
+
return delivery.finished;
|
|
7220
|
+
}
|
|
7221
|
+
|
|
7222
|
+
// src/providers/heygen/commands/catalog.ts
|
|
7223
|
+
function pageLimit(raw, max) {
|
|
7224
|
+
return numberInRange(raw, "--limit", 1, max, true);
|
|
7225
|
+
}
|
|
7226
|
+
function printTable(page, columns) {
|
|
7227
|
+
if (page.items.length === 0) {
|
|
7228
|
+
console.log("No results.");
|
|
7229
|
+
} else {
|
|
7230
|
+
const cells = page.items.map(
|
|
7231
|
+
(row) => columns.map(([, get]) => {
|
|
7232
|
+
const v = get(row);
|
|
7233
|
+
return Array.isArray(v) ? v.join(",") : v === null || v === void 0 ? "-" : String(v);
|
|
7234
|
+
})
|
|
7235
|
+
);
|
|
7236
|
+
const widths = columns.map(
|
|
7237
|
+
([h], i) => Math.min(48, Math.max(h.length, ...cells.map((c2) => (c2[i] ?? "").length)))
|
|
7238
|
+
);
|
|
7239
|
+
const fmt = (vals) => vals.map((v, i) => v.padEnd(widths[i] ?? 0)).join(" ").trimEnd();
|
|
7240
|
+
console.log(fmt(columns.map(([h]) => h)));
|
|
7241
|
+
for (const c2 of cells) console.log(fmt(c2));
|
|
7242
|
+
}
|
|
7243
|
+
if (page.hasMore && page.nextToken) console.log(`
|
|
7244
|
+
More results: --cursor ${page.nextToken}`);
|
|
7245
|
+
}
|
|
7246
|
+
function printPage(page, json, columns) {
|
|
7247
|
+
if (json) {
|
|
7248
|
+
console.log(
|
|
7249
|
+
JSON.stringify(
|
|
7250
|
+
{ data: page.items, has_more: page.hasMore, next_token: page.nextToken },
|
|
7251
|
+
null,
|
|
7252
|
+
2
|
|
7253
|
+
)
|
|
7254
|
+
);
|
|
7255
|
+
} else {
|
|
7256
|
+
printTable(page, columns);
|
|
7257
|
+
}
|
|
7258
|
+
}
|
|
7259
|
+
function registerHeygenCatalogCommands(parent) {
|
|
7260
|
+
parent.command("avatars").description(
|
|
7261
|
+
"List avatar looks (the look ID is the --avatar for videos); --groups lists characters instead"
|
|
7262
|
+
).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(
|
|
7263
|
+
async (opts) => {
|
|
7264
|
+
const ownership = oneOf(opts.ownership, OWNERSHIP_VALUES, "--ownership");
|
|
7265
|
+
const avatarType = oneOf(opts.type, AVATAR_TYPES, "--type");
|
|
7266
|
+
const limit = pageLimit(opts.limit, LIMITS.avatarListMax);
|
|
7267
|
+
const client = createHeygenClientFromEnv();
|
|
7268
|
+
if (opts.groups) {
|
|
7269
|
+
const page2 = await client.listAvatarGroups({ ownership, limit, token: opts.cursor });
|
|
7270
|
+
printPage(page2, opts.json, [
|
|
7271
|
+
["GROUP_ID", (g) => g.id],
|
|
7272
|
+
["NAME", (g) => g.name],
|
|
7273
|
+
["LOOKS", (g) => g.looks_count],
|
|
7274
|
+
["GENDER", (g) => g.gender],
|
|
7275
|
+
["DEFAULT_VOICE", (g) => g.default_voice_id]
|
|
7276
|
+
]);
|
|
7277
|
+
if (!opts.json)
|
|
7278
|
+
console.log("\nUse `heygen avatars --group <GROUP_ID>` to list its looks.");
|
|
7279
|
+
return;
|
|
7280
|
+
}
|
|
7281
|
+
const page = await client.listAvatarLooks({
|
|
7282
|
+
group_id: opts.group,
|
|
7283
|
+
avatar_type: avatarType,
|
|
7284
|
+
ownership,
|
|
7285
|
+
limit,
|
|
7286
|
+
token: opts.cursor
|
|
7287
|
+
});
|
|
7288
|
+
printPage(page, opts.json, [
|
|
7289
|
+
["LOOK_ID", (l) => l.id],
|
|
7290
|
+
["NAME", (l) => l.name],
|
|
7291
|
+
["TYPE", (l) => l.avatar_type],
|
|
7292
|
+
["GROUP_ID", (l) => l.group_id],
|
|
7293
|
+
["ENGINES", (l) => l.supported_api_engines],
|
|
7294
|
+
["DEFAULT_VOICE", (l) => l.default_voice_id]
|
|
7295
|
+
]);
|
|
7296
|
+
}
|
|
7297
|
+
);
|
|
7298
|
+
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(
|
|
7299
|
+
async (opts) => {
|
|
7300
|
+
const type = oneOf(opts.type, OWNERSHIP_VALUES, "--type");
|
|
7301
|
+
const limit = pageLimit(opts.limit, LIMITS.voiceListMax);
|
|
7302
|
+
const client = createHeygenClientFromEnv();
|
|
7303
|
+
const page = await client.listVoices({
|
|
7304
|
+
language: opts.language,
|
|
7305
|
+
gender: opts.gender,
|
|
7306
|
+
engine: opts.engine,
|
|
7307
|
+
type,
|
|
7308
|
+
limit,
|
|
7309
|
+
token: opts.cursor
|
|
7310
|
+
});
|
|
7311
|
+
printPage(page, opts.json, [
|
|
7312
|
+
["VOICE_ID", (v) => v.voice_id],
|
|
7313
|
+
["NAME", (v) => v.name],
|
|
7314
|
+
["LANGUAGE", (v) => v.language],
|
|
7315
|
+
["GENDER", (v) => v.gender],
|
|
7316
|
+
["ENGINES", (v) => v.available_engines]
|
|
7317
|
+
]);
|
|
7318
|
+
}
|
|
7319
|
+
);
|
|
7320
|
+
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) => {
|
|
7321
|
+
const limit = pageLimit(opts.limit, LIMITS.stylesListMax);
|
|
7322
|
+
const client = createHeygenClientFromEnv();
|
|
7323
|
+
const page = await client.listVideoAgentStyles({ tag: opts.tag, limit, token: opts.cursor });
|
|
7324
|
+
printPage(page, opts.json, [
|
|
7325
|
+
["STYLE_ID", (s) => s.style_id],
|
|
7326
|
+
["NAME", (s) => s.name],
|
|
7327
|
+
["TAGS", (s) => s.tags],
|
|
7328
|
+
["ASPECT", (s) => s.aspect_ratio]
|
|
7329
|
+
]);
|
|
7330
|
+
});
|
|
7331
|
+
parent.command("languages").description("List supported video translation target languages").option("--json", "Print JSON").action(async (opts) => {
|
|
7332
|
+
const client = createHeygenClientFromEnv();
|
|
7333
|
+
const languages = await client.listTranslationLanguages();
|
|
7334
|
+
if (opts.json) console.log(JSON.stringify(languages, null, 2));
|
|
7335
|
+
else for (const l of languages) console.log(l);
|
|
7336
|
+
});
|
|
7337
|
+
parent.command("me").description("Show the HeyGen account for the API key (billing type, API credits/balance)").option("--json", "Print JSON").action(async (opts) => {
|
|
7338
|
+
const client = createHeygenClientFromEnv();
|
|
7339
|
+
const me = await client.getCurrentUser();
|
|
7340
|
+
if (opts.json) {
|
|
7341
|
+
console.log(JSON.stringify(me, null, 2));
|
|
7342
|
+
return;
|
|
7343
|
+
}
|
|
7344
|
+
const name = [me.first_name, me.last_name].filter(Boolean).join(" ") || me.username || "-";
|
|
7345
|
+
console.log(`User: ${name}`);
|
|
7346
|
+
if (me.billing_type) console.log(`Billing type: ${me.billing_type}`);
|
|
7347
|
+
for (const key of ["wallet", "subscription", "usage_based"]) {
|
|
7348
|
+
if (me[key] !== void 0 && me[key] !== null) {
|
|
7349
|
+
console.log(`${key}: ${JSON.stringify(me[key])}`);
|
|
7350
|
+
}
|
|
7351
|
+
}
|
|
7352
|
+
});
|
|
7353
|
+
}
|
|
7354
|
+
|
|
7355
|
+
// src/providers/heygen/commands/lipsync.ts
|
|
7356
|
+
init_errors();
|
|
7357
|
+
function registerHeygenLipsyncCommand(parent) {
|
|
7358
|
+
const cmd = parent.command("lipsync").description(
|
|
7359
|
+
"Lip-sync an existing video to new audio (refs: https:// URL, asset:<id>, or file)"
|
|
7360
|
+
).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");
|
|
7361
|
+
addWaitOptions(cmd).action(async (opts) => {
|
|
7362
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7363
|
+
const mode = qualityMode(opts.mode);
|
|
7364
|
+
const start = numberInRange(opts.start, "--start", 0, Number.MAX_SAFE_INTEGER);
|
|
7365
|
+
const end = numberInRange(opts.end, "--end", 0, Number.MAX_SAFE_INTEGER);
|
|
7366
|
+
if (start !== void 0 && end !== void 0 && end <= start) {
|
|
7367
|
+
throw new ValidationError("--end must be greater than --start");
|
|
7368
|
+
}
|
|
7369
|
+
const callbackUrl = checkCallback(opts.callbackUrl);
|
|
7370
|
+
const client = createHeygenClientFromEnv();
|
|
7371
|
+
const body = {
|
|
7372
|
+
video: await resolveMediaInput(client, opts.video, logger),
|
|
7373
|
+
audio: await resolveMediaInput(client, opts.audio, logger)
|
|
7374
|
+
};
|
|
7375
|
+
if (mode) body.mode = mode;
|
|
7376
|
+
if (opts.keepFormat) body.keep_the_same_format = true;
|
|
7377
|
+
if (opts.dynamicDuration === false) body.enable_dynamic_duration = false;
|
|
7378
|
+
if (opts.disableMusic) body.disable_music_track = true;
|
|
7379
|
+
if (opts.enhanceSpeech) body.enable_speech_enhancement = true;
|
|
7380
|
+
if (start !== void 0) body.start_time = start;
|
|
7381
|
+
if (end !== void 0) body.end_time = end;
|
|
7382
|
+
if (opts.title) body.title = opts.title;
|
|
7383
|
+
if (opts.folder) body.folder_id = opts.folder;
|
|
7384
|
+
if (callbackUrl) body.callback_url = callbackUrl;
|
|
7385
|
+
if (opts.callbackId) body.callback_id = opts.callbackId;
|
|
7386
|
+
logger.info("Submitting lipsync...");
|
|
7387
|
+
const created = await client.createLipsync(body);
|
|
7388
|
+
await completeJobs(client, "lipsync", [created.lipsync_id], opts, logger);
|
|
7389
|
+
});
|
|
7390
|
+
}
|
|
7391
|
+
|
|
7392
|
+
// src/providers/heygen/commands/model-video.ts
|
|
7393
|
+
var PENDING = { type: "asset_id", asset_id: "pending" };
|
|
7394
|
+
var pending = (refs) => (refs ?? []).map(() => PENDING);
|
|
7395
|
+
function registerHeygenModelVideoCommand(parent) {
|
|
7396
|
+
const cmd = parent.command("model-video <prompt>").description(
|
|
7397
|
+
"HeyGen Video model (heygen-video-1): 5-15 s scene with audio from a prompt (not an avatar video)"
|
|
7398
|
+
).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");
|
|
7399
|
+
addWaitOptions(cmd).action(async (prompt, opts) => {
|
|
7400
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7401
|
+
const input = {
|
|
7402
|
+
prompt,
|
|
7403
|
+
mode: opts.mode,
|
|
7404
|
+
duration: opts.duration,
|
|
7405
|
+
resolution: opts.resolution,
|
|
7406
|
+
aspectRatio: opts.aspectRatio,
|
|
7407
|
+
promptEnhancement: opts.promptEnhancement,
|
|
7408
|
+
seed: opts.seed,
|
|
7409
|
+
callbackUrl: opts.callbackUrl,
|
|
7410
|
+
callbackId: opts.callbackId
|
|
7411
|
+
};
|
|
7412
|
+
buildModelVideoRequest({
|
|
7413
|
+
...input,
|
|
7414
|
+
image: opts.image ? PENDING : void 0,
|
|
7415
|
+
referenceImages: pending(opts.referenceImage),
|
|
7416
|
+
referenceVideos: pending(opts.referenceVideo),
|
|
7417
|
+
referenceAudio: pending(opts.referenceAudio)
|
|
7418
|
+
});
|
|
7419
|
+
const client = createHeygenClientFromEnv();
|
|
7420
|
+
if (opts.image) input.image = await resolveMediaInput(client, opts.image, logger);
|
|
7421
|
+
input.referenceImages = await resolveMediaInputs(client, opts.referenceImage, logger);
|
|
7422
|
+
input.referenceVideos = await resolveMediaInputs(client, opts.referenceVideo, logger);
|
|
7423
|
+
input.referenceAudio = await resolveMediaInputs(client, opts.referenceAudio, logger);
|
|
7424
|
+
const body = buildModelVideoRequest(input);
|
|
7425
|
+
logger.info(`Submitting HeyGen Video (${body.mode})...`);
|
|
7426
|
+
const created = await client.createModelVideo(body);
|
|
7427
|
+
await completeJobs(client, "model-video", [created.video_id], opts, logger);
|
|
7428
|
+
});
|
|
7429
|
+
}
|
|
7430
|
+
|
|
7431
|
+
// src/providers/heygen/commands/run.ts
|
|
7432
|
+
init_errors();
|
|
7433
|
+
import path33 from "path";
|
|
7434
|
+
function extractJobIds(operation, data) {
|
|
7435
|
+
const op = HEYGEN_OPERATIONS[operation];
|
|
7436
|
+
if (!op) return [];
|
|
7437
|
+
if (op.idsField) {
|
|
7438
|
+
const ids = data[op.idsField];
|
|
7439
|
+
return Array.isArray(ids) ? ids.filter((x) => typeof x === "string" && !!x) : [];
|
|
7440
|
+
}
|
|
7441
|
+
const id = op.idField ? data[op.idField] : void 0;
|
|
7442
|
+
return typeof id === "string" && id ? [id] : [];
|
|
7443
|
+
}
|
|
7444
|
+
function registerHeygenRunCommand(parent) {
|
|
7445
|
+
const names = Object.keys(HEYGEN_OPERATIONS);
|
|
7446
|
+
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");
|
|
7447
|
+
addWaitOptions(cmd).action(async (operation, opts) => {
|
|
7448
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7449
|
+
const op = HEYGEN_OPERATIONS[operation];
|
|
7450
|
+
if (!op) {
|
|
7451
|
+
throw new ValidationError(
|
|
7452
|
+
`Unknown operation "${operation}". Supported: ${names.map((n) => `${n} (${HEYGEN_OPERATIONS[n]?.description})`).join("; ")}`
|
|
7453
|
+
);
|
|
7454
|
+
}
|
|
7455
|
+
const body = parseJsonInput(opts.input);
|
|
7456
|
+
const client = createHeygenClientFromEnv();
|
|
7457
|
+
logger.info(`Calling ${op.description}...`);
|
|
7458
|
+
const json = await client.send(op.path, {
|
|
7459
|
+
method: "POST",
|
|
7460
|
+
body,
|
|
7461
|
+
idempotencyKey: op.idempotent
|
|
7462
|
+
});
|
|
7463
|
+
const data = unwrapData(json, op.path);
|
|
7464
|
+
if (op.kind === null) {
|
|
7465
|
+
const audioUrl = typeof data.audio_url === "string" ? data.audio_url : void 0;
|
|
7466
|
+
if (opts.download === false || !audioUrl) {
|
|
7467
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7468
|
+
return;
|
|
7469
|
+
}
|
|
7470
|
+
const ext = inferExtension(audioUrl, "mp3");
|
|
7471
|
+
const dest = opts.output ? path33.resolve(opts.output) : path33.join(getOutputDir(), `heygen-speech-${Date.now()}.${ext}`);
|
|
7472
|
+
await downloadOutput(audioUrl, dest, "Re-run the command to generate a fresh URL.");
|
|
7473
|
+
console.log(`Saved: ${dest}`);
|
|
7474
|
+
return;
|
|
7475
|
+
}
|
|
7476
|
+
const ids = extractJobIds(operation, data);
|
|
7477
|
+
if (ids.length === 0) {
|
|
7478
|
+
throw new ValidationError(
|
|
7479
|
+
`HeyGen response for ${operation} had no job ID: ${JSON.stringify(data).slice(0, 300)}`
|
|
7480
|
+
);
|
|
7481
|
+
}
|
|
7482
|
+
await completeJobs(client, op.kind, ids, opts, logger);
|
|
7483
|
+
});
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
// src/providers/heygen/commands/status.ts
|
|
7487
|
+
function registerHeygenStatusCommand(parent) {
|
|
7488
|
+
parent.command("status <id>").description("Check (or wait for and download) a HeyGen job by ID").option(
|
|
7489
|
+
"--type <type>",
|
|
7490
|
+
"agent (session ID) | video | translation | lipsync | model-video",
|
|
7491
|
+
"video"
|
|
7492
|
+
).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(
|
|
7493
|
+
"--poll-interval <ms>",
|
|
7494
|
+
"Initial poll interval (backs off to 30s)",
|
|
7495
|
+
String(DEFAULT_POLL_INTERVAL_MS)
|
|
7496
|
+
).option("--output <path>", "Output file path (with --download)").option("--captions", "Also download the caption/subtitle file when available").option(
|
|
7497
|
+
"--burn-captions",
|
|
7498
|
+
"Video: use the burned-in caption render (captioned_video_url); fails if absent"
|
|
7499
|
+
).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) => {
|
|
7500
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7501
|
+
const kind = oneOf(opts.type, JOB_KINDS, "--type") ?? "video";
|
|
7502
|
+
const client = createHeygenClientFromEnv();
|
|
7503
|
+
if (opts.download) {
|
|
7504
|
+
await completeJobs(client, kind, [id], { ...opts, async: false, download: true }, logger);
|
|
7505
|
+
return;
|
|
7506
|
+
}
|
|
7507
|
+
const read = { burnCaptions: opts.burnCaptions };
|
|
7508
|
+
const state = opts.wait ? await waitForJob(client, kind, id, {
|
|
7509
|
+
...read,
|
|
7510
|
+
timeoutMs: parsePositiveInt(
|
|
7511
|
+
opts.waitTimeout,
|
|
7512
|
+
"--wait-timeout",
|
|
7513
|
+
DEFAULT_WAIT_TIMEOUT_MS
|
|
7514
|
+
),
|
|
7515
|
+
intervalMs: parsePositiveInt(
|
|
7516
|
+
opts.pollInterval,
|
|
7517
|
+
"--poll-interval",
|
|
7518
|
+
DEFAULT_POLL_INTERVAL_MS
|
|
7519
|
+
),
|
|
7520
|
+
resumeHint: `Resume with: ${resumeCommand2(kind, id, read)}`
|
|
7521
|
+
}) : await getJobState(client, kind, id, read);
|
|
7522
|
+
console.log(JSON.stringify(opts.raw ? state.raw : summarizeState(state), null, 2));
|
|
7523
|
+
if (state.done) assertJobSucceeded(state);
|
|
7524
|
+
else if (!opts.json) logger.info(`Still ${state.status}. Re-run with --wait to poll.`);
|
|
7525
|
+
});
|
|
7526
|
+
}
|
|
7527
|
+
|
|
7528
|
+
// src/providers/heygen/commands/template.ts
|
|
7529
|
+
init_errors();
|
|
7530
|
+
function templateBody(input, title) {
|
|
7531
|
+
const body = "variables" in input ? { ...input } : { variables: input };
|
|
7532
|
+
const vars = body.variables;
|
|
7533
|
+
if (vars === null || typeof vars !== "object" || Array.isArray(vars)) {
|
|
7534
|
+
throw new ValidationError("Template input must contain a `variables` object");
|
|
7535
|
+
}
|
|
7536
|
+
if (title) body.title = title;
|
|
7537
|
+
return body;
|
|
7538
|
+
}
|
|
7539
|
+
function registerHeygenTemplateCommand(parent) {
|
|
7540
|
+
const cmd = parent.command("template <template-id>").description("Generate a video from a HeyGen Studio template by filling its variables").requiredOption(
|
|
7541
|
+
"--input <json>",
|
|
7542
|
+
"Variables map or full request body, inline JSON or @path/to/vars.json"
|
|
7543
|
+
).option("--title <text>", "Video title");
|
|
7544
|
+
addWaitOptions(cmd).action(async (templateId, opts) => {
|
|
7545
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7546
|
+
const body = templateBody(parseJsonInput(opts.input), opts.title);
|
|
7547
|
+
const client = createHeygenClientFromEnv();
|
|
7548
|
+
logger.info(`Generating from template ${templateId}...`);
|
|
7549
|
+
const video = await client.generateFromTemplate(templateId, body);
|
|
7550
|
+
await completeJobs(client, "video", [video.id], opts, logger);
|
|
7551
|
+
});
|
|
7552
|
+
}
|
|
7553
|
+
|
|
7554
|
+
// src/providers/heygen/commands/translate.ts
|
|
7555
|
+
init_errors();
|
|
7556
|
+
function registerHeygenTranslateCommand(parent) {
|
|
7557
|
+
const cmd = parent.command("translate <source>").description(
|
|
7558
|
+
"Translate a video (voice clone + lip-sync). Source: https:// URL, asset:<id>, or local file"
|
|
7559
|
+
).requiredOption(
|
|
7560
|
+
"--languages <list>",
|
|
7561
|
+
'Comma-separated target language names from `heygen languages`, e.g. "Spanish (Spain),English"'
|
|
7562
|
+
).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");
|
|
7563
|
+
addWaitOptions(cmd).action(async (source, opts) => {
|
|
7564
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7565
|
+
const languages = splitLanguages(opts.languages);
|
|
7566
|
+
const mode = qualityMode(opts.mode);
|
|
7567
|
+
const speakers = numberInRange(opts.speakers, "--speakers", 1, 100, true);
|
|
7568
|
+
const start = numberInRange(opts.start, "--start", 0, Number.MAX_SAFE_INTEGER);
|
|
7569
|
+
const end = numberInRange(opts.end, "--end", 0, Number.MAX_SAFE_INTEGER);
|
|
7570
|
+
if (start !== void 0 && end !== void 0 && end <= start) {
|
|
7571
|
+
throw new ValidationError("--end must be greater than --start");
|
|
7572
|
+
}
|
|
7573
|
+
const callbackUrl = checkCallback(opts.callbackUrl);
|
|
7574
|
+
const client = createHeygenClientFromEnv();
|
|
7575
|
+
const video = await resolveMediaInput(client, source, logger);
|
|
7576
|
+
const audio = opts.audio ? await resolveMediaInput(client, opts.audio, logger) : void 0;
|
|
7577
|
+
const body = { video, output_languages: languages };
|
|
7578
|
+
if (audio) body.audio = audio;
|
|
7579
|
+
if (mode) body.mode = mode;
|
|
7580
|
+
if (opts.inputLanguage) body.input_language = opts.inputLanguage;
|
|
7581
|
+
if (opts.audioOnly) body.translate_audio_only = true;
|
|
7582
|
+
if (speakers !== void 0) body.speaker_num = speakers;
|
|
7583
|
+
if (opts.keepFormat) body.keep_the_same_format = true;
|
|
7584
|
+
if (opts.dynamicDuration === false) body.enable_dynamic_duration = false;
|
|
7585
|
+
if (opts.disableMusic) body.disable_music_track = true;
|
|
7586
|
+
if (opts.enhanceSpeech) body.enable_speech_enhancement = true;
|
|
7587
|
+
if (start !== void 0) body.start_time = start;
|
|
7588
|
+
if (end !== void 0) body.end_time = end;
|
|
7589
|
+
if (opts.brandGlossary) body.brand_glossary_id = opts.brandGlossary;
|
|
7590
|
+
if (opts.title) body.title = opts.title;
|
|
7591
|
+
if (opts.folder) body.folder_id = opts.folder;
|
|
7592
|
+
if (callbackUrl) body.callback_url = callbackUrl;
|
|
7593
|
+
if (opts.callbackId) body.callback_id = opts.callbackId;
|
|
7594
|
+
logger.info(`Submitting translation into ${languages.length} language(s)...`);
|
|
7595
|
+
const created = await client.createVideoTranslation(body);
|
|
7596
|
+
await completeJobs(client, "translation", created.video_translation_ids, opts, logger);
|
|
7597
|
+
});
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
// src/providers/heygen/commands/tts.ts
|
|
7601
|
+
import path34 from "path";
|
|
7602
|
+
init_redact();
|
|
7603
|
+
function registerHeygenTtsCommand(parent) {
|
|
7604
|
+
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) => {
|
|
7605
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7606
|
+
const body = buildSpeechRequest({
|
|
7607
|
+
text,
|
|
7608
|
+
voiceId: opts.voice,
|
|
7609
|
+
ssml: opts.ssml,
|
|
7610
|
+
speed: opts.speed,
|
|
7611
|
+
language: opts.language,
|
|
7612
|
+
locale: opts.locale,
|
|
7613
|
+
engine: opts.engine
|
|
7614
|
+
});
|
|
7615
|
+
const client = createHeygenClientFromEnv();
|
|
7616
|
+
logger.info("Generating speech...");
|
|
7617
|
+
const res = await client.createSpeech(body);
|
|
7618
|
+
const summary = {
|
|
7619
|
+
duration: res.duration,
|
|
7620
|
+
engine: res.engine,
|
|
7621
|
+
request_id: res.request_id ?? null,
|
|
7622
|
+
audio_url: redactUrl(res.audio_url)
|
|
7623
|
+
};
|
|
7624
|
+
if (opts.download === false) {
|
|
7625
|
+
if (opts.json)
|
|
7626
|
+
console.log(JSON.stringify({ ...summary, audio_url: res.audio_url }, null, 2));
|
|
7627
|
+
else console.log(res.audio_url);
|
|
7628
|
+
return;
|
|
7629
|
+
}
|
|
7630
|
+
const ext = inferExtension(res.audio_url, "mp3");
|
|
7631
|
+
const name = `heygen-tts-${res.request_id ?? Date.now()}.${ext}`.replace(/[^\w.-]+/g, "-");
|
|
7632
|
+
const dest = opts.output ? path34.resolve(opts.output) : path34.join(getOutputDir(), name);
|
|
7633
|
+
await downloadOutput(res.audio_url, dest, "Re-run the command to generate a fresh URL.");
|
|
7634
|
+
if (opts.json) console.log(JSON.stringify({ ...summary, file: dest }, null, 2));
|
|
7635
|
+
else console.log(`Saved: ${dest} (${res.duration}s, ${res.engine})`);
|
|
7636
|
+
});
|
|
7637
|
+
}
|
|
7638
|
+
|
|
7639
|
+
// src/providers/heygen/commands/video-agent.ts
|
|
7640
|
+
function registerHeygenVideoAgentCommand(parent) {
|
|
7641
|
+
const cmd = parent.command("video-agent <prompt>").description(
|
|
7642
|
+
"Prompt-to-video with HeyGen Video Agent (agent writes the script and picks avatar/voice/scenes)"
|
|
7643
|
+
).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(
|
|
7644
|
+
"--file <ref>",
|
|
7645
|
+
"Attachment: https:// URL, asset:<asset_id>, or local file to upload (repeatable, max 20)",
|
|
7646
|
+
collect5
|
|
7647
|
+
).option("--callback-url <url>", "HTTPS webhook URL notified on completion/failure").option("--callback-id <id>", "Caller ID echoed in the webhook payload");
|
|
7648
|
+
addWaitOptions(cmd).action(async (prompt, opts) => {
|
|
7649
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7650
|
+
buildVideoAgentRequest({
|
|
7651
|
+
prompt,
|
|
7652
|
+
orientation: opts.orientation,
|
|
7653
|
+
callbackUrl: opts.callbackUrl
|
|
7654
|
+
});
|
|
7655
|
+
const client = createHeygenClientFromEnv();
|
|
7656
|
+
const files = await resolveMediaInputs(client, opts.file, logger);
|
|
7657
|
+
const body = buildVideoAgentRequest({
|
|
7658
|
+
prompt,
|
|
7659
|
+
avatarId: opts.avatar,
|
|
7660
|
+
voiceId: opts.voice,
|
|
7661
|
+
styleId: opts.style,
|
|
7662
|
+
brandKitId: opts.brandKit,
|
|
7663
|
+
brandGlossaryId: opts.brandGlossary,
|
|
7664
|
+
orientation: opts.orientation,
|
|
7665
|
+
files,
|
|
7666
|
+
callbackUrl: opts.callbackUrl,
|
|
7667
|
+
callbackId: opts.callbackId
|
|
7668
|
+
});
|
|
7669
|
+
logger.info("Creating Video Agent session...");
|
|
7670
|
+
const created = await client.createVideoAgent(body);
|
|
7671
|
+
logger.debug(`session ${created.session_id} status=${created.status}`);
|
|
7672
|
+
await completeJobs(client, "agent", [created.session_id], opts, logger);
|
|
7673
|
+
});
|
|
7674
|
+
}
|
|
7675
|
+
|
|
7676
|
+
// src/providers/heygen/commands/video.ts
|
|
7677
|
+
function toInput(script, opts) {
|
|
7678
|
+
return {
|
|
7679
|
+
avatarId: opts.avatar,
|
|
7680
|
+
script,
|
|
7681
|
+
voiceId: opts.voice,
|
|
7682
|
+
resolution: opts.resolution,
|
|
7683
|
+
aspectRatio: opts.aspectRatio,
|
|
7684
|
+
engine: opts.engine,
|
|
7685
|
+
referenceLookId: opts.referenceLook,
|
|
7686
|
+
motionPrompt: opts.motionPrompt,
|
|
7687
|
+
expressiveness: opts.expressiveness,
|
|
7688
|
+
fit: opts.fit,
|
|
7689
|
+
backgroundColor: opts.backgroundColor,
|
|
7690
|
+
removeBackground: opts.removeBackground,
|
|
7691
|
+
outputFormat: opts.outputFormat,
|
|
7692
|
+
captions: opts.captions,
|
|
7693
|
+
burnCaptions: opts.burnCaptions,
|
|
7694
|
+
speed: opts.speed,
|
|
7695
|
+
pitch: opts.pitch,
|
|
7696
|
+
volume: opts.volume,
|
|
7697
|
+
locale: opts.locale,
|
|
7698
|
+
title: opts.title,
|
|
7699
|
+
folderId: opts.folder,
|
|
7700
|
+
brandGlossaryId: opts.brandGlossary,
|
|
7701
|
+
callbackUrl: opts.callbackUrl,
|
|
7702
|
+
callbackId: opts.callbackId
|
|
7703
|
+
};
|
|
7704
|
+
}
|
|
7705
|
+
var PENDING_UPLOAD = { type: "asset_id", asset_id: "pending" };
|
|
7706
|
+
function registerHeygenVideoCommand(parent) {
|
|
7707
|
+
const cmd = parent.command("video [script]").description(
|
|
7708
|
+
"Scripted avatar video: your avatar look + voice speak your exact script (or lip-sync --audio)"
|
|
7709
|
+
).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(
|
|
7710
|
+
"--audio <ref>",
|
|
7711
|
+
"Recorded audio instead of a script: https:// URL, asset:<id>, or file"
|
|
7712
|
+
).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");
|
|
7713
|
+
addWaitOptions(cmd).action(async (script, opts) => {
|
|
7714
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7715
|
+
const input = toInput(script, opts);
|
|
7716
|
+
buildAvatarVideoRequest({
|
|
7717
|
+
...input,
|
|
7718
|
+
audio: opts.audio ? PENDING_UPLOAD : void 0,
|
|
7719
|
+
backgroundImage: opts.backgroundImage ? PENDING_UPLOAD : void 0
|
|
7720
|
+
});
|
|
7721
|
+
const client = createHeygenClientFromEnv();
|
|
7722
|
+
if (opts.audio) input.audio = await resolveMediaInput(client, opts.audio, logger);
|
|
7723
|
+
if (opts.backgroundImage) {
|
|
7724
|
+
input.backgroundImage = await resolveMediaInput(client, opts.backgroundImage, logger);
|
|
7725
|
+
}
|
|
7726
|
+
const body = buildAvatarVideoRequest(input);
|
|
7727
|
+
logger.info("Submitting avatar video...");
|
|
7728
|
+
const created = await client.createVideo(body);
|
|
7729
|
+
await completeJobs(client, "video", [created.video_id], opts, logger);
|
|
7730
|
+
});
|
|
7731
|
+
}
|
|
7732
|
+
|
|
7733
|
+
// src/providers/heygen/commands/index.ts
|
|
7734
|
+
function registerHeygenCommands(program2) {
|
|
7735
|
+
const heygen = program2.command("heygen").description(
|
|
7736
|
+
"HeyGen v3: Video Agent prompt-to-video, scripted avatar videos, translation, lipsync, TTS"
|
|
7737
|
+
);
|
|
7738
|
+
registerHeygenVideoAgentCommand(heygen);
|
|
7739
|
+
registerHeygenVideoCommand(heygen);
|
|
7740
|
+
registerHeygenStatusCommand(heygen);
|
|
7741
|
+
registerHeygenCatalogCommands(heygen);
|
|
7742
|
+
registerHeygenTranslateCommand(heygen);
|
|
7743
|
+
registerHeygenLipsyncCommand(heygen);
|
|
7744
|
+
registerHeygenTtsCommand(heygen);
|
|
7745
|
+
registerHeygenTemplateCommand(heygen);
|
|
7746
|
+
registerHeygenModelVideoCommand(heygen);
|
|
7747
|
+
registerHeygenRunCommand(heygen);
|
|
7748
|
+
}
|
|
7749
|
+
|
|
5967
7750
|
// src/providers/leonardo/commands/generate.ts
|
|
5968
|
-
import
|
|
5969
|
-
import
|
|
7751
|
+
import fs30 from "fs";
|
|
7752
|
+
import path35 from "path";
|
|
5970
7753
|
|
|
5971
7754
|
// src/providers/leonardo/client.ts
|
|
5972
7755
|
init_errors();
|
|
@@ -6039,11 +7822,11 @@ var LeonardoClient = class {
|
|
|
6039
7822
|
}
|
|
6040
7823
|
apiKey;
|
|
6041
7824
|
baseUrl;
|
|
6042
|
-
async request(
|
|
7825
|
+
async request(path57, opts = {}) {
|
|
6043
7826
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
6044
|
-
const v2Override =
|
|
7827
|
+
const v2Override = path57.startsWith("v2:");
|
|
6045
7828
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
6046
|
-
const finalPath = v2Override ?
|
|
7829
|
+
const finalPath = v2Override ? path57.slice(3) : path57;
|
|
6047
7830
|
const url = new URL(finalBase + finalPath);
|
|
6048
7831
|
if (opts.query) {
|
|
6049
7832
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -6062,14 +7845,14 @@ var LeonardoClient = class {
|
|
|
6062
7845
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
6063
7846
|
});
|
|
6064
7847
|
}
|
|
6065
|
-
get(
|
|
6066
|
-
return this.request(
|
|
7848
|
+
get(path57, query, logger) {
|
|
7849
|
+
return this.request(path57, { method: "GET", query, logger });
|
|
6067
7850
|
}
|
|
6068
|
-
post(
|
|
6069
|
-
return this.request(
|
|
7851
|
+
post(path57, body, logger) {
|
|
7852
|
+
return this.request(path57, { method: "POST", body, logger });
|
|
6070
7853
|
}
|
|
6071
|
-
delete(
|
|
6072
|
-
return this.request(
|
|
7854
|
+
delete(path57, logger) {
|
|
7855
|
+
return this.request(path57, { method: "DELETE", logger });
|
|
6073
7856
|
}
|
|
6074
7857
|
};
|
|
6075
7858
|
function createLeonardoClient() {
|
|
@@ -6080,7 +7863,7 @@ function createLeonardoClient() {
|
|
|
6080
7863
|
function extFromUrl2(url, fallback = ".png") {
|
|
6081
7864
|
try {
|
|
6082
7865
|
const u = new URL(url);
|
|
6083
|
-
const e =
|
|
7866
|
+
const e = path35.extname(u.pathname);
|
|
6084
7867
|
return e.length > 0 ? e : fallback;
|
|
6085
7868
|
} catch {
|
|
6086
7869
|
return fallback;
|
|
@@ -6165,7 +7948,7 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
6165
7948
|
if (!img) continue;
|
|
6166
7949
|
const ext = extFromUrl2(img.url);
|
|
6167
7950
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
6168
|
-
const filepath =
|
|
7951
|
+
const filepath = path35.join(outDir, filename);
|
|
6169
7952
|
try {
|
|
6170
7953
|
await downloadFile(img.url, filepath);
|
|
6171
7954
|
saved.push(filepath);
|
|
@@ -6175,9 +7958,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
6175
7958
|
}
|
|
6176
7959
|
}
|
|
6177
7960
|
if (opts.output && saved[0]) {
|
|
6178
|
-
const dest =
|
|
6179
|
-
|
|
6180
|
-
|
|
7961
|
+
const dest = path35.resolve(opts.output);
|
|
7962
|
+
fs30.mkdirSync(path35.dirname(dest), { recursive: true });
|
|
7963
|
+
fs30.copyFileSync(saved[0], dest);
|
|
6181
7964
|
logger.success(`Copied to ${dest}`);
|
|
6182
7965
|
}
|
|
6183
7966
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -6189,11 +7972,11 @@ Generated ${finalImages.length} image(s):`);
|
|
|
6189
7972
|
}
|
|
6190
7973
|
|
|
6191
7974
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
6192
|
-
import
|
|
6193
|
-
import
|
|
7975
|
+
import fs31 from "fs";
|
|
7976
|
+
import path36 from "path";
|
|
6194
7977
|
function extFromUrl3(url, fallback = ".png") {
|
|
6195
7978
|
try {
|
|
6196
|
-
return
|
|
7979
|
+
return path36.extname(new URL(url).pathname) || fallback;
|
|
6197
7980
|
} catch {
|
|
6198
7981
|
return fallback;
|
|
6199
7982
|
}
|
|
@@ -6255,7 +8038,7 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
6255
8038
|
if (!img) continue;
|
|
6256
8039
|
const ext = extFromUrl3(img.url);
|
|
6257
8040
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
6258
|
-
const filepath =
|
|
8041
|
+
const filepath = path36.join(outDir, filename);
|
|
6259
8042
|
try {
|
|
6260
8043
|
await downloadFile(img.url, filepath);
|
|
6261
8044
|
saved.push(filepath);
|
|
@@ -6265,9 +8048,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
6265
8048
|
}
|
|
6266
8049
|
}
|
|
6267
8050
|
if (opts.output && saved[0]) {
|
|
6268
|
-
const dest =
|
|
6269
|
-
|
|
6270
|
-
|
|
8051
|
+
const dest = path36.resolve(opts.output);
|
|
8052
|
+
fs31.mkdirSync(path36.dirname(dest), { recursive: true });
|
|
8053
|
+
fs31.copyFileSync(saved[0], dest);
|
|
6271
8054
|
logger.success(`Copied to ${dest}`);
|
|
6272
8055
|
}
|
|
6273
8056
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -6279,7 +8062,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
6279
8062
|
}
|
|
6280
8063
|
|
|
6281
8064
|
// src/providers/leonardo/commands/video.ts
|
|
6282
|
-
import
|
|
8065
|
+
import path37 from "path";
|
|
6283
8066
|
function v2DimsFor(resolution) {
|
|
6284
8067
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
6285
8068
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -6390,7 +8173,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
6390
8173
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
6391
8174
|
process.exit(1);
|
|
6392
8175
|
}
|
|
6393
|
-
const outPath = opts.output ??
|
|
8176
|
+
const outPath = opts.output ?? path37.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
6394
8177
|
await downloadFile(videoUrl2, outPath);
|
|
6395
8178
|
logger.success(`Saved ${outPath}`);
|
|
6396
8179
|
await maybeDownloadThumb(gen, outPath, {
|
|
@@ -6496,7 +8279,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
6496
8279
|
}
|
|
6497
8280
|
|
|
6498
8281
|
// src/providers/leonardo/commands/status.ts
|
|
6499
|
-
import
|
|
8282
|
+
import path38 from "path";
|
|
6500
8283
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
6501
8284
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
6502
8285
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -6561,7 +8344,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
6561
8344
|
const outDir = getOutputDir();
|
|
6562
8345
|
let idx = 0;
|
|
6563
8346
|
for (const url of videos) {
|
|
6564
|
-
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output :
|
|
8347
|
+
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path38.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
|
|
6565
8348
|
await downloadFile(url, dest);
|
|
6566
8349
|
logger.success(`Saved ${dest}`);
|
|
6567
8350
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -6574,7 +8357,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
6574
8357
|
for (const url of images) {
|
|
6575
8358
|
const m = url.match(IMAGE_EXT_RE2);
|
|
6576
8359
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
6577
|
-
const dest =
|
|
8360
|
+
const dest = path38.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
6578
8361
|
await downloadFile(url, dest);
|
|
6579
8362
|
logger.success(`Saved ${dest}`);
|
|
6580
8363
|
idx++;
|
|
@@ -6731,8 +8514,8 @@ function sleep3(ms) {
|
|
|
6731
8514
|
}
|
|
6732
8515
|
|
|
6733
8516
|
// src/providers/minimax/generators/music.ts
|
|
6734
|
-
import
|
|
6735
|
-
import
|
|
8517
|
+
import fs32 from "fs";
|
|
8518
|
+
import path39 from "path";
|
|
6736
8519
|
async function generateMinimaxMusic(opts) {
|
|
6737
8520
|
const {
|
|
6738
8521
|
apiKey,
|
|
@@ -6777,14 +8560,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
6777
8560
|
}
|
|
6778
8561
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
6779
8562
|
const outDir = getOutputDir();
|
|
6780
|
-
const dest =
|
|
8563
|
+
const dest = path39.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
6781
8564
|
try {
|
|
6782
8565
|
if (audioData.startsWith("http")) {
|
|
6783
8566
|
const bytes = await fetchBytes(audioData);
|
|
6784
|
-
|
|
8567
|
+
fs32.writeFileSync(dest, bytes);
|
|
6785
8568
|
} else {
|
|
6786
8569
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
6787
|
-
|
|
8570
|
+
fs32.writeFileSync(dest, audioBytes);
|
|
6788
8571
|
}
|
|
6789
8572
|
} catch (e) {
|
|
6790
8573
|
return {
|
|
@@ -6795,8 +8578,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
6795
8578
|
const durSec = durationMs / 1e3;
|
|
6796
8579
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
6797
8580
|
if (output) {
|
|
6798
|
-
|
|
6799
|
-
|
|
8581
|
+
fs32.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
|
|
8582
|
+
fs32.copyFileSync(dest, output);
|
|
6800
8583
|
}
|
|
6801
8584
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
6802
8585
|
}
|
|
@@ -6870,8 +8653,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
6870
8653
|
init_errors();
|
|
6871
8654
|
|
|
6872
8655
|
// src/providers/minimax/generators/speech.ts
|
|
6873
|
-
import
|
|
6874
|
-
import
|
|
8656
|
+
import fs33 from "fs";
|
|
8657
|
+
import path40 from "path";
|
|
6875
8658
|
async function generateMinimaxSpeech(opts) {
|
|
6876
8659
|
const {
|
|
6877
8660
|
apiKey,
|
|
@@ -6918,12 +8701,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
6918
8701
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
6919
8702
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
6920
8703
|
const outDir = getOutputDir();
|
|
6921
|
-
const dest =
|
|
6922
|
-
|
|
8704
|
+
const dest = path40.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
8705
|
+
fs33.writeFileSync(dest, audioBytes);
|
|
6923
8706
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
6924
8707
|
if (output) {
|
|
6925
|
-
|
|
6926
|
-
|
|
8708
|
+
fs33.mkdirSync(path40.dirname(path40.resolve(output)), { recursive: true });
|
|
8709
|
+
fs33.copyFileSync(dest, output);
|
|
6927
8710
|
}
|
|
6928
8711
|
return { status: "success", generatedAudio: dest, model };
|
|
6929
8712
|
}
|
|
@@ -6970,8 +8753,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
6970
8753
|
init_errors();
|
|
6971
8754
|
|
|
6972
8755
|
// src/providers/minimax/generators/video.ts
|
|
6973
|
-
import
|
|
6974
|
-
import
|
|
8756
|
+
import fs34 from "fs";
|
|
8757
|
+
import path41 from "path";
|
|
6975
8758
|
async function generateMinimaxVideo(opts) {
|
|
6976
8759
|
const {
|
|
6977
8760
|
apiKey,
|
|
@@ -7017,18 +8800,18 @@ async function generateMinimaxVideo(opts) {
|
|
|
7017
8800
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
7018
8801
|
}
|
|
7019
8802
|
const outDir = getOutputDir();
|
|
7020
|
-
const dest =
|
|
8803
|
+
const dest = path41.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
7021
8804
|
try {
|
|
7022
8805
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
7023
8806
|
} catch (e) {
|
|
7024
8807
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
7025
8808
|
}
|
|
7026
8809
|
const elapsed = (Date.now() - start) / 1e3;
|
|
7027
|
-
const stat =
|
|
8810
|
+
const stat = fs34.statSync(dest);
|
|
7028
8811
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
7029
8812
|
if (output) {
|
|
7030
|
-
|
|
7031
|
-
|
|
8813
|
+
fs34.mkdirSync(path41.dirname(path41.resolve(output)), { recursive: true });
|
|
8814
|
+
fs34.copyFileSync(dest, output);
|
|
7032
8815
|
}
|
|
7033
8816
|
await maybeDownloadThumb(pollResult, dest, {
|
|
7034
8817
|
skip: thumb === false,
|
|
@@ -7090,8 +8873,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
7090
8873
|
|
|
7091
8874
|
// src/providers/minimax/generators/image.ts
|
|
7092
8875
|
init_errors();
|
|
7093
|
-
import
|
|
7094
|
-
import
|
|
8876
|
+
import fs35 from "fs";
|
|
8877
|
+
import path42 from "path";
|
|
7095
8878
|
async function generateMinimaxImage(opts) {
|
|
7096
8879
|
const {
|
|
7097
8880
|
apiKey,
|
|
@@ -7129,8 +8912,8 @@ async function generateMinimaxImage(opts) {
|
|
|
7129
8912
|
try {
|
|
7130
8913
|
const bytes = await fetchBytes(url);
|
|
7131
8914
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
7132
|
-
const dest =
|
|
7133
|
-
|
|
8915
|
+
const dest = path42.join(outDir, fname);
|
|
8916
|
+
fs35.writeFileSync(dest, bytes);
|
|
7134
8917
|
savedFiles.push(dest);
|
|
7135
8918
|
logger?.success(`Saved: ${dest}`);
|
|
7136
8919
|
} catch (e) {
|
|
@@ -7141,8 +8924,8 @@ async function generateMinimaxImage(opts) {
|
|
|
7141
8924
|
}
|
|
7142
8925
|
}
|
|
7143
8926
|
if (output && savedFiles[0]) {
|
|
7144
|
-
|
|
7145
|
-
|
|
8927
|
+
fs35.mkdirSync(path42.dirname(path42.resolve(output)), { recursive: true });
|
|
8928
|
+
fs35.copyFileSync(savedFiles[0], output);
|
|
7146
8929
|
}
|
|
7147
8930
|
return { status: "success", generatedImages: savedFiles, model };
|
|
7148
8931
|
}
|
|
@@ -7181,8 +8964,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
7181
8964
|
|
|
7182
8965
|
// src/providers/minimax/commands/image-to-image.ts
|
|
7183
8966
|
init_errors();
|
|
7184
|
-
import
|
|
7185
|
-
import
|
|
8967
|
+
import fs36 from "fs";
|
|
8968
|
+
import path43 from "path";
|
|
7186
8969
|
function registerMinimaxImageToImageCommand(parent) {
|
|
7187
8970
|
parent.command("image-to-image").alias("i2i").description(
|
|
7188
8971
|
"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 +9020,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
7237
9020
|
if (!url) continue;
|
|
7238
9021
|
try {
|
|
7239
9022
|
const bytes = await fetchBytes(url);
|
|
7240
|
-
const dest =
|
|
7241
|
-
|
|
9023
|
+
const dest = path43.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
9024
|
+
fs36.writeFileSync(dest, bytes);
|
|
7242
9025
|
saved.push(dest);
|
|
7243
9026
|
logger.success(`Saved: ${dest}`);
|
|
7244
9027
|
} catch (e) {
|
|
@@ -7249,8 +9032,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
7249
9032
|
}
|
|
7250
9033
|
}
|
|
7251
9034
|
if (opts.output && saved[0]) {
|
|
7252
|
-
|
|
7253
|
-
|
|
9035
|
+
fs36.mkdirSync(path43.dirname(path43.resolve(opts.output)), { recursive: true });
|
|
9036
|
+
fs36.copyFileSync(saved[0], opts.output);
|
|
7254
9037
|
logger.success(`Copied to: ${opts.output}`);
|
|
7255
9038
|
}
|
|
7256
9039
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -7384,12 +9167,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
7384
9167
|
}
|
|
7385
9168
|
|
|
7386
9169
|
// src/providers/openai/openai-audio.ts
|
|
7387
|
-
import
|
|
7388
|
-
import
|
|
9170
|
+
import fs38 from "fs";
|
|
9171
|
+
import path45 from "path";
|
|
7389
9172
|
|
|
7390
9173
|
// src/providers/openai/client.ts
|
|
7391
|
-
import
|
|
7392
|
-
import
|
|
9174
|
+
import fs37 from "fs";
|
|
9175
|
+
import path44 from "path";
|
|
7393
9176
|
init_errors();
|
|
7394
9177
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
7395
9178
|
function requireOpenAIKey() {
|
|
@@ -7455,10 +9238,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
7455
9238
|
return Buffer.from(await res.arrayBuffer());
|
|
7456
9239
|
}
|
|
7457
9240
|
function fileFromPath(filePath) {
|
|
7458
|
-
const abs =
|
|
7459
|
-
if (!
|
|
7460
|
-
const bytes =
|
|
7461
|
-
return new File([new Uint8Array(bytes)],
|
|
9241
|
+
const abs = path44.resolve(filePath);
|
|
9242
|
+
if (!fs37.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
9243
|
+
const bytes = fs37.readFileSync(abs);
|
|
9244
|
+
return new File([new Uint8Array(bytes)], path44.basename(abs));
|
|
7462
9245
|
}
|
|
7463
9246
|
|
|
7464
9247
|
// src/providers/openai/openai-audio.ts
|
|
@@ -7497,7 +9280,7 @@ async function resolveAudioReferenceDataUrl(ref) {
|
|
|
7497
9280
|
}
|
|
7498
9281
|
function audioMimeFromReference(ref) {
|
|
7499
9282
|
const pathname = new URL(ref).pathname;
|
|
7500
|
-
const ext =
|
|
9283
|
+
const ext = path45.extname(pathname).toLowerCase();
|
|
7501
9284
|
switch (ext) {
|
|
7502
9285
|
case ".mp3":
|
|
7503
9286
|
return "audio/mpeg";
|
|
@@ -7516,12 +9299,12 @@ function audioMimeFromReference(ref) {
|
|
|
7516
9299
|
async function generateOpenAISpeech(opts) {
|
|
7517
9300
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
7518
9301
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
7519
|
-
const dest =
|
|
7520
|
-
|
|
9302
|
+
const dest = path45.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
9303
|
+
fs38.writeFileSync(dest, bytes);
|
|
7521
9304
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
7522
9305
|
if (opts.output) {
|
|
7523
|
-
|
|
7524
|
-
|
|
9306
|
+
fs38.mkdirSync(path45.dirname(path45.resolve(opts.output)), { recursive: true });
|
|
9307
|
+
fs38.copyFileSync(dest, opts.output);
|
|
7525
9308
|
}
|
|
7526
9309
|
return dest;
|
|
7527
9310
|
}
|
|
@@ -7543,9 +9326,9 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
7543
9326
|
);
|
|
7544
9327
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
7545
9328
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
7546
|
-
const dest = opts.output ?
|
|
7547
|
-
|
|
7548
|
-
|
|
9329
|
+
const dest = opts.output ? path45.resolve(opts.output) : path45.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
|
|
9330
|
+
fs38.mkdirSync(path45.dirname(dest), { recursive: true });
|
|
9331
|
+
fs38.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
7549
9332
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
7550
9333
|
return { path: dest, text };
|
|
7551
9334
|
}
|
|
@@ -7572,9 +9355,9 @@ Generated audio: ${dest}`);
|
|
|
7572
9355
|
}
|
|
7573
9356
|
|
|
7574
9357
|
// src/providers/openai/codex-image-driver.ts
|
|
7575
|
-
import
|
|
9358
|
+
import fs39 from "fs";
|
|
7576
9359
|
import os3 from "os";
|
|
7577
|
-
import
|
|
9360
|
+
import path46 from "path";
|
|
7578
9361
|
import { execa } from "execa";
|
|
7579
9362
|
init_errors();
|
|
7580
9363
|
var defaultRunner = async (cmd, args, opts) => {
|
|
@@ -7623,12 +9406,12 @@ function sanitizeCodexEnv() {
|
|
|
7623
9406
|
async function runCodexImageGeneration(opts) {
|
|
7624
9407
|
const runner = opts.runner ?? defaultRunner;
|
|
7625
9408
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
7626
|
-
const dest = opts.output ?
|
|
7627
|
-
|
|
7628
|
-
const tmp =
|
|
7629
|
-
const schema =
|
|
7630
|
-
const result =
|
|
7631
|
-
|
|
9409
|
+
const dest = opts.output ? path46.resolve(opts.output) : path46.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
|
|
9410
|
+
fs39.mkdirSync(path46.dirname(dest), { recursive: true });
|
|
9411
|
+
const tmp = fs39.mkdtempSync(path46.join(os3.tmpdir(), "multix-codex-image-"));
|
|
9412
|
+
const schema = path46.join(tmp, "schema.json");
|
|
9413
|
+
const result = path46.join(tmp, "result.json");
|
|
9414
|
+
fs39.writeFileSync(
|
|
7632
9415
|
schema,
|
|
7633
9416
|
JSON.stringify({
|
|
7634
9417
|
type: "object",
|
|
@@ -7658,7 +9441,7 @@ async function runCodexImageGeneration(opts) {
|
|
|
7658
9441
|
if (res.exitCode !== 0) {
|
|
7659
9442
|
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
7660
9443
|
}
|
|
7661
|
-
if (!
|
|
9444
|
+
if (!fs39.existsSync(dest) || fs39.statSync(dest).size === 0) {
|
|
7662
9445
|
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
7663
9446
|
}
|
|
7664
9447
|
return { path: dest };
|
|
@@ -7675,8 +9458,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
7675
9458
|
|
|
7676
9459
|
// src/providers/openai/openai-image.ts
|
|
7677
9460
|
init_errors();
|
|
7678
|
-
import
|
|
7679
|
-
import
|
|
9461
|
+
import fs40 from "fs";
|
|
9462
|
+
import path47 from "path";
|
|
7680
9463
|
function buildImageGenerationBody(opts) {
|
|
7681
9464
|
return {
|
|
7682
9465
|
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
@@ -7709,14 +9492,14 @@ function saveOpenAIImages(opts) {
|
|
|
7709
9492
|
for (let i = 0; i < items.length; i++) {
|
|
7710
9493
|
const b64 = items[i]?.b64_json;
|
|
7711
9494
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
7712
|
-
const dest =
|
|
7713
|
-
|
|
9495
|
+
const dest = path47.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
9496
|
+
fs40.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
7714
9497
|
saved.push(dest);
|
|
7715
9498
|
opts.logger?.success(`Saved: ${dest}`);
|
|
7716
9499
|
}
|
|
7717
9500
|
if (opts.output && saved[0]) {
|
|
7718
|
-
|
|
7719
|
-
|
|
9501
|
+
fs40.mkdirSync(path47.dirname(path47.resolve(opts.output)), { recursive: true });
|
|
9502
|
+
fs40.copyFileSync(saved[0], opts.output);
|
|
7720
9503
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
7721
9504
|
}
|
|
7722
9505
|
return saved;
|
|
@@ -7725,7 +9508,7 @@ async function resolveImageRefBytes(ref) {
|
|
|
7725
9508
|
if (isHttpUrl(ref)) {
|
|
7726
9509
|
const bytes2 = await fetchBytes(ref);
|
|
7727
9510
|
const pathname = new URL(ref).pathname;
|
|
7728
|
-
const filename =
|
|
9511
|
+
const filename = path47.basename(pathname) || "reference.png";
|
|
7729
9512
|
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
7730
9513
|
}
|
|
7731
9514
|
const file = fileFromPath(ref);
|
|
@@ -7770,7 +9553,7 @@ function sniffImageMime2(buf, name) {
|
|
|
7770
9553
|
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
7771
9554
|
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
7772
9555
|
return "image/webp";
|
|
7773
|
-
const ext =
|
|
9556
|
+
const ext = path47.extname(name).toLowerCase();
|
|
7774
9557
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
7775
9558
|
if (ext === ".webp") return "image/webp";
|
|
7776
9559
|
return "image/png";
|
|
@@ -7822,7 +9605,7 @@ function registerOpenAIImageToImageCommand(parent) {
|
|
|
7822
9605
|
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
9606
|
"--ref <path>",
|
|
7824
9607
|
"Reference image path or URL; repeatable",
|
|
7825
|
-
|
|
9608
|
+
collect6,
|
|
7826
9609
|
[]
|
|
7827
9610
|
).option("--driver <driver>", "api|codex|auto", "auto").option(
|
|
7828
9611
|
"-m, --model <id>",
|
|
@@ -7865,7 +9648,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
7865
9648
|
for (const f of finalImages) console.log(` ${f}`);
|
|
7866
9649
|
});
|
|
7867
9650
|
}
|
|
7868
|
-
function
|
|
9651
|
+
function collect6(value, prev) {
|
|
7869
9652
|
return [...prev, value];
|
|
7870
9653
|
}
|
|
7871
9654
|
|
|
@@ -7874,12 +9657,12 @@ function registerOpenAITranscribeCommand(parent) {
|
|
|
7874
9657
|
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
9658
|
"--known-speaker-name <name>",
|
|
7876
9659
|
"Known speaker name; repeatable",
|
|
7877
|
-
|
|
9660
|
+
collect7,
|
|
7878
9661
|
[]
|
|
7879
9662
|
).option(
|
|
7880
9663
|
"--known-speaker-reference <path>",
|
|
7881
9664
|
"2-10s reference audio; repeatable",
|
|
7882
|
-
|
|
9665
|
+
collect7,
|
|
7883
9666
|
[]
|
|
7884
9667
|
).option("--output <path>", "Save transcript to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
7885
9668
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -7898,7 +9681,7 @@ function registerOpenAITranscribeCommand(parent) {
|
|
|
7898
9681
|
${result.text.trim()}`);
|
|
7899
9682
|
});
|
|
7900
9683
|
}
|
|
7901
|
-
function
|
|
9684
|
+
function collect7(value, prev) {
|
|
7902
9685
|
return [...prev, value];
|
|
7903
9686
|
}
|
|
7904
9687
|
|
|
@@ -7914,8 +9697,8 @@ function registerOpenAICommands(program2) {
|
|
|
7914
9697
|
}
|
|
7915
9698
|
|
|
7916
9699
|
// src/providers/openrouter/client.ts
|
|
7917
|
-
import
|
|
7918
|
-
import
|
|
9700
|
+
import fs41 from "fs";
|
|
9701
|
+
import path48 from "path";
|
|
7919
9702
|
init_errors();
|
|
7920
9703
|
|
|
7921
9704
|
// src/providers/openrouter/payload.ts
|
|
@@ -8079,8 +9862,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
8079
9862
|
if (!url) continue;
|
|
8080
9863
|
try {
|
|
8081
9864
|
const bytes = await fetchBytes(url);
|
|
8082
|
-
const dest =
|
|
8083
|
-
|
|
9865
|
+
const dest = path48.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
9866
|
+
fs41.writeFileSync(dest, bytes);
|
|
8084
9867
|
savedFiles.push(dest);
|
|
8085
9868
|
logger?.success(`Saved: ${dest}`);
|
|
8086
9869
|
} catch (e) {
|
|
@@ -8091,8 +9874,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
8091
9874
|
}
|
|
8092
9875
|
}
|
|
8093
9876
|
if (output && savedFiles[0]) {
|
|
8094
|
-
|
|
8095
|
-
|
|
9877
|
+
fs41.mkdirSync(path48.dirname(path48.resolve(output)), { recursive: true });
|
|
9878
|
+
fs41.writeFileSync(output, fs41.readFileSync(savedFiles[0]));
|
|
8096
9879
|
}
|
|
8097
9880
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
8098
9881
|
}
|
|
@@ -8133,8 +9916,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
8133
9916
|
}
|
|
8134
9917
|
|
|
8135
9918
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
8136
|
-
import
|
|
8137
|
-
import
|
|
9919
|
+
import fs42 from "fs";
|
|
9920
|
+
import path49 from "path";
|
|
8138
9921
|
init_errors();
|
|
8139
9922
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
8140
9923
|
var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
|
|
@@ -8145,7 +9928,7 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
8145
9928
|
).requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
8146
9929
|
"--ref <path>",
|
|
8147
9930
|
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
8148
|
-
|
|
9931
|
+
collect8,
|
|
8149
9932
|
[]
|
|
8150
9933
|
).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
9934
|
async (opts) => {
|
|
@@ -8187,7 +9970,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
8187
9970
|
}
|
|
8188
9971
|
);
|
|
8189
9972
|
}
|
|
8190
|
-
function
|
|
9973
|
+
function collect8(value, prev) {
|
|
8191
9974
|
return [...prev, value];
|
|
8192
9975
|
}
|
|
8193
9976
|
function parseStrength(raw) {
|
|
@@ -8226,14 +10009,14 @@ async function runImageToImage(opts) {
|
|
|
8226
10009
|
const url = parsed.urls[i];
|
|
8227
10010
|
if (!url) continue;
|
|
8228
10011
|
const bytes = await fetchBytes(url);
|
|
8229
|
-
const dest =
|
|
8230
|
-
|
|
10012
|
+
const dest = path49.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
10013
|
+
fs42.writeFileSync(dest, bytes);
|
|
8231
10014
|
saved.push(dest);
|
|
8232
10015
|
opts.logger.success(`Saved: ${dest}`);
|
|
8233
10016
|
}
|
|
8234
10017
|
if (opts.output && saved[0]) {
|
|
8235
|
-
|
|
8236
|
-
|
|
10018
|
+
fs42.mkdirSync(path49.dirname(path49.resolve(opts.output)), { recursive: true });
|
|
10019
|
+
fs42.copyFileSync(saved[0], opts.output);
|
|
8237
10020
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
8238
10021
|
}
|
|
8239
10022
|
return saved;
|
|
@@ -8278,13 +10061,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
8278
10061
|
}
|
|
8279
10062
|
|
|
8280
10063
|
// src/providers/openrouter/commands/video-status.ts
|
|
8281
|
-
import
|
|
8282
|
-
import
|
|
10064
|
+
import fs43 from "fs";
|
|
10065
|
+
import path50 from "path";
|
|
8283
10066
|
async function downloadOpenRouterVideo(opts) {
|
|
8284
10067
|
const { jobId, logger, source } = opts;
|
|
8285
10068
|
const apiKey = requireOpenRouterKey();
|
|
8286
10069
|
const url = videoContentUrl(jobId);
|
|
8287
|
-
const dest =
|
|
10070
|
+
const dest = path50.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
8288
10071
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
8289
10072
|
if (!r.ok) {
|
|
8290
10073
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -8294,14 +10077,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
8294
10077
|
logger.error("Empty response body");
|
|
8295
10078
|
process.exit(1);
|
|
8296
10079
|
}
|
|
8297
|
-
|
|
8298
|
-
const ws =
|
|
10080
|
+
fs43.mkdirSync(path50.dirname(dest), { recursive: true });
|
|
10081
|
+
const ws = fs43.createWriteStream(dest);
|
|
8299
10082
|
const { pipeline: pipeline3 } = await import("stream/promises");
|
|
8300
10083
|
await pipeline3(r.body, ws);
|
|
8301
10084
|
logger.success(`Saved ${dest}`);
|
|
8302
10085
|
if (opts.output) {
|
|
8303
|
-
|
|
8304
|
-
|
|
10086
|
+
fs43.mkdirSync(path50.dirname(path50.resolve(opts.output)), { recursive: true });
|
|
10087
|
+
fs43.copyFileSync(dest, opts.output);
|
|
8305
10088
|
logger.success(`Copied to ${opts.output}`);
|
|
8306
10089
|
}
|
|
8307
10090
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -8494,8 +10277,8 @@ function parseModelRef(ref, explicitVersion) {
|
|
|
8494
10277
|
}
|
|
8495
10278
|
return { kind: "version", version: trimmed };
|
|
8496
10279
|
}
|
|
8497
|
-
const [
|
|
8498
|
-
const [owner, name, ...extra] = (
|
|
10280
|
+
const [path57, inlineVersion, ...rest] = trimmed.split(":");
|
|
10281
|
+
const [owner, name, ...extra] = (path57 ?? "").split("/");
|
|
8499
10282
|
if (rest.length > 0 || extra.length > 0 || !owner || !name) {
|
|
8500
10283
|
throw new ValidationError(
|
|
8501
10284
|
`Invalid Replicate model "${ref}" \u2014 expected owner/name, owner/name:version, or a 64-char version id`
|
|
@@ -8625,7 +10408,7 @@ function assertSucceeded(prediction2) {
|
|
|
8625
10408
|
PROVIDER
|
|
8626
10409
|
);
|
|
8627
10410
|
}
|
|
8628
|
-
var
|
|
10411
|
+
var defaultSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8629
10412
|
var ReplicateClient = class {
|
|
8630
10413
|
apiKey;
|
|
8631
10414
|
baseUrl;
|
|
@@ -8636,15 +10419,15 @@ var ReplicateClient = class {
|
|
|
8636
10419
|
this.apiKey = opts.apiKey;
|
|
8637
10420
|
this.baseUrl = (opts.baseUrl || DEFAULT_REPLICATE_BASE_URL).replace(/\/+$/, "");
|
|
8638
10421
|
this.doFetch = opts.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
8639
|
-
this.sleep = opts.sleep ??
|
|
10422
|
+
this.sleep = opts.sleep ?? defaultSleep3;
|
|
8640
10423
|
}
|
|
8641
10424
|
headers(extra = {}) {
|
|
8642
10425
|
return { accept: "application/json", authorization: `Bearer ${this.apiKey}`, ...extra };
|
|
8643
10426
|
}
|
|
8644
10427
|
/** Raw request: throws HttpError so polling can classify transient failures. */
|
|
8645
|
-
raw(
|
|
10428
|
+
raw(path57, method, body, headers2, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
8646
10429
|
return fetchJson({
|
|
8647
|
-
url: `${this.baseUrl}${
|
|
10430
|
+
url: `${this.baseUrl}${path57}`,
|
|
8648
10431
|
method,
|
|
8649
10432
|
headers: this.headers(headers2),
|
|
8650
10433
|
body,
|
|
@@ -8656,7 +10439,7 @@ var ReplicateClient = class {
|
|
|
8656
10439
|
async createPrediction(opts) {
|
|
8657
10440
|
const { model, input } = opts;
|
|
8658
10441
|
const isOfficial = model.kind === "official";
|
|
8659
|
-
const
|
|
10442
|
+
const path57 = isOfficial ? `/models/${model.owner}/${model.name}/predictions` : "/predictions";
|
|
8660
10443
|
const body = isOfficial ? { input } : { version: model.version, input };
|
|
8661
10444
|
const subject = isOfficial ? `model ${model.owner}/${model.name}` : `model version ${model.version}`;
|
|
8662
10445
|
const headers2 = {};
|
|
@@ -8668,7 +10451,7 @@ var ReplicateClient = class {
|
|
|
8668
10451
|
}
|
|
8669
10452
|
for (let attempt = 0; ; attempt++) {
|
|
8670
10453
|
try {
|
|
8671
|
-
const res = await this.raw(
|
|
10454
|
+
const res = await this.raw(path57, "POST", body, headers2, timeoutMs);
|
|
8672
10455
|
return validatePrediction(res);
|
|
8673
10456
|
} catch (error) {
|
|
8674
10457
|
if (error instanceof HttpError && error.status === 429 && attempt < CREATE_MAX_RETRIES) {
|
|
@@ -8828,8 +10611,8 @@ init_errors();
|
|
|
8828
10611
|
init_errors();
|
|
8829
10612
|
init_redact();
|
|
8830
10613
|
import { once as once2 } from "events";
|
|
8831
|
-
import
|
|
8832
|
-
import
|
|
10614
|
+
import fs44 from "fs";
|
|
10615
|
+
import path51 from "path";
|
|
8833
10616
|
import { Readable } from "stream";
|
|
8834
10617
|
import { pipeline as pipeline2 } from "stream/promises";
|
|
8835
10618
|
|
|
@@ -8843,15 +10626,15 @@ function isMediaRef(value) {
|
|
|
8843
10626
|
function extractOutput(output) {
|
|
8844
10627
|
const result = { media: [], texts: [] };
|
|
8845
10628
|
const seen = /* @__PURE__ */ new Set();
|
|
8846
|
-
const visit = (value,
|
|
10629
|
+
const visit = (value, path57) => {
|
|
8847
10630
|
if (value === null || value === void 0) return;
|
|
8848
10631
|
if (typeof value === "string") {
|
|
8849
|
-
if (isMediaRef(value)) result.media.push({ url: value, path:
|
|
8850
|
-
else if (value.length > 0) result.texts.push(
|
|
10632
|
+
if (isMediaRef(value)) result.media.push({ url: value, path: path57 || "output" });
|
|
10633
|
+
else if (value.length > 0) result.texts.push(path57 ? `${path57}: ${value}` : value);
|
|
8851
10634
|
return;
|
|
8852
10635
|
}
|
|
8853
10636
|
if (typeof value !== "object") {
|
|
8854
|
-
result.texts.push(
|
|
10637
|
+
result.texts.push(path57 ? `${path57}: ${String(value)}` : String(value));
|
|
8855
10638
|
return;
|
|
8856
10639
|
}
|
|
8857
10640
|
if (seen.has(value)) return;
|
|
@@ -8859,14 +10642,14 @@ function extractOutput(output) {
|
|
|
8859
10642
|
if (Array.isArray(value)) {
|
|
8860
10643
|
if (value.length > 0 && value.every((v) => typeof v === "string" && !isMediaRef(v))) {
|
|
8861
10644
|
const joined = value.join("");
|
|
8862
|
-
if (joined.length > 0) result.texts.push(
|
|
10645
|
+
if (joined.length > 0) result.texts.push(path57 ? `${path57}: ${joined}` : joined);
|
|
8863
10646
|
return;
|
|
8864
10647
|
}
|
|
8865
|
-
value.forEach((item, i) => visit(item, `${
|
|
10648
|
+
value.forEach((item, i) => visit(item, `${path57}[${i}]`));
|
|
8866
10649
|
return;
|
|
8867
10650
|
}
|
|
8868
10651
|
for (const [key, val] of Object.entries(value)) {
|
|
8869
|
-
visit(val,
|
|
10652
|
+
visit(val, path57 ? `${path57}.${key}` : key);
|
|
8870
10653
|
}
|
|
8871
10654
|
};
|
|
8872
10655
|
visit(output, "");
|
|
@@ -8984,23 +10767,23 @@ var DOWNLOAD_TIMEOUT_MS = 3e5;
|
|
|
8984
10767
|
function isDirTarget(output) {
|
|
8985
10768
|
if (/[\\/]$/.test(output)) return true;
|
|
8986
10769
|
try {
|
|
8987
|
-
return
|
|
10770
|
+
return fs44.statSync(output).isDirectory();
|
|
8988
10771
|
} catch {
|
|
8989
10772
|
return false;
|
|
8990
10773
|
}
|
|
8991
10774
|
}
|
|
8992
10775
|
function resolveDestination(predictionId, index, count, ext, opts) {
|
|
8993
|
-
if (!opts.output) return
|
|
8994
|
-
const output =
|
|
8995
|
-
if (isDirTarget(opts.output)) return
|
|
8996
|
-
const givenExt =
|
|
10776
|
+
if (!opts.output) return path51.join(opts.outDir, outputFilename(predictionId, index, ext));
|
|
10777
|
+
const output = path51.resolve(opts.output);
|
|
10778
|
+
if (isDirTarget(opts.output)) return path51.join(output, outputFilename(predictionId, index, ext));
|
|
10779
|
+
const givenExt = path51.extname(output);
|
|
8997
10780
|
if (count === 1) return givenExt ? output : `${output}${ext}`;
|
|
8998
10781
|
const base = givenExt ? output.slice(0, -givenExt.length) : output;
|
|
8999
10782
|
return `${base}-${String(index + 1).padStart(2, "0")}${ext}`;
|
|
9000
10783
|
}
|
|
9001
10784
|
async function writeResponse(response, dest) {
|
|
9002
10785
|
if (!response.body) throw new HttpError(0, "Response body is null", response.url);
|
|
9003
|
-
const fileStream =
|
|
10786
|
+
const fileStream = fs44.createWriteStream(dest);
|
|
9004
10787
|
try {
|
|
9005
10788
|
await pipeline2(
|
|
9006
10789
|
Readable.fromWeb(response.body),
|
|
@@ -9024,8 +10807,8 @@ async function saveOne(item, predictionId, index, count, opts) {
|
|
|
9024
10807
|
extFromUrl4(item.url) ?? ".bin",
|
|
9025
10808
|
opts
|
|
9026
10809
|
);
|
|
9027
|
-
|
|
9028
|
-
|
|
10810
|
+
fs44.mkdirSync(path51.dirname(dest2), { recursive: true });
|
|
10811
|
+
fs44.writeFileSync(dest2, bytes);
|
|
9029
10812
|
return dest2;
|
|
9030
10813
|
}
|
|
9031
10814
|
const doFetch = opts.fetch ?? ((url, init) => globalThis.fetch(url, init));
|
|
@@ -9041,11 +10824,11 @@ async function saveOne(item, predictionId, index, count, opts) {
|
|
|
9041
10824
|
}
|
|
9042
10825
|
const ext = extFromUrl4(item.url) ?? extFromContentType(response.headers.get("content-type")) ?? ".bin";
|
|
9043
10826
|
const dest = resolveDestination(predictionId, index, count, ext, opts);
|
|
9044
|
-
|
|
10827
|
+
fs44.mkdirSync(path51.dirname(dest), { recursive: true });
|
|
9045
10828
|
try {
|
|
9046
10829
|
await writeResponse(response, dest);
|
|
9047
10830
|
} catch (err2) {
|
|
9048
|
-
|
|
10831
|
+
fs44.rmSync(dest, { force: true });
|
|
9049
10832
|
throw err2;
|
|
9050
10833
|
}
|
|
9051
10834
|
return dest;
|
|
@@ -9081,7 +10864,7 @@ function addFlowOptions(cmd, defaultTimeoutMs) {
|
|
|
9081
10864
|
"Ask Replicate to hold the create request open (1-60s, default 60); keeps polling if still running"
|
|
9082
10865
|
).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
10866
|
}
|
|
9084
|
-
function
|
|
10867
|
+
function parsePositiveInt2(raw, flag, max) {
|
|
9085
10868
|
const n = Number(raw);
|
|
9086
10869
|
if (!Number.isInteger(n) || n <= 0 || max !== void 0 && n > max) {
|
|
9087
10870
|
throw new ValidationError(
|
|
@@ -9093,15 +10876,15 @@ function parsePositiveInt(raw, flag, max) {
|
|
|
9093
10876
|
function parsePreferWait(value) {
|
|
9094
10877
|
if (value === void 0 || value === false) return void 0;
|
|
9095
10878
|
if (value === true) return 60;
|
|
9096
|
-
return
|
|
10879
|
+
return parsePositiveInt2(value, "--prefer-wait", 60);
|
|
9097
10880
|
}
|
|
9098
|
-
function
|
|
10881
|
+
function resumeCommand3(id) {
|
|
9099
10882
|
return `multix replicate status ${id} --wait --download`;
|
|
9100
10883
|
}
|
|
9101
10884
|
function printAsyncHandle(prediction2) {
|
|
9102
10885
|
console.log(`Prediction submitted: ${prediction2.id} (status: ${prediction2.status})`);
|
|
9103
10886
|
console.log(` Check: multix replicate status ${prediction2.id}`);
|
|
9104
|
-
console.log(` Resume: ${
|
|
10887
|
+
console.log(` Resume: ${resumeCommand3(prediction2.id)}`);
|
|
9105
10888
|
console.log(` Cancel: multix replicate cancel ${prediction2.id}`);
|
|
9106
10889
|
}
|
|
9107
10890
|
async function reportOutputs(prediction2, opts) {
|
|
@@ -9135,7 +10918,7 @@ async function completePrediction(client, prediction2, opts) {
|
|
|
9135
10918
|
}
|
|
9136
10919
|
const final = await client.waitForPrediction(prediction2, {
|
|
9137
10920
|
timeoutMs: opts.waitTimeoutMs,
|
|
9138
|
-
resumeHint: `Resume with: ${
|
|
10921
|
+
resumeHint: `Resume with: ${resumeCommand3(id)}`,
|
|
9139
10922
|
onPoll: (p, attempt) => {
|
|
9140
10923
|
if (p.status !== lastStatus) opts.logger.debug(`attempt ${attempt}: ${p.status}`);
|
|
9141
10924
|
lastStatus = p.status;
|
|
@@ -9152,7 +10935,7 @@ async function executePrediction2(params) {
|
|
|
9152
10935
|
if (opts.async && opts.preferWait) {
|
|
9153
10936
|
throw new ValidationError("--async and --prefer-wait are mutually exclusive");
|
|
9154
10937
|
}
|
|
9155
|
-
const waitTimeoutMs =
|
|
10938
|
+
const waitTimeoutMs = parsePositiveInt2(opts.waitTimeout, "--wait-timeout");
|
|
9156
10939
|
const preferWaitSeconds = parsePreferWait(opts.preferWait);
|
|
9157
10940
|
logger.info(`Submitting to Replicate ${describeModelRef(model)}...`);
|
|
9158
10941
|
const prediction2 = await client.createPrediction({ model, input, preferWaitSeconds });
|
|
@@ -9186,7 +10969,7 @@ function buildImageInput(prompt, opts) {
|
|
|
9186
10969
|
...extra,
|
|
9187
10970
|
prompt,
|
|
9188
10971
|
...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
|
|
9189
|
-
...opts.numOutputs ? { num_outputs:
|
|
10972
|
+
...opts.numOutputs ? { num_outputs: parsePositiveInt2(opts.numOutputs, "--num-outputs", 10) } : {},
|
|
9190
10973
|
...opts.seed !== void 0 ? { seed: parseSeed(opts.seed) } : {},
|
|
9191
10974
|
...opts.outputFormat ? { output_format: opts.outputFormat } : {}
|
|
9192
10975
|
};
|
|
@@ -9223,8 +11006,8 @@ function registerReplicateImageCommand(parent) {
|
|
|
9223
11006
|
|
|
9224
11007
|
// src/providers/replicate/node-input-files.ts
|
|
9225
11008
|
init_errors();
|
|
9226
|
-
import
|
|
9227
|
-
import
|
|
11009
|
+
import fs45 from "fs";
|
|
11010
|
+
import path52 from "path";
|
|
9228
11011
|
var DATA_URI_MAX_BYTES = 256 * 1024;
|
|
9229
11012
|
var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
9230
11013
|
function formatMb2(bytes) {
|
|
@@ -9232,10 +11015,10 @@ function formatMb2(bytes) {
|
|
|
9232
11015
|
}
|
|
9233
11016
|
async function resolveFileInput(value, client, logger) {
|
|
9234
11017
|
if (isMediaRef(value)) return value;
|
|
9235
|
-
const filePath =
|
|
11018
|
+
const filePath = path52.resolve(value);
|
|
9236
11019
|
let stat;
|
|
9237
11020
|
try {
|
|
9238
|
-
stat =
|
|
11021
|
+
stat = fs45.statSync(filePath);
|
|
9239
11022
|
} catch {
|
|
9240
11023
|
throw new ValidationError(`Input file not found: ${value} (pass a local path or an https URL)`);
|
|
9241
11024
|
}
|
|
@@ -9246,9 +11029,9 @@ async function resolveFileInput(value, client, logger) {
|
|
|
9246
11029
|
`Input file ${value} is ${formatMb2(stat.size)}; Replicate accepts uploads under 100MB. Host it and pass an https URL instead.`
|
|
9247
11030
|
);
|
|
9248
11031
|
}
|
|
9249
|
-
const filename =
|
|
11032
|
+
const filename = path52.basename(filePath);
|
|
9250
11033
|
const contentType = mimeFromFilename(filename);
|
|
9251
|
-
const bytes =
|
|
11034
|
+
const bytes = fs45.readFileSync(filePath);
|
|
9252
11035
|
if (stat.size <= DATA_URI_MAX_BYTES) {
|
|
9253
11036
|
logger.debug(`Inlining ${filename} (${stat.size} bytes) as a data URI`);
|
|
9254
11037
|
return `data:${contentType};base64,${bytes.toString("base64")}`;
|
|
@@ -9275,14 +11058,14 @@ function parseFileAssignments(values) {
|
|
|
9275
11058
|
}
|
|
9276
11059
|
|
|
9277
11060
|
// src/providers/replicate/commands/run.ts
|
|
9278
|
-
var
|
|
11061
|
+
var collect9 = (value, previous = []) => [...previous, value];
|
|
9279
11062
|
function registerReplicateRunCommand(parent) {
|
|
9280
11063
|
const cmd = parent.command("run <model>").description(
|
|
9281
11064
|
"Run any Replicate model: owner/name (official), owner/name:version, or a version id"
|
|
9282
11065
|
).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
11066
|
"--file <key=path>",
|
|
9284
11067
|
"Set input <key> to a local file or URL (uploaded if needed); repeatable",
|
|
9285
|
-
|
|
11068
|
+
collect9
|
|
9286
11069
|
);
|
|
9287
11070
|
addFlowOptions(cmd, 6e5).action(
|
|
9288
11071
|
async (model, opts) => {
|
|
@@ -9306,7 +11089,7 @@ function registerReplicateStatusCommand(parent) {
|
|
|
9306
11089
|
const client = createReplicateClient();
|
|
9307
11090
|
if (opts.wait) {
|
|
9308
11091
|
await completePrediction(client, predictionId, {
|
|
9309
|
-
waitTimeoutMs:
|
|
11092
|
+
waitTimeoutMs: parsePositiveInt2(opts.waitTimeout, "--wait-timeout"),
|
|
9310
11093
|
logger,
|
|
9311
11094
|
download: Boolean(opts.download),
|
|
9312
11095
|
output: opts.output
|
|
@@ -9320,7 +11103,7 @@ function registerReplicateStatusCommand(parent) {
|
|
|
9320
11103
|
}
|
|
9321
11104
|
console.log(JSON.stringify(summarizePrediction(prediction2), null, 2));
|
|
9322
11105
|
if (!isTerminalStatus2(prediction2.status)) {
|
|
9323
|
-
logger.info(`Still running. Resume with: ${
|
|
11106
|
+
logger.info(`Still running. Resume with: ${resumeCommand3(prediction2.id)}`);
|
|
9324
11107
|
} else if (prediction2.status !== "succeeded") {
|
|
9325
11108
|
throw new ReplicatePredictionError(prediction2);
|
|
9326
11109
|
}
|
|
@@ -9346,7 +11129,7 @@ function buildVideoInput(prompt, opts) {
|
|
|
9346
11129
|
return {
|
|
9347
11130
|
...extra,
|
|
9348
11131
|
prompt,
|
|
9349
|
-
...opts.duration ? { duration:
|
|
11132
|
+
...opts.duration ? { duration: parsePositiveInt2(opts.duration, "--duration", 60) } : {},
|
|
9350
11133
|
...opts.aspectRatio ? { aspect_ratio: opts.aspectRatio } : {},
|
|
9351
11134
|
...opts.resolution ? { resolution: opts.resolution } : {},
|
|
9352
11135
|
...opts.seed !== void 0 ? { seed: parseSeed(opts.seed) } : {}
|
|
@@ -9375,7 +11158,7 @@ function registerReplicateCommands(program2) {
|
|
|
9375
11158
|
}
|
|
9376
11159
|
|
|
9377
11160
|
// src/commands/check.ts
|
|
9378
|
-
import
|
|
11161
|
+
import fs46 from "fs";
|
|
9379
11162
|
import os4 from "os";
|
|
9380
11163
|
|
|
9381
11164
|
// src/commands/check-helpers/binary-check.ts
|
|
@@ -9434,6 +11217,7 @@ To configure multix, set at least one provider API key:
|
|
|
9434
11217
|
fal.ai : https://fal.ai/dashboard/keys
|
|
9435
11218
|
Atlas Cloud : https://www.atlascloud.ai/console/api-keys
|
|
9436
11219
|
Replicate : https://replicate.com/account/api-tokens
|
|
11220
|
+
HeyGen : https://app.heygen.com/developers/api
|
|
9437
11221
|
|
|
9438
11222
|
Option A \u2014 user global config (recommended):
|
|
9439
11223
|
echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
|
|
@@ -9515,6 +11299,12 @@ var PROVIDERS = [
|
|
|
9515
11299
|
envFallback: "REPLICATE_API_KEY",
|
|
9516
11300
|
optional: true,
|
|
9517
11301
|
link: "https://replicate.com/account/api-tokens"
|
|
11302
|
+
},
|
|
11303
|
+
{
|
|
11304
|
+
name: "HeyGen",
|
|
11305
|
+
envPrimary: "HEYGEN_API_KEY",
|
|
11306
|
+
optional: true,
|
|
11307
|
+
link: "https://app.heygen.com/developers/api"
|
|
9518
11308
|
}
|
|
9519
11309
|
];
|
|
9520
11310
|
function resolveProviderKey(p) {
|
|
@@ -9566,7 +11356,7 @@ function registerCheckCommand(program2) {
|
|
|
9566
11356
|
}
|
|
9567
11357
|
if (!legacyClaudeEnvEnabled()) {
|
|
9568
11358
|
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os4.homedir();
|
|
9569
|
-
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) =>
|
|
11359
|
+
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs46.existsSync(f));
|
|
9570
11360
|
if (legacy.length > 0) {
|
|
9571
11361
|
logger.info(
|
|
9572
11362
|
`Found ${legacy.length} legacy .claude env file(s); set MULTIX_LEGACY_CLAUDE_ENV=1 to load them`
|
|
@@ -9671,8 +11461,8 @@ function registerCheckCommand(program2) {
|
|
|
9671
11461
|
|
|
9672
11462
|
// src/commands/doc/convert.ts
|
|
9673
11463
|
init_errors();
|
|
9674
|
-
import
|
|
9675
|
-
import
|
|
11464
|
+
import fs47 from "fs";
|
|
11465
|
+
import path53 from "path";
|
|
9676
11466
|
|
|
9677
11467
|
// src/commands/doc/default-prompt.ts
|
|
9678
11468
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -9693,18 +11483,18 @@ async function convertDocuments(opts) {
|
|
|
9693
11483
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
9694
11484
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
9695
11485
|
for (const f of inputs) {
|
|
9696
|
-
if (!
|
|
11486
|
+
if (!fs47.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
9697
11487
|
}
|
|
9698
11488
|
let outputPath;
|
|
9699
11489
|
if (output) {
|
|
9700
|
-
outputPath =
|
|
11490
|
+
outputPath = path53.resolve(output);
|
|
9701
11491
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
9702
|
-
const base =
|
|
9703
|
-
outputPath =
|
|
11492
|
+
const base = path53.basename(inputs[0], path53.extname(inputs[0]));
|
|
11493
|
+
outputPath = path53.join(getOutputDir(), `${base}-extraction.md`);
|
|
9704
11494
|
} else {
|
|
9705
|
-
outputPath =
|
|
11495
|
+
outputPath = path53.join(getOutputDir(), "document-extraction.md");
|
|
9706
11496
|
}
|
|
9707
|
-
|
|
11497
|
+
fs47.mkdirSync(path53.dirname(outputPath), { recursive: true });
|
|
9708
11498
|
const results = [];
|
|
9709
11499
|
for (let i = 0; i < inputs.length; i++) {
|
|
9710
11500
|
const filePath = inputs[i];
|
|
@@ -9764,7 +11554,7 @@ async function convertDocuments(opts) {
|
|
|
9764
11554
|
`);
|
|
9765
11555
|
for (const r of results) {
|
|
9766
11556
|
lines.push(`
|
|
9767
|
-
## ${
|
|
11557
|
+
## ${path53.basename(r.file)}
|
|
9768
11558
|
`);
|
|
9769
11559
|
if (r.status === "success" && r.markdown) {
|
|
9770
11560
|
lines.push(`${r.markdown}
|
|
@@ -9777,7 +11567,7 @@ async function convertDocuments(opts) {
|
|
|
9777
11567
|
}
|
|
9778
11568
|
lines.push("\n---\n");
|
|
9779
11569
|
}
|
|
9780
|
-
|
|
11570
|
+
fs47.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
9781
11571
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
9782
11572
|
console.log(`
|
|
9783
11573
|
${"=".repeat(50)}`);
|
|
@@ -9825,12 +11615,12 @@ function registerDocCommands(program2) {
|
|
|
9825
11615
|
|
|
9826
11616
|
// src/commands/media/index.ts
|
|
9827
11617
|
init_errors();
|
|
9828
|
-
import
|
|
9829
|
-
import
|
|
11618
|
+
import fs50 from "fs";
|
|
11619
|
+
import path56 from "path";
|
|
9830
11620
|
|
|
9831
11621
|
// src/commands/media/batch.ts
|
|
9832
|
-
import
|
|
9833
|
-
import
|
|
11622
|
+
import fs48 from "fs";
|
|
11623
|
+
import path54 from "path";
|
|
9834
11624
|
|
|
9835
11625
|
// src/commands/media/detect-type.ts
|
|
9836
11626
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -10026,10 +11816,10 @@ async function batchOptimize(opts) {
|
|
|
10026
11816
|
verbose,
|
|
10027
11817
|
logger
|
|
10028
11818
|
} = opts;
|
|
10029
|
-
|
|
10030
|
-
const entries =
|
|
11819
|
+
fs48.mkdirSync(outputDir, { recursive: true });
|
|
11820
|
+
const entries = fs48.readdirSync(inputDir);
|
|
10031
11821
|
const mediaFiles = entries.filter((f) => {
|
|
10032
|
-
const ext =
|
|
11822
|
+
const ext = path54.extname(f).toLowerCase();
|
|
10033
11823
|
return ALL_EXTS.has(ext);
|
|
10034
11824
|
});
|
|
10035
11825
|
if (mediaFiles.length === 0) {
|
|
@@ -10040,9 +11830,9 @@ async function batchOptimize(opts) {
|
|
|
10040
11830
|
let succeeded = 0;
|
|
10041
11831
|
const errors = [];
|
|
10042
11832
|
for (const file of mediaFiles) {
|
|
10043
|
-
const inputPath =
|
|
10044
|
-
const outputPath =
|
|
10045
|
-
const ext =
|
|
11833
|
+
const inputPath = path54.join(inputDir, file);
|
|
11834
|
+
const outputPath = path54.join(outputDir, file);
|
|
11835
|
+
const ext = path54.extname(file).toLowerCase();
|
|
10046
11836
|
const kind = extToKind(ext);
|
|
10047
11837
|
logger?.debug(`Processing: ${file}`);
|
|
10048
11838
|
try {
|
|
@@ -10079,8 +11869,8 @@ async function batchOptimize(opts) {
|
|
|
10079
11869
|
|
|
10080
11870
|
// src/commands/media/split-video.ts
|
|
10081
11871
|
init_errors();
|
|
10082
|
-
import
|
|
10083
|
-
import
|
|
11872
|
+
import fs49 from "fs";
|
|
11873
|
+
import path55 from "path";
|
|
10084
11874
|
async function splitVideo(opts) {
|
|
10085
11875
|
await assertFfmpeg();
|
|
10086
11876
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -10091,10 +11881,10 @@ async function splitVideo(opts) {
|
|
|
10091
11881
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
10092
11882
|
return [input];
|
|
10093
11883
|
}
|
|
10094
|
-
|
|
10095
|
-
const ext =
|
|
10096
|
-
const baseName =
|
|
10097
|
-
const outputPattern =
|
|
11884
|
+
fs49.mkdirSync(outputDir, { recursive: true });
|
|
11885
|
+
const ext = path55.extname(input) || ".mp4";
|
|
11886
|
+
const baseName = path55.basename(input, ext);
|
|
11887
|
+
const outputPattern = path55.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
10098
11888
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
10099
11889
|
const args = [
|
|
10100
11890
|
"-i",
|
|
@@ -10113,8 +11903,8 @@ async function splitVideo(opts) {
|
|
|
10113
11903
|
await runFfmpeg(args, { verbose, logger });
|
|
10114
11904
|
const files = [];
|
|
10115
11905
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
10116
|
-
const fname =
|
|
10117
|
-
if (
|
|
11906
|
+
const fname = path55.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
11907
|
+
if (fs49.existsSync(fname)) files.push(fname);
|
|
10118
11908
|
}
|
|
10119
11909
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
10120
11910
|
return files;
|
|
@@ -10128,9 +11918,9 @@ function registerMediaCommands(program2) {
|
|
|
10128
11918
|
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
11919
|
async (opts) => {
|
|
10130
11920
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
10131
|
-
if (!
|
|
10132
|
-
|
|
10133
|
-
const ext =
|
|
11921
|
+
if (!fs50.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
11922
|
+
fs50.mkdirSync(path56.dirname(path56.resolve(opts.output)), { recursive: true });
|
|
11923
|
+
const ext = path56.extname(opts.input).toLowerCase();
|
|
10134
11924
|
const kind = extToKind(ext);
|
|
10135
11925
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
10136
11926
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -10170,7 +11960,7 @@ function registerMediaCommands(program2) {
|
|
|
10170
11960
|
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
11961
|
async (opts) => {
|
|
10172
11962
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
10173
|
-
if (!
|
|
11963
|
+
if (!fs50.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
10174
11964
|
const chunks = await splitVideo({
|
|
10175
11965
|
input: opts.input,
|
|
10176
11966
|
outputDir: opts.outputDir,
|
|
@@ -10186,7 +11976,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
10186
11976
|
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
11977
|
async (opts) => {
|
|
10188
11978
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
10189
|
-
if (!
|
|
11979
|
+
if (!fs50.existsSync(opts.inputDir))
|
|
10190
11980
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
10191
11981
|
const result = await batchOptimize({
|
|
10192
11982
|
inputDir: opts.inputDir,
|
|
@@ -10368,6 +12158,7 @@ function registerCommands(program2) {
|
|
|
10368
12158
|
registerFalCommands(program2);
|
|
10369
12159
|
registerAtlasCloudCommands(program2);
|
|
10370
12160
|
registerReplicateCommands(program2);
|
|
12161
|
+
registerHeygenCommands(program2);
|
|
10371
12162
|
registerMediaCommands(program2);
|
|
10372
12163
|
registerDocCommands(program2);
|
|
10373
12164
|
}
|
|
@@ -10381,7 +12172,7 @@ loadEnv();
|
|
|
10381
12172
|
var require3 = createRequire2(import.meta.url);
|
|
10382
12173
|
var pkg = require3("../package.json");
|
|
10383
12174
|
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"
|
|
12175
|
+
"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
12176
|
).version(pkg.version).enablePositionalOptions();
|
|
10386
12177
|
registerCommands(program);
|
|
10387
12178
|
program.parseAsync(process.argv).catch((err2) => {
|