@mrgoonie/multix 0.10.0 → 0.11.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 +48 -1
- package/dist/cli.js +2225 -933
- package/package.json +3 -2
- package/skill/SKILL.md +35 -2
package/dist/cli.js
CHANGED
|
@@ -99,12 +99,1923 @@ var init_errors = __esm({
|
|
|
99
99
|
import { createRequire as createRequire2 } from "module";
|
|
100
100
|
import { Command } from "commander";
|
|
101
101
|
|
|
102
|
+
// src/providers/atlascloud/commands/audio.ts
|
|
103
|
+
init_errors();
|
|
104
|
+
|
|
105
|
+
// src/providers/atlascloud/model-input.ts
|
|
106
|
+
init_errors();
|
|
107
|
+
var SUBMIT_PATH_RE = /\/model\/generate(Image|Video|Audio)$/;
|
|
108
|
+
function resolveRef(doc, ref) {
|
|
109
|
+
if (typeof ref !== "string" || !ref.startsWith("#/")) return void 0;
|
|
110
|
+
let node = doc;
|
|
111
|
+
for (const part of ref.slice(2).split("/")) {
|
|
112
|
+
node = node?.[part];
|
|
113
|
+
if (node === void 0) return void 0;
|
|
114
|
+
}
|
|
115
|
+
return node;
|
|
116
|
+
}
|
|
117
|
+
function parseModelSpec(model, doc) {
|
|
118
|
+
if (!doc || typeof doc !== "object") return void 0;
|
|
119
|
+
const d = doc;
|
|
120
|
+
let endpoint2;
|
|
121
|
+
let input;
|
|
122
|
+
for (const [p, ops] of Object.entries(d.paths ?? {})) {
|
|
123
|
+
const m = SUBMIT_PATH_RE.exec(p);
|
|
124
|
+
if (!m?.[1] || !ops?.post) continue;
|
|
125
|
+
endpoint2 = m[1].toLowerCase();
|
|
126
|
+
const schema = ops.post.requestBody?.content?.["application/json"]?.schema;
|
|
127
|
+
input = schema?.$ref ? resolveRef(d, schema.$ref) : schema;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
input ??= d.components?.schemas?.Input;
|
|
131
|
+
if (!input || typeof input.properties !== "object") return void 0;
|
|
132
|
+
return {
|
|
133
|
+
model,
|
|
134
|
+
endpoint: endpoint2,
|
|
135
|
+
properties: input.properties,
|
|
136
|
+
required: Array.isArray(input.required) ? input.required.filter((r) => typeof r === "string") : []
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
var FIELD_CANDIDATES = {
|
|
140
|
+
prompt: ["prompt"],
|
|
141
|
+
negativePrompt: ["negative_prompt"],
|
|
142
|
+
seed: ["seed"],
|
|
143
|
+
size: ["size", "image_size"],
|
|
144
|
+
aspectRatio: ["aspect_ratio"],
|
|
145
|
+
resolution: ["resolution"],
|
|
146
|
+
duration: ["duration"],
|
|
147
|
+
numImages: ["num_images"],
|
|
148
|
+
editImages: [
|
|
149
|
+
"images",
|
|
150
|
+
"image",
|
|
151
|
+
"image_urls",
|
|
152
|
+
"reference_images",
|
|
153
|
+
"image_url",
|
|
154
|
+
"reference_image_urls"
|
|
155
|
+
],
|
|
156
|
+
primaryImage: [
|
|
157
|
+
"image",
|
|
158
|
+
"image_url",
|
|
159
|
+
"images",
|
|
160
|
+
"image_urls",
|
|
161
|
+
"reference_images",
|
|
162
|
+
"reference_image_urls"
|
|
163
|
+
],
|
|
164
|
+
speechText: ["text", "prompt"],
|
|
165
|
+
audioInput: ["audio_url", "audio"],
|
|
166
|
+
voice: ["voice_id", "voice"],
|
|
167
|
+
audioFormat: ["format", "codec", "output_format"],
|
|
168
|
+
language: ["language"],
|
|
169
|
+
modelFormat: ["format", "output_format", "target_formats"]
|
|
170
|
+
};
|
|
171
|
+
var FALLBACK_FIELD_TYPES = {
|
|
172
|
+
seed: "integer",
|
|
173
|
+
num_images: "integer",
|
|
174
|
+
duration: "number",
|
|
175
|
+
images: "array"
|
|
176
|
+
};
|
|
177
|
+
function coerce(flag, field, prop, value) {
|
|
178
|
+
const type = prop.type;
|
|
179
|
+
if (Array.isArray(value)) {
|
|
180
|
+
if (type === "array") return value;
|
|
181
|
+
if (value.length === 1) return coerceScalar(flag, field, type, value[0]);
|
|
182
|
+
throw new ValidationError(
|
|
183
|
+
`${flag}: field "${field}" of this model accepts a single value, got ${value.length}`
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (type === "array") return [coerceScalar(flag, field, prop.items?.type, value)];
|
|
187
|
+
return coerceScalar(flag, field, type, value);
|
|
188
|
+
}
|
|
189
|
+
function coerceScalar(flag, field, type, value) {
|
|
190
|
+
if (type === "integer" || type === "number") {
|
|
191
|
+
const n = typeof value === "number" ? value : Number(String(value).trim());
|
|
192
|
+
if (!Number.isFinite(n) || type === "integer" && !Number.isInteger(n)) {
|
|
193
|
+
throw new ValidationError(`${flag}: "${value}" is not a valid ${type} for "${field}"`);
|
|
194
|
+
}
|
|
195
|
+
return n;
|
|
196
|
+
}
|
|
197
|
+
if (type === "boolean") {
|
|
198
|
+
if (typeof value === "boolean") return value;
|
|
199
|
+
const s = String(value).toLowerCase();
|
|
200
|
+
if (s === "true") return true;
|
|
201
|
+
if (s === "false") return false;
|
|
202
|
+
throw new ValidationError(`${flag}: "${value}" is not a boolean for "${field}"`);
|
|
203
|
+
}
|
|
204
|
+
if (type === "string") return String(value);
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
function checkConstraints(flag, field, prop, v) {
|
|
208
|
+
const values = Array.isArray(v) ? v : [v];
|
|
209
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0 && !Array.isArray(v)) {
|
|
210
|
+
if (!prop.enum.some((e) => e === v || String(e) === String(v))) {
|
|
211
|
+
throw new ValidationError(
|
|
212
|
+
`${flag}: "${String(v)}" is not supported by this model. Valid ${field}: ${prop.enum.map(String).join(", ")}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
for (const item of values) {
|
|
217
|
+
if (typeof item !== "number") continue;
|
|
218
|
+
if (prop.minimum !== void 0 && item < prop.minimum) {
|
|
219
|
+
throw new ValidationError(
|
|
220
|
+
`${flag}: ${item} is below the minimum ${prop.minimum} for "${field}"`
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (prop.maximum !== void 0 && item > prop.maximum) {
|
|
224
|
+
throw new ValidationError(
|
|
225
|
+
`${flag}: ${item} exceeds the maximum ${prop.maximum} for "${field}"`
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (Array.isArray(v) && prop.maxItems !== void 0 && v.length > prop.maxItems) {
|
|
230
|
+
throw new ValidationError(
|
|
231
|
+
`${flag}: this model accepts at most ${prop.maxItems} item(s) for "${field}", got ${v.length}`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function pickField(spec, req) {
|
|
236
|
+
const declared = req.candidates.filter((c2) => spec.properties[c2] !== void 0);
|
|
237
|
+
const compatible = declared.filter((c2) => {
|
|
238
|
+
if (spec.properties[c2]?.type !== "boolean") return true;
|
|
239
|
+
const v = req.value;
|
|
240
|
+
return typeof v === "boolean" || typeof v === "string" && /^(true|false)$/i.test(v);
|
|
241
|
+
});
|
|
242
|
+
if (Array.isArray(req.value) && req.value.length > 1) {
|
|
243
|
+
return compatible.find((c2) => spec.properties[c2]?.type === "array") ?? compatible[0];
|
|
244
|
+
}
|
|
245
|
+
return compatible[0];
|
|
246
|
+
}
|
|
247
|
+
function buildModelInput(spec, requests, base = {}) {
|
|
248
|
+
const body = { ...base };
|
|
249
|
+
for (const req of requests) {
|
|
250
|
+
if (req.value === void 0 || Array.isArray(req.value) && req.value.length === 0) continue;
|
|
251
|
+
if (!spec) {
|
|
252
|
+
const field2 = req.candidates[0];
|
|
253
|
+
if (field2) {
|
|
254
|
+
const type = FALLBACK_FIELD_TYPES[field2] ?? "string";
|
|
255
|
+
body[field2] = coerce(req.flag, field2, { type }, req.value);
|
|
256
|
+
}
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
const field = pickField(spec, req);
|
|
260
|
+
if (!field) {
|
|
261
|
+
throw new ValidationError(
|
|
262
|
+
`Model ${spec.model} does not accept ${req.flag}. Its inputs: ${listInputs(spec)}. Use \`multix atlas run ${spec.model} --input '{...}'\` for model-specific fields.`
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const prop = spec.properties[field];
|
|
266
|
+
const value = coerce(req.flag, field, prop, req.value);
|
|
267
|
+
checkConstraints(req.flag, field, prop, value);
|
|
268
|
+
body[field] = value;
|
|
269
|
+
}
|
|
270
|
+
if (spec) finalizeAgainstSpec(spec, body, requests);
|
|
271
|
+
return body;
|
|
272
|
+
}
|
|
273
|
+
function listInputs(spec) {
|
|
274
|
+
return Object.keys(spec.properties).filter((k) => k !== "model").join(", ");
|
|
275
|
+
}
|
|
276
|
+
var CONTENT_FIELDS = /* @__PURE__ */ new Set(["prompt", "text", "lyrics"]);
|
|
277
|
+
function finalizeAgainstSpec(spec, body, requests = []) {
|
|
278
|
+
const missing = [];
|
|
279
|
+
for (const field of spec.required) {
|
|
280
|
+
if (field === "model" || body[field] !== void 0) continue;
|
|
281
|
+
const def = spec.properties[field]?.default;
|
|
282
|
+
if (def !== void 0 && def !== "" && !CONTENT_FIELDS.has(field)) {
|
|
283
|
+
body[field] = def;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const flag = requests.find((r) => r.candidates.includes(field))?.flag;
|
|
287
|
+
missing.push(flag ? `${field} (pass ${flag})` : field);
|
|
288
|
+
}
|
|
289
|
+
if (missing.length > 0) {
|
|
290
|
+
throw new ValidationError(`Model ${spec.model} requires: ${missing.join(", ")}`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function validateRawInput(spec, body) {
|
|
294
|
+
const unknown = [];
|
|
295
|
+
for (const [field, value] of Object.entries(body)) {
|
|
296
|
+
if (field === "model") continue;
|
|
297
|
+
const prop = spec.properties[field];
|
|
298
|
+
if (!prop) {
|
|
299
|
+
unknown.push(field);
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
checkConstraints(`--input.${field}`, field, prop, value);
|
|
303
|
+
}
|
|
304
|
+
finalizeAgainstSpec(spec, body);
|
|
305
|
+
return unknown;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/providers/atlascloud/models.ts
|
|
309
|
+
var DEFAULT_ATLAS_BASE_URL = "https://api.atlascloud.ai/api/v1";
|
|
310
|
+
var SUBMIT_PATHS = {
|
|
311
|
+
image: "/model/generateImage",
|
|
312
|
+
video: "/model/generateVideo",
|
|
313
|
+
audio: "/model/generateAudio"
|
|
314
|
+
};
|
|
315
|
+
var ATLAS_MEDIA_KINDS = ["image", "video", "audio"];
|
|
316
|
+
var ATLAS_DEFAULT_MODELS = {
|
|
317
|
+
/** FLUX.1 [schnell], $0.003/image — fastest, cheapest text-to-image. */
|
|
318
|
+
image: "black-forest-labs/flux-schnell",
|
|
319
|
+
/** GPT Image 1 Mini edit — cheapest image edit; takes `images[]`. */
|
|
320
|
+
imageEdit: "openai/gpt-image-1-mini/edit",
|
|
321
|
+
/** Seedance 1.0 Pro Fast — low-cost text-to-video, 2-12s, 480p-1080p. */
|
|
322
|
+
video: "bytedance/seedance-v1-pro-fast/text-to-video",
|
|
323
|
+
/** Matching image-to-video model; takes a single `image`. */
|
|
324
|
+
imageToVideo: "bytedance/seedance-v1-pro-fast/image-to-video",
|
|
325
|
+
/** xAI TTS v1 — cheapest text-to-speech. */
|
|
326
|
+
tts: "xai/tts-v1",
|
|
327
|
+
/** Seed ASR 2.0 — documented speech-to-text example; returns text. */
|
|
328
|
+
stt: "bytedance/seed-asr-2.0",
|
|
329
|
+
/** Hunyuan 3D Rapid — low-cost text-to-3D (GLB by default). */
|
|
330
|
+
textTo3d: "tencent/hunyuan3d-rapid/text-to-3d",
|
|
331
|
+
/** Hunyuan 3D Rapid — low-cost image-to-3D (GLB by default). */
|
|
332
|
+
imageTo3d: "tencent/hunyuan3d-rapid/image-to-3d"
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// src/providers/atlascloud/commands/env.ts
|
|
336
|
+
import { openAsBlob } from "fs";
|
|
337
|
+
import fs2 from "fs/promises";
|
|
338
|
+
import path2 from "path";
|
|
339
|
+
|
|
340
|
+
// src/core/env-loader.ts
|
|
341
|
+
init_redact();
|
|
342
|
+
import fs from "fs";
|
|
343
|
+
import os from "os";
|
|
344
|
+
import path from "path";
|
|
345
|
+
import { config as dotenvConfig, parse as dotenvParse } from "dotenv";
|
|
346
|
+
var loaded = false;
|
|
347
|
+
var loadedFiles = [];
|
|
348
|
+
var LEGACY_SKILL_DIRS = ["ai-multimodal", "ak-ai-multimodal"];
|
|
349
|
+
function legacyFilesUnder(root) {
|
|
350
|
+
const claude = path.join(root, ".claude");
|
|
351
|
+
return [
|
|
352
|
+
...LEGACY_SKILL_DIRS.map((dir) => path.join(claude, "skills", dir, ".env")),
|
|
353
|
+
path.join(claude, "skills", ".env"),
|
|
354
|
+
path.join(claude, ".env")
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
function samePath(a, b) {
|
|
358
|
+
const norm2 = (p) => {
|
|
359
|
+
const r = path.resolve(p);
|
|
360
|
+
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
361
|
+
};
|
|
362
|
+
return norm2(a) === norm2(b);
|
|
363
|
+
}
|
|
364
|
+
function legacyClaudeEnvFiles(cwd, home) {
|
|
365
|
+
const files = legacyFilesUnder(cwd);
|
|
366
|
+
if (home && !samePath(home, cwd)) files.push(...legacyFilesUnder(home));
|
|
367
|
+
return files;
|
|
368
|
+
}
|
|
369
|
+
function legacyClaudeEnvEnabled(env = process.env) {
|
|
370
|
+
const value = env.MULTIX_LEGACY_CLAUDE_ENV;
|
|
371
|
+
return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
372
|
+
}
|
|
373
|
+
var LEGACY_ALLOWED_NAME = /^[A-Z][A-Z0-9_]*_(API_KEY(_\d+)?|API_TOKEN|ACCOUNT_ID|GATEWAY_ID)$|^(FAL_KEY|ARK_API_KEY)$/;
|
|
374
|
+
function isLegacyAllowedName(name) {
|
|
375
|
+
return LEGACY_ALLOWED_NAME.test(name);
|
|
376
|
+
}
|
|
377
|
+
function isReadableFile(file) {
|
|
378
|
+
try {
|
|
379
|
+
return fs.statSync(file).isFile();
|
|
380
|
+
} catch {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function loadFile(file) {
|
|
385
|
+
if (!isReadableFile(file)) return;
|
|
386
|
+
const result = dotenvConfig({ path: file, override: false });
|
|
387
|
+
if (!result.error) loadedFiles.push(file);
|
|
388
|
+
}
|
|
389
|
+
function loadLegacyFile(file) {
|
|
390
|
+
if (!isReadableFile(file)) return;
|
|
391
|
+
let parsed;
|
|
392
|
+
try {
|
|
393
|
+
parsed = dotenvParse(fs.readFileSync(file));
|
|
394
|
+
} catch {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
for (const [name, value] of Object.entries(parsed)) {
|
|
398
|
+
if (isLegacyAllowedName(name) && process.env[name] === void 0) process.env[name] = value;
|
|
399
|
+
}
|
|
400
|
+
loadedFiles.push(file);
|
|
401
|
+
}
|
|
402
|
+
function loadEnv() {
|
|
403
|
+
if (loaded) return;
|
|
404
|
+
loaded = true;
|
|
405
|
+
loadedFiles = [];
|
|
406
|
+
const cwd = process.cwd();
|
|
407
|
+
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os.homedir();
|
|
408
|
+
loadFile(path.resolve(cwd, ".env"));
|
|
409
|
+
if (home) loadFile(path.join(home, ".multix", ".env"));
|
|
410
|
+
if (legacyClaudeEnvEnabled()) {
|
|
411
|
+
for (const file of legacyClaudeEnvFiles(cwd, home)) loadLegacyFile(file);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function getLoadedEnvFiles() {
|
|
415
|
+
return [...loadedFiles];
|
|
416
|
+
}
|
|
417
|
+
function resolveKey(name) {
|
|
418
|
+
return process.env[name] || void 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/providers/atlascloud/commands/env.ts
|
|
422
|
+
init_errors();
|
|
423
|
+
|
|
424
|
+
// src/core/logger.ts
|
|
425
|
+
var ANSI = {
|
|
426
|
+
reset: "\x1B[0m",
|
|
427
|
+
bold: "\x1B[1m",
|
|
428
|
+
green: "\x1B[32m",
|
|
429
|
+
yellow: "\x1B[33m",
|
|
430
|
+
red: "\x1B[31m",
|
|
431
|
+
blue: "\x1B[34m",
|
|
432
|
+
cyan: "\x1B[36m",
|
|
433
|
+
dim: "\x1B[2m"
|
|
434
|
+
};
|
|
435
|
+
function c(color, text) {
|
|
436
|
+
if (process.env.NO_COLOR || !process.stdout.isTTY) return text;
|
|
437
|
+
return `${color}${text}${ANSI.reset}`;
|
|
438
|
+
}
|
|
439
|
+
function createLogger(opts) {
|
|
440
|
+
const { verbose } = opts;
|
|
441
|
+
return {
|
|
442
|
+
info(msg) {
|
|
443
|
+
console.log(c(ANSI.blue, ` ${msg}`));
|
|
444
|
+
},
|
|
445
|
+
success(msg) {
|
|
446
|
+
console.log(c(ANSI.green, `\u2713 ${msg}`));
|
|
447
|
+
},
|
|
448
|
+
warn(msg) {
|
|
449
|
+
console.warn(c(ANSI.yellow, `\u26A0 ${msg}`));
|
|
450
|
+
},
|
|
451
|
+
error(msg) {
|
|
452
|
+
console.error(c(ANSI.red, `\u2717 ${msg}`));
|
|
453
|
+
},
|
|
454
|
+
debug(msg) {
|
|
455
|
+
if (verbose) console.log(c(ANSI.dim, ` [debug] ${msg}`));
|
|
456
|
+
},
|
|
457
|
+
header(msg) {
|
|
458
|
+
const line = "=".repeat(60);
|
|
459
|
+
console.log(c(ANSI.bold + ANSI.cyan, `
|
|
460
|
+
${line}
|
|
461
|
+
${msg}
|
|
462
|
+
${line}
|
|
463
|
+
`));
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
var defaultLogger = createLogger({ verbose: false });
|
|
468
|
+
|
|
469
|
+
// src/providers/atlascloud/commands/env.ts
|
|
470
|
+
init_redact();
|
|
471
|
+
|
|
472
|
+
// src/core/async-poll.ts
|
|
473
|
+
init_errors();
|
|
474
|
+
var PollTimeoutError = class extends MultixError {
|
|
475
|
+
constructor(message, lastStatus) {
|
|
476
|
+
super(message, "POLL_TIMEOUT");
|
|
477
|
+
this.lastStatus = lastStatus;
|
|
478
|
+
this.name = "PollTimeoutError";
|
|
479
|
+
}
|
|
480
|
+
lastStatus;
|
|
481
|
+
};
|
|
482
|
+
function isTransientHttpError(error) {
|
|
483
|
+
if (!(error instanceof HttpError)) return false;
|
|
484
|
+
return error.status === 0 || error.status === 429 || error.status >= 500;
|
|
485
|
+
}
|
|
486
|
+
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
487
|
+
async function pollUntil(opts) {
|
|
488
|
+
const now = opts.now ?? Date.now;
|
|
489
|
+
const sleep5 = opts.sleep ?? defaultSleep;
|
|
490
|
+
const random = opts.random ?? Math.random;
|
|
491
|
+
const isTransient = opts.isTransientError ?? isTransientHttpError;
|
|
492
|
+
const maxInterval = opts.maxIntervalMs ?? 15e3;
|
|
493
|
+
const minInterval = opts.minIntervalMs ?? 500;
|
|
494
|
+
const factor = opts.backoffFactor ?? 1.5;
|
|
495
|
+
const jitter = opts.jitter ?? true;
|
|
496
|
+
const deadline = now() + opts.timeoutMs;
|
|
497
|
+
let interval = opts.intervalMs ?? 3e3;
|
|
498
|
+
let attempt = 0;
|
|
499
|
+
let last;
|
|
500
|
+
let lastError;
|
|
501
|
+
for (; ; ) {
|
|
502
|
+
attempt++;
|
|
503
|
+
let retryAfterMs;
|
|
504
|
+
try {
|
|
505
|
+
last = await opts.fetchStatus({ remainingMs: Math.max(1, deadline - now()), attempt });
|
|
506
|
+
lastError = void 0;
|
|
507
|
+
opts.onPoll?.(last, attempt);
|
|
508
|
+
if (opts.isTerminal(last)) return last;
|
|
509
|
+
} catch (error) {
|
|
510
|
+
if (!isTransient(error)) throw error;
|
|
511
|
+
lastError = error;
|
|
512
|
+
if (error instanceof HttpError) retryAfterMs = error.retryAfterMs;
|
|
513
|
+
}
|
|
514
|
+
const remaining = deadline - now();
|
|
515
|
+
if (remaining <= 0) {
|
|
516
|
+
const hint = opts.resumeHint ? ` ${opts.resumeHint}` : "";
|
|
517
|
+
const cause = lastError instanceof Error ? ` Last error: ${lastError.message}.` : "";
|
|
518
|
+
throw new PollTimeoutError(
|
|
519
|
+
`Timed out after ${opts.timeoutMs}ms waiting for job.${cause}${hint}`,
|
|
520
|
+
last
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
let delay = retryAfterMs ?? interval;
|
|
524
|
+
if (jitter && retryAfterMs === void 0) delay *= 0.8 + random() * 0.4;
|
|
525
|
+
await sleep5(Math.min(Math.max(delay, minInterval), remaining));
|
|
526
|
+
interval = Math.min(interval * factor, maxInterval);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// src/providers/atlascloud/client.ts
|
|
531
|
+
init_errors();
|
|
532
|
+
|
|
533
|
+
// src/core/fetch-json.ts
|
|
534
|
+
init_errors();
|
|
535
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
536
|
+
var SNIPPET_MAX = 500;
|
|
537
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
538
|
+
if (!value) return void 0;
|
|
539
|
+
const trimmed = value.trim();
|
|
540
|
+
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
|
|
541
|
+
const date = Date.parse(trimmed);
|
|
542
|
+
if (Number.isNaN(date)) return void 0;
|
|
543
|
+
return Math.max(0, date - now);
|
|
544
|
+
}
|
|
545
|
+
async function httpErrorFromResponse(response, url) {
|
|
546
|
+
let snippet = "";
|
|
547
|
+
try {
|
|
548
|
+
const text = await response.text();
|
|
549
|
+
snippet = text.slice(0, SNIPPET_MAX);
|
|
550
|
+
} catch {
|
|
551
|
+
snippet = "(could not read body)";
|
|
552
|
+
}
|
|
553
|
+
return new HttpError(
|
|
554
|
+
response.status,
|
|
555
|
+
snippet,
|
|
556
|
+
url,
|
|
557
|
+
parseRetryAfter(response.headers.get("retry-after"))
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
async function fetchJson(opts) {
|
|
561
|
+
const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
562
|
+
const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
|
|
563
|
+
const init = {
|
|
564
|
+
method,
|
|
565
|
+
headers: {
|
|
566
|
+
"Content-Type": "application/json",
|
|
567
|
+
...headers2
|
|
568
|
+
},
|
|
569
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
570
|
+
};
|
|
571
|
+
if (body !== void 0) {
|
|
572
|
+
init.body = JSON.stringify(body);
|
|
573
|
+
}
|
|
574
|
+
let response;
|
|
575
|
+
try {
|
|
576
|
+
response = await doFetch(url, init);
|
|
577
|
+
} catch (cause) {
|
|
578
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
579
|
+
throw new HttpError(0, msg, url);
|
|
580
|
+
}
|
|
581
|
+
if (!response.ok) {
|
|
582
|
+
throw await httpErrorFromResponse(response, url);
|
|
583
|
+
}
|
|
584
|
+
let text;
|
|
585
|
+
try {
|
|
586
|
+
text = await response.text();
|
|
587
|
+
} catch (cause) {
|
|
588
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
589
|
+
throw new HttpError(0, `Failed to read response body: ${msg}`, url);
|
|
590
|
+
}
|
|
591
|
+
if (text.trim() === "") return void 0;
|
|
592
|
+
try {
|
|
593
|
+
return JSON.parse(text);
|
|
594
|
+
} catch {
|
|
595
|
+
throw new HttpError(
|
|
596
|
+
response.status,
|
|
597
|
+
`Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
|
|
598
|
+
url
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// src/providers/atlascloud/prediction-output.ts
|
|
604
|
+
var SUCCESS_STATUSES = /* @__PURE__ */ new Set(["completed", "succeeded", "success"]);
|
|
605
|
+
var FAILURE_STATUSES = /* @__PURE__ */ new Set(["failed", "error", "canceled", "cancelled", "timeout"]);
|
|
606
|
+
function norm(status) {
|
|
607
|
+
return typeof status === "string" ? status.trim().toLowerCase() : "";
|
|
608
|
+
}
|
|
609
|
+
function isSuccessStatus(status) {
|
|
610
|
+
return SUCCESS_STATUSES.has(norm(status));
|
|
611
|
+
}
|
|
612
|
+
function isFailureStatus(status) {
|
|
613
|
+
return FAILURE_STATUSES.has(norm(status));
|
|
614
|
+
}
|
|
615
|
+
function isTerminalStatus(status) {
|
|
616
|
+
return isSuccessStatus(status) || isFailureStatus(status);
|
|
617
|
+
}
|
|
618
|
+
function predictionErrorMessage(error) {
|
|
619
|
+
if (error === null || error === void 0) return "";
|
|
620
|
+
if (typeof error === "string") return error.trim();
|
|
621
|
+
if (typeof error === "number" || typeof error === "boolean") return String(error);
|
|
622
|
+
if (typeof error === "object") {
|
|
623
|
+
const obj = error;
|
|
624
|
+
for (const key of ["message", "msg", "error", "detail", "reason"]) {
|
|
625
|
+
const v = obj[key];
|
|
626
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
return JSON.stringify(error);
|
|
630
|
+
} catch {
|
|
631
|
+
return "";
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return "";
|
|
635
|
+
}
|
|
636
|
+
function collectOutputs(prediction2) {
|
|
637
|
+
const raw = prediction2.outputs ?? prediction2.output;
|
|
638
|
+
const list = Array.isArray(raw) ? raw : raw ? [raw] : [];
|
|
639
|
+
const out = [];
|
|
640
|
+
for (const item of list) {
|
|
641
|
+
if (typeof item === "string") {
|
|
642
|
+
if (item.trim()) out.push(item);
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (item && typeof item === "object") {
|
|
646
|
+
const obj = item;
|
|
647
|
+
const url = ["url", "download_url", "audio_url", "video_url"].map((k) => obj[k]).find((v) => typeof v === "string" && v.length > 0);
|
|
648
|
+
out.push(url ?? JSON.stringify(item));
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return out;
|
|
652
|
+
}
|
|
653
|
+
function isMediaOutput(value) {
|
|
654
|
+
const s = value.trim();
|
|
655
|
+
return /^https?:\/\//i.test(s) || /^data:[^,]*,/i.test(s);
|
|
656
|
+
}
|
|
657
|
+
var EXT_CATEGORY = {
|
|
658
|
+
png: "image",
|
|
659
|
+
jpg: "image",
|
|
660
|
+
jpeg: "image",
|
|
661
|
+
webp: "image",
|
|
662
|
+
gif: "image",
|
|
663
|
+
bmp: "image",
|
|
664
|
+
avif: "image",
|
|
665
|
+
mp4: "video",
|
|
666
|
+
webm: "video",
|
|
667
|
+
mov: "video",
|
|
668
|
+
mkv: "video",
|
|
669
|
+
avi: "video",
|
|
670
|
+
mp3: "audio",
|
|
671
|
+
wav: "audio",
|
|
672
|
+
aac: "audio",
|
|
673
|
+
m4a: "audio",
|
|
674
|
+
flac: "audio",
|
|
675
|
+
ogg: "audio",
|
|
676
|
+
opus: "audio",
|
|
677
|
+
pcm: "audio",
|
|
678
|
+
glb: "3d",
|
|
679
|
+
gltf: "3d",
|
|
680
|
+
obj: "3d",
|
|
681
|
+
fbx: "3d",
|
|
682
|
+
stl: "3d",
|
|
683
|
+
ply: "3d",
|
|
684
|
+
usdz: "3d",
|
|
685
|
+
usdc: "3d",
|
|
686
|
+
usd: "3d",
|
|
687
|
+
zip: "3d"
|
|
688
|
+
};
|
|
689
|
+
var MIME_EXT = {
|
|
690
|
+
"image/png": "png",
|
|
691
|
+
"image/jpeg": "jpg",
|
|
692
|
+
"image/webp": "webp",
|
|
693
|
+
"image/gif": "gif",
|
|
694
|
+
"video/mp4": "mp4",
|
|
695
|
+
"video/webm": "webm",
|
|
696
|
+
"audio/mpeg": "mp3",
|
|
697
|
+
"audio/mp3": "mp3",
|
|
698
|
+
"audio/wav": "wav",
|
|
699
|
+
"audio/x-wav": "wav",
|
|
700
|
+
"audio/ogg": "ogg",
|
|
701
|
+
"audio/flac": "flac",
|
|
702
|
+
"model/gltf-binary": "glb",
|
|
703
|
+
"video/quicktime": "mov",
|
|
704
|
+
"audio/aac": "aac",
|
|
705
|
+
"audio/mp4": "m4a",
|
|
706
|
+
"application/zip": "zip"
|
|
707
|
+
};
|
|
708
|
+
function extensionForContentType(contentType) {
|
|
709
|
+
const mime = contentType?.split(";")[0]?.trim().toLowerCase();
|
|
710
|
+
return mime ? MIME_EXT[mime] : void 0;
|
|
711
|
+
}
|
|
712
|
+
function outputExtension(value) {
|
|
713
|
+
const s = value.trim();
|
|
714
|
+
const dataMatch = /^data:([^;,]+)/i.exec(s);
|
|
715
|
+
if (dataMatch?.[1]) return MIME_EXT[dataMatch[1].toLowerCase()];
|
|
716
|
+
let pathname;
|
|
717
|
+
try {
|
|
718
|
+
pathname = new URL(s).pathname;
|
|
719
|
+
} catch {
|
|
720
|
+
pathname = s.split(/[?#]/)[0] ?? s;
|
|
721
|
+
}
|
|
722
|
+
const m = /\.([a-z0-9]{2,5})$/i.exec(pathname);
|
|
723
|
+
if (!m?.[1]) return void 0;
|
|
724
|
+
const ext = m[1].toLowerCase();
|
|
725
|
+
return ext === "jpeg" ? "jpg" : ext;
|
|
726
|
+
}
|
|
727
|
+
function outputCategory(value) {
|
|
728
|
+
const ext = outputExtension(value);
|
|
729
|
+
return ext ? EXT_CATEGORY[ext] ?? "unknown" : "unknown";
|
|
730
|
+
}
|
|
731
|
+
function defaultExtensionFor(category) {
|
|
732
|
+
switch (category) {
|
|
733
|
+
case "image":
|
|
734
|
+
return "png";
|
|
735
|
+
case "video":
|
|
736
|
+
return "mp4";
|
|
737
|
+
case "audio":
|
|
738
|
+
return "mp3";
|
|
739
|
+
case "3d":
|
|
740
|
+
return "glb";
|
|
741
|
+
case "text":
|
|
742
|
+
return "txt";
|
|
743
|
+
default:
|
|
744
|
+
return "bin";
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
function classifyOutputs(prediction2) {
|
|
748
|
+
const outputs = collectOutputs(prediction2);
|
|
749
|
+
if (outputs.length === 0) return { mode: "empty" };
|
|
750
|
+
const urls = outputs.filter(isMediaOutput);
|
|
751
|
+
const texts = outputs.filter((o) => !isMediaOutput(o));
|
|
752
|
+
if (urls.length === 0) return { mode: "text", texts };
|
|
753
|
+
return { mode: "media", urls, texts };
|
|
754
|
+
}
|
|
755
|
+
function hasNsfwFlag(prediction2) {
|
|
756
|
+
return Array.isArray(prediction2.has_nsfw_contents) && prediction2.has_nsfw_contents.some(Boolean);
|
|
757
|
+
}
|
|
758
|
+
function outputFileName(predictionId, index, ext) {
|
|
759
|
+
const prefix = predictionId.replace(/[^a-zA-Z0-9_-]/g, "").slice(0, 8) || "output";
|
|
760
|
+
return `atlas-${prefix}-${String(index + 1).padStart(2, "0")}.${ext}`;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/providers/atlascloud/client.ts
|
|
764
|
+
var ATLAS_PROVIDER = "atlascloud";
|
|
765
|
+
var DEFAULT_UPLOAD_TIMEOUT_MS = 3e5;
|
|
766
|
+
var CATALOG_TIMEOUT_MS = 15e3;
|
|
767
|
+
var PREDICTION_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
|
768
|
+
var AtlasPredictionFailedError = class extends ProviderError {
|
|
769
|
+
constructor(prediction2) {
|
|
770
|
+
const reason = predictionErrorMessage(prediction2.error) || `status "${prediction2.status}"`;
|
|
771
|
+
super(`Atlas Cloud prediction ${prediction2.id} failed: ${reason}`, ATLAS_PROVIDER);
|
|
772
|
+
this.prediction = prediction2;
|
|
773
|
+
this.name = "AtlasPredictionFailedError";
|
|
774
|
+
}
|
|
775
|
+
prediction;
|
|
776
|
+
};
|
|
777
|
+
function assertValidPredictionId(id) {
|
|
778
|
+
if (!PREDICTION_ID_RE.test(id)) {
|
|
779
|
+
throw new ValidationError(`Invalid Atlas Cloud prediction id: "${id}"`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function assertPredictionSucceeded(prediction2) {
|
|
783
|
+
if (isFailureStatus(prediction2.status)) throw new AtlasPredictionFailedError(prediction2);
|
|
784
|
+
if (!isSuccessStatus(prediction2.status)) {
|
|
785
|
+
throw new ProviderError(
|
|
786
|
+
`Atlas Cloud prediction ${prediction2.id} is not finished (status "${prediction2.status}")`,
|
|
787
|
+
ATLAS_PROVIDER
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
function terminalPredictionFromError(error, id) {
|
|
792
|
+
if (!(error instanceof HttpError) || error.status < 500) return null;
|
|
793
|
+
const snippet = error.snippet;
|
|
794
|
+
try {
|
|
795
|
+
const parsed = JSON.parse(snippet);
|
|
796
|
+
const data = parsed?.data;
|
|
797
|
+
if (data && typeof data === "object" && isTerminalStatus(data.status)) {
|
|
798
|
+
return { ...data, id: data.id || id };
|
|
799
|
+
}
|
|
800
|
+
return null;
|
|
801
|
+
} catch {
|
|
802
|
+
}
|
|
803
|
+
const dataAt = snippet.search(/"data"\s*:\s*\{/);
|
|
804
|
+
if (dataAt === -1) return null;
|
|
805
|
+
const inData = snippet.slice(dataAt);
|
|
806
|
+
const status = /"status"\s*:\s*"([^"]+)"/.exec(inData)?.[1];
|
|
807
|
+
if (status?.toLowerCase() !== "failed") return null;
|
|
808
|
+
const err2 = /"error"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(inData)?.[1];
|
|
809
|
+
return { id, status, error: err2 ?? snippet };
|
|
810
|
+
}
|
|
811
|
+
function envelopeData(env, what) {
|
|
812
|
+
if (!env || typeof env !== "object") {
|
|
813
|
+
throw new ProviderError(`Atlas Cloud ${what}: empty response`, ATLAS_PROVIDER);
|
|
814
|
+
}
|
|
815
|
+
if (env.code !== void 0 && env.code !== 200 && env.code !== 0 && env.data === void 0) {
|
|
816
|
+
throw new ProviderError(
|
|
817
|
+
`Atlas Cloud ${what} failed (code ${env.code}): ${env.msg || env.message || "unknown error"}`,
|
|
818
|
+
ATLAS_PROVIDER
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
if (env.data === void 0 || env.data === null) {
|
|
822
|
+
throw new ProviderError(`Atlas Cloud ${what}: response has no data`, ATLAS_PROVIDER);
|
|
823
|
+
}
|
|
824
|
+
return env.data;
|
|
825
|
+
}
|
|
826
|
+
var AtlasCloudClient = class {
|
|
827
|
+
baseUrl;
|
|
828
|
+
apiKey;
|
|
829
|
+
fetchImpl;
|
|
830
|
+
timeoutMs;
|
|
831
|
+
uploadTimeoutMs;
|
|
832
|
+
catalog;
|
|
833
|
+
constructor(opts) {
|
|
834
|
+
if (!opts.apiKey?.trim()) {
|
|
835
|
+
throw new ConfigError(
|
|
836
|
+
"ATLASCLOUD_API_KEY is not set. Get one at https://www.atlascloud.ai/console/api-keys"
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
this.apiKey = opts.apiKey.trim();
|
|
840
|
+
this.baseUrl = (opts.baseUrl?.trim() || DEFAULT_ATLAS_BASE_URL).replace(/\/+$/, "");
|
|
841
|
+
this.fetchImpl = opts.fetch;
|
|
842
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
843
|
+
this.uploadTimeoutMs = opts.uploadTimeoutMs ?? DEFAULT_UPLOAD_TIMEOUT_MS;
|
|
844
|
+
}
|
|
845
|
+
doFetch(input, init) {
|
|
846
|
+
const f = this.fetchImpl;
|
|
847
|
+
return f ? f(input, init) : globalThis.fetch(input, init);
|
|
848
|
+
}
|
|
849
|
+
/** Catalog and schema lookups are advisory; fail fast so a slow catalog cannot stall a command. */
|
|
850
|
+
catalogTimeoutMs() {
|
|
851
|
+
return Math.min(this.timeoutMs, CATALOG_TIMEOUT_MS);
|
|
852
|
+
}
|
|
853
|
+
authHeaders() {
|
|
854
|
+
return { Authorization: `Bearer ${this.apiKey}`, Accept: "application/json" };
|
|
855
|
+
}
|
|
856
|
+
/** Submit a generation job. `body` holds `model` plus the model's own fields, flattened. */
|
|
857
|
+
async submit(kind, body) {
|
|
858
|
+
const path52 = SUBMIT_PATHS[kind];
|
|
859
|
+
if (!path52) throw new ValidationError(`Unknown Atlas Cloud media type: ${String(kind)}`);
|
|
860
|
+
if (typeof body.model !== "string" || !body.model.trim()) {
|
|
861
|
+
throw new ValidationError("Atlas Cloud request body needs a non-empty `model`");
|
|
862
|
+
}
|
|
863
|
+
const env = await fetchJson({
|
|
864
|
+
url: `${this.baseUrl}${path52}`,
|
|
865
|
+
method: "POST",
|
|
866
|
+
headers: this.authHeaders(),
|
|
867
|
+
body,
|
|
868
|
+
timeoutMs: this.timeoutMs,
|
|
869
|
+
fetch: this.fetchImpl
|
|
870
|
+
});
|
|
871
|
+
const data = envelopeData(env, "submit");
|
|
872
|
+
if (typeof data.id !== "string" || !data.id) {
|
|
873
|
+
throw new ProviderError("Atlas Cloud submit returned no prediction id", ATLAS_PROVIDER);
|
|
874
|
+
}
|
|
875
|
+
return data;
|
|
876
|
+
}
|
|
877
|
+
/** Fetch a prediction once. A failed task reported as HTTP 5xx is returned, not thrown. */
|
|
878
|
+
async getPrediction(id, timeoutMs) {
|
|
879
|
+
assertValidPredictionId(id);
|
|
880
|
+
try {
|
|
881
|
+
const env = await fetchJson({
|
|
882
|
+
url: `${this.baseUrl}/model/prediction/${id}`,
|
|
883
|
+
headers: this.authHeaders(),
|
|
884
|
+
timeoutMs: timeoutMs ?? this.timeoutMs,
|
|
885
|
+
fetch: this.fetchImpl
|
|
886
|
+
});
|
|
887
|
+
const data = envelopeData(env, "prediction");
|
|
888
|
+
return { ...data, id: data.id || id };
|
|
889
|
+
} catch (error) {
|
|
890
|
+
const terminal = terminalPredictionFromError(error, id);
|
|
891
|
+
if (terminal) return terminal;
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Poll until the prediction is terminal (success or failure) or the deadline
|
|
897
|
+
* passes. Network errors, 429 (honouring Retry-After) and 5xx are retried.
|
|
898
|
+
* Throws PollTimeoutError on timeout; returns the terminal prediction otherwise.
|
|
899
|
+
*/
|
|
900
|
+
async waitForPrediction(id, opts) {
|
|
901
|
+
assertValidPredictionId(id);
|
|
902
|
+
return pollUntil({
|
|
903
|
+
// Cap each request at the time left so one hung poll cannot overrun the deadline.
|
|
904
|
+
fetchStatus: ({ remainingMs }) => this.getPrediction(id, Math.ceil(Math.max(1e3, Math.min(this.timeoutMs, remainingMs)))),
|
|
905
|
+
isTerminal: (p) => isTerminalStatus(p.status),
|
|
906
|
+
timeoutMs: opts.timeoutMs,
|
|
907
|
+
intervalMs: opts.intervalMs ?? 3e3,
|
|
908
|
+
maxIntervalMs: opts.maxIntervalMs ?? 15e3,
|
|
909
|
+
isTransientError: isTransientHttpError,
|
|
910
|
+
onPoll: opts.onPoll,
|
|
911
|
+
resumeHint: opts.resumeHint,
|
|
912
|
+
sleep: opts.sleep,
|
|
913
|
+
now: opts.now
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* Upload bytes and return a temporary URL usable as model input.
|
|
918
|
+
* The filename extension matters: Atlas and downstream models classify by it.
|
|
919
|
+
*/
|
|
920
|
+
async uploadMedia(file, filename, mimeType) {
|
|
921
|
+
if (!filename.trim()) throw new ValidationError("uploadMedia needs a filename");
|
|
922
|
+
const blob = file instanceof Blob ? file : new Blob([file], { type: mimeType ?? "application/octet-stream" });
|
|
923
|
+
const form = new FormData();
|
|
924
|
+
form.append("file", blob, filename);
|
|
925
|
+
const url = `${this.baseUrl}/model/uploadMedia`;
|
|
926
|
+
let response;
|
|
927
|
+
try {
|
|
928
|
+
response = await this.doFetch(url, {
|
|
929
|
+
method: "POST",
|
|
930
|
+
headers: this.authHeaders(),
|
|
931
|
+
body: form,
|
|
932
|
+
signal: AbortSignal.timeout(this.uploadTimeoutMs)
|
|
933
|
+
});
|
|
934
|
+
} catch (cause) {
|
|
935
|
+
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
936
|
+
}
|
|
937
|
+
if (!response.ok) throw await httpErrorFromResponse(response, url);
|
|
938
|
+
const json = await response.json();
|
|
939
|
+
const data = json.data ?? {};
|
|
940
|
+
const fileUrl = [data.download_url, data.url, json.url].find(
|
|
941
|
+
(v) => typeof v === "string" && /^https?:\/\//i.test(v)
|
|
942
|
+
);
|
|
943
|
+
if (!fileUrl) {
|
|
944
|
+
throw new ProviderError(
|
|
945
|
+
`Atlas Cloud upload returned no URL (code ${json.code ?? "?"}: ${json.msg ?? json.message ?? "no message"})`,
|
|
946
|
+
ATLAS_PROVIDER
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
url: fileUrl,
|
|
951
|
+
filename: typeof data.filename === "string" ? data.filename : void 0,
|
|
952
|
+
size: typeof data.size === "number" ? data.size : void 0,
|
|
953
|
+
type: typeof data.type === "string" ? data.type : void 0
|
|
954
|
+
};
|
|
955
|
+
}
|
|
956
|
+
/** Public model catalog, fetched once per client. */
|
|
957
|
+
listModels() {
|
|
958
|
+
this.catalog ??= fetchJson({
|
|
959
|
+
url: `${this.baseUrl}/models`,
|
|
960
|
+
timeoutMs: this.catalogTimeoutMs(),
|
|
961
|
+
fetch: this.fetchImpl
|
|
962
|
+
}).then((env) => {
|
|
963
|
+
const data = envelopeData(env, "model catalog");
|
|
964
|
+
if (!Array.isArray(data)) {
|
|
965
|
+
throw new ProviderError("Atlas Cloud model catalog is not a list", ATLAS_PROVIDER);
|
|
966
|
+
}
|
|
967
|
+
return data.filter((m) => m && typeof m.model === "string");
|
|
968
|
+
}).catch((error) => {
|
|
969
|
+
this.catalog = void 0;
|
|
970
|
+
throw error;
|
|
971
|
+
});
|
|
972
|
+
return this.catalog;
|
|
973
|
+
}
|
|
974
|
+
/** Exact (case-sensitive) catalog lookup; model ids are never rewritten. */
|
|
975
|
+
async findModel(model) {
|
|
976
|
+
return (await this.listModels()).find((m) => m.model === model);
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Load the model's published request schema and submit endpoint.
|
|
980
|
+
* Returns undefined when the model is not in the public catalog or publishes no schema.
|
|
981
|
+
*/
|
|
982
|
+
async getModelSpec(model) {
|
|
983
|
+
const entry = await this.findModel(model);
|
|
984
|
+
if (!entry?.schema || !/^https:\/\//i.test(entry.schema)) return void 0;
|
|
985
|
+
const doc = await fetchJson({
|
|
986
|
+
url: entry.schema,
|
|
987
|
+
timeoutMs: this.catalogTimeoutMs(),
|
|
988
|
+
fetch: this.fetchImpl
|
|
989
|
+
});
|
|
990
|
+
return parseModelSpec(model, doc);
|
|
991
|
+
}
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
// src/providers/atlascloud/upload-mime.ts
|
|
995
|
+
var UPLOAD_MIME_TYPES = {
|
|
996
|
+
png: "image/png",
|
|
997
|
+
jpg: "image/jpeg",
|
|
998
|
+
jpeg: "image/jpeg",
|
|
999
|
+
webp: "image/webp",
|
|
1000
|
+
gif: "image/gif",
|
|
1001
|
+
bmp: "image/bmp",
|
|
1002
|
+
avif: "image/avif",
|
|
1003
|
+
heic: "image/heic",
|
|
1004
|
+
mp4: "video/mp4",
|
|
1005
|
+
mov: "video/quicktime",
|
|
1006
|
+
webm: "video/webm",
|
|
1007
|
+
mkv: "video/x-matroska",
|
|
1008
|
+
avi: "video/x-msvideo",
|
|
1009
|
+
mp3: "audio/mpeg",
|
|
1010
|
+
wav: "audio/wav",
|
|
1011
|
+
m4a: "audio/mp4",
|
|
1012
|
+
aac: "audio/aac",
|
|
1013
|
+
ogg: "audio/ogg",
|
|
1014
|
+
flac: "audio/flac",
|
|
1015
|
+
opus: "audio/opus"
|
|
1016
|
+
};
|
|
1017
|
+
var UPLOAD_EXTENSIONS = Object.keys(UPLOAD_MIME_TYPES);
|
|
1018
|
+
function uploadMimeTypeFor(filename) {
|
|
1019
|
+
const ext = /\.([a-z0-9]+)$/i.exec(filename)?.[1]?.toLowerCase() ?? "";
|
|
1020
|
+
return UPLOAD_MIME_TYPES[ext];
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/providers/atlascloud/commands/env.ts
|
|
1024
|
+
var ATLAS_KEY_URL = "https://www.atlascloud.ai/console/api-keys";
|
|
1025
|
+
var MODEL_ENV = {
|
|
1026
|
+
image: "ATLASCLOUD_IMAGE_MODEL",
|
|
1027
|
+
imageEdit: "ATLASCLOUD_IMAGE_EDIT_MODEL",
|
|
1028
|
+
video: "ATLASCLOUD_VIDEO_MODEL",
|
|
1029
|
+
imageToVideo: "ATLASCLOUD_IMAGE_TO_VIDEO_MODEL",
|
|
1030
|
+
tts: "ATLASCLOUD_TTS_MODEL",
|
|
1031
|
+
stt: "ATLASCLOUD_STT_MODEL",
|
|
1032
|
+
textTo3d: "ATLASCLOUD_TEXT_TO_3D_MODEL",
|
|
1033
|
+
imageTo3d: "ATLASCLOUD_IMAGE_TO_3D_MODEL"
|
|
1034
|
+
};
|
|
1035
|
+
function atlasDefaultModel(key) {
|
|
1036
|
+
return resolveKey(MODEL_ENV[key])?.trim() || ATLAS_DEFAULT_MODELS[key];
|
|
1037
|
+
}
|
|
1038
|
+
function createAtlasClientFromEnv() {
|
|
1039
|
+
const apiKey = resolveKey("ATLASCLOUD_API_KEY")?.trim();
|
|
1040
|
+
if (!apiKey) {
|
|
1041
|
+
throw new ConfigError(`ATLASCLOUD_API_KEY is not set. Get one at ${ATLAS_KEY_URL}`);
|
|
1042
|
+
}
|
|
1043
|
+
return new AtlasCloudClient({ apiKey, baseUrl: resolveKey("ATLASCLOUD_BASE_URL") });
|
|
1044
|
+
}
|
|
1045
|
+
async function resolveMediaRef(client, ref, logger) {
|
|
1046
|
+
const trimmed = ref.trim();
|
|
1047
|
+
if (/^https?:\/\//i.test(trimmed) || /^data:/i.test(trimmed)) return trimmed;
|
|
1048
|
+
const abs = path2.resolve(trimmed);
|
|
1049
|
+
const name = path2.basename(abs);
|
|
1050
|
+
const mime = uploadMimeTypeFor(name);
|
|
1051
|
+
if (!mime) {
|
|
1052
|
+
throw new ValidationError(
|
|
1053
|
+
`Refusing to upload ${trimmed}: not a supported media file. Allowed extensions: ${UPLOAD_EXTENSIONS.join(", ")}`
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
let stat;
|
|
1057
|
+
try {
|
|
1058
|
+
stat = await fs2.stat(abs);
|
|
1059
|
+
} catch {
|
|
1060
|
+
throw new ValidationError(`Reference file not found: ${trimmed}`);
|
|
1061
|
+
}
|
|
1062
|
+
if (!stat.isFile()) throw new ValidationError(`Reference is not a file: ${trimmed}`);
|
|
1063
|
+
if (stat.size === 0) throw new ValidationError(`Reference file is empty: ${trimmed}`);
|
|
1064
|
+
const maxBytes = maxUploadBytes();
|
|
1065
|
+
if (stat.size > maxBytes) {
|
|
1066
|
+
throw new ValidationError(
|
|
1067
|
+
`${trimmed} is ${formatMb(stat.size)}, above the ${formatMb(maxBytes)} upload limit (set ${MAX_UPLOAD_ENV} to change it)`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
logger.info(`Uploading ${name} (${Math.ceil(stat.size / 1024)} KB) to Atlas Cloud...`);
|
|
1071
|
+
const blob = await openAsBlob(abs, { type: mime });
|
|
1072
|
+
const uploaded = await client.uploadMedia(blob, name, mime);
|
|
1073
|
+
logger.debug(`Uploaded ${name} -> ${redactUrl(uploaded.url)}`);
|
|
1074
|
+
return uploaded.url;
|
|
1075
|
+
}
|
|
1076
|
+
var MAX_UPLOAD_ENV = "ATLASCLOUD_MAX_UPLOAD_MB";
|
|
1077
|
+
var DEFAULT_MAX_UPLOAD_MB = 200;
|
|
1078
|
+
function maxUploadBytes() {
|
|
1079
|
+
const raw = resolveKey(MAX_UPLOAD_ENV)?.trim();
|
|
1080
|
+
if (!raw) return DEFAULT_MAX_UPLOAD_MB * 1024 * 1024;
|
|
1081
|
+
const mb = Number(raw);
|
|
1082
|
+
if (!Number.isFinite(mb) || mb <= 0) {
|
|
1083
|
+
throw new ConfigError(`${MAX_UPLOAD_ENV} must be a positive number of megabytes, got "${raw}"`);
|
|
1084
|
+
}
|
|
1085
|
+
return Math.floor(mb * 1024 * 1024);
|
|
1086
|
+
}
|
|
1087
|
+
function formatMb(bytes) {
|
|
1088
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1089
|
+
}
|
|
1090
|
+
function createAtlasLogger(opts) {
|
|
1091
|
+
const verbose = opts.verbose ?? false;
|
|
1092
|
+
const base = createLogger({ verbose });
|
|
1093
|
+
if (!opts.stdoutReserved) return base;
|
|
1094
|
+
return {
|
|
1095
|
+
...base,
|
|
1096
|
+
info: (msg) => console.error(` ${msg}`),
|
|
1097
|
+
success: (msg) => console.error(`\u2713 ${msg}`),
|
|
1098
|
+
debug: (msg) => {
|
|
1099
|
+
if (verbose) console.error(` [debug] ${msg}`);
|
|
1100
|
+
},
|
|
1101
|
+
header: (msg) => console.error(msg)
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
async function resolveMediaRefs(client, refs, logger) {
|
|
1105
|
+
const urls = [];
|
|
1106
|
+
for (const ref of refs) urls.push(await resolveMediaRef(client, ref, logger));
|
|
1107
|
+
return urls;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// src/providers/atlascloud/commands/prediction-runner.ts
|
|
1111
|
+
init_errors();
|
|
1112
|
+
import fs8 from "fs";
|
|
1113
|
+
import path7 from "path";
|
|
1114
|
+
|
|
1115
|
+
// src/core/http-client.ts
|
|
1116
|
+
init_errors();
|
|
1117
|
+
import { once } from "events";
|
|
1118
|
+
import fs3 from "fs";
|
|
1119
|
+
import path3 from "path";
|
|
1120
|
+
import { pipeline } from "stream/promises";
|
|
1121
|
+
async function httpJson(opts) {
|
|
1122
|
+
return fetchJson(opts);
|
|
1123
|
+
}
|
|
1124
|
+
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers2) {
|
|
1125
|
+
fs3.mkdirSync(path3.dirname(dest), { recursive: true });
|
|
1126
|
+
let response;
|
|
1127
|
+
try {
|
|
1128
|
+
response = await globalThis.fetch(url, {
|
|
1129
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
1130
|
+
...headers2 ? { headers: headers2 } : {}
|
|
1131
|
+
});
|
|
1132
|
+
} catch (cause) {
|
|
1133
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
1134
|
+
throw new HttpError(0, msg, url);
|
|
1135
|
+
}
|
|
1136
|
+
if (!response.ok) {
|
|
1137
|
+
throw new HttpError(response.status, "(download failed)", url);
|
|
1138
|
+
}
|
|
1139
|
+
if (!response.body) {
|
|
1140
|
+
throw new HttpError(0, "Response body is null", url);
|
|
1141
|
+
}
|
|
1142
|
+
const fileStream = fs3.createWriteStream(dest);
|
|
1143
|
+
try {
|
|
1144
|
+
await pipeline(response.body, fileStream);
|
|
1145
|
+
} catch (err2) {
|
|
1146
|
+
if (!fileStream.closed) await once(fileStream, "close");
|
|
1147
|
+
throw err2;
|
|
1148
|
+
}
|
|
1149
|
+
return response.headers.get("content-type") ?? void 0;
|
|
1150
|
+
}
|
|
1151
|
+
async function fetchBytes(urlOrDataUri) {
|
|
1152
|
+
if (urlOrDataUri.startsWith("data:")) {
|
|
1153
|
+
const commaIdx = urlOrDataUri.indexOf(",");
|
|
1154
|
+
if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
|
|
1155
|
+
const encoded = urlOrDataUri.slice(commaIdx + 1);
|
|
1156
|
+
return Buffer.from(encoded, "base64");
|
|
1157
|
+
}
|
|
1158
|
+
let response;
|
|
1159
|
+
try {
|
|
1160
|
+
response = await globalThis.fetch(urlOrDataUri, {
|
|
1161
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
1162
|
+
});
|
|
1163
|
+
} catch (cause) {
|
|
1164
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
1165
|
+
throw new HttpError(0, msg, urlOrDataUri);
|
|
1166
|
+
}
|
|
1167
|
+
if (!response.ok) {
|
|
1168
|
+
throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
|
|
1169
|
+
}
|
|
1170
|
+
return Buffer.from(await response.arrayBuffer());
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
// src/core/image-webp-finalize.ts
|
|
1174
|
+
init_errors();
|
|
1175
|
+
import { spawnSync } from "child_process";
|
|
1176
|
+
import fs5 from "fs";
|
|
1177
|
+
import path4 from "path";
|
|
1178
|
+
import { Option } from "commander";
|
|
1179
|
+
|
|
1180
|
+
// src/core/image-header.ts
|
|
1181
|
+
import fs4 from "fs";
|
|
1182
|
+
function sniffImageKind(buf) {
|
|
1183
|
+
if (buf.length >= 8 && buf.readUInt32BE(0) === 2303741511) return "png";
|
|
1184
|
+
if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpeg";
|
|
1185
|
+
if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP")
|
|
1186
|
+
return "webp";
|
|
1187
|
+
return void 0;
|
|
1188
|
+
}
|
|
1189
|
+
function readPng(buf) {
|
|
1190
|
+
if (buf.length < 33) return void 0;
|
|
1191
|
+
const width = buf.readUInt32BE(16);
|
|
1192
|
+
const height = buf.readUInt32BE(20);
|
|
1193
|
+
const colorType = buf[25];
|
|
1194
|
+
let hasAlpha = colorType === 4 || colorType === 6;
|
|
1195
|
+
let offset = 8;
|
|
1196
|
+
while (!hasAlpha && offset + 8 <= buf.length) {
|
|
1197
|
+
const len = buf.readUInt32BE(offset);
|
|
1198
|
+
const type = buf.toString("ascii", offset + 4, offset + 8);
|
|
1199
|
+
if (type === "tRNS") hasAlpha = true;
|
|
1200
|
+
if (type === "IDAT" || type === "IEND") break;
|
|
1201
|
+
offset += 12 + len;
|
|
1202
|
+
}
|
|
1203
|
+
return { kind: "png", width, height, hasAlpha };
|
|
1204
|
+
}
|
|
1205
|
+
function readJpeg(buf) {
|
|
1206
|
+
let offset = 2;
|
|
1207
|
+
while (offset + 9 < buf.length) {
|
|
1208
|
+
if (buf[offset] !== 255) {
|
|
1209
|
+
offset++;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
const marker = buf[offset + 1] ?? 0;
|
|
1213
|
+
if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
|
|
1214
|
+
return {
|
|
1215
|
+
kind: "jpeg",
|
|
1216
|
+
height: buf.readUInt16BE(offset + 5),
|
|
1217
|
+
width: buf.readUInt16BE(offset + 7),
|
|
1218
|
+
hasAlpha: false
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
1222
|
+
offset += 2;
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
offset += 2 + buf.readUInt16BE(offset + 2);
|
|
1226
|
+
}
|
|
1227
|
+
return void 0;
|
|
1228
|
+
}
|
|
1229
|
+
function readWebp(buf) {
|
|
1230
|
+
if (buf.length < 30) return void 0;
|
|
1231
|
+
const chunk = buf.toString("ascii", 12, 16);
|
|
1232
|
+
if (chunk === "VP8X") {
|
|
1233
|
+
const flags = buf[20] ?? 0;
|
|
1234
|
+
return {
|
|
1235
|
+
kind: "webp",
|
|
1236
|
+
width: 1 + buf.readUIntLE(24, 3),
|
|
1237
|
+
height: 1 + buf.readUIntLE(27, 3),
|
|
1238
|
+
hasAlpha: (flags & 16) !== 0
|
|
1239
|
+
};
|
|
1240
|
+
}
|
|
1241
|
+
if (chunk === "VP8L") {
|
|
1242
|
+
const bits = buf.readUInt32LE(21);
|
|
1243
|
+
return {
|
|
1244
|
+
kind: "webp",
|
|
1245
|
+
width: 1 + (bits & 16383),
|
|
1246
|
+
height: 1 + (bits >>> 14 & 16383),
|
|
1247
|
+
hasAlpha: (bits >>> 28 & 1) === 1
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
if (chunk === "VP8 ") {
|
|
1251
|
+
return {
|
|
1252
|
+
kind: "webp",
|
|
1253
|
+
width: buf.readUInt16LE(26) & 16383,
|
|
1254
|
+
height: buf.readUInt16LE(28) & 16383,
|
|
1255
|
+
hasAlpha: false
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
return void 0;
|
|
1259
|
+
}
|
|
1260
|
+
function readImageHeader(buf) {
|
|
1261
|
+
const kind = sniffImageKind(buf);
|
|
1262
|
+
if (kind === "png") return readPng(buf);
|
|
1263
|
+
if (kind === "jpeg") return readJpeg(buf);
|
|
1264
|
+
if (kind === "webp") return readWebp(buf);
|
|
1265
|
+
return void 0;
|
|
1266
|
+
}
|
|
1267
|
+
function readImageHeaderFromFile(file) {
|
|
1268
|
+
return readImageHeader(fs4.readFileSync(file));
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/core/image-webp-finalize.ts
|
|
1272
|
+
var CWEBP_ARGS = ["-q", "85", "-m", "6", "-metadata", "none"];
|
|
1273
|
+
var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"]);
|
|
1274
|
+
function imageFormatOption() {
|
|
1275
|
+
return new Option(
|
|
1276
|
+
"--image-format <mode>",
|
|
1277
|
+
"Final image format: webp (default, cwebp -q 85) | original (keep provider format/quality)"
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
function noWebpOption() {
|
|
1281
|
+
return new Option("--no-webp", "Keep the provider's original image format and quality");
|
|
1282
|
+
}
|
|
1283
|
+
function resolveImageFormatMode(opts, explicitProviderFormat = false) {
|
|
1284
|
+
if (opts.webp === false && opts.imageFormat === void 0) return "original";
|
|
1285
|
+
const raw = opts.imageFormat ?? process.env.MULTIX_IMAGE_FORMAT;
|
|
1286
|
+
if (raw !== void 0 && raw !== "") {
|
|
1287
|
+
const mode = raw.toLowerCase();
|
|
1288
|
+
if (mode !== "webp" && mode !== "original") {
|
|
1289
|
+
throw new ValidationError(`Invalid --image-format: ${raw}. Valid: webp, original`);
|
|
1290
|
+
}
|
|
1291
|
+
return mode;
|
|
1292
|
+
}
|
|
1293
|
+
if (explicitProviderFormat) return "original";
|
|
1294
|
+
const ext = opts.output ? path4.extname(opts.output).toLowerCase() : "";
|
|
1295
|
+
if (ext && ext !== ".webp" && IMAGE_EXTS.has(ext)) return "original";
|
|
1296
|
+
return "webp";
|
|
1297
|
+
}
|
|
1298
|
+
function hasCwebp() {
|
|
1299
|
+
const res = spawnSync("cwebp", ["-version"], { stdio: "ignore" });
|
|
1300
|
+
return !res.error && res.status === 0;
|
|
1301
|
+
}
|
|
1302
|
+
function webpTarget(file) {
|
|
1303
|
+
const ext = path4.extname(file);
|
|
1304
|
+
return ext.toLowerCase() === ".webp" ? file : `${file.slice(0, -ext.length || void 0)}.webp`;
|
|
1305
|
+
}
|
|
1306
|
+
function convertToWebp(file, logger, keepPath = false) {
|
|
1307
|
+
const src = readImageHeaderFromFile(file);
|
|
1308
|
+
if (!src) {
|
|
1309
|
+
logger?.warn(`Skipping WebP conversion (unrecognized image): ${file}`);
|
|
1310
|
+
return file;
|
|
1311
|
+
}
|
|
1312
|
+
if (src.kind === "webp") {
|
|
1313
|
+
const target2 = keepPath ? file : webpTarget(file);
|
|
1314
|
+
if (target2 !== file) fs5.renameSync(file, target2);
|
|
1315
|
+
return target2;
|
|
1316
|
+
}
|
|
1317
|
+
const target = keepPath ? file : webpTarget(file);
|
|
1318
|
+
const tmp = `${target}.tmp-${process.pid}.webp`;
|
|
1319
|
+
const res = spawnSync("cwebp", [...CWEBP_ARGS, file, "-o", tmp], { encoding: "utf8" });
|
|
1320
|
+
if (res.error || res.status !== 0) {
|
|
1321
|
+
fs5.rmSync(tmp, { force: true });
|
|
1322
|
+
logger?.warn(`cwebp failed, keeping original ${file}: ${res.stderr || res.error?.message}`);
|
|
1323
|
+
return file;
|
|
1324
|
+
}
|
|
1325
|
+
const out = fs5.existsSync(tmp) ? readImageHeaderFromFile(tmp) : void 0;
|
|
1326
|
+
const problem = !out ? "output is not a valid WebP" : out.width !== src.width || out.height !== src.height ? `size changed ${src.width}x${src.height} -> ${out.width}x${out.height}` : src.hasAlpha && !out.hasAlpha ? "transparency lost" : void 0;
|
|
1327
|
+
if (problem) {
|
|
1328
|
+
fs5.rmSync(tmp, { force: true });
|
|
1329
|
+
logger?.warn(`WebP verification failed (${problem}), keeping original ${file}`);
|
|
1330
|
+
return file;
|
|
1331
|
+
}
|
|
1332
|
+
if (target !== file) fs5.rmSync(file, { force: true });
|
|
1333
|
+
fs5.renameSync(tmp, target);
|
|
1334
|
+
logger?.debug(`WebP verified ${out?.width}x${out?.height} alpha=${out?.hasAlpha}: ${target}`);
|
|
1335
|
+
return target;
|
|
1336
|
+
}
|
|
1337
|
+
function finalizeGeneratedImages(files, opts) {
|
|
1338
|
+
const mode = resolveImageFormatMode(opts, opts.explicitProviderFormat);
|
|
1339
|
+
if (mode === "original" || files.length === 0) return files;
|
|
1340
|
+
if (!hasCwebp()) {
|
|
1341
|
+
opts.logger?.warn(
|
|
1342
|
+
"cwebp not found on PATH; keeping original image format (install libwebp, or pass --image-format original)"
|
|
1343
|
+
);
|
|
1344
|
+
return files;
|
|
1345
|
+
}
|
|
1346
|
+
const outputAbs = opts.output ? path4.resolve(opts.output) : void 0;
|
|
1347
|
+
const wroteOutputDirectly = outputAbs !== void 0 && files.some((f) => path4.resolve(f) === outputAbs);
|
|
1348
|
+
const finals = files.map(
|
|
1349
|
+
(f) => convertToWebp(f, opts.logger, outputAbs !== void 0 && path4.resolve(f) === outputAbs)
|
|
1350
|
+
);
|
|
1351
|
+
if (opts.output && outputAbs && !wroteOutputDirectly && finals[0] && fs5.existsSync(outputAbs)) {
|
|
1352
|
+
const outExt = path4.extname(outputAbs).toLowerCase();
|
|
1353
|
+
if ((outExt === ".webp" || outExt === "") && path4.extname(finals[0]).toLowerCase() === ".webp") {
|
|
1354
|
+
fs5.copyFileSync(finals[0], outputAbs);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
return finals;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// src/core/output-dir.ts
|
|
1361
|
+
import fs6 from "fs";
|
|
1362
|
+
import path5 from "path";
|
|
1363
|
+
var cachedOutputDir;
|
|
1364
|
+
function getOutputDir() {
|
|
1365
|
+
if (cachedOutputDir) return cachedOutputDir;
|
|
1366
|
+
const dir = process.env.MULTIX_OUTPUT_DIR ? path5.resolve(process.env.MULTIX_OUTPUT_DIR) : path5.resolve(process.cwd(), "multix-output");
|
|
1367
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
1368
|
+
cachedOutputDir = dir;
|
|
1369
|
+
return dir;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/providers/atlascloud/commands/prediction-runner.ts
|
|
1373
|
+
init_redact();
|
|
1374
|
+
|
|
1375
|
+
// src/core/video-thumb.ts
|
|
1376
|
+
import fs7 from "fs";
|
|
1377
|
+
import path6 from "path";
|
|
1378
|
+
var THUMB_KEYS = /* @__PURE__ */ new Set([
|
|
1379
|
+
"cover_image_url",
|
|
1380
|
+
"coverImageUrl",
|
|
1381
|
+
"cover_url",
|
|
1382
|
+
"coverUrl",
|
|
1383
|
+
"thumbnail_url",
|
|
1384
|
+
"thumbnailUrl",
|
|
1385
|
+
"thumb_url",
|
|
1386
|
+
"thumbUrl",
|
|
1387
|
+
"preview_url",
|
|
1388
|
+
"previewUrl",
|
|
1389
|
+
"first_frame_url",
|
|
1390
|
+
"firstFrameUrl",
|
|
1391
|
+
"poster_url",
|
|
1392
|
+
"posterUrl",
|
|
1393
|
+
"image_url",
|
|
1394
|
+
"imageUrl"
|
|
1395
|
+
]);
|
|
1396
|
+
var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
|
|
1397
|
+
function isImageUrl(value) {
|
|
1398
|
+
if (typeof value !== "string") return false;
|
|
1399
|
+
if (!/^https?:\/\//i.test(value)) return false;
|
|
1400
|
+
return IMAGE_EXT_RE.test(value);
|
|
1401
|
+
}
|
|
1402
|
+
function detectThumbUrl(obj) {
|
|
1403
|
+
if (obj === null || obj === void 0) return null;
|
|
1404
|
+
if (Array.isArray(obj)) {
|
|
1405
|
+
for (const item of obj) {
|
|
1406
|
+
const found = detectThumbUrl(item);
|
|
1407
|
+
if (found) return found;
|
|
1408
|
+
}
|
|
1409
|
+
return null;
|
|
1410
|
+
}
|
|
1411
|
+
if (typeof obj !== "object") return null;
|
|
1412
|
+
const record = obj;
|
|
1413
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1414
|
+
if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
|
|
1415
|
+
}
|
|
1416
|
+
for (const value of Object.values(record)) {
|
|
1417
|
+
if (value && typeof value === "object") {
|
|
1418
|
+
const found = detectThumbUrl(value);
|
|
1419
|
+
if (found) return found;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
return null;
|
|
1423
|
+
}
|
|
1424
|
+
async function downloadThumbBeside(thumbUrl, videoPath, logger) {
|
|
1425
|
+
const match = thumbUrl.match(IMAGE_EXT_RE);
|
|
1426
|
+
const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
|
|
1427
|
+
const dir = path6.dirname(videoPath);
|
|
1428
|
+
const base = path6.basename(videoPath, path6.extname(videoPath));
|
|
1429
|
+
const dest = path6.join(dir, `${base}_thumb.${ext}`);
|
|
1430
|
+
try {
|
|
1431
|
+
await downloadFile(thumbUrl, dest);
|
|
1432
|
+
logger?.success(`Saved thumb ${dest}`);
|
|
1433
|
+
return dest;
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
1440
|
+
if (opts.skip) return null;
|
|
1441
|
+
const url = detectThumbUrl(source);
|
|
1442
|
+
if (!url) {
|
|
1443
|
+
opts.logger?.debug("No thumbnail URL detected in provider response");
|
|
1444
|
+
return null;
|
|
1445
|
+
}
|
|
1446
|
+
const saved = await downloadThumbBeside(url, videoPath, opts.logger);
|
|
1447
|
+
if (saved && opts.copyTo) {
|
|
1448
|
+
const copyExt = path6.extname(saved);
|
|
1449
|
+
const copyBase = path6.basename(opts.copyTo, path6.extname(opts.copyTo));
|
|
1450
|
+
const copyDir = path6.dirname(path6.resolve(opts.copyTo));
|
|
1451
|
+
const copyDest = path6.join(copyDir, `${copyBase}_thumb${copyExt}`);
|
|
1452
|
+
fs7.mkdirSync(copyDir, { recursive: true });
|
|
1453
|
+
fs7.copyFileSync(saved, copyDest);
|
|
1454
|
+
opts.logger?.success(`Copied thumb to ${copyDest}`);
|
|
1455
|
+
}
|
|
1456
|
+
return saved;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
// src/providers/atlascloud/commands/prediction-runner.ts
|
|
1460
|
+
function addRunOptions(cmd, cfg) {
|
|
1461
|
+
cmd.option("--async", "Submit and exit; print the prediction id (resume with `atlas status`)").option("--wait-timeout <ms>", "Max time to wait for completion", String(cfg.waitTimeoutMs)).option("--output <path>", "Output file (single output) or directory (path without extension)").option("--no-download", "Do not download media; print output URLs instead").option("--no-validate", "Skip client-side validation against the model's published schema");
|
|
1462
|
+
if (cfg.images) cmd.addOption(imageFormatOption()).addOption(noWebpOption());
|
|
1463
|
+
if (cfg.video) cmd.option("--no-thumb", "Skip downloading the video thumbnail if present");
|
|
1464
|
+
return cmd.option("-v, --verbose", "Verbose logging");
|
|
1465
|
+
}
|
|
1466
|
+
function addRefOption(cmd, description) {
|
|
1467
|
+
return cmd.option(
|
|
1468
|
+
"--ref <pathOrUrl>",
|
|
1469
|
+
`${description} (repeat --ref for several)`,
|
|
1470
|
+
(value, prev) => [...prev, value],
|
|
1471
|
+
[]
|
|
1472
|
+
);
|
|
1473
|
+
}
|
|
1474
|
+
function requireRefs(refs) {
|
|
1475
|
+
if (!refs || refs.length === 0) {
|
|
1476
|
+
throw new ValidationError("Missing required option --ref <pathOrUrl>");
|
|
1477
|
+
}
|
|
1478
|
+
return refs;
|
|
1479
|
+
}
|
|
1480
|
+
function parseTimeoutMs(raw, fallback) {
|
|
1481
|
+
if (raw === void 0) return fallback;
|
|
1482
|
+
const n = Number(raw);
|
|
1483
|
+
if (!Number.isInteger(n) || n <= 0) {
|
|
1484
|
+
throw new ValidationError(`--wait-timeout must be a positive integer (ms), got "${raw}"`);
|
|
1485
|
+
}
|
|
1486
|
+
return n;
|
|
1487
|
+
}
|
|
1488
|
+
var KIND_COMMAND = {
|
|
1489
|
+
image: "image",
|
|
1490
|
+
video: "video",
|
|
1491
|
+
audio: "audio"
|
|
1492
|
+
};
|
|
1493
|
+
async function loadModelSpec(client, model, logger, validate) {
|
|
1494
|
+
if (validate === false) return void 0;
|
|
1495
|
+
try {
|
|
1496
|
+
const spec = await client.getModelSpec(model);
|
|
1497
|
+
if (!spec) {
|
|
1498
|
+
logger.warn(
|
|
1499
|
+
`Model ${model} is not in the Atlas Cloud public catalog or has no published schema; sending without validation.`
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
return spec;
|
|
1503
|
+
} catch (e) {
|
|
1504
|
+
logger.warn(
|
|
1505
|
+
`Could not load the schema for ${model} (${e instanceof Error ? e.message : String(e)}); sending without validation.`
|
|
1506
|
+
);
|
|
1507
|
+
return void 0;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function assertEndpoint(spec, expected, model) {
|
|
1511
|
+
if (!spec?.endpoint || spec.endpoint === expected) return;
|
|
1512
|
+
throw new ValidationError(
|
|
1513
|
+
`Model ${model} is a ${spec.endpoint} model (served by generate${capitalize(spec.endpoint)}), not ${expected}. Use \`multix atlas ${KIND_COMMAND[spec.endpoint]} ...\` or \`multix atlas run ${model} --type ${spec.endpoint} --input '{...}'\`.`
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
function capitalize(s) {
|
|
1517
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
1518
|
+
}
|
|
1519
|
+
function resumeCommand(id) {
|
|
1520
|
+
return `multix atlas status ${id} --wait --download`;
|
|
1521
|
+
}
|
|
1522
|
+
async function executePrediction(o) {
|
|
1523
|
+
const { client, logger, opts } = o;
|
|
1524
|
+
const timeoutMs = parseTimeoutMs(opts.waitTimeout, o.defaultWaitTimeoutMs);
|
|
1525
|
+
logger.info(`Submitting to ${o.model} (generate${capitalize(o.kind)})...`);
|
|
1526
|
+
const submitted = await client.submit(o.kind, { ...o.body, model: o.model });
|
|
1527
|
+
const id = submitted.id;
|
|
1528
|
+
if (opts.async) {
|
|
1529
|
+
console.log(id);
|
|
1530
|
+
console.error(`Prediction submitted. Resume with: ${resumeCommand(id)}`);
|
|
1531
|
+
return { predictionId: id, files: [], texts: [] };
|
|
1532
|
+
}
|
|
1533
|
+
logger.info(`Prediction ${id} \u2014 waiting up to ${Math.round(timeoutMs / 1e3)}s...`);
|
|
1534
|
+
const final = isTerminalStatus(submitted.status) ? submitted : await client.waitForPrediction(id, {
|
|
1535
|
+
timeoutMs,
|
|
1536
|
+
intervalMs: o.pollIntervalMs,
|
|
1537
|
+
resumeHint: `Resume with: ${resumeCommand(id)}`,
|
|
1538
|
+
onPoll: (p, n) => logger.debug(`poll ${n}: ${p.status}`)
|
|
1539
|
+
});
|
|
1540
|
+
assertPredictionSucceeded(final);
|
|
1541
|
+
const delivered = await deliverPrediction(final, { logger, opts, hint: o.hint });
|
|
1542
|
+
return { predictionId: id, prediction: final, ...delivered };
|
|
1543
|
+
}
|
|
1544
|
+
function isDirectoryOutput(output) {
|
|
1545
|
+
return /[\\/]$/.test(output) || path7.extname(output) === "" || fs8.existsSync(output) && fs8.statSync(output).isDirectory();
|
|
1546
|
+
}
|
|
1547
|
+
function planOutputPaths(urls, cfg) {
|
|
1548
|
+
const exts = urls.map((u) => {
|
|
1549
|
+
const cat = outputCategory(u);
|
|
1550
|
+
return outputExtension(u) ?? defaultExtensionFor(cat !== "unknown" ? cat : cfg.hint ?? "unknown");
|
|
1551
|
+
});
|
|
1552
|
+
const output = cfg.output;
|
|
1553
|
+
if (!output || isDirectoryOutput(output)) {
|
|
1554
|
+
const dir = output ? path7.resolve(output) : cfg.outDir();
|
|
1555
|
+
return exts.map((ext, i) => path7.join(dir, outputFileName(cfg.predictionId, i, ext)));
|
|
1556
|
+
}
|
|
1557
|
+
const abs = path7.resolve(output);
|
|
1558
|
+
if (urls.length === 1) return [abs];
|
|
1559
|
+
const base = abs.slice(0, -path7.extname(abs).length);
|
|
1560
|
+
return exts.map((ext, i) => `${base}-${String(i + 1).padStart(2, "0")}.${ext}`);
|
|
1561
|
+
}
|
|
1562
|
+
async function deliverPrediction(prediction2, cfg) {
|
|
1563
|
+
const { logger, opts } = cfg;
|
|
1564
|
+
if (prediction2.price !== void 0 && prediction2.price !== "") {
|
|
1565
|
+
logger.info(`Cost: $${prediction2.price}`);
|
|
1566
|
+
}
|
|
1567
|
+
const classified = classifyOutputs(prediction2);
|
|
1568
|
+
if (classified.mode === "empty") {
|
|
1569
|
+
throw new ProviderError(
|
|
1570
|
+
hasNsfwFlag(prediction2) ? `Prediction ${prediction2.id} returned no output: blocked by content safety (has_nsfw_contents). Try a different prompt or input.` : `Prediction ${prediction2.id} completed but returned no outputs.`,
|
|
1571
|
+
ATLAS_PROVIDER
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
for (const text of classified.texts) console.log(text);
|
|
1575
|
+
if (classified.mode === "text") {
|
|
1576
|
+
if (opts.output) {
|
|
1577
|
+
const dest = isDirectoryOutput(opts.output) ? path7.resolve(opts.output, outputFileName(prediction2.id, 0, "txt")) : path7.resolve(opts.output);
|
|
1578
|
+
fs8.mkdirSync(path7.dirname(dest), { recursive: true });
|
|
1579
|
+
fs8.writeFileSync(dest, `${classified.texts.join("\n\n")}
|
|
1580
|
+
`, "utf8");
|
|
1581
|
+
logger.success(`Saved text output to ${dest}`);
|
|
1582
|
+
return { files: [dest], texts: classified.texts };
|
|
1583
|
+
}
|
|
1584
|
+
return { files: [], texts: classified.texts };
|
|
1585
|
+
}
|
|
1586
|
+
if (opts.download === false) {
|
|
1587
|
+
for (const u of classified.urls) console.log(u);
|
|
1588
|
+
return { files: [], texts: classified.texts };
|
|
1589
|
+
}
|
|
1590
|
+
const dests = planOutputPaths(classified.urls, {
|
|
1591
|
+
predictionId: prediction2.id,
|
|
1592
|
+
outDir: getOutputDir,
|
|
1593
|
+
output: opts.output,
|
|
1594
|
+
hint: cfg.hint
|
|
1595
|
+
});
|
|
1596
|
+
const explicitFile = classified.urls.length === 1 && opts.output !== void 0 && !isDirectoryOutput(opts.output);
|
|
1597
|
+
const saved = [];
|
|
1598
|
+
let failed = 0;
|
|
1599
|
+
for (let i = 0; i < classified.urls.length; i++) {
|
|
1600
|
+
const url = classified.urls[i];
|
|
1601
|
+
const planned = dests[i];
|
|
1602
|
+
const part = `${planned}.part`;
|
|
1603
|
+
try {
|
|
1604
|
+
const contentType = await downloadFile(url, part);
|
|
1605
|
+
const dest = explicitFile || outputExtension(url) ? planned : withContentTypeExtension(planned, contentType);
|
|
1606
|
+
fs8.renameSync(part, dest);
|
|
1607
|
+
saved.push(dest);
|
|
1608
|
+
logger.debug(`Downloaded ${redactUrl(url)} -> ${dest}`);
|
|
1609
|
+
} catch (e) {
|
|
1610
|
+
fs8.rmSync(part, { force: true });
|
|
1611
|
+
failed++;
|
|
1612
|
+
logger.warn(
|
|
1613
|
+
`Download failed (${redactUrl(url)}): ${e instanceof Error ? e.message : String(e)}`
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
if (saved.length === 0) {
|
|
1618
|
+
throw new ProviderError(
|
|
1619
|
+
`Prediction ${prediction2.id} produced ${classified.urls.length} output(s) but none downloaded. Retry with: ${resumeCommand(prediction2.id)}`,
|
|
1620
|
+
ATLAS_PROVIDER
|
|
1621
|
+
);
|
|
1622
|
+
}
|
|
1623
|
+
const images = saved.filter((f) => outputCategory(f) === "image");
|
|
1624
|
+
const fileOutput = opts.output && !isDirectoryOutput(opts.output) ? opts.output : void 0;
|
|
1625
|
+
const finalImages = finalizeGeneratedImages(images, {
|
|
1626
|
+
imageFormat: opts.imageFormat,
|
|
1627
|
+
webp: opts.webp,
|
|
1628
|
+
output: fileOutput,
|
|
1629
|
+
logger
|
|
1630
|
+
});
|
|
1631
|
+
const files = saved.map((f) => finalImages[images.indexOf(f)] ?? f);
|
|
1632
|
+
const firstVideo = files.find((f) => outputCategory(f) === "video");
|
|
1633
|
+
if (firstVideo) {
|
|
1634
|
+
await maybeDownloadThumb(prediction2, firstVideo, { skip: opts.thumb === false, logger });
|
|
1635
|
+
}
|
|
1636
|
+
console.log(`
|
|
1637
|
+
Prediction ${prediction2.id}: saved ${files.length} file(s)`);
|
|
1638
|
+
for (const f of files) console.log(` ${f}`);
|
|
1639
|
+
if (failed > 0) {
|
|
1640
|
+
throw new ProviderError(
|
|
1641
|
+
`Prediction ${prediction2.id}: downloaded ${saved.length} of ${classified.urls.length} output(s); ${failed} failed. Saved: ${files.join(", ")}. Retry with: ${resumeCommand(prediction2.id)}`,
|
|
1642
|
+
ATLAS_PROVIDER
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
return { files, texts: classified.texts };
|
|
1646
|
+
}
|
|
1647
|
+
function withContentTypeExtension(planned, contentType) {
|
|
1648
|
+
const ext = extensionForContentType(contentType);
|
|
1649
|
+
if (!ext) return planned;
|
|
1650
|
+
const current = path7.extname(planned);
|
|
1651
|
+
return `${current ? planned.slice(0, -current.length) : planned}.${ext}`;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
// src/providers/atlascloud/commands/generation-command.ts
|
|
1655
|
+
async function runGenerationCommand(cfg) {
|
|
1656
|
+
const logger = createAtlasLogger({
|
|
1657
|
+
verbose: cfg.opts.verbose,
|
|
1658
|
+
stdoutReserved: cfg.opts.async
|
|
1659
|
+
});
|
|
1660
|
+
const client = createAtlasClientFromEnv();
|
|
1661
|
+
const spec = await loadModelSpec(client, cfg.model, logger, cfg.opts.validate);
|
|
1662
|
+
const kind = cfg.endpointFromSchema ? spec?.endpoint ?? cfg.kind : cfg.kind;
|
|
1663
|
+
assertEndpoint(spec, kind, cfg.model);
|
|
1664
|
+
const refs = cfg.refs ?? [];
|
|
1665
|
+
buildModelInput(spec, cfg.requests(refs));
|
|
1666
|
+
const refUrls = refs.length > 0 ? await resolveMediaRefs(client, refs, logger) : [];
|
|
1667
|
+
const body = buildModelInput(spec, cfg.requests(refUrls));
|
|
1668
|
+
logger.debug(`Request fields: ${Object.keys(body).join(", ")}`);
|
|
1669
|
+
await executePrediction({
|
|
1670
|
+
client,
|
|
1671
|
+
logger,
|
|
1672
|
+
kind,
|
|
1673
|
+
model: cfg.model,
|
|
1674
|
+
body,
|
|
1675
|
+
opts: cfg.opts,
|
|
1676
|
+
hint: cfg.hint,
|
|
1677
|
+
defaultWaitTimeoutMs: cfg.defaultWaitTimeoutMs,
|
|
1678
|
+
pollIntervalMs: cfg.pollIntervalMs
|
|
1679
|
+
});
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
// src/providers/atlascloud/commands/audio.ts
|
|
1683
|
+
var AUDIO_WAIT_MS = 6e5;
|
|
1684
|
+
var AUDIO_POLL_MS = 3e3;
|
|
1685
|
+
function registerAtlasAudioCommand(parent) {
|
|
1686
|
+
const audio = parent.command("audio [text]").description(
|
|
1687
|
+
`Speech, music, lyrics or transcription via Atlas Cloud (default TTS: ${ATLAS_DEFAULT_MODELS.tts}; with --audio, STT: ${ATLAS_DEFAULT_MODELS.stt})`
|
|
1688
|
+
).option(
|
|
1689
|
+
"-m, --model <id>",
|
|
1690
|
+
"Atlas Cloud audio model id (env: ATLASCLOUD_TTS_MODEL / ATLASCLOUD_STT_MODEL)"
|
|
1691
|
+
).option("--audio <pathOrUrl>", "Input audio for speech-to-text (local files are uploaded)").option("--voice <id>", "Voice id (model-dependent)").option("--format <fmt>", "Output audio format, e.g. mp3, wav (model-dependent)").option("--language <code>", "Language (model-dependent)");
|
|
1692
|
+
addRunOptions(audio, { waitTimeoutMs: AUDIO_WAIT_MS }).action(
|
|
1693
|
+
async (text, opts) => {
|
|
1694
|
+
if (!text && !opts.audio) {
|
|
1695
|
+
throw new ValidationError(
|
|
1696
|
+
"Provide text to synthesize, or --audio <pathOrUrl> to transcribe"
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
const model = opts.model ?? atlasDefaultModel(opts.audio ? "stt" : "tts");
|
|
1700
|
+
await runGenerationCommand({
|
|
1701
|
+
model,
|
|
1702
|
+
kind: "audio",
|
|
1703
|
+
refs: opts.audio ? [opts.audio] : [],
|
|
1704
|
+
requests: (refs) => [
|
|
1705
|
+
{ flag: "[text]", value: text, candidates: FIELD_CANDIDATES.speechText },
|
|
1706
|
+
{ flag: "--audio", value: refs[0], candidates: FIELD_CANDIDATES.audioInput },
|
|
1707
|
+
{ flag: "--voice", value: opts.voice, candidates: FIELD_CANDIDATES.voice },
|
|
1708
|
+
{ flag: "--format", value: opts.format, candidates: FIELD_CANDIDATES.audioFormat },
|
|
1709
|
+
{ flag: "--language", value: opts.language, candidates: FIELD_CANDIDATES.language }
|
|
1710
|
+
],
|
|
1711
|
+
opts,
|
|
1712
|
+
hint: "audio",
|
|
1713
|
+
defaultWaitTimeoutMs: AUDIO_WAIT_MS,
|
|
1714
|
+
pollIntervalMs: AUDIO_POLL_MS
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// src/providers/atlascloud/commands/image.ts
|
|
1721
|
+
var IMAGE_WAIT_MS = 3e5;
|
|
1722
|
+
var IMAGE_POLL_MS = 2e3;
|
|
1723
|
+
function imageRequests(prompt, o) {
|
|
1724
|
+
return [
|
|
1725
|
+
{ flag: "--prompt", value: prompt, candidates: FIELD_CANDIDATES.prompt },
|
|
1726
|
+
{ flag: "--size", value: o.size, candidates: FIELD_CANDIDATES.size },
|
|
1727
|
+
{ flag: "--aspect-ratio", value: o.aspectRatio, candidates: FIELD_CANDIDATES.aspectRatio },
|
|
1728
|
+
{ flag: "--resolution", value: o.resolution, candidates: FIELD_CANDIDATES.resolution },
|
|
1729
|
+
{ flag: "--num-images", value: o.numImages, candidates: FIELD_CANDIDATES.numImages },
|
|
1730
|
+
{ flag: "--seed", value: o.seed, candidates: FIELD_CANDIDATES.seed },
|
|
1731
|
+
{ flag: "--negative-prompt", value: o.negativePrompt, candidates: FIELD_CANDIDATES.negativePrompt }
|
|
1732
|
+
];
|
|
1733
|
+
}
|
|
1734
|
+
function addImageFlags(cmd) {
|
|
1735
|
+
return cmd.option("--size <size>", "Output size in the model's format (e.g. 1024*1024 or 1024x1024)").option("--aspect-ratio <ratio>", "Aspect ratio, e.g. 1:1, 16:9 (model-dependent)").option("--resolution <res>", "Resolution tier, e.g. 1k, 2k (model-dependent)").option("-n, --num-images <n>", "Number of images (model-dependent)").option("--seed <n>", "Seed").option("--negative-prompt <text>", "Negative prompt (model-dependent)");
|
|
1736
|
+
}
|
|
1737
|
+
function registerAtlasImageCommands(parent) {
|
|
1738
|
+
const image = parent.command("image <prompt>").description(`Text-to-image via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.image})`).option("-m, --model <id>", "Atlas Cloud image model id (env: ATLASCLOUD_IMAGE_MODEL)");
|
|
1739
|
+
addImageFlags(image);
|
|
1740
|
+
addRunOptions(image, { waitTimeoutMs: IMAGE_WAIT_MS, images: true }).action(
|
|
1741
|
+
async (prompt, opts) => {
|
|
1742
|
+
await runGenerationCommand({
|
|
1743
|
+
model: opts.model ?? atlasDefaultModel("image"),
|
|
1744
|
+
kind: "image",
|
|
1745
|
+
requests: () => imageRequests(prompt, opts),
|
|
1746
|
+
opts,
|
|
1747
|
+
hint: "image",
|
|
1748
|
+
defaultWaitTimeoutMs: IMAGE_WAIT_MS,
|
|
1749
|
+
pollIntervalMs: IMAGE_POLL_MS
|
|
1750
|
+
});
|
|
1751
|
+
}
|
|
1752
|
+
);
|
|
1753
|
+
const edit = parent.command("image-to-image").description(
|
|
1754
|
+
`Edit or restyle reference images via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.imageEdit})`
|
|
1755
|
+
).requiredOption("--prompt <text>", "Edit instruction");
|
|
1756
|
+
addRefOption(edit, "Reference image; local files are uploaded first; required").option(
|
|
1757
|
+
"-m, --model <id>",
|
|
1758
|
+
"Atlas Cloud image-edit model id (env: ATLASCLOUD_IMAGE_EDIT_MODEL)"
|
|
1759
|
+
);
|
|
1760
|
+
addImageFlags(edit);
|
|
1761
|
+
addRunOptions(edit, { waitTimeoutMs: IMAGE_WAIT_MS, images: true }).action(
|
|
1762
|
+
async (opts) => {
|
|
1763
|
+
await runGenerationCommand({
|
|
1764
|
+
model: opts.model ?? atlasDefaultModel("imageEdit"),
|
|
1765
|
+
kind: "image",
|
|
1766
|
+
refs: requireRefs(opts.ref),
|
|
1767
|
+
requests: (refs) => [
|
|
1768
|
+
...imageRequests(opts.prompt, opts),
|
|
1769
|
+
{ flag: "--ref", value: refs, candidates: FIELD_CANDIDATES.editImages }
|
|
1770
|
+
],
|
|
1771
|
+
opts,
|
|
1772
|
+
hint: "image",
|
|
1773
|
+
defaultWaitTimeoutMs: IMAGE_WAIT_MS,
|
|
1774
|
+
pollIntervalMs: IMAGE_POLL_MS
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
);
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
// src/providers/atlascloud/commands/run.ts
|
|
1781
|
+
init_errors();
|
|
1782
|
+
|
|
1783
|
+
// src/core/json-input.ts
|
|
1784
|
+
init_errors();
|
|
1785
|
+
import fs9 from "fs";
|
|
1786
|
+
function parseJsonInput(raw, flag = "--input") {
|
|
1787
|
+
let text;
|
|
1788
|
+
if (raw.startsWith("@")) {
|
|
1789
|
+
try {
|
|
1790
|
+
text = fs9.readFileSync(raw.slice(1), "utf8");
|
|
1791
|
+
} catch (e) {
|
|
1792
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1793
|
+
throw new ValidationError(`Cannot read ${flag} file: ${msg}`);
|
|
1794
|
+
}
|
|
1795
|
+
} else {
|
|
1796
|
+
text = raw;
|
|
1797
|
+
}
|
|
1798
|
+
try {
|
|
1799
|
+
const parsed = JSON.parse(text);
|
|
1800
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1801
|
+
throw new Error("input must be a JSON object");
|
|
1802
|
+
}
|
|
1803
|
+
return parsed;
|
|
1804
|
+
} catch (e) {
|
|
1805
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1806
|
+
throw new ValidationError(`Invalid ${flag} JSON: ${msg}`);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
// src/providers/atlascloud/commands/run.ts
|
|
1811
|
+
var RUN_WAIT_MS = 6e5;
|
|
1812
|
+
function parseMediaKind(raw) {
|
|
1813
|
+
if (raw === void 0) return void 0;
|
|
1814
|
+
const kind = raw.toLowerCase();
|
|
1815
|
+
if (!ATLAS_MEDIA_KINDS.includes(kind)) {
|
|
1816
|
+
throw new ValidationError(`Invalid --type "${raw}". Valid: ${ATLAS_MEDIA_KINDS.join(", ")}`);
|
|
1817
|
+
}
|
|
1818
|
+
return kind;
|
|
1819
|
+
}
|
|
1820
|
+
function registerAtlasRunCommand(parent) {
|
|
1821
|
+
const run = parent.command("run <modelId>").description("Run any Atlas Cloud model with a raw JSON payload (fields per the model's docs)").requiredOption("--input <json>", "Model input as JSON, or @path/to/input.json").option(
|
|
1822
|
+
"--type <type>",
|
|
1823
|
+
"Endpoint: image | video | audio (default: from the model's published schema; 3D uses image). Required with --no-validate or for models without a schema"
|
|
1824
|
+
);
|
|
1825
|
+
addRunOptions(run, { waitTimeoutMs: RUN_WAIT_MS, images: true, video: true }).action(
|
|
1826
|
+
async (modelId, opts) => {
|
|
1827
|
+
const logger = createAtlasLogger({ verbose: opts.verbose, stdoutReserved: opts.async });
|
|
1828
|
+
const input = parseJsonInput(opts.input);
|
|
1829
|
+
if (input.model !== void 0 && input.model !== modelId) {
|
|
1830
|
+
throw new ValidationError(
|
|
1831
|
+
`--input.model "${String(input.model)}" does not match <modelId> "${modelId}"`
|
|
1832
|
+
);
|
|
1833
|
+
}
|
|
1834
|
+
const explicit = parseMediaKind(opts.type);
|
|
1835
|
+
const client = createAtlasClientFromEnv();
|
|
1836
|
+
const spec = await loadModelSpec(client, modelId, logger, opts.validate);
|
|
1837
|
+
const kind = explicit ?? spec?.endpoint;
|
|
1838
|
+
if (!kind) {
|
|
1839
|
+
throw new ValidationError(
|
|
1840
|
+
`Cannot determine the endpoint for ${modelId}; pass --type ${ATLAS_MEDIA_KINDS.join("|")}`
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
assertEndpoint(spec, kind, modelId);
|
|
1844
|
+
if (spec) {
|
|
1845
|
+
const unknown = validateRawInput(spec, input);
|
|
1846
|
+
if (unknown.length > 0) {
|
|
1847
|
+
logger.warn(
|
|
1848
|
+
`Fields not in ${modelId}'s published schema (sent anyway): ${unknown.join(", ")}`
|
|
1849
|
+
);
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
await executePrediction({
|
|
1853
|
+
client,
|
|
1854
|
+
logger,
|
|
1855
|
+
kind,
|
|
1856
|
+
model: modelId,
|
|
1857
|
+
body: input,
|
|
1858
|
+
opts,
|
|
1859
|
+
defaultWaitTimeoutMs: RUN_WAIT_MS,
|
|
1860
|
+
pollIntervalMs: kind === "video" ? 5e3 : 3e3
|
|
1861
|
+
});
|
|
1862
|
+
}
|
|
1863
|
+
);
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
// src/providers/atlascloud/commands/status.ts
|
|
1867
|
+
var STATUS_WAIT_MS = 6e5;
|
|
1868
|
+
function registerAtlasStatusCommand(parent) {
|
|
1869
|
+
parent.command("status <predictionId>").description("Get an Atlas Cloud prediction; --wait to poll, --download to save outputs").option("--wait", "Poll until the prediction finishes").option("--wait-timeout <ms>", "Max time to wait (with --wait)", String(STATUS_WAIT_MS)).option("--download", "Save outputs when finished (implies --wait)").option("--output <path>", "Output file (single output) or directory (path without extension)").addOption(imageFormatOption()).addOption(noWebpOption()).option("--no-thumb", "Skip downloading the video thumbnail if present").option("-v, --verbose", "Verbose logging").action(async (predictionId, opts) => {
|
|
1870
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
1871
|
+
const client = createAtlasClientFromEnv();
|
|
1872
|
+
const wait = Boolean(opts.wait || opts.download);
|
|
1873
|
+
const prediction2 = wait ? await client.waitForPrediction(predictionId, {
|
|
1874
|
+
timeoutMs: parseTimeoutMs(opts.waitTimeout, STATUS_WAIT_MS),
|
|
1875
|
+
resumeHint: `Resume with: ${resumeCommand(predictionId)}`,
|
|
1876
|
+
onPoll: (p, n) => logger.debug(`poll ${n}: ${p.status}`)
|
|
1877
|
+
}) : await client.getPrediction(predictionId);
|
|
1878
|
+
if (!opts.download) {
|
|
1879
|
+
console.log(JSON.stringify(prediction2, null, 2));
|
|
1880
|
+
if (isFailureStatus(prediction2.status)) throw new AtlasPredictionFailedError(prediction2);
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
assertPredictionSucceeded(prediction2);
|
|
1884
|
+
await deliverPrediction(prediction2, { logger, opts: { ...opts, download: true } });
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// src/providers/atlascloud/commands/three-d.ts
|
|
1889
|
+
init_errors();
|
|
1890
|
+
var THREE_D_WAIT_MS = 9e5;
|
|
1891
|
+
var THREE_D_POLL_MS = 5e3;
|
|
1892
|
+
function registerAtlas3dCommand(parent) {
|
|
1893
|
+
const cmd = parent.command("3d [prompt]").alias("generate-3d").description(
|
|
1894
|
+
`Text- or image-to-3D via Atlas Cloud (defaults: ${ATLAS_DEFAULT_MODELS.textTo3d}; with --ref ${ATLAS_DEFAULT_MODELS.imageTo3d})`
|
|
1895
|
+
);
|
|
1896
|
+
addRefOption(cmd, "Reference image for image-to-3D (local files are uploaded)").option(
|
|
1897
|
+
"-m, --model <id>",
|
|
1898
|
+
"Atlas Cloud 3D model id (env: ATLASCLOUD_TEXT_TO_3D_MODEL / ATLASCLOUD_IMAGE_TO_3D_MODEL)"
|
|
1899
|
+
).option("--format <fmt>", "Mesh format, e.g. GLB, OBJ, FBX (model-dependent)").option("--seed <n>", "Seed (model-dependent)");
|
|
1900
|
+
addRunOptions(cmd, { waitTimeoutMs: THREE_D_WAIT_MS }).action(
|
|
1901
|
+
async (prompt, opts) => {
|
|
1902
|
+
const refs = opts.ref ?? [];
|
|
1903
|
+
if (!prompt && refs.length === 0) {
|
|
1904
|
+
throw new ValidationError("Provide a prompt (text-to-3D) or --ref <image> (image-to-3D)");
|
|
1905
|
+
}
|
|
1906
|
+
await runGenerationCommand({
|
|
1907
|
+
model: opts.model ?? atlasDefaultModel(refs.length > 0 ? "imageTo3d" : "textTo3d"),
|
|
1908
|
+
kind: "image",
|
|
1909
|
+
endpointFromSchema: true,
|
|
1910
|
+
refs,
|
|
1911
|
+
requests: (urls) => [
|
|
1912
|
+
{ flag: "[prompt]", value: prompt, candidates: FIELD_CANDIDATES.prompt },
|
|
1913
|
+
{ flag: "--ref", value: urls, candidates: FIELD_CANDIDATES.primaryImage },
|
|
1914
|
+
{ flag: "--format", value: opts.format, candidates: FIELD_CANDIDATES.modelFormat },
|
|
1915
|
+
{ flag: "--seed", value: opts.seed, candidates: FIELD_CANDIDATES.seed }
|
|
1916
|
+
],
|
|
1917
|
+
opts,
|
|
1918
|
+
hint: "3d",
|
|
1919
|
+
defaultWaitTimeoutMs: THREE_D_WAIT_MS,
|
|
1920
|
+
pollIntervalMs: THREE_D_POLL_MS
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
// src/providers/atlascloud/commands/upload.ts
|
|
1927
|
+
function registerAtlasUploadCommand(parent) {
|
|
1928
|
+
parent.command("upload <file>").description("Upload a local file to Atlas Cloud and print its temporary URL").option("-v, --verbose", "Verbose logging").action(async (file, opts) => {
|
|
1929
|
+
const logger = createAtlasLogger({ verbose: opts.verbose, stdoutReserved: true });
|
|
1930
|
+
if (/^(https?|data):/i.test(file.trim())) {
|
|
1931
|
+
logger.warn("Input is already a URL; nothing to upload.");
|
|
1932
|
+
console.log(file.trim());
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
const client = createAtlasClientFromEnv();
|
|
1936
|
+
console.log(await resolveMediaRef(client, file, logger));
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/providers/atlascloud/commands/video.ts
|
|
1941
|
+
var VIDEO_WAIT_MS = 9e5;
|
|
1942
|
+
var VIDEO_POLL_MS = 5e3;
|
|
1943
|
+
function videoRequests(prompt, o) {
|
|
1944
|
+
return [
|
|
1945
|
+
{ flag: "--prompt", value: prompt, candidates: FIELD_CANDIDATES.prompt },
|
|
1946
|
+
{ flag: "--duration", value: o.duration, candidates: FIELD_CANDIDATES.duration },
|
|
1947
|
+
{ flag: "--resolution", value: o.resolution, candidates: FIELD_CANDIDATES.resolution },
|
|
1948
|
+
{ flag: "--aspect-ratio", value: o.aspectRatio, candidates: FIELD_CANDIDATES.aspectRatio },
|
|
1949
|
+
{ flag: "--negative-prompt", value: o.negativePrompt, candidates: FIELD_CANDIDATES.negativePrompt },
|
|
1950
|
+
{ flag: "--seed", value: o.seed, candidates: FIELD_CANDIDATES.seed }
|
|
1951
|
+
];
|
|
1952
|
+
}
|
|
1953
|
+
function addVideoFlags(cmd) {
|
|
1954
|
+
return cmd.option("--duration <s>", "Duration in seconds (model-dependent values)").option("--resolution <res>", "Resolution, e.g. 480p, 720p, 1080p (model-dependent)").option("--aspect-ratio <ratio>", "Aspect ratio, e.g. 16:9, 9:16 (model-dependent)").option("--negative-prompt <text>", "Negative prompt (model-dependent)").option("--seed <n>", "Seed");
|
|
1955
|
+
}
|
|
1956
|
+
function registerAtlasVideoCommands(parent) {
|
|
1957
|
+
const video = parent.command("video <prompt>").description(`Text-to-video via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.video})`).option("-m, --model <id>", "Atlas Cloud video model id (env: ATLASCLOUD_VIDEO_MODEL)");
|
|
1958
|
+
addVideoFlags(video);
|
|
1959
|
+
addRunOptions(video, { waitTimeoutMs: VIDEO_WAIT_MS, video: true }).action(
|
|
1960
|
+
async (prompt, opts) => {
|
|
1961
|
+
await runGenerationCommand({
|
|
1962
|
+
model: opts.model ?? atlasDefaultModel("video"),
|
|
1963
|
+
kind: "video",
|
|
1964
|
+
requests: () => videoRequests(prompt, opts),
|
|
1965
|
+
opts,
|
|
1966
|
+
hint: "video",
|
|
1967
|
+
defaultWaitTimeoutMs: VIDEO_WAIT_MS,
|
|
1968
|
+
pollIntervalMs: VIDEO_POLL_MS
|
|
1969
|
+
});
|
|
1970
|
+
}
|
|
1971
|
+
);
|
|
1972
|
+
const i2v = parent.command("image-to-video").description(
|
|
1973
|
+
`Animate reference image(s) via Atlas Cloud (default: ${ATLAS_DEFAULT_MODELS.imageToVideo})`
|
|
1974
|
+
).option("--prompt <text>", "Motion / scene prompt");
|
|
1975
|
+
addRefOption(i2v, "Reference image; local files are uploaded first; required").option(
|
|
1976
|
+
"-m, --model <id>",
|
|
1977
|
+
"Atlas Cloud image-to-video model id (env: ATLASCLOUD_IMAGE_TO_VIDEO_MODEL)"
|
|
1978
|
+
);
|
|
1979
|
+
addVideoFlags(i2v);
|
|
1980
|
+
addRunOptions(i2v, { waitTimeoutMs: VIDEO_WAIT_MS, video: true }).action(
|
|
1981
|
+
async (opts) => {
|
|
1982
|
+
await runGenerationCommand({
|
|
1983
|
+
model: opts.model ?? atlasDefaultModel("imageToVideo"),
|
|
1984
|
+
kind: "video",
|
|
1985
|
+
refs: requireRefs(opts.ref),
|
|
1986
|
+
requests: (refs) => [
|
|
1987
|
+
...videoRequests(opts.prompt, opts),
|
|
1988
|
+
{ flag: "--ref", value: refs, candidates: FIELD_CANDIDATES.primaryImage }
|
|
1989
|
+
],
|
|
1990
|
+
opts,
|
|
1991
|
+
hint: "video",
|
|
1992
|
+
defaultWaitTimeoutMs: VIDEO_WAIT_MS,
|
|
1993
|
+
pollIntervalMs: VIDEO_POLL_MS
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
// src/providers/atlascloud/commands/index.ts
|
|
2000
|
+
function registerAtlasCloudCommands(program2) {
|
|
2001
|
+
const atlas = program2.command("atlas").alias("atlascloud").description(
|
|
2002
|
+
"Atlas Cloud: image, image-to-image, video, image-to-video, audio (TTS/STT/music), 3D, generic model runner, async status"
|
|
2003
|
+
);
|
|
2004
|
+
registerAtlasImageCommands(atlas);
|
|
2005
|
+
registerAtlasVideoCommands(atlas);
|
|
2006
|
+
registerAtlasAudioCommand(atlas);
|
|
2007
|
+
registerAtlas3dCommand(atlas);
|
|
2008
|
+
registerAtlasRunCommand(atlas);
|
|
2009
|
+
registerAtlasStatusCommand(atlas);
|
|
2010
|
+
registerAtlasUploadCommand(atlas);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
102
2013
|
// src/providers/byteplus/commands/generate-3d.ts
|
|
103
|
-
import
|
|
2014
|
+
import path9 from "path";
|
|
104
2015
|
|
|
105
2016
|
// src/core/image-input.ts
|
|
106
|
-
import
|
|
107
|
-
import
|
|
2017
|
+
import fs10 from "fs/promises";
|
|
2018
|
+
import path8 from "path";
|
|
108
2019
|
var IMAGE_MIME_BY_EXT = {
|
|
109
2020
|
".jpg": "image/jpeg",
|
|
110
2021
|
".jpeg": "image/jpeg",
|
|
@@ -131,7 +2042,7 @@ function isHttpUrl(input) {
|
|
|
131
2042
|
return /^https?:\/\//i.test(input);
|
|
132
2043
|
}
|
|
133
2044
|
async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
|
|
134
|
-
const ext =
|
|
2045
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
135
2046
|
const mime = mimeTable[ext];
|
|
136
2047
|
if (!mime) {
|
|
137
2048
|
throw new Error(
|
|
@@ -140,7 +2051,7 @@ async function readAsDataUrl(filePath, mimeTable, kindLabel, opts = {}) {
|
|
|
140
2051
|
}
|
|
141
2052
|
const hard = opts.hardLimitBytes ?? DEFAULT_HARD_LIMIT_BYTES;
|
|
142
2053
|
const soft = opts.softLimitBytes ?? DEFAULT_SOFT_LIMIT_BYTES;
|
|
143
|
-
const buf = await
|
|
2054
|
+
const buf = await fs10.readFile(filePath);
|
|
144
2055
|
if (buf.byteLength > hard) {
|
|
145
2056
|
throw new Error(
|
|
146
2057
|
`${kindLabel} file ${filePath} is ${(buf.byteLength / 1024 / 1024).toFixed(1)}MB \u2014 exceeds ${(hard / 1024 / 1024).toFixed(0)}MB limit. Upload to a URL instead.`
|
|
@@ -183,65 +2094,8 @@ function normalizeOpts(opts) {
|
|
|
183
2094
|
return opts;
|
|
184
2095
|
}
|
|
185
2096
|
|
|
186
|
-
// src/core/logger.ts
|
|
187
|
-
var ANSI = {
|
|
188
|
-
reset: "\x1B[0m",
|
|
189
|
-
bold: "\x1B[1m",
|
|
190
|
-
green: "\x1B[32m",
|
|
191
|
-
yellow: "\x1B[33m",
|
|
192
|
-
red: "\x1B[31m",
|
|
193
|
-
blue: "\x1B[34m",
|
|
194
|
-
cyan: "\x1B[36m",
|
|
195
|
-
dim: "\x1B[2m"
|
|
196
|
-
};
|
|
197
|
-
function c(color, text) {
|
|
198
|
-
if (process.env.NO_COLOR || !process.stdout.isTTY) return text;
|
|
199
|
-
return `${color}${text}${ANSI.reset}`;
|
|
200
|
-
}
|
|
201
|
-
function createLogger(opts) {
|
|
202
|
-
const { verbose } = opts;
|
|
203
|
-
return {
|
|
204
|
-
info(msg) {
|
|
205
|
-
console.log(c(ANSI.blue, ` ${msg}`));
|
|
206
|
-
},
|
|
207
|
-
success(msg) {
|
|
208
|
-
console.log(c(ANSI.green, `\u2713 ${msg}`));
|
|
209
|
-
},
|
|
210
|
-
warn(msg) {
|
|
211
|
-
console.warn(c(ANSI.yellow, `\u26A0 ${msg}`));
|
|
212
|
-
},
|
|
213
|
-
error(msg) {
|
|
214
|
-
console.error(c(ANSI.red, `\u2717 ${msg}`));
|
|
215
|
-
},
|
|
216
|
-
debug(msg) {
|
|
217
|
-
if (verbose) console.log(c(ANSI.dim, ` [debug] ${msg}`));
|
|
218
|
-
},
|
|
219
|
-
header(msg) {
|
|
220
|
-
const line = "=".repeat(60);
|
|
221
|
-
console.log(c(ANSI.bold + ANSI.cyan, `
|
|
222
|
-
${line}
|
|
223
|
-
${msg}
|
|
224
|
-
${line}
|
|
225
|
-
`));
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
var defaultLogger = createLogger({ verbose: false });
|
|
230
|
-
|
|
231
|
-
// src/core/output-dir.ts
|
|
232
|
-
import fs2 from "fs";
|
|
233
|
-
import path2 from "path";
|
|
234
|
-
var cachedOutputDir;
|
|
235
|
-
function getOutputDir() {
|
|
236
|
-
if (cachedOutputDir) return cachedOutputDir;
|
|
237
|
-
const dir = process.env.MULTIX_OUTPUT_DIR ? path2.resolve(process.env.MULTIX_OUTPUT_DIR) : path2.resolve(process.cwd(), "multix-output");
|
|
238
|
-
fs2.mkdirSync(dir, { recursive: true });
|
|
239
|
-
cachedOutputDir = dir;
|
|
240
|
-
return dir;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
2097
|
// src/providers/leonardo/poll.ts
|
|
244
|
-
var
|
|
2098
|
+
var PollTimeoutError2 = class extends Error {
|
|
245
2099
|
constructor() {
|
|
246
2100
|
super("Polling timed out before the operation completed.");
|
|
247
2101
|
this.name = "PollTimeoutError";
|
|
@@ -272,221 +2126,11 @@ async function poll(opts) {
|
|
|
272
2126
|
});
|
|
273
2127
|
});
|
|
274
2128
|
}
|
|
275
|
-
throw new
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// src/core/env-loader.ts
|
|
279
|
-
init_redact();
|
|
280
|
-
import fs3 from "fs";
|
|
281
|
-
import os from "os";
|
|
282
|
-
import path3 from "path";
|
|
283
|
-
import { config as dotenvConfig, parse as dotenvParse } from "dotenv";
|
|
284
|
-
var loaded = false;
|
|
285
|
-
var loadedFiles = [];
|
|
286
|
-
var LEGACY_SKILL_DIRS = ["ai-multimodal", "ak-ai-multimodal"];
|
|
287
|
-
function legacyFilesUnder(root) {
|
|
288
|
-
const claude = path3.join(root, ".claude");
|
|
289
|
-
return [
|
|
290
|
-
...LEGACY_SKILL_DIRS.map((dir) => path3.join(claude, "skills", dir, ".env")),
|
|
291
|
-
path3.join(claude, "skills", ".env"),
|
|
292
|
-
path3.join(claude, ".env")
|
|
293
|
-
];
|
|
294
|
-
}
|
|
295
|
-
function samePath(a, b) {
|
|
296
|
-
const norm = (p) => {
|
|
297
|
-
const r = path3.resolve(p);
|
|
298
|
-
return process.platform === "win32" ? r.toLowerCase() : r;
|
|
299
|
-
};
|
|
300
|
-
return norm(a) === norm(b);
|
|
301
|
-
}
|
|
302
|
-
function legacyClaudeEnvFiles(cwd, home) {
|
|
303
|
-
const files = legacyFilesUnder(cwd);
|
|
304
|
-
if (home && !samePath(home, cwd)) files.push(...legacyFilesUnder(home));
|
|
305
|
-
return files;
|
|
306
|
-
}
|
|
307
|
-
function legacyClaudeEnvEnabled(env = process.env) {
|
|
308
|
-
const value = env.MULTIX_LEGACY_CLAUDE_ENV;
|
|
309
|
-
return value !== void 0 && ["1", "true", "yes", "on"].includes(value.trim().toLowerCase());
|
|
310
|
-
}
|
|
311
|
-
var LEGACY_ALLOWED_NAME = /^[A-Z][A-Z0-9_]*_(API_KEY(_\d+)?|API_TOKEN|ACCOUNT_ID|GATEWAY_ID)$|^(FAL_KEY|ARK_API_KEY)$/;
|
|
312
|
-
function isLegacyAllowedName(name) {
|
|
313
|
-
return LEGACY_ALLOWED_NAME.test(name);
|
|
314
|
-
}
|
|
315
|
-
function isReadableFile(file) {
|
|
316
|
-
try {
|
|
317
|
-
return fs3.statSync(file).isFile();
|
|
318
|
-
} catch {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
function loadFile(file) {
|
|
323
|
-
if (!isReadableFile(file)) return;
|
|
324
|
-
const result = dotenvConfig({ path: file, override: false });
|
|
325
|
-
if (!result.error) loadedFiles.push(file);
|
|
326
|
-
}
|
|
327
|
-
function loadLegacyFile(file) {
|
|
328
|
-
if (!isReadableFile(file)) return;
|
|
329
|
-
let parsed;
|
|
330
|
-
try {
|
|
331
|
-
parsed = dotenvParse(fs3.readFileSync(file));
|
|
332
|
-
} catch {
|
|
333
|
-
return;
|
|
334
|
-
}
|
|
335
|
-
for (const [name, value] of Object.entries(parsed)) {
|
|
336
|
-
if (isLegacyAllowedName(name) && process.env[name] === void 0) process.env[name] = value;
|
|
337
|
-
}
|
|
338
|
-
loadedFiles.push(file);
|
|
339
|
-
}
|
|
340
|
-
function loadEnv() {
|
|
341
|
-
if (loaded) return;
|
|
342
|
-
loaded = true;
|
|
343
|
-
loadedFiles = [];
|
|
344
|
-
const cwd = process.cwd();
|
|
345
|
-
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os.homedir();
|
|
346
|
-
loadFile(path3.resolve(cwd, ".env"));
|
|
347
|
-
if (home) loadFile(path3.join(home, ".multix", ".env"));
|
|
348
|
-
if (legacyClaudeEnvEnabled()) {
|
|
349
|
-
for (const file of legacyClaudeEnvFiles(cwd, home)) loadLegacyFile(file);
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
function getLoadedEnvFiles() {
|
|
353
|
-
return [...loadedFiles];
|
|
354
|
-
}
|
|
355
|
-
function resolveKey(name) {
|
|
356
|
-
return process.env[name] || void 0;
|
|
2129
|
+
throw new PollTimeoutError2();
|
|
357
2130
|
}
|
|
358
2131
|
|
|
359
2132
|
// src/providers/byteplus/client.ts
|
|
360
|
-
init_errors();
|
|
361
|
-
|
|
362
|
-
// src/core/http-client.ts
|
|
363
|
-
init_errors();
|
|
364
|
-
import { once } from "events";
|
|
365
|
-
import fs4 from "fs";
|
|
366
|
-
import path4 from "path";
|
|
367
|
-
import { pipeline } from "stream/promises";
|
|
368
|
-
|
|
369
|
-
// src/core/fetch-json.ts
|
|
370
|
-
init_errors();
|
|
371
|
-
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
372
|
-
var SNIPPET_MAX = 500;
|
|
373
|
-
function parseRetryAfter(value, now = Date.now()) {
|
|
374
|
-
if (!value) return void 0;
|
|
375
|
-
const trimmed = value.trim();
|
|
376
|
-
if (/^\d+(\.\d+)?$/.test(trimmed)) return Math.round(Number(trimmed) * 1e3);
|
|
377
|
-
const date = Date.parse(trimmed);
|
|
378
|
-
if (Number.isNaN(date)) return void 0;
|
|
379
|
-
return Math.max(0, date - now);
|
|
380
|
-
}
|
|
381
|
-
async function httpErrorFromResponse(response, url) {
|
|
382
|
-
let snippet = "";
|
|
383
|
-
try {
|
|
384
|
-
const text = await response.text();
|
|
385
|
-
snippet = text.slice(0, SNIPPET_MAX);
|
|
386
|
-
} catch {
|
|
387
|
-
snippet = "(could not read body)";
|
|
388
|
-
}
|
|
389
|
-
return new HttpError(
|
|
390
|
-
response.status,
|
|
391
|
-
snippet,
|
|
392
|
-
url,
|
|
393
|
-
parseRetryAfter(response.headers.get("retry-after"))
|
|
394
|
-
);
|
|
395
|
-
}
|
|
396
|
-
async function fetchJson(opts) {
|
|
397
|
-
const { url, method = "GET", headers: headers2 = {}, body, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
398
|
-
const doFetch = opts.fetch ?? ((input, init2) => globalThis.fetch(input, init2));
|
|
399
|
-
const init = {
|
|
400
|
-
method,
|
|
401
|
-
headers: {
|
|
402
|
-
"Content-Type": "application/json",
|
|
403
|
-
...headers2
|
|
404
|
-
},
|
|
405
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
406
|
-
};
|
|
407
|
-
if (body !== void 0) {
|
|
408
|
-
init.body = JSON.stringify(body);
|
|
409
|
-
}
|
|
410
|
-
let response;
|
|
411
|
-
try {
|
|
412
|
-
response = await doFetch(url, init);
|
|
413
|
-
} catch (cause) {
|
|
414
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
415
|
-
throw new HttpError(0, msg, url);
|
|
416
|
-
}
|
|
417
|
-
if (!response.ok) {
|
|
418
|
-
throw await httpErrorFromResponse(response, url);
|
|
419
|
-
}
|
|
420
|
-
let text;
|
|
421
|
-
try {
|
|
422
|
-
text = await response.text();
|
|
423
|
-
} catch (cause) {
|
|
424
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
425
|
-
throw new HttpError(0, `Failed to read response body: ${msg}`, url);
|
|
426
|
-
}
|
|
427
|
-
if (text.trim() === "") return void 0;
|
|
428
|
-
try {
|
|
429
|
-
return JSON.parse(text);
|
|
430
|
-
} catch {
|
|
431
|
-
throw new HttpError(
|
|
432
|
-
response.status,
|
|
433
|
-
`Invalid JSON response: ${text.slice(0, SNIPPET_MAX)}`,
|
|
434
|
-
url
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// src/core/http-client.ts
|
|
440
|
-
async function httpJson(opts) {
|
|
441
|
-
return fetchJson(opts);
|
|
442
|
-
}
|
|
443
|
-
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers2) {
|
|
444
|
-
fs4.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
445
|
-
let response;
|
|
446
|
-
try {
|
|
447
|
-
response = await globalThis.fetch(url, {
|
|
448
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
449
|
-
...headers2 ? { headers: headers2 } : {}
|
|
450
|
-
});
|
|
451
|
-
} catch (cause) {
|
|
452
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
453
|
-
throw new HttpError(0, msg, url);
|
|
454
|
-
}
|
|
455
|
-
if (!response.ok) {
|
|
456
|
-
throw new HttpError(response.status, "(download failed)", url);
|
|
457
|
-
}
|
|
458
|
-
if (!response.body) {
|
|
459
|
-
throw new HttpError(0, "Response body is null", url);
|
|
460
|
-
}
|
|
461
|
-
const fileStream = fs4.createWriteStream(dest);
|
|
462
|
-
try {
|
|
463
|
-
await pipeline(response.body, fileStream);
|
|
464
|
-
} catch (err2) {
|
|
465
|
-
if (!fileStream.closed) await once(fileStream, "close");
|
|
466
|
-
throw err2;
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
async function fetchBytes(urlOrDataUri) {
|
|
470
|
-
if (urlOrDataUri.startsWith("data:")) {
|
|
471
|
-
const commaIdx = urlOrDataUri.indexOf(",");
|
|
472
|
-
if (commaIdx === -1) throw new Error("Malformed data URI: missing comma");
|
|
473
|
-
const encoded = urlOrDataUri.slice(commaIdx + 1);
|
|
474
|
-
return Buffer.from(encoded, "base64");
|
|
475
|
-
}
|
|
476
|
-
let response;
|
|
477
|
-
try {
|
|
478
|
-
response = await globalThis.fetch(urlOrDataUri, {
|
|
479
|
-
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS)
|
|
480
|
-
});
|
|
481
|
-
} catch (cause) {
|
|
482
|
-
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
483
|
-
throw new HttpError(0, msg, urlOrDataUri);
|
|
484
|
-
}
|
|
485
|
-
if (!response.ok) {
|
|
486
|
-
throw new HttpError(response.status, "(bytes fetch failed)", urlOrDataUri);
|
|
487
|
-
}
|
|
488
|
-
return Buffer.from(await response.arrayBuffer());
|
|
489
|
-
}
|
|
2133
|
+
init_errors();
|
|
490
2134
|
|
|
491
2135
|
// src/providers/byteplus/models.ts
|
|
492
2136
|
var DEFAULT_BYTEPLUS_BASE_URL = "https://ark.ap-southeast.bytepluses.com/api/v3";
|
|
@@ -531,9 +2175,9 @@ var BytePlusClient = class {
|
|
|
531
2175
|
}
|
|
532
2176
|
apiKey;
|
|
533
2177
|
baseUrl;
|
|
534
|
-
async request(
|
|
2178
|
+
async request(path52, opts = {}) {
|
|
535
2179
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
536
|
-
const url = new URL(base +
|
|
2180
|
+
const url = new URL(base + path52);
|
|
537
2181
|
if (opts.query) {
|
|
538
2182
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
539
2183
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -551,14 +2195,14 @@ var BytePlusClient = class {
|
|
|
551
2195
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
552
2196
|
});
|
|
553
2197
|
}
|
|
554
|
-
get(
|
|
555
|
-
return this.request(
|
|
2198
|
+
get(path52, query, logger) {
|
|
2199
|
+
return this.request(path52, { method: "GET", query, logger });
|
|
556
2200
|
}
|
|
557
|
-
post(
|
|
558
|
-
return this.request(
|
|
2201
|
+
post(path52, body, logger) {
|
|
2202
|
+
return this.request(path52, { method: "POST", body, logger });
|
|
559
2203
|
}
|
|
560
|
-
delete(
|
|
561
|
-
return this.request(
|
|
2204
|
+
delete(path52, logger) {
|
|
2205
|
+
return this.request(path52, { method: "DELETE", logger });
|
|
562
2206
|
}
|
|
563
2207
|
};
|
|
564
2208
|
function createBytePlusClient() {
|
|
@@ -663,13 +2307,13 @@ function registerBytePlusGenerate3DCommand(parent) {
|
|
|
663
2307
|
process.exit(1);
|
|
664
2308
|
}
|
|
665
2309
|
const ext = inferModelFileExt(url);
|
|
666
|
-
const outPath = opts.output ??
|
|
2310
|
+
const outPath = opts.output ?? path9.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}-${Date.now()}.${ext}`);
|
|
667
2311
|
await downloadModelFile(url, outPath, logger);
|
|
668
2312
|
console.log(`
|
|
669
2313
|
Task: ${taskId}`);
|
|
670
2314
|
console.log(`Saved: ${outPath}`);
|
|
671
2315
|
} catch (e) {
|
|
672
|
-
if (e instanceof
|
|
2316
|
+
if (e instanceof PollTimeoutError2) {
|
|
673
2317
|
logger.error(
|
|
674
2318
|
`Polling timed out \u2014 keep the task and check later: multix byteplus status ${taskId} --wait --download`
|
|
675
2319
|
);
|
|
@@ -685,196 +2329,9 @@ Task: ${taskId}`);
|
|
|
685
2329
|
);
|
|
686
2330
|
}
|
|
687
2331
|
|
|
688
|
-
// src/core/image-webp-finalize.ts
|
|
689
|
-
init_errors();
|
|
690
|
-
import { spawnSync } from "child_process";
|
|
691
|
-
import fs6 from "fs";
|
|
692
|
-
import path6 from "path";
|
|
693
|
-
import { Option } from "commander";
|
|
694
|
-
|
|
695
|
-
// src/core/image-header.ts
|
|
696
|
-
import fs5 from "fs";
|
|
697
|
-
function sniffImageKind(buf) {
|
|
698
|
-
if (buf.length >= 8 && buf.readUInt32BE(0) === 2303741511) return "png";
|
|
699
|
-
if (buf.length >= 3 && buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "jpeg";
|
|
700
|
-
if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP")
|
|
701
|
-
return "webp";
|
|
702
|
-
return void 0;
|
|
703
|
-
}
|
|
704
|
-
function readPng(buf) {
|
|
705
|
-
if (buf.length < 33) return void 0;
|
|
706
|
-
const width = buf.readUInt32BE(16);
|
|
707
|
-
const height = buf.readUInt32BE(20);
|
|
708
|
-
const colorType = buf[25];
|
|
709
|
-
let hasAlpha = colorType === 4 || colorType === 6;
|
|
710
|
-
let offset = 8;
|
|
711
|
-
while (!hasAlpha && offset + 8 <= buf.length) {
|
|
712
|
-
const len = buf.readUInt32BE(offset);
|
|
713
|
-
const type = buf.toString("ascii", offset + 4, offset + 8);
|
|
714
|
-
if (type === "tRNS") hasAlpha = true;
|
|
715
|
-
if (type === "IDAT" || type === "IEND") break;
|
|
716
|
-
offset += 12 + len;
|
|
717
|
-
}
|
|
718
|
-
return { kind: "png", width, height, hasAlpha };
|
|
719
|
-
}
|
|
720
|
-
function readJpeg(buf) {
|
|
721
|
-
let offset = 2;
|
|
722
|
-
while (offset + 9 < buf.length) {
|
|
723
|
-
if (buf[offset] !== 255) {
|
|
724
|
-
offset++;
|
|
725
|
-
continue;
|
|
726
|
-
}
|
|
727
|
-
const marker = buf[offset + 1] ?? 0;
|
|
728
|
-
if (marker >= 192 && marker <= 207 && ![196, 200, 204].includes(marker)) {
|
|
729
|
-
return {
|
|
730
|
-
kind: "jpeg",
|
|
731
|
-
height: buf.readUInt16BE(offset + 5),
|
|
732
|
-
width: buf.readUInt16BE(offset + 7),
|
|
733
|
-
hasAlpha: false
|
|
734
|
-
};
|
|
735
|
-
}
|
|
736
|
-
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
737
|
-
offset += 2;
|
|
738
|
-
continue;
|
|
739
|
-
}
|
|
740
|
-
offset += 2 + buf.readUInt16BE(offset + 2);
|
|
741
|
-
}
|
|
742
|
-
return void 0;
|
|
743
|
-
}
|
|
744
|
-
function readWebp(buf) {
|
|
745
|
-
if (buf.length < 30) return void 0;
|
|
746
|
-
const chunk = buf.toString("ascii", 12, 16);
|
|
747
|
-
if (chunk === "VP8X") {
|
|
748
|
-
const flags = buf[20] ?? 0;
|
|
749
|
-
return {
|
|
750
|
-
kind: "webp",
|
|
751
|
-
width: 1 + buf.readUIntLE(24, 3),
|
|
752
|
-
height: 1 + buf.readUIntLE(27, 3),
|
|
753
|
-
hasAlpha: (flags & 16) !== 0
|
|
754
|
-
};
|
|
755
|
-
}
|
|
756
|
-
if (chunk === "VP8L") {
|
|
757
|
-
const bits = buf.readUInt32LE(21);
|
|
758
|
-
return {
|
|
759
|
-
kind: "webp",
|
|
760
|
-
width: 1 + (bits & 16383),
|
|
761
|
-
height: 1 + (bits >>> 14 & 16383),
|
|
762
|
-
hasAlpha: (bits >>> 28 & 1) === 1
|
|
763
|
-
};
|
|
764
|
-
}
|
|
765
|
-
if (chunk === "VP8 ") {
|
|
766
|
-
return {
|
|
767
|
-
kind: "webp",
|
|
768
|
-
width: buf.readUInt16LE(26) & 16383,
|
|
769
|
-
height: buf.readUInt16LE(28) & 16383,
|
|
770
|
-
hasAlpha: false
|
|
771
|
-
};
|
|
772
|
-
}
|
|
773
|
-
return void 0;
|
|
774
|
-
}
|
|
775
|
-
function readImageHeader(buf) {
|
|
776
|
-
const kind = sniffImageKind(buf);
|
|
777
|
-
if (kind === "png") return readPng(buf);
|
|
778
|
-
if (kind === "jpeg") return readJpeg(buf);
|
|
779
|
-
if (kind === "webp") return readWebp(buf);
|
|
780
|
-
return void 0;
|
|
781
|
-
}
|
|
782
|
-
function readImageHeaderFromFile(file) {
|
|
783
|
-
return readImageHeader(fs5.readFileSync(file));
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// src/core/image-webp-finalize.ts
|
|
787
|
-
var CWEBP_ARGS = ["-q", "85", "-m", "6", "-metadata", "none"];
|
|
788
|
-
var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tif", ".tiff"]);
|
|
789
|
-
function imageFormatOption() {
|
|
790
|
-
return new Option(
|
|
791
|
-
"--image-format <mode>",
|
|
792
|
-
"Final image format: webp (default, cwebp -q 85) | original (keep provider format/quality)"
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
function noWebpOption() {
|
|
796
|
-
return new Option("--no-webp", "Keep the provider's original image format and quality");
|
|
797
|
-
}
|
|
798
|
-
function resolveImageFormatMode(opts, explicitProviderFormat = false) {
|
|
799
|
-
if (opts.webp === false && opts.imageFormat === void 0) return "original";
|
|
800
|
-
const raw = opts.imageFormat ?? process.env.MULTIX_IMAGE_FORMAT;
|
|
801
|
-
if (raw !== void 0 && raw !== "") {
|
|
802
|
-
const mode = raw.toLowerCase();
|
|
803
|
-
if (mode !== "webp" && mode !== "original") {
|
|
804
|
-
throw new ValidationError(`Invalid --image-format: ${raw}. Valid: webp, original`);
|
|
805
|
-
}
|
|
806
|
-
return mode;
|
|
807
|
-
}
|
|
808
|
-
if (explicitProviderFormat) return "original";
|
|
809
|
-
const ext = opts.output ? path6.extname(opts.output).toLowerCase() : "";
|
|
810
|
-
if (ext && ext !== ".webp" && IMAGE_EXTS.has(ext)) return "original";
|
|
811
|
-
return "webp";
|
|
812
|
-
}
|
|
813
|
-
function hasCwebp() {
|
|
814
|
-
const res = spawnSync("cwebp", ["-version"], { stdio: "ignore" });
|
|
815
|
-
return !res.error && res.status === 0;
|
|
816
|
-
}
|
|
817
|
-
function webpTarget(file) {
|
|
818
|
-
const ext = path6.extname(file);
|
|
819
|
-
return ext.toLowerCase() === ".webp" ? file : `${file.slice(0, -ext.length || void 0)}.webp`;
|
|
820
|
-
}
|
|
821
|
-
function convertToWebp(file, logger, keepPath = false) {
|
|
822
|
-
const src = readImageHeaderFromFile(file);
|
|
823
|
-
if (!src) {
|
|
824
|
-
logger?.warn(`Skipping WebP conversion (unrecognized image): ${file}`);
|
|
825
|
-
return file;
|
|
826
|
-
}
|
|
827
|
-
if (src.kind === "webp") {
|
|
828
|
-
const target2 = keepPath ? file : webpTarget(file);
|
|
829
|
-
if (target2 !== file) fs6.renameSync(file, target2);
|
|
830
|
-
return target2;
|
|
831
|
-
}
|
|
832
|
-
const target = keepPath ? file : webpTarget(file);
|
|
833
|
-
const tmp = `${target}.tmp-${process.pid}.webp`;
|
|
834
|
-
const res = spawnSync("cwebp", [...CWEBP_ARGS, file, "-o", tmp], { encoding: "utf8" });
|
|
835
|
-
if (res.error || res.status !== 0) {
|
|
836
|
-
fs6.rmSync(tmp, { force: true });
|
|
837
|
-
logger?.warn(`cwebp failed, keeping original ${file}: ${res.stderr || res.error?.message}`);
|
|
838
|
-
return file;
|
|
839
|
-
}
|
|
840
|
-
const out = fs6.existsSync(tmp) ? readImageHeaderFromFile(tmp) : void 0;
|
|
841
|
-
const problem = !out ? "output is not a valid WebP" : out.width !== src.width || out.height !== src.height ? `size changed ${src.width}x${src.height} -> ${out.width}x${out.height}` : src.hasAlpha && !out.hasAlpha ? "transparency lost" : void 0;
|
|
842
|
-
if (problem) {
|
|
843
|
-
fs6.rmSync(tmp, { force: true });
|
|
844
|
-
logger?.warn(`WebP verification failed (${problem}), keeping original ${file}`);
|
|
845
|
-
return file;
|
|
846
|
-
}
|
|
847
|
-
if (target !== file) fs6.rmSync(file, { force: true });
|
|
848
|
-
fs6.renameSync(tmp, target);
|
|
849
|
-
logger?.debug(`WebP verified ${out?.width}x${out?.height} alpha=${out?.hasAlpha}: ${target}`);
|
|
850
|
-
return target;
|
|
851
|
-
}
|
|
852
|
-
function finalizeGeneratedImages(files, opts) {
|
|
853
|
-
const mode = resolveImageFormatMode(opts, opts.explicitProviderFormat);
|
|
854
|
-
if (mode === "original" || files.length === 0) return files;
|
|
855
|
-
if (!hasCwebp()) {
|
|
856
|
-
opts.logger?.warn(
|
|
857
|
-
"cwebp not found on PATH; keeping original image format (install libwebp, or pass --image-format original)"
|
|
858
|
-
);
|
|
859
|
-
return files;
|
|
860
|
-
}
|
|
861
|
-
const outputAbs = opts.output ? path6.resolve(opts.output) : void 0;
|
|
862
|
-
const wroteOutputDirectly = outputAbs !== void 0 && files.some((f) => path6.resolve(f) === outputAbs);
|
|
863
|
-
const finals = files.map(
|
|
864
|
-
(f) => convertToWebp(f, opts.logger, outputAbs !== void 0 && path6.resolve(f) === outputAbs)
|
|
865
|
-
);
|
|
866
|
-
if (opts.output && outputAbs && !wroteOutputDirectly && finals[0] && fs6.existsSync(outputAbs)) {
|
|
867
|
-
const outExt = path6.extname(outputAbs).toLowerCase();
|
|
868
|
-
if ((outExt === ".webp" || outExt === "") && path6.extname(finals[0]).toLowerCase() === ".webp") {
|
|
869
|
-
fs6.copyFileSync(finals[0], outputAbs);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
return finals;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
2332
|
// src/providers/byteplus/generators/image.ts
|
|
876
|
-
import
|
|
877
|
-
import
|
|
2333
|
+
import fs11 from "fs";
|
|
2334
|
+
import path10 from "path";
|
|
878
2335
|
var ASPECT_TO_SIZE = {
|
|
879
2336
|
"1:1": "2048x2048",
|
|
880
2337
|
"16:9": "2560x1440",
|
|
@@ -938,14 +2395,14 @@ async function generateSeedream(client, opts) {
|
|
|
938
2395
|
}
|
|
939
2396
|
const ext = (() => {
|
|
940
2397
|
try {
|
|
941
|
-
const e =
|
|
2398
|
+
const e = path10.extname(new URL(url).pathname);
|
|
942
2399
|
return e || ".jpg";
|
|
943
2400
|
} catch {
|
|
944
2401
|
return ".jpg";
|
|
945
2402
|
}
|
|
946
2403
|
})();
|
|
947
|
-
const filename = opts.output && items.length === 1 ?
|
|
948
|
-
|
|
2404
|
+
const filename = opts.output && items.length === 1 ? path10.resolve(opts.output) : path10.join(outDir, `byteplus-image-${ts}-${i + 1}${ext}`);
|
|
2405
|
+
fs11.mkdirSync(path10.dirname(filename), { recursive: true });
|
|
949
2406
|
await downloadFile(url, filename);
|
|
950
2407
|
saved.push({ path: filename, url, seed: item.seed });
|
|
951
2408
|
logger?.success(`Saved ${filename}`);
|
|
@@ -1028,97 +2485,13 @@ Generated ${finalImages.length} image(s):`);
|
|
|
1028
2485
|
}
|
|
1029
2486
|
);
|
|
1030
2487
|
}
|
|
1031
|
-
function collect2(value, prev) {
|
|
1032
|
-
return [...prev, value];
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// src/providers/byteplus/commands/image-to-video.ts
|
|
1036
|
-
import path9 from "path";
|
|
1037
|
-
|
|
1038
|
-
// src/core/video-thumb.ts
|
|
1039
|
-
import fs8 from "fs";
|
|
1040
|
-
import path8 from "path";
|
|
1041
|
-
var THUMB_KEYS = /* @__PURE__ */ new Set([
|
|
1042
|
-
"cover_image_url",
|
|
1043
|
-
"coverImageUrl",
|
|
1044
|
-
"cover_url",
|
|
1045
|
-
"coverUrl",
|
|
1046
|
-
"thumbnail_url",
|
|
1047
|
-
"thumbnailUrl",
|
|
1048
|
-
"thumb_url",
|
|
1049
|
-
"thumbUrl",
|
|
1050
|
-
"preview_url",
|
|
1051
|
-
"previewUrl",
|
|
1052
|
-
"first_frame_url",
|
|
1053
|
-
"firstFrameUrl",
|
|
1054
|
-
"poster_url",
|
|
1055
|
-
"posterUrl",
|
|
1056
|
-
"image_url",
|
|
1057
|
-
"imageUrl"
|
|
1058
|
-
]);
|
|
1059
|
-
var IMAGE_EXT_RE = /\.(jpe?g|png|webp|gif|bmp)(\?|$)/i;
|
|
1060
|
-
function isImageUrl(value) {
|
|
1061
|
-
if (typeof value !== "string") return false;
|
|
1062
|
-
if (!/^https?:\/\//i.test(value)) return false;
|
|
1063
|
-
return IMAGE_EXT_RE.test(value);
|
|
1064
|
-
}
|
|
1065
|
-
function detectThumbUrl(obj) {
|
|
1066
|
-
if (obj === null || obj === void 0) return null;
|
|
1067
|
-
if (Array.isArray(obj)) {
|
|
1068
|
-
for (const item of obj) {
|
|
1069
|
-
const found = detectThumbUrl(item);
|
|
1070
|
-
if (found) return found;
|
|
1071
|
-
}
|
|
1072
|
-
return null;
|
|
1073
|
-
}
|
|
1074
|
-
if (typeof obj !== "object") return null;
|
|
1075
|
-
const record = obj;
|
|
1076
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1077
|
-
if (THUMB_KEYS.has(key) && isImageUrl(value)) return value;
|
|
1078
|
-
}
|
|
1079
|
-
for (const value of Object.values(record)) {
|
|
1080
|
-
if (value && typeof value === "object") {
|
|
1081
|
-
const found = detectThumbUrl(value);
|
|
1082
|
-
if (found) return found;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
return null;
|
|
1086
|
-
}
|
|
1087
|
-
async function downloadThumbBeside(thumbUrl, videoPath, logger) {
|
|
1088
|
-
const match = thumbUrl.match(IMAGE_EXT_RE);
|
|
1089
|
-
const ext = match?.[1] ? match[1].toLowerCase().replace("jpeg", "jpg") : "jpg";
|
|
1090
|
-
const dir = path8.dirname(videoPath);
|
|
1091
|
-
const base = path8.basename(videoPath, path8.extname(videoPath));
|
|
1092
|
-
const dest = path8.join(dir, `${base}_thumb.${ext}`);
|
|
1093
|
-
try {
|
|
1094
|
-
await downloadFile(thumbUrl, dest);
|
|
1095
|
-
logger?.success(`Saved thumb ${dest}`);
|
|
1096
|
-
return dest;
|
|
1097
|
-
} catch (e) {
|
|
1098
|
-
logger?.warn(`Thumb download failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1099
|
-
return null;
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
async function maybeDownloadThumb(source, videoPath, opts = {}) {
|
|
1103
|
-
if (opts.skip) return null;
|
|
1104
|
-
const url = detectThumbUrl(source);
|
|
1105
|
-
if (!url) {
|
|
1106
|
-
opts.logger?.debug("No thumbnail URL detected in provider response");
|
|
1107
|
-
return null;
|
|
1108
|
-
}
|
|
1109
|
-
const saved = await downloadThumbBeside(url, videoPath, opts.logger);
|
|
1110
|
-
if (saved && opts.copyTo) {
|
|
1111
|
-
const copyExt = path8.extname(saved);
|
|
1112
|
-
const copyBase = path8.basename(opts.copyTo, path8.extname(opts.copyTo));
|
|
1113
|
-
const copyDir = path8.dirname(path8.resolve(opts.copyTo));
|
|
1114
|
-
const copyDest = path8.join(copyDir, `${copyBase}_thumb${copyExt}`);
|
|
1115
|
-
fs8.mkdirSync(copyDir, { recursive: true });
|
|
1116
|
-
fs8.copyFileSync(saved, copyDest);
|
|
1117
|
-
opts.logger?.success(`Copied thumb to ${copyDest}`);
|
|
1118
|
-
}
|
|
1119
|
-
return saved;
|
|
2488
|
+
function collect2(value, prev) {
|
|
2489
|
+
return [...prev, value];
|
|
1120
2490
|
}
|
|
1121
2491
|
|
|
2492
|
+
// src/providers/byteplus/commands/image-to-video.ts
|
|
2493
|
+
import path11 from "path";
|
|
2494
|
+
|
|
1122
2495
|
// src/providers/byteplus/generators/video.ts
|
|
1123
2496
|
function buildFlagSuffix(opts) {
|
|
1124
2497
|
const parts = [];
|
|
@@ -1240,7 +2613,7 @@ function registerBytePlusImageToVideoCommand(parent) {
|
|
|
1240
2613
|
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
1241
2614
|
process.exit(1);
|
|
1242
2615
|
}
|
|
1243
|
-
const outPath = opts.output ??
|
|
2616
|
+
const outPath = opts.output ?? path11.join(getOutputDir(), `byteplus-i2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1244
2617
|
await downloadVideo(url, outPath, logger);
|
|
1245
2618
|
await maybeDownloadThumb(final, outPath, {
|
|
1246
2619
|
skip: opts.thumb === false,
|
|
@@ -1251,7 +2624,7 @@ function registerBytePlusImageToVideoCommand(parent) {
|
|
|
1251
2624
|
Task: ${taskId}`);
|
|
1252
2625
|
console.log(`Saved: ${outPath}`);
|
|
1253
2626
|
} catch (e) {
|
|
1254
|
-
if (e instanceof
|
|
2627
|
+
if (e instanceof PollTimeoutError2) {
|
|
1255
2628
|
logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
|
|
1256
2629
|
process.exit(1);
|
|
1257
2630
|
}
|
|
@@ -1266,7 +2639,7 @@ Task: ${taskId}`);
|
|
|
1266
2639
|
}
|
|
1267
2640
|
|
|
1268
2641
|
// src/providers/byteplus/commands/reference-to-video.ts
|
|
1269
|
-
import
|
|
2642
|
+
import path12 from "path";
|
|
1270
2643
|
|
|
1271
2644
|
// src/providers/byteplus/reference-input.ts
|
|
1272
2645
|
var MAX_TOTAL_REFS = 12;
|
|
@@ -1298,10 +2671,10 @@ function toRef(kind, resolved, role) {
|
|
|
1298
2671
|
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
1299
2672
|
}
|
|
1300
2673
|
async function resolveRefSpec(kind, spec, logger) {
|
|
1301
|
-
const { path:
|
|
1302
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
1303
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
1304
|
-
return toRef(kind, await resolveAudioInput(
|
|
2674
|
+
const { path: path52, role } = parseRefSpec(spec);
|
|
2675
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path52, logger), role);
|
|
2676
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path52, logger), role);
|
|
2677
|
+
return toRef(kind, await resolveAudioInput(path52, logger), role);
|
|
1305
2678
|
}
|
|
1306
2679
|
async function resolveRefSpecs(specs, logger) {
|
|
1307
2680
|
const out = [];
|
|
@@ -1398,7 +2771,7 @@ function registerBytePlusReferenceToVideoCommand(parent) {
|
|
|
1398
2771
|
logger.error(`Task succeeded but no video_url: ${JSON.stringify(final)}`);
|
|
1399
2772
|
process.exit(1);
|
|
1400
2773
|
}
|
|
1401
|
-
const outPath = opts.output ??
|
|
2774
|
+
const outPath = opts.output ?? path12.join(getOutputDir(), `byteplus-r2v-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1402
2775
|
await downloadVideo(url, outPath, logger);
|
|
1403
2776
|
await maybeDownloadThumb(final, outPath, {
|
|
1404
2777
|
skip: opts.thumb === false,
|
|
@@ -1409,7 +2782,7 @@ function registerBytePlusReferenceToVideoCommand(parent) {
|
|
|
1409
2782
|
Task: ${taskId}`);
|
|
1410
2783
|
console.log(`Saved: ${outPath}`);
|
|
1411
2784
|
} catch (e) {
|
|
1412
|
-
if (e instanceof
|
|
2785
|
+
if (e instanceof PollTimeoutError2) {
|
|
1413
2786
|
logger.error(`Timed out \u2014 multix byteplus status ${taskId}`);
|
|
1414
2787
|
process.exit(1);
|
|
1415
2788
|
}
|
|
@@ -1424,7 +2797,7 @@ Task: ${taskId}`);
|
|
|
1424
2797
|
}
|
|
1425
2798
|
|
|
1426
2799
|
// src/providers/byteplus/commands/status.ts
|
|
1427
|
-
import
|
|
2800
|
+
import path13 from "path";
|
|
1428
2801
|
function registerBytePlusStatusCommand(parent) {
|
|
1429
2802
|
parent.command("status <taskId>").description("Get a Seedance task's status; --wait + --download to fetch the result MP4").option("--wait", "Poll until terminal status").option("--wait-timeout <ms>", "Poll timeout in ms (with --wait)", "600000").option("--download", "Download MP4 on success (implies --wait)").option("--output <path>", "Output MP4 path (with --download)").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
1430
2803
|
async (taskId, opts) => {
|
|
@@ -1442,7 +2815,7 @@ function registerBytePlusStatusCommand(parent) {
|
|
|
1442
2815
|
const videoUrl2 = content?.video_url;
|
|
1443
2816
|
const fileUrl = content?.file_url;
|
|
1444
2817
|
if (videoUrl2) {
|
|
1445
|
-
const outPath = opts.output ??
|
|
2818
|
+
const outPath = opts.output ?? path13.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}.mp4`);
|
|
1446
2819
|
await downloadVideo(videoUrl2, outPath, logger);
|
|
1447
2820
|
await maybeDownloadThumb(status, outPath, {
|
|
1448
2821
|
skip: opts.thumb === false,
|
|
@@ -1452,7 +2825,7 @@ function registerBytePlusStatusCommand(parent) {
|
|
|
1452
2825
|
console.log(`Saved: ${outPath}`);
|
|
1453
2826
|
} else if (fileUrl) {
|
|
1454
2827
|
const ext = inferModelFileExt(fileUrl);
|
|
1455
|
-
const outPath = opts.output ??
|
|
2828
|
+
const outPath = opts.output ?? path13.join(getOutputDir(), `byteplus-3d-${taskId.slice(0, 8)}.${ext}`);
|
|
1456
2829
|
await downloadModelFile(fileUrl, outPath, logger);
|
|
1457
2830
|
console.log(`Saved: ${outPath}`);
|
|
1458
2831
|
} else {
|
|
@@ -1461,7 +2834,7 @@ function registerBytePlusStatusCommand(parent) {
|
|
|
1461
2834
|
}
|
|
1462
2835
|
}
|
|
1463
2836
|
} catch (e) {
|
|
1464
|
-
if (e instanceof
|
|
2837
|
+
if (e instanceof PollTimeoutError2) {
|
|
1465
2838
|
logger.error(`Timed out waiting for task ${taskId}`);
|
|
1466
2839
|
process.exit(1);
|
|
1467
2840
|
}
|
|
@@ -1476,7 +2849,7 @@ function registerBytePlusStatusCommand(parent) {
|
|
|
1476
2849
|
}
|
|
1477
2850
|
|
|
1478
2851
|
// src/providers/byteplus/commands/video.ts
|
|
1479
|
-
import
|
|
2852
|
+
import path14 from "path";
|
|
1480
2853
|
function registerBytePlusVideoCommand(parent) {
|
|
1481
2854
|
parent.command("video").description("Generate a video from a text prompt via BytePlus Seedance 2.0").requiredOption("--prompt <text>", "Text prompt").option("-m, --model <id>", "Model id", BYTEPLUS_DEFAULTS.videoModel).option("--resolution <r>", "480p|720p|1080p|2k", BYTEPLUS_DEFAULTS.videoResolution).option("--duration <n>", "Duration seconds (4-15)", String(BYTEPLUS_DEFAULTS.videoDuration)).option(
|
|
1482
2855
|
"--aspect-ratio <r>",
|
|
@@ -1518,7 +2891,7 @@ function registerBytePlusVideoCommand(parent) {
|
|
|
1518
2891
|
logger.error(`Task succeeded but no video_url in response: ${JSON.stringify(final)}`);
|
|
1519
2892
|
process.exit(1);
|
|
1520
2893
|
}
|
|
1521
|
-
const outPath = opts.output ??
|
|
2894
|
+
const outPath = opts.output ?? path14.join(getOutputDir(), `byteplus-video-${taskId.slice(0, 8)}-${Date.now()}.mp4`);
|
|
1522
2895
|
await downloadVideo(url, outPath, logger);
|
|
1523
2896
|
await maybeDownloadThumb(final, outPath, {
|
|
1524
2897
|
skip: opts.thumb === false,
|
|
@@ -1529,7 +2902,7 @@ function registerBytePlusVideoCommand(parent) {
|
|
|
1529
2902
|
Task: ${taskId}`);
|
|
1530
2903
|
console.log(`Saved: ${outPath}`);
|
|
1531
2904
|
} catch (e) {
|
|
1532
|
-
if (e instanceof
|
|
2905
|
+
if (e instanceof PollTimeoutError2) {
|
|
1533
2906
|
logger.error(
|
|
1534
2907
|
`Polling timed out \u2014 try --async to keep the task and check later: multix byteplus status ${taskId}`
|
|
1535
2908
|
);
|
|
@@ -1736,15 +3109,15 @@ async function downloadVideo2(url) {
|
|
|
1736
3109
|
|
|
1737
3110
|
// src/providers/cloudflare/media-output.ts
|
|
1738
3111
|
init_errors();
|
|
1739
|
-
import
|
|
1740
|
-
import
|
|
3112
|
+
import fs12 from "fs";
|
|
3113
|
+
import path15 from "path";
|
|
1741
3114
|
function destination(filename, output) {
|
|
1742
|
-
return output ?
|
|
3115
|
+
return output ? path15.resolve(output) : path15.join(getOutputDir(), filename);
|
|
1743
3116
|
}
|
|
1744
3117
|
function saveBytes(bytes, filename, output) {
|
|
1745
3118
|
const dest = destination(filename, output);
|
|
1746
|
-
|
|
1747
|
-
|
|
3119
|
+
fs12.mkdirSync(path15.dirname(dest), { recursive: true });
|
|
3120
|
+
fs12.writeFileSync(dest, bytes);
|
|
1748
3121
|
return dest;
|
|
1749
3122
|
}
|
|
1750
3123
|
function saveBase64Image(image, output) {
|
|
@@ -1927,8 +3300,8 @@ function registerCloudflareCommands(program2) {
|
|
|
1927
3300
|
}
|
|
1928
3301
|
|
|
1929
3302
|
// src/providers/elevenlabs/client.ts
|
|
1930
|
-
import
|
|
1931
|
-
import
|
|
3303
|
+
import fs13 from "fs";
|
|
3304
|
+
import path16 from "path";
|
|
1932
3305
|
init_errors();
|
|
1933
3306
|
|
|
1934
3307
|
// src/providers/elevenlabs/models.ts
|
|
@@ -2089,13 +3462,13 @@ async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
|
|
|
2089
3462
|
return res.json();
|
|
2090
3463
|
}
|
|
2091
3464
|
function readFileAsBlob(filePath, fieldFilename) {
|
|
2092
|
-
const abs =
|
|
2093
|
-
if (!
|
|
2094
|
-
const buf =
|
|
2095
|
-
const name = fieldFilename ??
|
|
3465
|
+
const abs = path16.resolve(filePath);
|
|
3466
|
+
if (!fs13.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
3467
|
+
const buf = fs13.readFileSync(abs);
|
|
3468
|
+
const name = fieldFilename ?? path16.basename(abs);
|
|
2096
3469
|
return new File([new Uint8Array(buf)], name);
|
|
2097
3470
|
}
|
|
2098
|
-
async function
|
|
3471
|
+
async function pollUntil2(fetchStatus, isDone, isFailed, opts = {}) {
|
|
2099
3472
|
const { intervalMs = 5e3, timeoutMs = 6e5, logger, label = "task" } = opts;
|
|
2100
3473
|
const deadline = Date.now() + timeoutMs;
|
|
2101
3474
|
let elapsed = 0;
|
|
@@ -2116,12 +3489,12 @@ async function pollUntil(fetchStatus, isDone, isFailed, opts = {}) {
|
|
|
2116
3489
|
function saveBytes2(opts) {
|
|
2117
3490
|
const { bytes, task, ext, outputCopy, logger } = opts;
|
|
2118
3491
|
const dir = getOutputDir();
|
|
2119
|
-
const dest =
|
|
2120
|
-
|
|
3492
|
+
const dest = path16.join(dir, `elevenlabs_${task}_${Date.now()}.${ext}`);
|
|
3493
|
+
fs13.writeFileSync(dest, bytes);
|
|
2121
3494
|
logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
2122
3495
|
if (outputCopy) {
|
|
2123
|
-
|
|
2124
|
-
|
|
3496
|
+
fs13.mkdirSync(path16.dirname(path16.resolve(outputCopy)), { recursive: true });
|
|
3497
|
+
fs13.copyFileSync(dest, outputCopy);
|
|
2125
3498
|
}
|
|
2126
3499
|
return dest;
|
|
2127
3500
|
}
|
|
@@ -2160,14 +3533,14 @@ ${models.length} models`);
|
|
|
2160
3533
|
}
|
|
2161
3534
|
|
|
2162
3535
|
// src/providers/elevenlabs/commands/align.ts
|
|
2163
|
-
import
|
|
2164
|
-
import
|
|
3536
|
+
import fs14 from "fs";
|
|
3537
|
+
import path17 from "path";
|
|
2165
3538
|
function registerAlignCommand(parent) {
|
|
2166
3539
|
parent.command("align").description("Forced alignment of audio to a transcript (returns word/char timestamps)").requiredOption("--input <path>", "Source audio file").requiredOption("--text <str>", "Transcript text (or pass --text-file)").option("--text-file <path>", "Read transcript from file").option("--output <path>", "Save JSON to this path (default: multix-output)").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
2167
3540
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2168
3541
|
const apiKey = requireElevenLabsKey();
|
|
2169
3542
|
let text = opts.text;
|
|
2170
|
-
if (opts.textFile) text =
|
|
3543
|
+
if (opts.textFile) text = fs14.readFileSync(path17.resolve(opts.textFile), "utf8");
|
|
2171
3544
|
const form = new FormData();
|
|
2172
3545
|
form.append("file", readFileAsBlob(opts.input));
|
|
2173
3546
|
form.append("text", text);
|
|
@@ -2175,9 +3548,9 @@ function registerAlignCommand(parent) {
|
|
|
2175
3548
|
const data = await apiPostMultipart("forced-alignment", form, apiKey, {
|
|
2176
3549
|
timeoutMs: 6e5
|
|
2177
3550
|
});
|
|
2178
|
-
const dest = opts.output ?
|
|
2179
|
-
|
|
2180
|
-
|
|
3551
|
+
const dest = opts.output ? path17.resolve(opts.output) : path17.join(getOutputDir(), `elevenlabs_align_${Date.now()}.json`);
|
|
3552
|
+
fs14.mkdirSync(path17.dirname(dest), { recursive: true });
|
|
3553
|
+
fs14.writeFileSync(dest, JSON.stringify(data, null, 2));
|
|
2181
3554
|
logger.success(`Saved alignment: ${dest}`);
|
|
2182
3555
|
if (data.words) console.log(`Aligned ${data.words.length} words (loss=${data.loss ?? "?"})`);
|
|
2183
3556
|
});
|
|
@@ -2219,8 +3592,8 @@ function registerCloneCommand(parent) {
|
|
|
2219
3592
|
|
|
2220
3593
|
// src/providers/elevenlabs/commands/dub.ts
|
|
2221
3594
|
init_errors();
|
|
2222
|
-
import
|
|
2223
|
-
import
|
|
3595
|
+
import fs15 from "fs";
|
|
3596
|
+
import path18 from "path";
|
|
2224
3597
|
function registerDubCommand(parent) {
|
|
2225
3598
|
parent.command("dub").description("Dub a video/audio file into target languages (async)").option("--input <path>", "Source media file (use --input or --source-url)").option("--source-url <url>", "Public URL of source media").requiredOption("--target-lang <code>", "Target language ISO code (e.g. es, fr, de)").option("--source-lang <code>", "Source language (auto if omitted)").option("--num-speakers <n>", "Speaker count (auto if omitted)").option("--watermark", "Add ElevenLabs watermark", false).option("--start-time <s>", "Start time in seconds").option("--end-time <s>", "End time in seconds").option("--highest-resolution", "Use highest resolution video", false).option("--name <n>", "Project display name").option("--async", "Return job id immediately, do not wait", false).option("--wait", "Wait until status=dubbed (implied by --download)", false).option("--download", "Download dubbed media when ready", false).option("--poll-interval <s>", "Poll interval seconds", "10").option("--wait-timeout <ms>", "Poll timeout", "1800000").option("--output <path>", "Output path for downloaded dub").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
2226
3599
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -2250,7 +3623,7 @@ function registerDubCommand(parent) {
|
|
|
2250
3623
|
console.log("Run `multix elevenlabs dub-status <id>` to check progress.");
|
|
2251
3624
|
return;
|
|
2252
3625
|
}
|
|
2253
|
-
const final = await
|
|
3626
|
+
const final = await pollUntil2(
|
|
2254
3627
|
() => apiGet(`dubbing/${start.dubbing_id}`, apiKey),
|
|
2255
3628
|
(s) => s.status === "dubbed",
|
|
2256
3629
|
(s) => s.status === "failed",
|
|
@@ -2263,7 +3636,7 @@ function registerDubCommand(parent) {
|
|
|
2263
3636
|
);
|
|
2264
3637
|
logger.success(`Dub complete: ${start.dubbing_id}`);
|
|
2265
3638
|
if (opts.download) {
|
|
2266
|
-
const dest = opts.output ?
|
|
3639
|
+
const dest = opts.output ? path18.resolve(opts.output) : path18.join(getOutputDir(), `elevenlabs_dub_${start.dubbing_id}_${opts.targetLang}.mp4`);
|
|
2267
3640
|
const url = `${ELEVENLABS_BASE_URL}/dubbing/${start.dubbing_id}/audio/${opts.targetLang}`;
|
|
2268
3641
|
await downloadDub(url, apiKey, dest);
|
|
2269
3642
|
logger.success(`Saved: ${dest}`);
|
|
@@ -2278,7 +3651,7 @@ Dubbed file: ${dest}`);
|
|
|
2278
3651
|
const status = await apiGet(`dubbing/${dubbingId}`, apiKey);
|
|
2279
3652
|
console.log(JSON.stringify(status, null, 2));
|
|
2280
3653
|
if (opts.download) {
|
|
2281
|
-
const dest = opts.output ?
|
|
3654
|
+
const dest = opts.output ? path18.resolve(opts.output) : path18.join(getOutputDir(), `elevenlabs_dub_${dubbingId}_${opts.download}.mp4`);
|
|
2282
3655
|
const url = `${ELEVENLABS_BASE_URL}/dubbing/${dubbingId}/audio/${opts.download}`;
|
|
2283
3656
|
await downloadDub(url, apiKey, dest);
|
|
2284
3657
|
console.log(`Saved: ${dest}`);
|
|
@@ -2286,7 +3659,7 @@ Dubbed file: ${dest}`);
|
|
|
2286
3659
|
});
|
|
2287
3660
|
}
|
|
2288
3661
|
async function downloadDub(url, apiKey, dest) {
|
|
2289
|
-
|
|
3662
|
+
fs15.mkdirSync(path18.dirname(dest), { recursive: true });
|
|
2290
3663
|
const res = await globalThis.fetch(url, {
|
|
2291
3664
|
headers: { "xi-api-key": apiKey },
|
|
2292
3665
|
signal: AbortSignal.timeout(6e5)
|
|
@@ -2295,7 +3668,7 @@ async function downloadDub(url, apiKey, dest) {
|
|
|
2295
3668
|
throw new Error(`Failed to download dub: HTTP ${res.status}`);
|
|
2296
3669
|
}
|
|
2297
3670
|
const arr = new Uint8Array(await res.arrayBuffer());
|
|
2298
|
-
|
|
3671
|
+
fs15.writeFileSync(dest, arr);
|
|
2299
3672
|
void downloadFile;
|
|
2300
3673
|
}
|
|
2301
3674
|
|
|
@@ -2324,8 +3697,8 @@ Isolated audio: ${dest}`);
|
|
|
2324
3697
|
|
|
2325
3698
|
// src/providers/elevenlabs/commands/music.ts
|
|
2326
3699
|
init_errors();
|
|
2327
|
-
import
|
|
2328
|
-
import
|
|
3700
|
+
import fs16 from "fs";
|
|
3701
|
+
import path19 from "path";
|
|
2329
3702
|
function registerMusicCommand(parent) {
|
|
2330
3703
|
parent.command("music").description("Generate music from a text prompt or composition plan").option("--prompt <str>", "Music prompt (mutually exclusive with --plan)").option("--plan <path>", "Path to composition plan JSON file").option("--music-length-ms <n>", "Total length in milliseconds (default 30000)").option("--model <id>", "Music model", "music_v1").option(
|
|
2331
3704
|
"--format <fmt>",
|
|
@@ -2340,9 +3713,9 @@ function registerMusicCommand(parent) {
|
|
|
2340
3713
|
const payload = { model_id: opts.model };
|
|
2341
3714
|
if (opts.prompt) payload.prompt = opts.prompt;
|
|
2342
3715
|
if (opts.plan) {
|
|
2343
|
-
const planPath =
|
|
2344
|
-
if (!
|
|
2345
|
-
const planJson = JSON.parse(
|
|
3716
|
+
const planPath = path19.resolve(opts.plan);
|
|
3717
|
+
if (!fs16.existsSync(planPath)) throw new ValidationError(`Plan file not found: ${planPath}`);
|
|
3718
|
+
const planJson = JSON.parse(fs16.readFileSync(planPath, "utf8"));
|
|
2346
3719
|
payload.composition_plan = planJson;
|
|
2347
3720
|
}
|
|
2348
3721
|
if (opts.musicLengthMs) payload.music_length_ms = Number.parseInt(opts.musicLengthMs, 10);
|
|
@@ -2401,8 +3774,8 @@ Generated SFX: ${dest}`);
|
|
|
2401
3774
|
}
|
|
2402
3775
|
|
|
2403
3776
|
// src/providers/elevenlabs/commands/transcribe.ts
|
|
2404
|
-
import
|
|
2405
|
-
import
|
|
3777
|
+
import fs17 from "fs";
|
|
3778
|
+
import path20 from "path";
|
|
2406
3779
|
function registerTranscribeCommand(parent) {
|
|
2407
3780
|
parent.command("transcribe").description("Speech-to-text transcription with Scribe").requiredOption("--input <path>", "Audio or video file to transcribe").option(
|
|
2408
3781
|
"--model <id>",
|
|
@@ -2425,7 +3798,7 @@ function registerTranscribeCommand(parent) {
|
|
|
2425
3798
|
});
|
|
2426
3799
|
const ext = opts.format === "json" ? "json" : opts.format === "srt" ? "srt" : opts.format === "vtt" ? "vtt" : "txt";
|
|
2427
3800
|
const dir = getOutputDir();
|
|
2428
|
-
const dest = opts.output ?
|
|
3801
|
+
const dest = opts.output ? path20.resolve(opts.output) : path20.join(dir, `elevenlabs_transcribe_${Date.now()}.${ext}`);
|
|
2429
3802
|
let body;
|
|
2430
3803
|
switch (opts.format) {
|
|
2431
3804
|
case "json":
|
|
@@ -2440,8 +3813,8 @@ function registerTranscribeCommand(parent) {
|
|
|
2440
3813
|
default:
|
|
2441
3814
|
body = data.text ?? "";
|
|
2442
3815
|
}
|
|
2443
|
-
|
|
2444
|
-
|
|
3816
|
+
fs17.mkdirSync(path20.dirname(dest), { recursive: true });
|
|
3817
|
+
fs17.writeFileSync(dest, body);
|
|
2445
3818
|
logger.success(`Saved transcript: ${dest}`);
|
|
2446
3819
|
if (data.language_code) {
|
|
2447
3820
|
logger.info(
|
|
@@ -2687,8 +4060,8 @@ function registerElevenLabsCommands(program2) {
|
|
|
2687
4060
|
}
|
|
2688
4061
|
|
|
2689
4062
|
// src/providers/fal/commands/image.ts
|
|
2690
|
-
import
|
|
2691
|
-
import
|
|
4063
|
+
import fs19 from "fs";
|
|
4064
|
+
import path22 from "path";
|
|
2692
4065
|
|
|
2693
4066
|
// src/providers/fal/models.ts
|
|
2694
4067
|
var DEFAULT_FAL_BASE_URL = "https://queue.fal.run";
|
|
@@ -2702,37 +4075,8 @@ var FAL_DEFAULTS = {
|
|
|
2702
4075
|
};
|
|
2703
4076
|
|
|
2704
4077
|
// src/providers/fal/run-helpers.ts
|
|
2705
|
-
import
|
|
2706
|
-
import
|
|
2707
|
-
|
|
2708
|
-
// src/core/json-input.ts
|
|
2709
|
-
init_errors();
|
|
2710
|
-
import fs15 from "fs";
|
|
2711
|
-
function parseJsonInput(raw, flag = "--input") {
|
|
2712
|
-
let text;
|
|
2713
|
-
if (raw.startsWith("@")) {
|
|
2714
|
-
try {
|
|
2715
|
-
text = fs15.readFileSync(raw.slice(1), "utf8");
|
|
2716
|
-
} catch (e) {
|
|
2717
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2718
|
-
throw new ValidationError(`Cannot read ${flag} file: ${msg}`);
|
|
2719
|
-
}
|
|
2720
|
-
} else {
|
|
2721
|
-
text = raw;
|
|
2722
|
-
}
|
|
2723
|
-
try {
|
|
2724
|
-
const parsed = JSON.parse(text);
|
|
2725
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
2726
|
-
throw new Error("input must be a JSON object");
|
|
2727
|
-
}
|
|
2728
|
-
return parsed;
|
|
2729
|
-
} catch (e) {
|
|
2730
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
2731
|
-
throw new ValidationError(`Invalid ${flag} JSON: ${msg}`);
|
|
2732
|
-
}
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
// src/providers/fal/run-helpers.ts
|
|
4078
|
+
import fs18 from "fs";
|
|
4079
|
+
import path21 from "path";
|
|
2736
4080
|
init_redact();
|
|
2737
4081
|
|
|
2738
4082
|
// src/providers/fal/client.ts
|
|
@@ -2757,9 +4101,9 @@ var FalClient = class {
|
|
|
2757
4101
|
apiKey;
|
|
2758
4102
|
baseUrl;
|
|
2759
4103
|
/** `path` is appended verbatim to the base queue URL, e.g. "/fal-ai/flux/schnell". */
|
|
2760
|
-
async request(
|
|
4104
|
+
async request(path52, opts = {}) {
|
|
2761
4105
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
2762
|
-
const suffix =
|
|
4106
|
+
const suffix = path52.startsWith("/") ? path52 : `/${path52}`;
|
|
2763
4107
|
return this.requestAbsolute(base + suffix, opts);
|
|
2764
4108
|
}
|
|
2765
4109
|
/** Same auth/headers as `request`, but `url` is used verbatim (e.g. a status_url/response_url returned by submit). */
|
|
@@ -2776,11 +4120,11 @@ var FalClient = class {
|
|
|
2776
4120
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
2777
4121
|
});
|
|
2778
4122
|
}
|
|
2779
|
-
get(
|
|
2780
|
-
return this.request(
|
|
4123
|
+
get(path52, logger) {
|
|
4124
|
+
return this.request(path52, { method: "GET", logger });
|
|
2781
4125
|
}
|
|
2782
|
-
post(
|
|
2783
|
-
return this.request(
|
|
4126
|
+
post(path52, body, logger) {
|
|
4127
|
+
return this.request(path52, { method: "POST", body, logger });
|
|
2784
4128
|
}
|
|
2785
4129
|
getAbsolute(url, logger) {
|
|
2786
4130
|
return this.requestAbsolute(url, { method: "GET", logger });
|
|
@@ -2922,7 +4266,7 @@ function extractMediaUrls(result) {
|
|
|
2922
4266
|
function extFromUrl(url, fallback = ".bin") {
|
|
2923
4267
|
try {
|
|
2924
4268
|
const u = new URL(url);
|
|
2925
|
-
const e =
|
|
4269
|
+
const e = path21.extname(u.pathname);
|
|
2926
4270
|
return e.length > 0 ? e : fallback;
|
|
2927
4271
|
} catch {
|
|
2928
4272
|
return fallback;
|
|
@@ -2935,13 +4279,13 @@ async function downloadResultMedia(urls, outDir, requestId, logger) {
|
|
|
2935
4279
|
if (!url) continue;
|
|
2936
4280
|
const ext = extFromUrl(url);
|
|
2937
4281
|
const filename = `fal-${requestId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
2938
|
-
const filepath =
|
|
4282
|
+
const filepath = path21.join(outDir, filename);
|
|
2939
4283
|
try {
|
|
2940
4284
|
await downloadFile(url, filepath);
|
|
2941
4285
|
saved.push(filepath);
|
|
2942
4286
|
logger.success(`Saved ${filepath}`);
|
|
2943
4287
|
} catch (e) {
|
|
2944
|
-
|
|
4288
|
+
fs18.rmSync(filepath, { force: true });
|
|
2945
4289
|
logger.warn(`Download failed for ${redactUrl(url)}: ${e instanceof Error ? e.message : e}`);
|
|
2946
4290
|
}
|
|
2947
4291
|
}
|
|
@@ -2986,9 +4330,9 @@ function registerFalImageCommand(parent) {
|
|
|
2986
4330
|
process.exit(1);
|
|
2987
4331
|
}
|
|
2988
4332
|
if (opts.output && saved[0]) {
|
|
2989
|
-
const dest =
|
|
2990
|
-
|
|
2991
|
-
|
|
4333
|
+
const dest = path22.resolve(opts.output);
|
|
4334
|
+
fs19.mkdirSync(path22.dirname(dest), { recursive: true });
|
|
4335
|
+
fs19.copyFileSync(saved[0], dest);
|
|
2992
4336
|
logger.success(`Copied to ${dest}`);
|
|
2993
4337
|
}
|
|
2994
4338
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -3143,9 +4487,9 @@ function registerFalCommands(program2) {
|
|
|
3143
4487
|
|
|
3144
4488
|
// src/core/stdin-file.ts
|
|
3145
4489
|
init_errors();
|
|
3146
|
-
import
|
|
4490
|
+
import fs20 from "fs";
|
|
3147
4491
|
import os2 from "os";
|
|
3148
|
-
import
|
|
4492
|
+
import path23 from "path";
|
|
3149
4493
|
var STDIN_ARG = "-";
|
|
3150
4494
|
var STDIN_LABEL = "<stdin>";
|
|
3151
4495
|
var HEAD_BYTES = 4096;
|
|
@@ -3241,7 +4585,7 @@ function trackTempDir(dir) {
|
|
|
3241
4585
|
let done = false;
|
|
3242
4586
|
const remove = () => {
|
|
3243
4587
|
try {
|
|
3244
|
-
|
|
4588
|
+
fs20.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
3245
4589
|
} catch {
|
|
3246
4590
|
}
|
|
3247
4591
|
};
|
|
@@ -3269,9 +4613,9 @@ async function stdinToTempFile(opts = {}) {
|
|
|
3269
4613
|
}
|
|
3270
4614
|
const forcedExt = opts.ext ? normalizeExt(opts.ext) : void 0;
|
|
3271
4615
|
const maxBytes = opts.maxBytes ?? MAX_STDIN_BYTES;
|
|
3272
|
-
const dir =
|
|
4616
|
+
const dir = fs20.mkdtempSync(path23.join(os2.tmpdir(), "multix-stdin-"));
|
|
3273
4617
|
const cleanup = trackTempDir(dir);
|
|
3274
|
-
const rawPath =
|
|
4618
|
+
const rawPath = path23.join(dir, "input");
|
|
3275
4619
|
const hint = setTimeout(() => {
|
|
3276
4620
|
process.stderr.write("Waiting for data on stdin (`--files -`)... press Ctrl+C to cancel\n");
|
|
3277
4621
|
}, WAIT_HINT_MS);
|
|
@@ -3279,7 +4623,7 @@ async function stdinToTempFile(opts = {}) {
|
|
|
3279
4623
|
let head = Buffer.alloc(0);
|
|
3280
4624
|
let size = 0;
|
|
3281
4625
|
try {
|
|
3282
|
-
const fd =
|
|
4626
|
+
const fd = fs20.openSync(rawPath, "w", 384);
|
|
3283
4627
|
try {
|
|
3284
4628
|
for await (const chunk of input) {
|
|
3285
4629
|
clearTimeout(hint);
|
|
@@ -3293,17 +4637,17 @@ async function stdinToTempFile(opts = {}) {
|
|
|
3293
4637
|
head = Buffer.concat([head, buf.subarray(0, HEAD_BYTES - head.length)]);
|
|
3294
4638
|
}
|
|
3295
4639
|
let offset = 0;
|
|
3296
|
-
while (offset < buf.length) offset +=
|
|
4640
|
+
while (offset < buf.length) offset += fs20.writeSync(fd, buf, offset);
|
|
3297
4641
|
size += buf.length;
|
|
3298
4642
|
}
|
|
3299
4643
|
} finally {
|
|
3300
4644
|
clearTimeout(hint);
|
|
3301
|
-
|
|
4645
|
+
fs20.closeSync(fd);
|
|
3302
4646
|
}
|
|
3303
4647
|
if (size === 0) throw new ValidationError("`--files -` was given but stdin was empty");
|
|
3304
4648
|
const detectedExt = forcedExt ?? detectExtension(head);
|
|
3305
|
-
const finalPath =
|
|
3306
|
-
|
|
4649
|
+
const finalPath = path23.join(dir, `stdin${detectedExt ?? ".bin"}`);
|
|
4650
|
+
fs20.renameSync(rawPath, finalPath);
|
|
3307
4651
|
return { path: finalPath, size, detected: detectedExt !== void 0, cleanup };
|
|
3308
4652
|
} catch (error) {
|
|
3309
4653
|
cleanup();
|
|
@@ -3320,7 +4664,7 @@ async function resolveFileInputs(files, opts = {}) {
|
|
|
3320
4664
|
throw new ValidationError("--stdin-ext only applies when --files includes `-`");
|
|
3321
4665
|
}
|
|
3322
4666
|
for (const f of files) {
|
|
3323
|
-
if (f !== STDIN_ARG && !
|
|
4667
|
+
if (f !== STDIN_ARG && !fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
3324
4668
|
}
|
|
3325
4669
|
if (stdinCount === 0) {
|
|
3326
4670
|
return { inputs: files.map((f) => ({ path: f, label: f })), cleanup: () => {
|
|
@@ -3347,8 +4691,8 @@ async function resolveFileInputs(files, opts = {}) {
|
|
|
3347
4691
|
|
|
3348
4692
|
// src/providers/gemini/client.ts
|
|
3349
4693
|
init_errors();
|
|
3350
|
-
import
|
|
3351
|
-
import
|
|
4694
|
+
import fs21 from "fs";
|
|
4695
|
+
import path25 from "path";
|
|
3352
4696
|
|
|
3353
4697
|
// src/providers/gemini/key-pool.ts
|
|
3354
4698
|
init_errors();
|
|
@@ -3414,7 +4758,7 @@ async function withGeminiKey(fn, opts = {}) {
|
|
|
3414
4758
|
}
|
|
3415
4759
|
|
|
3416
4760
|
// src/providers/gemini/task-resolver.ts
|
|
3417
|
-
import
|
|
4761
|
+
import path24 from "path";
|
|
3418
4762
|
var MIME_MAP = {
|
|
3419
4763
|
// Audio
|
|
3420
4764
|
".mp3": "audio/mp3",
|
|
@@ -3455,7 +4799,7 @@ var MIME_MAP = {
|
|
|
3455
4799
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
3456
4800
|
};
|
|
3457
4801
|
function getMimeType(filePath) {
|
|
3458
|
-
const ext =
|
|
4802
|
+
const ext = path24.extname(filePath).toLowerCase();
|
|
3459
4803
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
3460
4804
|
}
|
|
3461
4805
|
function requiresProcessingWait(mimeType) {
|
|
@@ -3478,8 +4822,8 @@ async function uploadFile(filePath, opts = {}) {
|
|
|
3478
4822
|
async function uploadFileWithKey(filePath, apiKey, opts) {
|
|
3479
4823
|
const { timeoutMs = 3e5, logger } = opts;
|
|
3480
4824
|
const mimeType = getMimeType(filePath);
|
|
3481
|
-
const fileBytes =
|
|
3482
|
-
const displayName =
|
|
4825
|
+
const fileBytes = fs21.readFileSync(filePath);
|
|
4826
|
+
const displayName = path25.basename(filePath);
|
|
3483
4827
|
const boundary = `multix_${Date.now()}`;
|
|
3484
4828
|
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
3485
4829
|
const body = Buffer.concat([
|
|
@@ -3596,12 +4940,12 @@ function extractImages(resp) {
|
|
|
3596
4940
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
3597
4941
|
}
|
|
3598
4942
|
function inlinePart(filePath) {
|
|
3599
|
-
const data =
|
|
4943
|
+
const data = fs21.readFileSync(filePath).toString("base64");
|
|
3600
4944
|
const mimeType = getMimeType(filePath);
|
|
3601
4945
|
return { inlineData: { mimeType, data } };
|
|
3602
4946
|
}
|
|
3603
4947
|
function shouldUseFileApi(filePath) {
|
|
3604
|
-
const stat =
|
|
4948
|
+
const stat = fs21.statSync(filePath);
|
|
3605
4949
|
return stat.size > 20 * 1024 * 1024;
|
|
3606
4950
|
}
|
|
3607
4951
|
function sleep2(ms) {
|
|
@@ -3655,13 +4999,13 @@ function getDefaultModel(task) {
|
|
|
3655
4999
|
}
|
|
3656
5000
|
|
|
3657
5001
|
// src/providers/gemini/result-formatter.ts
|
|
3658
|
-
import
|
|
3659
|
-
import
|
|
5002
|
+
import fs22 from "fs";
|
|
5003
|
+
import path26 from "path";
|
|
3660
5004
|
function formatResults(results, format, outputPath) {
|
|
3661
5005
|
const content = renderFormat(results, format);
|
|
3662
5006
|
if (outputPath) {
|
|
3663
|
-
|
|
3664
|
-
|
|
5007
|
+
fs22.mkdirSync(path26.dirname(path26.resolve(outputPath)), { recursive: true });
|
|
5008
|
+
fs22.writeFileSync(outputPath, content, "utf8");
|
|
3665
5009
|
} else {
|
|
3666
5010
|
process.stdout.write(content);
|
|
3667
5011
|
}
|
|
@@ -3845,8 +5189,8 @@ function registerExtractCommand(parent) {
|
|
|
3845
5189
|
init_errors();
|
|
3846
5190
|
|
|
3847
5191
|
// src/providers/gemini/generators/speech.ts
|
|
3848
|
-
import
|
|
3849
|
-
import
|
|
5192
|
+
import fs23 from "fs";
|
|
5193
|
+
import path27 from "path";
|
|
3850
5194
|
|
|
3851
5195
|
// src/providers/gemini/voices.ts
|
|
3852
5196
|
var GEMINI_TTS_VOICES = [
|
|
@@ -4012,12 +5356,12 @@ function extractWavData(wav) {
|
|
|
4012
5356
|
}
|
|
4013
5357
|
function saveSpeech(outBytes, outputFormat, output, logger) {
|
|
4014
5358
|
const outDir = getOutputDir();
|
|
4015
|
-
const dest =
|
|
4016
|
-
|
|
5359
|
+
const dest = path27.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
5360
|
+
fs23.writeFileSync(dest, outBytes);
|
|
4017
5361
|
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
4018
5362
|
if (output) {
|
|
4019
|
-
|
|
4020
|
-
|
|
5363
|
+
fs23.mkdirSync(path27.dirname(path27.resolve(output)), { recursive: true });
|
|
5364
|
+
fs23.copyFileSync(dest, output);
|
|
4021
5365
|
logger?.success(`Copied to: ${output}`);
|
|
4022
5366
|
}
|
|
4023
5367
|
return dest;
|
|
@@ -4130,84 +5474,24 @@ function collectSpeakers(value, prev) {
|
|
|
4130
5474
|
|
|
4131
5475
|
// src/providers/gemini/commands/generate-video.ts
|
|
4132
5476
|
init_errors();
|
|
4133
|
-
import
|
|
4134
|
-
import
|
|
4135
|
-
|
|
4136
|
-
// src/providers/gemini/veo.ts
|
|
4137
|
-
import fs22 from "fs";
|
|
4138
|
-
|
|
4139
|
-
// src/core/async-poll.ts
|
|
4140
|
-
init_errors();
|
|
4141
|
-
var PollTimeoutError2 = class extends MultixError {
|
|
4142
|
-
constructor(message, lastStatus) {
|
|
4143
|
-
super(message, "POLL_TIMEOUT");
|
|
4144
|
-
this.lastStatus = lastStatus;
|
|
4145
|
-
this.name = "PollTimeoutError";
|
|
4146
|
-
}
|
|
4147
|
-
lastStatus;
|
|
4148
|
-
};
|
|
4149
|
-
function isTransientHttpError(error) {
|
|
4150
|
-
if (!(error instanceof HttpError)) return false;
|
|
4151
|
-
return error.status === 0 || error.status === 429 || error.status >= 500;
|
|
4152
|
-
}
|
|
4153
|
-
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4154
|
-
async function pollUntil2(opts) {
|
|
4155
|
-
const now = opts.now ?? Date.now;
|
|
4156
|
-
const sleep5 = opts.sleep ?? defaultSleep;
|
|
4157
|
-
const random = opts.random ?? Math.random;
|
|
4158
|
-
const isTransient = opts.isTransientError ?? isTransientHttpError;
|
|
4159
|
-
const maxInterval = opts.maxIntervalMs ?? 15e3;
|
|
4160
|
-
const minInterval = opts.minIntervalMs ?? 500;
|
|
4161
|
-
const factor = opts.backoffFactor ?? 1.5;
|
|
4162
|
-
const jitter = opts.jitter ?? true;
|
|
4163
|
-
const deadline = now() + opts.timeoutMs;
|
|
4164
|
-
let interval = opts.intervalMs ?? 3e3;
|
|
4165
|
-
let attempt = 0;
|
|
4166
|
-
let last;
|
|
4167
|
-
let lastError;
|
|
4168
|
-
for (; ; ) {
|
|
4169
|
-
attempt++;
|
|
4170
|
-
let retryAfterMs;
|
|
4171
|
-
try {
|
|
4172
|
-
last = await opts.fetchStatus({ remainingMs: Math.max(1, deadline - now()), attempt });
|
|
4173
|
-
lastError = void 0;
|
|
4174
|
-
opts.onPoll?.(last, attempt);
|
|
4175
|
-
if (opts.isTerminal(last)) return last;
|
|
4176
|
-
} catch (error) {
|
|
4177
|
-
if (!isTransient(error)) throw error;
|
|
4178
|
-
lastError = error;
|
|
4179
|
-
if (error instanceof HttpError) retryAfterMs = error.retryAfterMs;
|
|
4180
|
-
}
|
|
4181
|
-
const remaining = deadline - now();
|
|
4182
|
-
if (remaining <= 0) {
|
|
4183
|
-
const hint = opts.resumeHint ? ` ${opts.resumeHint}` : "";
|
|
4184
|
-
const cause = lastError instanceof Error ? ` Last error: ${lastError.message}.` : "";
|
|
4185
|
-
throw new PollTimeoutError2(
|
|
4186
|
-
`Timed out after ${opts.timeoutMs}ms waiting for job.${cause}${hint}`,
|
|
4187
|
-
last
|
|
4188
|
-
);
|
|
4189
|
-
}
|
|
4190
|
-
let delay = retryAfterMs ?? interval;
|
|
4191
|
-
if (jitter && retryAfterMs === void 0) delay *= 0.8 + random() * 0.4;
|
|
4192
|
-
await sleep5(Math.min(Math.max(delay, minInterval), remaining));
|
|
4193
|
-
interval = Math.min(interval * factor, maxInterval);
|
|
4194
|
-
}
|
|
4195
|
-
}
|
|
5477
|
+
import fs25 from "fs";
|
|
5478
|
+
import path28 from "path";
|
|
4196
5479
|
|
|
4197
5480
|
// src/providers/gemini/veo.ts
|
|
5481
|
+
import fs24 from "fs";
|
|
4198
5482
|
init_errors();
|
|
4199
5483
|
var API_HOST = "generativelanguage.googleapis.com";
|
|
4200
5484
|
var BASE2 = `https://${API_HOST}/v1beta`;
|
|
4201
5485
|
var VEO_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
4202
5486
|
var VEO_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
4203
5487
|
function veoImageFromFile(imgPath) {
|
|
4204
|
-
if (!
|
|
5488
|
+
if (!fs24.existsSync(imgPath)) throw new ValidationError(`Image not found: ${imgPath}`);
|
|
4205
5489
|
const detected = getMimeType(imgPath);
|
|
4206
5490
|
const mimeType = detected === "application/octet-stream" ? "image/jpeg" : detected;
|
|
4207
5491
|
if (!VEO_IMAGE_TYPES.has(mimeType)) {
|
|
4208
5492
|
throw new ValidationError(`Veo accepts JPEG, PNG or WebP images: ${imgPath} (${mimeType})`);
|
|
4209
5493
|
}
|
|
4210
|
-
return { bytesBase64Encoded:
|
|
5494
|
+
return { bytesBase64Encoded: fs24.readFileSync(imgPath).toString("base64"), mimeType };
|
|
4211
5495
|
}
|
|
4212
5496
|
function veoFramesFromPaths(paths = []) {
|
|
4213
5497
|
if (paths.length > 2) {
|
|
@@ -4256,7 +5540,7 @@ async function submitVeo(apiKey, model, body) {
|
|
|
4256
5540
|
}
|
|
4257
5541
|
async function waitForVeo(apiKey, operation, logger, opts = {}) {
|
|
4258
5542
|
if (operation.done) return checkVeoResult(operation);
|
|
4259
|
-
const finalOp = await
|
|
5543
|
+
const finalOp = await pollUntil({
|
|
4260
5544
|
fetchStatus: ({ remainingMs }) => httpJson({
|
|
4261
5545
|
url: `${BASE2}/${operation.name}`,
|
|
4262
5546
|
headers: headers(apiKey),
|
|
@@ -4332,7 +5616,7 @@ function registerGenerateVideoCommand(parent) {
|
|
|
4332
5616
|
logger.debug(`Submitting video generation with ${model}...`);
|
|
4333
5617
|
logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
|
|
4334
5618
|
const frames = veoFramesFromPaths(opts.referenceImages);
|
|
4335
|
-
const dest =
|
|
5619
|
+
const dest = path28.join(getOutputDir(), `veo_generated_${Date.now()}.mp4`);
|
|
4336
5620
|
const finalOp = await runVeo(
|
|
4337
5621
|
model,
|
|
4338
5622
|
{
|
|
@@ -4346,8 +5630,8 @@ function registerGenerateVideoCommand(parent) {
|
|
|
4346
5630
|
);
|
|
4347
5631
|
logger.success(`Saved video: ${dest}`);
|
|
4348
5632
|
if (opts.output) {
|
|
4349
|
-
|
|
4350
|
-
|
|
5633
|
+
fs25.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
|
|
5634
|
+
fs25.copyFileSync(dest, opts.output);
|
|
4351
5635
|
logger.success(`Copied to: ${opts.output}`);
|
|
4352
5636
|
}
|
|
4353
5637
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -4363,8 +5647,8 @@ Generated video: ${dest}`);
|
|
|
4363
5647
|
|
|
4364
5648
|
// src/providers/gemini/commands/generate.ts
|
|
4365
5649
|
init_errors();
|
|
4366
|
-
import
|
|
4367
|
-
import
|
|
5650
|
+
import fs26 from "fs";
|
|
5651
|
+
import path29 from "path";
|
|
4368
5652
|
function registerGenerateCommand(parent) {
|
|
4369
5653
|
parent.command("generate").description("Generate images with Gemini (Nano Banana / Imagen 4)").requiredOption("--prompt <text>", "Image generation prompt").option("--model <id>", "Gemini image model to use").option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--num-images <n>", "Number of images to generate (1-4)", "1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Copy first generated image to this path").addOption(imageFormatOption()).addOption(noWebpOption()).option("-v, --verbose", "Verbose logging").action(
|
|
4370
5654
|
async (opts) => {
|
|
@@ -4409,8 +5693,8 @@ function registerGenerateCommand(parent) {
|
|
|
4409
5693
|
const outDir = getOutputDir();
|
|
4410
5694
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
4411
5695
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
4412
|
-
const dest =
|
|
4413
|
-
|
|
5696
|
+
const dest = path29.join(outDir, fname);
|
|
5697
|
+
fs26.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
4414
5698
|
savedFiles.push(dest);
|
|
4415
5699
|
logger.success(`Saved: ${dest}`);
|
|
4416
5700
|
}
|
|
@@ -4425,8 +5709,8 @@ function registerGenerateCommand(parent) {
|
|
|
4425
5709
|
process.exit(1);
|
|
4426
5710
|
}
|
|
4427
5711
|
if (opts.output && savedFiles[0]) {
|
|
4428
|
-
|
|
4429
|
-
|
|
5712
|
+
fs26.mkdirSync(path29.dirname(path29.resolve(opts.output)), { recursive: true });
|
|
5713
|
+
fs26.copyFileSync(savedFiles[0], opts.output);
|
|
4430
5714
|
logger.success(`Copied to: ${opts.output}`);
|
|
4431
5715
|
}
|
|
4432
5716
|
const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
|
|
@@ -4438,8 +5722,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4438
5722
|
}
|
|
4439
5723
|
|
|
4440
5724
|
// src/providers/gemini/commands/image-to-image.ts
|
|
4441
|
-
import
|
|
4442
|
-
import
|
|
5725
|
+
import fs27 from "fs";
|
|
5726
|
+
import path30 from "path";
|
|
4443
5727
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
4444
5728
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
4445
5729
|
function registerGeminiImageToImageCommand(parent) {
|
|
@@ -4500,14 +5784,14 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
4500
5784
|
const img = images[i];
|
|
4501
5785
|
if (!img) continue;
|
|
4502
5786
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
4503
|
-
const dest =
|
|
4504
|
-
|
|
5787
|
+
const dest = path30.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
5788
|
+
fs27.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
4505
5789
|
saved.push(dest);
|
|
4506
5790
|
logger.success(`Saved: ${dest}`);
|
|
4507
5791
|
}
|
|
4508
5792
|
if (opts.output && saved[0]) {
|
|
4509
|
-
|
|
4510
|
-
|
|
5793
|
+
fs27.mkdirSync(path30.dirname(path30.resolve(opts.output)), { recursive: true });
|
|
5794
|
+
fs27.copyFileSync(saved[0], opts.output);
|
|
4511
5795
|
logger.success(`Copied to: ${opts.output}`);
|
|
4512
5796
|
}
|
|
4513
5797
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4535,7 +5819,7 @@ function sniffImageMime(buf, url) {
|
|
|
4535
5819
|
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
4536
5820
|
return "image/webp";
|
|
4537
5821
|
}
|
|
4538
|
-
const ext =
|
|
5822
|
+
const ext = path30.extname(new URL(url).pathname).toLowerCase();
|
|
4539
5823
|
if (ext === ".png") return "image/png";
|
|
4540
5824
|
if (ext === ".webp") return "image/webp";
|
|
4541
5825
|
if (ext === ".gif") return "image/gif";
|
|
@@ -4544,8 +5828,8 @@ function sniffImageMime(buf, url) {
|
|
|
4544
5828
|
|
|
4545
5829
|
// src/providers/gemini/commands/image-to-video.ts
|
|
4546
5830
|
init_errors();
|
|
4547
|
-
import
|
|
4548
|
-
import
|
|
5831
|
+
import fs28 from "fs";
|
|
5832
|
+
import path31 from "path";
|
|
4549
5833
|
function registerGeminiImageToVideoCommand(parent) {
|
|
4550
5834
|
parent.command("image-to-video <imagePath>").alias("i2v").description("Generate video from a starting image with Veo (image-to-video) [EXPERIMENTAL]").requiredOption("--prompt <text>", "Motion/scene prompt").option("--last-frame <path>", "Optional closing frame image (Veo 3.1)").option("--model <id>", "Veo model id").option("--resolution <res>", "720p|1080p", "1080p").option("--aspect-ratio <ratio>", "Aspect ratio", "16:9").option("--output <path>", "Copy generated video to this path").option("--no-thumb", "Skip downloading the thumbnail if the API returns one").option("-v, --verbose", "Verbose logging").action(
|
|
4551
5835
|
async (imagePath, opts) => {
|
|
@@ -4565,7 +5849,7 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
4565
5849
|
logger.info(
|
|
4566
5850
|
"Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
|
|
4567
5851
|
);
|
|
4568
|
-
const dest =
|
|
5852
|
+
const dest = path31.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
4569
5853
|
const finalOp = await runVeo(
|
|
4570
5854
|
model,
|
|
4571
5855
|
{
|
|
@@ -4580,8 +5864,8 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
4580
5864
|
);
|
|
4581
5865
|
logger.success(`Saved video: ${dest}`);
|
|
4582
5866
|
if (opts.output) {
|
|
4583
|
-
|
|
4584
|
-
|
|
5867
|
+
fs28.mkdirSync(path31.dirname(path31.resolve(opts.output)), { recursive: true });
|
|
5868
|
+
fs28.copyFileSync(dest, opts.output);
|
|
4585
5869
|
logger.success(`Copied to: ${opts.output}`);
|
|
4586
5870
|
}
|
|
4587
5871
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -4674,8 +5958,8 @@ function registerGeminiCommands(program2) {
|
|
|
4674
5958
|
}
|
|
4675
5959
|
|
|
4676
5960
|
// src/providers/leonardo/commands/generate.ts
|
|
4677
|
-
import
|
|
4678
|
-
import
|
|
5961
|
+
import fs29 from "fs";
|
|
5962
|
+
import path32 from "path";
|
|
4679
5963
|
|
|
4680
5964
|
// src/providers/leonardo/client.ts
|
|
4681
5965
|
init_errors();
|
|
@@ -4748,11 +6032,11 @@ var LeonardoClient = class {
|
|
|
4748
6032
|
}
|
|
4749
6033
|
apiKey;
|
|
4750
6034
|
baseUrl;
|
|
4751
|
-
async request(
|
|
6035
|
+
async request(path52, opts = {}) {
|
|
4752
6036
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
4753
|
-
const v2Override =
|
|
6037
|
+
const v2Override = path52.startsWith("v2:");
|
|
4754
6038
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
4755
|
-
const finalPath = v2Override ?
|
|
6039
|
+
const finalPath = v2Override ? path52.slice(3) : path52;
|
|
4756
6040
|
const url = new URL(finalBase + finalPath);
|
|
4757
6041
|
if (opts.query) {
|
|
4758
6042
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -4771,14 +6055,14 @@ var LeonardoClient = class {
|
|
|
4771
6055
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
4772
6056
|
});
|
|
4773
6057
|
}
|
|
4774
|
-
get(
|
|
4775
|
-
return this.request(
|
|
6058
|
+
get(path52, query, logger) {
|
|
6059
|
+
return this.request(path52, { method: "GET", query, logger });
|
|
4776
6060
|
}
|
|
4777
|
-
post(
|
|
4778
|
-
return this.request(
|
|
6061
|
+
post(path52, body, logger) {
|
|
6062
|
+
return this.request(path52, { method: "POST", body, logger });
|
|
4779
6063
|
}
|
|
4780
|
-
delete(
|
|
4781
|
-
return this.request(
|
|
6064
|
+
delete(path52, logger) {
|
|
6065
|
+
return this.request(path52, { method: "DELETE", logger });
|
|
4782
6066
|
}
|
|
4783
6067
|
};
|
|
4784
6068
|
function createLeonardoClient() {
|
|
@@ -4789,7 +6073,7 @@ function createLeonardoClient() {
|
|
|
4789
6073
|
function extFromUrl2(url, fallback = ".png") {
|
|
4790
6074
|
try {
|
|
4791
6075
|
const u = new URL(url);
|
|
4792
|
-
const e =
|
|
6076
|
+
const e = path32.extname(u.pathname);
|
|
4793
6077
|
return e.length > 0 ? e : fallback;
|
|
4794
6078
|
} catch {
|
|
4795
6079
|
return fallback;
|
|
@@ -4874,7 +6158,7 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
4874
6158
|
if (!img) continue;
|
|
4875
6159
|
const ext = extFromUrl2(img.url);
|
|
4876
6160
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
4877
|
-
const filepath =
|
|
6161
|
+
const filepath = path32.join(outDir, filename);
|
|
4878
6162
|
try {
|
|
4879
6163
|
await downloadFile(img.url, filepath);
|
|
4880
6164
|
saved.push(filepath);
|
|
@@ -4884,9 +6168,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
4884
6168
|
}
|
|
4885
6169
|
}
|
|
4886
6170
|
if (opts.output && saved[0]) {
|
|
4887
|
-
const dest =
|
|
4888
|
-
|
|
4889
|
-
|
|
6171
|
+
const dest = path32.resolve(opts.output);
|
|
6172
|
+
fs29.mkdirSync(path32.dirname(dest), { recursive: true });
|
|
6173
|
+
fs29.copyFileSync(saved[0], dest);
|
|
4890
6174
|
logger.success(`Copied to ${dest}`);
|
|
4891
6175
|
}
|
|
4892
6176
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4898,11 +6182,11 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4898
6182
|
}
|
|
4899
6183
|
|
|
4900
6184
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
4901
|
-
import
|
|
4902
|
-
import
|
|
6185
|
+
import fs30 from "fs";
|
|
6186
|
+
import path33 from "path";
|
|
4903
6187
|
function extFromUrl3(url, fallback = ".png") {
|
|
4904
6188
|
try {
|
|
4905
|
-
return
|
|
6189
|
+
return path33.extname(new URL(url).pathname) || fallback;
|
|
4906
6190
|
} catch {
|
|
4907
6191
|
return fallback;
|
|
4908
6192
|
}
|
|
@@ -4964,7 +6248,7 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
4964
6248
|
if (!img) continue;
|
|
4965
6249
|
const ext = extFromUrl3(img.url);
|
|
4966
6250
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
4967
|
-
const filepath =
|
|
6251
|
+
const filepath = path33.join(outDir, filename);
|
|
4968
6252
|
try {
|
|
4969
6253
|
await downloadFile(img.url, filepath);
|
|
4970
6254
|
saved.push(filepath);
|
|
@@ -4974,9 +6258,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
4974
6258
|
}
|
|
4975
6259
|
}
|
|
4976
6260
|
if (opts.output && saved[0]) {
|
|
4977
|
-
const dest =
|
|
4978
|
-
|
|
4979
|
-
|
|
6261
|
+
const dest = path33.resolve(opts.output);
|
|
6262
|
+
fs30.mkdirSync(path33.dirname(dest), { recursive: true });
|
|
6263
|
+
fs30.copyFileSync(saved[0], dest);
|
|
4980
6264
|
logger.success(`Copied to ${dest}`);
|
|
4981
6265
|
}
|
|
4982
6266
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4988,7 +6272,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4988
6272
|
}
|
|
4989
6273
|
|
|
4990
6274
|
// src/providers/leonardo/commands/video.ts
|
|
4991
|
-
import
|
|
6275
|
+
import path34 from "path";
|
|
4992
6276
|
function v2DimsFor(resolution) {
|
|
4993
6277
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
4994
6278
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -5073,7 +6357,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
5073
6357
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
5074
6358
|
});
|
|
5075
6359
|
} catch (e) {
|
|
5076
|
-
if (e instanceof
|
|
6360
|
+
if (e instanceof PollTimeoutError2) {
|
|
5077
6361
|
logger.error(`Timed out \u2014 multix leonardo status ${jobId}`);
|
|
5078
6362
|
process.exit(1);
|
|
5079
6363
|
}
|
|
@@ -5099,7 +6383,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
5099
6383
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
5100
6384
|
process.exit(1);
|
|
5101
6385
|
}
|
|
5102
|
-
const outPath = opts.output ??
|
|
6386
|
+
const outPath = opts.output ?? path34.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
5103
6387
|
await downloadFile(videoUrl2, outPath);
|
|
5104
6388
|
logger.success(`Saved ${outPath}`);
|
|
5105
6389
|
await maybeDownloadThumb(gen, outPath, {
|
|
@@ -5205,7 +6489,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
5205
6489
|
}
|
|
5206
6490
|
|
|
5207
6491
|
// src/providers/leonardo/commands/status.ts
|
|
5208
|
-
import
|
|
6492
|
+
import path35 from "path";
|
|
5209
6493
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
5210
6494
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
5211
6495
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -5245,7 +6529,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
5245
6529
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
5246
6530
|
}) : await fetchOnce();
|
|
5247
6531
|
} catch (e) {
|
|
5248
|
-
if (e instanceof
|
|
6532
|
+
if (e instanceof PollTimeoutError2) {
|
|
5249
6533
|
logger.error(`Timed out waiting for generation ${generationId}`);
|
|
5250
6534
|
process.exit(1);
|
|
5251
6535
|
}
|
|
@@ -5270,7 +6554,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
5270
6554
|
const outDir = getOutputDir();
|
|
5271
6555
|
let idx = 0;
|
|
5272
6556
|
for (const url of videos) {
|
|
5273
|
-
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output :
|
|
6557
|
+
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path35.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
|
|
5274
6558
|
await downloadFile(url, dest);
|
|
5275
6559
|
logger.success(`Saved ${dest}`);
|
|
5276
6560
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -5283,7 +6567,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
5283
6567
|
for (const url of images) {
|
|
5284
6568
|
const m = url.match(IMAGE_EXT_RE2);
|
|
5285
6569
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
5286
|
-
const dest =
|
|
6570
|
+
const dest = path35.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
5287
6571
|
await downloadFile(url, dest);
|
|
5288
6572
|
logger.success(`Saved ${dest}`);
|
|
5289
6573
|
idx++;
|
|
@@ -5440,8 +6724,8 @@ function sleep3(ms) {
|
|
|
5440
6724
|
}
|
|
5441
6725
|
|
|
5442
6726
|
// src/providers/minimax/generators/music.ts
|
|
5443
|
-
import
|
|
5444
|
-
import
|
|
6727
|
+
import fs31 from "fs";
|
|
6728
|
+
import path36 from "path";
|
|
5445
6729
|
async function generateMinimaxMusic(opts) {
|
|
5446
6730
|
const {
|
|
5447
6731
|
apiKey,
|
|
@@ -5486,14 +6770,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
5486
6770
|
}
|
|
5487
6771
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
5488
6772
|
const outDir = getOutputDir();
|
|
5489
|
-
const dest =
|
|
6773
|
+
const dest = path36.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
5490
6774
|
try {
|
|
5491
6775
|
if (audioData.startsWith("http")) {
|
|
5492
6776
|
const bytes = await fetchBytes(audioData);
|
|
5493
|
-
|
|
6777
|
+
fs31.writeFileSync(dest, bytes);
|
|
5494
6778
|
} else {
|
|
5495
6779
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
5496
|
-
|
|
6780
|
+
fs31.writeFileSync(dest, audioBytes);
|
|
5497
6781
|
}
|
|
5498
6782
|
} catch (e) {
|
|
5499
6783
|
return {
|
|
@@ -5504,8 +6788,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
5504
6788
|
const durSec = durationMs / 1e3;
|
|
5505
6789
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
5506
6790
|
if (output) {
|
|
5507
|
-
|
|
5508
|
-
|
|
6791
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
|
|
6792
|
+
fs31.copyFileSync(dest, output);
|
|
5509
6793
|
}
|
|
5510
6794
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
5511
6795
|
}
|
|
@@ -5579,8 +6863,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
5579
6863
|
init_errors();
|
|
5580
6864
|
|
|
5581
6865
|
// src/providers/minimax/generators/speech.ts
|
|
5582
|
-
import
|
|
5583
|
-
import
|
|
6866
|
+
import fs32 from "fs";
|
|
6867
|
+
import path37 from "path";
|
|
5584
6868
|
async function generateMinimaxSpeech(opts) {
|
|
5585
6869
|
const {
|
|
5586
6870
|
apiKey,
|
|
@@ -5627,12 +6911,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
5627
6911
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
5628
6912
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
5629
6913
|
const outDir = getOutputDir();
|
|
5630
|
-
const dest =
|
|
5631
|
-
|
|
6914
|
+
const dest = path37.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
6915
|
+
fs32.writeFileSync(dest, audioBytes);
|
|
5632
6916
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
5633
6917
|
if (output) {
|
|
5634
|
-
|
|
5635
|
-
|
|
6918
|
+
fs32.mkdirSync(path37.dirname(path37.resolve(output)), { recursive: true });
|
|
6919
|
+
fs32.copyFileSync(dest, output);
|
|
5636
6920
|
}
|
|
5637
6921
|
return { status: "success", generatedAudio: dest, model };
|
|
5638
6922
|
}
|
|
@@ -5679,8 +6963,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
5679
6963
|
init_errors();
|
|
5680
6964
|
|
|
5681
6965
|
// src/providers/minimax/generators/video.ts
|
|
5682
|
-
import
|
|
5683
|
-
import
|
|
6966
|
+
import fs33 from "fs";
|
|
6967
|
+
import path38 from "path";
|
|
5684
6968
|
async function generateMinimaxVideo(opts) {
|
|
5685
6969
|
const {
|
|
5686
6970
|
apiKey,
|
|
@@ -5726,18 +7010,18 @@ async function generateMinimaxVideo(opts) {
|
|
|
5726
7010
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
5727
7011
|
}
|
|
5728
7012
|
const outDir = getOutputDir();
|
|
5729
|
-
const dest =
|
|
7013
|
+
const dest = path38.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
5730
7014
|
try {
|
|
5731
7015
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
5732
7016
|
} catch (e) {
|
|
5733
7017
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
5734
7018
|
}
|
|
5735
7019
|
const elapsed = (Date.now() - start) / 1e3;
|
|
5736
|
-
const stat =
|
|
7020
|
+
const stat = fs33.statSync(dest);
|
|
5737
7021
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
5738
7022
|
if (output) {
|
|
5739
|
-
|
|
5740
|
-
|
|
7023
|
+
fs33.mkdirSync(path38.dirname(path38.resolve(output)), { recursive: true });
|
|
7024
|
+
fs33.copyFileSync(dest, output);
|
|
5741
7025
|
}
|
|
5742
7026
|
await maybeDownloadThumb(pollResult, dest, {
|
|
5743
7027
|
skip: thumb === false,
|
|
@@ -5799,8 +7083,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
5799
7083
|
|
|
5800
7084
|
// src/providers/minimax/generators/image.ts
|
|
5801
7085
|
init_errors();
|
|
5802
|
-
import
|
|
5803
|
-
import
|
|
7086
|
+
import fs34 from "fs";
|
|
7087
|
+
import path39 from "path";
|
|
5804
7088
|
async function generateMinimaxImage(opts) {
|
|
5805
7089
|
const {
|
|
5806
7090
|
apiKey,
|
|
@@ -5838,8 +7122,8 @@ async function generateMinimaxImage(opts) {
|
|
|
5838
7122
|
try {
|
|
5839
7123
|
const bytes = await fetchBytes(url);
|
|
5840
7124
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
5841
|
-
const dest =
|
|
5842
|
-
|
|
7125
|
+
const dest = path39.join(outDir, fname);
|
|
7126
|
+
fs34.writeFileSync(dest, bytes);
|
|
5843
7127
|
savedFiles.push(dest);
|
|
5844
7128
|
logger?.success(`Saved: ${dest}`);
|
|
5845
7129
|
} catch (e) {
|
|
@@ -5850,8 +7134,8 @@ async function generateMinimaxImage(opts) {
|
|
|
5850
7134
|
}
|
|
5851
7135
|
}
|
|
5852
7136
|
if (output && savedFiles[0]) {
|
|
5853
|
-
|
|
5854
|
-
|
|
7137
|
+
fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
|
|
7138
|
+
fs34.copyFileSync(savedFiles[0], output);
|
|
5855
7139
|
}
|
|
5856
7140
|
return { status: "success", generatedImages: savedFiles, model };
|
|
5857
7141
|
}
|
|
@@ -5890,8 +7174,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
5890
7174
|
|
|
5891
7175
|
// src/providers/minimax/commands/image-to-image.ts
|
|
5892
7176
|
init_errors();
|
|
5893
|
-
import
|
|
5894
|
-
import
|
|
7177
|
+
import fs35 from "fs";
|
|
7178
|
+
import path40 from "path";
|
|
5895
7179
|
function registerMinimaxImageToImageCommand(parent) {
|
|
5896
7180
|
parent.command("image-to-image").alias("i2i").description(
|
|
5897
7181
|
"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."
|
|
@@ -5946,8 +7230,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5946
7230
|
if (!url) continue;
|
|
5947
7231
|
try {
|
|
5948
7232
|
const bytes = await fetchBytes(url);
|
|
5949
|
-
const dest =
|
|
5950
|
-
|
|
7233
|
+
const dest = path40.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
7234
|
+
fs35.writeFileSync(dest, bytes);
|
|
5951
7235
|
saved.push(dest);
|
|
5952
7236
|
logger.success(`Saved: ${dest}`);
|
|
5953
7237
|
} catch (e) {
|
|
@@ -5958,8 +7242,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5958
7242
|
}
|
|
5959
7243
|
}
|
|
5960
7244
|
if (opts.output && saved[0]) {
|
|
5961
|
-
|
|
5962
|
-
|
|
7245
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
7246
|
+
fs35.copyFileSync(saved[0], opts.output);
|
|
5963
7247
|
logger.success(`Copied to: ${opts.output}`);
|
|
5964
7248
|
}
|
|
5965
7249
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -6093,12 +7377,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
6093
7377
|
}
|
|
6094
7378
|
|
|
6095
7379
|
// src/providers/openai/openai-audio.ts
|
|
6096
|
-
import
|
|
6097
|
-
import
|
|
7380
|
+
import fs37 from "fs";
|
|
7381
|
+
import path42 from "path";
|
|
6098
7382
|
|
|
6099
7383
|
// src/providers/openai/client.ts
|
|
6100
|
-
import
|
|
6101
|
-
import
|
|
7384
|
+
import fs36 from "fs";
|
|
7385
|
+
import path41 from "path";
|
|
6102
7386
|
init_errors();
|
|
6103
7387
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
6104
7388
|
function requireOpenAIKey() {
|
|
@@ -6164,10 +7448,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
6164
7448
|
return Buffer.from(await res.arrayBuffer());
|
|
6165
7449
|
}
|
|
6166
7450
|
function fileFromPath(filePath) {
|
|
6167
|
-
const abs =
|
|
6168
|
-
if (!
|
|
6169
|
-
const bytes =
|
|
6170
|
-
return new File([new Uint8Array(bytes)],
|
|
7451
|
+
const abs = path41.resolve(filePath);
|
|
7452
|
+
if (!fs36.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
7453
|
+
const bytes = fs36.readFileSync(abs);
|
|
7454
|
+
return new File([new Uint8Array(bytes)], path41.basename(abs));
|
|
6171
7455
|
}
|
|
6172
7456
|
|
|
6173
7457
|
// src/providers/openai/openai-audio.ts
|
|
@@ -6206,7 +7490,7 @@ async function resolveAudioReferenceDataUrl(ref) {
|
|
|
6206
7490
|
}
|
|
6207
7491
|
function audioMimeFromReference(ref) {
|
|
6208
7492
|
const pathname = new URL(ref).pathname;
|
|
6209
|
-
const ext =
|
|
7493
|
+
const ext = path42.extname(pathname).toLowerCase();
|
|
6210
7494
|
switch (ext) {
|
|
6211
7495
|
case ".mp3":
|
|
6212
7496
|
return "audio/mpeg";
|
|
@@ -6225,12 +7509,12 @@ function audioMimeFromReference(ref) {
|
|
|
6225
7509
|
async function generateOpenAISpeech(opts) {
|
|
6226
7510
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
6227
7511
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
6228
|
-
const dest =
|
|
6229
|
-
|
|
7512
|
+
const dest = path42.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
7513
|
+
fs37.writeFileSync(dest, bytes);
|
|
6230
7514
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
6231
7515
|
if (opts.output) {
|
|
6232
|
-
|
|
6233
|
-
|
|
7516
|
+
fs37.mkdirSync(path42.dirname(path42.resolve(opts.output)), { recursive: true });
|
|
7517
|
+
fs37.copyFileSync(dest, opts.output);
|
|
6234
7518
|
}
|
|
6235
7519
|
return dest;
|
|
6236
7520
|
}
|
|
@@ -6252,9 +7536,9 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
6252
7536
|
);
|
|
6253
7537
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
6254
7538
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
6255
|
-
const dest = opts.output ?
|
|
6256
|
-
|
|
6257
|
-
|
|
7539
|
+
const dest = opts.output ? path42.resolve(opts.output) : path42.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
|
|
7540
|
+
fs37.mkdirSync(path42.dirname(dest), { recursive: true });
|
|
7541
|
+
fs37.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
6258
7542
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
6259
7543
|
return { path: dest, text };
|
|
6260
7544
|
}
|
|
@@ -6281,9 +7565,9 @@ Generated audio: ${dest}`);
|
|
|
6281
7565
|
}
|
|
6282
7566
|
|
|
6283
7567
|
// src/providers/openai/codex-image-driver.ts
|
|
6284
|
-
import
|
|
7568
|
+
import fs38 from "fs";
|
|
6285
7569
|
import os3 from "os";
|
|
6286
|
-
import
|
|
7570
|
+
import path43 from "path";
|
|
6287
7571
|
import { execa } from "execa";
|
|
6288
7572
|
init_errors();
|
|
6289
7573
|
var defaultRunner = async (cmd, args, opts) => {
|
|
@@ -6332,12 +7616,12 @@ function sanitizeCodexEnv() {
|
|
|
6332
7616
|
async function runCodexImageGeneration(opts) {
|
|
6333
7617
|
const runner = opts.runner ?? defaultRunner;
|
|
6334
7618
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
6335
|
-
const dest = opts.output ?
|
|
6336
|
-
|
|
6337
|
-
const tmp =
|
|
6338
|
-
const schema =
|
|
6339
|
-
const result =
|
|
6340
|
-
|
|
7619
|
+
const dest = opts.output ? path43.resolve(opts.output) : path43.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
|
|
7620
|
+
fs38.mkdirSync(path43.dirname(dest), { recursive: true });
|
|
7621
|
+
const tmp = fs38.mkdtempSync(path43.join(os3.tmpdir(), "multix-codex-image-"));
|
|
7622
|
+
const schema = path43.join(tmp, "schema.json");
|
|
7623
|
+
const result = path43.join(tmp, "result.json");
|
|
7624
|
+
fs38.writeFileSync(
|
|
6341
7625
|
schema,
|
|
6342
7626
|
JSON.stringify({
|
|
6343
7627
|
type: "object",
|
|
@@ -6367,7 +7651,7 @@ async function runCodexImageGeneration(opts) {
|
|
|
6367
7651
|
if (res.exitCode !== 0) {
|
|
6368
7652
|
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
6369
7653
|
}
|
|
6370
|
-
if (!
|
|
7654
|
+
if (!fs38.existsSync(dest) || fs38.statSync(dest).size === 0) {
|
|
6371
7655
|
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
6372
7656
|
}
|
|
6373
7657
|
return { path: dest };
|
|
@@ -6384,8 +7668,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
6384
7668
|
|
|
6385
7669
|
// src/providers/openai/openai-image.ts
|
|
6386
7670
|
init_errors();
|
|
6387
|
-
import
|
|
6388
|
-
import
|
|
7671
|
+
import fs39 from "fs";
|
|
7672
|
+
import path44 from "path";
|
|
6389
7673
|
function buildImageGenerationBody(opts) {
|
|
6390
7674
|
return {
|
|
6391
7675
|
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
@@ -6418,14 +7702,14 @@ function saveOpenAIImages(opts) {
|
|
|
6418
7702
|
for (let i = 0; i < items.length; i++) {
|
|
6419
7703
|
const b64 = items[i]?.b64_json;
|
|
6420
7704
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
6421
|
-
const dest =
|
|
6422
|
-
|
|
7705
|
+
const dest = path44.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
7706
|
+
fs39.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
6423
7707
|
saved.push(dest);
|
|
6424
7708
|
opts.logger?.success(`Saved: ${dest}`);
|
|
6425
7709
|
}
|
|
6426
7710
|
if (opts.output && saved[0]) {
|
|
6427
|
-
|
|
6428
|
-
|
|
7711
|
+
fs39.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
|
|
7712
|
+
fs39.copyFileSync(saved[0], opts.output);
|
|
6429
7713
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
6430
7714
|
}
|
|
6431
7715
|
return saved;
|
|
@@ -6434,7 +7718,7 @@ async function resolveImageRefBytes(ref) {
|
|
|
6434
7718
|
if (isHttpUrl(ref)) {
|
|
6435
7719
|
const bytes2 = await fetchBytes(ref);
|
|
6436
7720
|
const pathname = new URL(ref).pathname;
|
|
6437
|
-
const filename =
|
|
7721
|
+
const filename = path44.basename(pathname) || "reference.png";
|
|
6438
7722
|
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
6439
7723
|
}
|
|
6440
7724
|
const file = fileFromPath(ref);
|
|
@@ -6479,7 +7763,7 @@ function sniffImageMime2(buf, name) {
|
|
|
6479
7763
|
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
6480
7764
|
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
6481
7765
|
return "image/webp";
|
|
6482
|
-
const ext =
|
|
7766
|
+
const ext = path44.extname(name).toLowerCase();
|
|
6483
7767
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
6484
7768
|
if (ext === ".webp") return "image/webp";
|
|
6485
7769
|
return "image/png";
|
|
@@ -6623,8 +7907,8 @@ function registerOpenAICommands(program2) {
|
|
|
6623
7907
|
}
|
|
6624
7908
|
|
|
6625
7909
|
// src/providers/openrouter/client.ts
|
|
6626
|
-
import
|
|
6627
|
-
import
|
|
7910
|
+
import fs40 from "fs";
|
|
7911
|
+
import path45 from "path";
|
|
6628
7912
|
init_errors();
|
|
6629
7913
|
|
|
6630
7914
|
// src/providers/openrouter/payload.ts
|
|
@@ -6788,8 +8072,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
6788
8072
|
if (!url) continue;
|
|
6789
8073
|
try {
|
|
6790
8074
|
const bytes = await fetchBytes(url);
|
|
6791
|
-
const dest =
|
|
6792
|
-
|
|
8075
|
+
const dest = path45.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
8076
|
+
fs40.writeFileSync(dest, bytes);
|
|
6793
8077
|
savedFiles.push(dest);
|
|
6794
8078
|
logger?.success(`Saved: ${dest}`);
|
|
6795
8079
|
} catch (e) {
|
|
@@ -6800,8 +8084,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
6800
8084
|
}
|
|
6801
8085
|
}
|
|
6802
8086
|
if (output && savedFiles[0]) {
|
|
6803
|
-
|
|
6804
|
-
|
|
8087
|
+
fs40.mkdirSync(path45.dirname(path45.resolve(output)), { recursive: true });
|
|
8088
|
+
fs40.writeFileSync(output, fs40.readFileSync(savedFiles[0]));
|
|
6805
8089
|
}
|
|
6806
8090
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
6807
8091
|
}
|
|
@@ -6842,8 +8126,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
6842
8126
|
}
|
|
6843
8127
|
|
|
6844
8128
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
6845
|
-
import
|
|
6846
|
-
import
|
|
8129
|
+
import fs41 from "fs";
|
|
8130
|
+
import path46 from "path";
|
|
6847
8131
|
init_errors();
|
|
6848
8132
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
6849
8133
|
var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
|
|
@@ -6935,14 +8219,14 @@ async function runImageToImage(opts) {
|
|
|
6935
8219
|
const url = parsed.urls[i];
|
|
6936
8220
|
if (!url) continue;
|
|
6937
8221
|
const bytes = await fetchBytes(url);
|
|
6938
|
-
const dest =
|
|
6939
|
-
|
|
8222
|
+
const dest = path46.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
8223
|
+
fs41.writeFileSync(dest, bytes);
|
|
6940
8224
|
saved.push(dest);
|
|
6941
8225
|
opts.logger.success(`Saved: ${dest}`);
|
|
6942
8226
|
}
|
|
6943
8227
|
if (opts.output && saved[0]) {
|
|
6944
|
-
|
|
6945
|
-
|
|
8228
|
+
fs41.mkdirSync(path46.dirname(path46.resolve(opts.output)), { recursive: true });
|
|
8229
|
+
fs41.copyFileSync(saved[0], opts.output);
|
|
6946
8230
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
6947
8231
|
}
|
|
6948
8232
|
return saved;
|
|
@@ -6987,13 +8271,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
6987
8271
|
}
|
|
6988
8272
|
|
|
6989
8273
|
// src/providers/openrouter/commands/video-status.ts
|
|
6990
|
-
import
|
|
6991
|
-
import
|
|
8274
|
+
import fs42 from "fs";
|
|
8275
|
+
import path47 from "path";
|
|
6992
8276
|
async function downloadOpenRouterVideo(opts) {
|
|
6993
8277
|
const { jobId, logger, source } = opts;
|
|
6994
8278
|
const apiKey = requireOpenRouterKey();
|
|
6995
8279
|
const url = videoContentUrl(jobId);
|
|
6996
|
-
const dest =
|
|
8280
|
+
const dest = path47.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
6997
8281
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
6998
8282
|
if (!r.ok) {
|
|
6999
8283
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -7003,14 +8287,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
7003
8287
|
logger.error("Empty response body");
|
|
7004
8288
|
process.exit(1);
|
|
7005
8289
|
}
|
|
7006
|
-
|
|
7007
|
-
const ws =
|
|
8290
|
+
fs42.mkdirSync(path47.dirname(dest), { recursive: true });
|
|
8291
|
+
const ws = fs42.createWriteStream(dest);
|
|
7008
8292
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
7009
8293
|
await pipeline2(r.body, ws);
|
|
7010
8294
|
logger.success(`Saved ${dest}`);
|
|
7011
8295
|
if (opts.output) {
|
|
7012
|
-
|
|
7013
|
-
|
|
8296
|
+
fs42.mkdirSync(path47.dirname(path47.resolve(opts.output)), { recursive: true });
|
|
8297
|
+
fs42.copyFileSync(dest, opts.output);
|
|
7014
8298
|
logger.success(`Copied to ${opts.output}`);
|
|
7015
8299
|
}
|
|
7016
8300
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -7039,7 +8323,7 @@ function registerOpenRouterVideoStatusCommand(parent) {
|
|
|
7039
8323
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
7040
8324
|
}) : await pollVideoJob(jobId);
|
|
7041
8325
|
} catch (e) {
|
|
7042
|
-
if (e instanceof
|
|
8326
|
+
if (e instanceof PollTimeoutError2) {
|
|
7043
8327
|
logger.error(`Timed out waiting for job ${jobId}`);
|
|
7044
8328
|
process.exit(1);
|
|
7045
8329
|
}
|
|
@@ -7123,7 +8407,7 @@ function registerOpenRouterImageToVideoCommand(parent) {
|
|
|
7123
8407
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
7124
8408
|
});
|
|
7125
8409
|
} catch (e) {
|
|
7126
|
-
if (e instanceof
|
|
8410
|
+
if (e instanceof PollTimeoutError2) {
|
|
7127
8411
|
logger.error(`Timed out \u2014 multix openrouter video-status ${res.id}`);
|
|
7128
8412
|
process.exit(1);
|
|
7129
8413
|
}
|
|
@@ -7176,7 +8460,7 @@ function registerOpenRouterCommands(program2) {
|
|
|
7176
8460
|
}
|
|
7177
8461
|
|
|
7178
8462
|
// src/commands/check.ts
|
|
7179
|
-
import
|
|
8463
|
+
import fs43 from "fs";
|
|
7180
8464
|
import os4 from "os";
|
|
7181
8465
|
|
|
7182
8466
|
// src/commands/check-helpers/binary-check.ts
|
|
@@ -7233,6 +8517,7 @@ To configure multix, set at least one provider API key:
|
|
|
7233
8517
|
Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
|
|
7234
8518
|
BytePlus : https://console.byteplus.com/auth/api-keys
|
|
7235
8519
|
fal.ai : https://fal.ai/dashboard/keys
|
|
8520
|
+
Atlas Cloud : https://www.atlascloud.ai/console/api-keys
|
|
7236
8521
|
|
|
7237
8522
|
Option A \u2014 user global config (recommended):
|
|
7238
8523
|
echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
|
|
@@ -7301,6 +8586,12 @@ var PROVIDERS = [
|
|
|
7301
8586
|
envPrimary: "FAL_KEY",
|
|
7302
8587
|
optional: true,
|
|
7303
8588
|
link: "https://fal.ai/dashboard/keys"
|
|
8589
|
+
},
|
|
8590
|
+
{
|
|
8591
|
+
name: "Atlas Cloud",
|
|
8592
|
+
envPrimary: "ATLASCLOUD_API_KEY",
|
|
8593
|
+
optional: true,
|
|
8594
|
+
link: "https://www.atlascloud.ai/console/api-keys"
|
|
7304
8595
|
}
|
|
7305
8596
|
];
|
|
7306
8597
|
function resolveProviderKey(p) {
|
|
@@ -7356,7 +8647,7 @@ function registerCheckCommand(program2) {
|
|
|
7356
8647
|
}
|
|
7357
8648
|
if (!legacyClaudeEnvEnabled()) {
|
|
7358
8649
|
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os4.homedir();
|
|
7359
|
-
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) =>
|
|
8650
|
+
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs43.existsSync(f));
|
|
7360
8651
|
if (legacy.length > 0) {
|
|
7361
8652
|
logger.info(
|
|
7362
8653
|
`Found ${legacy.length} legacy .claude env file(s); set MULTIX_LEGACY_CLAUDE_ENV=1 to load them`
|
|
@@ -7461,8 +8752,8 @@ function registerCheckCommand(program2) {
|
|
|
7461
8752
|
|
|
7462
8753
|
// src/commands/doc/convert.ts
|
|
7463
8754
|
init_errors();
|
|
7464
|
-
import
|
|
7465
|
-
import
|
|
8755
|
+
import fs44 from "fs";
|
|
8756
|
+
import path48 from "path";
|
|
7466
8757
|
|
|
7467
8758
|
// src/commands/doc/default-prompt.ts
|
|
7468
8759
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -7483,18 +8774,18 @@ async function convertDocuments(opts) {
|
|
|
7483
8774
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
7484
8775
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
7485
8776
|
for (const f of inputs) {
|
|
7486
|
-
if (!
|
|
8777
|
+
if (!fs44.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
7487
8778
|
}
|
|
7488
8779
|
let outputPath;
|
|
7489
8780
|
if (output) {
|
|
7490
|
-
outputPath =
|
|
8781
|
+
outputPath = path48.resolve(output);
|
|
7491
8782
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
7492
|
-
const base =
|
|
7493
|
-
outputPath =
|
|
8783
|
+
const base = path48.basename(inputs[0], path48.extname(inputs[0]));
|
|
8784
|
+
outputPath = path48.join(getOutputDir(), `${base}-extraction.md`);
|
|
7494
8785
|
} else {
|
|
7495
|
-
outputPath =
|
|
8786
|
+
outputPath = path48.join(getOutputDir(), "document-extraction.md");
|
|
7496
8787
|
}
|
|
7497
|
-
|
|
8788
|
+
fs44.mkdirSync(path48.dirname(outputPath), { recursive: true });
|
|
7498
8789
|
const results = [];
|
|
7499
8790
|
for (let i = 0; i < inputs.length; i++) {
|
|
7500
8791
|
const filePath = inputs[i];
|
|
@@ -7554,7 +8845,7 @@ async function convertDocuments(opts) {
|
|
|
7554
8845
|
`);
|
|
7555
8846
|
for (const r of results) {
|
|
7556
8847
|
lines.push(`
|
|
7557
|
-
## ${
|
|
8848
|
+
## ${path48.basename(r.file)}
|
|
7558
8849
|
`);
|
|
7559
8850
|
if (r.status === "success" && r.markdown) {
|
|
7560
8851
|
lines.push(`${r.markdown}
|
|
@@ -7567,7 +8858,7 @@ async function convertDocuments(opts) {
|
|
|
7567
8858
|
}
|
|
7568
8859
|
lines.push("\n---\n");
|
|
7569
8860
|
}
|
|
7570
|
-
|
|
8861
|
+
fs44.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
7571
8862
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
7572
8863
|
console.log(`
|
|
7573
8864
|
${"=".repeat(50)}`);
|
|
@@ -7615,12 +8906,12 @@ function registerDocCommands(program2) {
|
|
|
7615
8906
|
|
|
7616
8907
|
// src/commands/media/index.ts
|
|
7617
8908
|
init_errors();
|
|
7618
|
-
import
|
|
7619
|
-
import
|
|
8909
|
+
import fs47 from "fs";
|
|
8910
|
+
import path51 from "path";
|
|
7620
8911
|
|
|
7621
8912
|
// src/commands/media/batch.ts
|
|
7622
|
-
import
|
|
7623
|
-
import
|
|
8913
|
+
import fs45 from "fs";
|
|
8914
|
+
import path49 from "path";
|
|
7624
8915
|
|
|
7625
8916
|
// src/commands/media/detect-type.ts
|
|
7626
8917
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -7816,10 +9107,10 @@ async function batchOptimize(opts) {
|
|
|
7816
9107
|
verbose,
|
|
7817
9108
|
logger
|
|
7818
9109
|
} = opts;
|
|
7819
|
-
|
|
7820
|
-
const entries =
|
|
9110
|
+
fs45.mkdirSync(outputDir, { recursive: true });
|
|
9111
|
+
const entries = fs45.readdirSync(inputDir);
|
|
7821
9112
|
const mediaFiles = entries.filter((f) => {
|
|
7822
|
-
const ext =
|
|
9113
|
+
const ext = path49.extname(f).toLowerCase();
|
|
7823
9114
|
return ALL_EXTS.has(ext);
|
|
7824
9115
|
});
|
|
7825
9116
|
if (mediaFiles.length === 0) {
|
|
@@ -7830,9 +9121,9 @@ async function batchOptimize(opts) {
|
|
|
7830
9121
|
let succeeded = 0;
|
|
7831
9122
|
const errors = [];
|
|
7832
9123
|
for (const file of mediaFiles) {
|
|
7833
|
-
const inputPath =
|
|
7834
|
-
const outputPath =
|
|
7835
|
-
const ext =
|
|
9124
|
+
const inputPath = path49.join(inputDir, file);
|
|
9125
|
+
const outputPath = path49.join(outputDir, file);
|
|
9126
|
+
const ext = path49.extname(file).toLowerCase();
|
|
7836
9127
|
const kind = extToKind(ext);
|
|
7837
9128
|
logger?.debug(`Processing: ${file}`);
|
|
7838
9129
|
try {
|
|
@@ -7869,8 +9160,8 @@ async function batchOptimize(opts) {
|
|
|
7869
9160
|
|
|
7870
9161
|
// src/commands/media/split-video.ts
|
|
7871
9162
|
init_errors();
|
|
7872
|
-
import
|
|
7873
|
-
import
|
|
9163
|
+
import fs46 from "fs";
|
|
9164
|
+
import path50 from "path";
|
|
7874
9165
|
async function splitVideo(opts) {
|
|
7875
9166
|
await assertFfmpeg();
|
|
7876
9167
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -7881,10 +9172,10 @@ async function splitVideo(opts) {
|
|
|
7881
9172
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
7882
9173
|
return [input];
|
|
7883
9174
|
}
|
|
7884
|
-
|
|
7885
|
-
const ext =
|
|
7886
|
-
const baseName =
|
|
7887
|
-
const outputPattern =
|
|
9175
|
+
fs46.mkdirSync(outputDir, { recursive: true });
|
|
9176
|
+
const ext = path50.extname(input) || ".mp4";
|
|
9177
|
+
const baseName = path50.basename(input, ext);
|
|
9178
|
+
const outputPattern = path50.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
7888
9179
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
7889
9180
|
const args = [
|
|
7890
9181
|
"-i",
|
|
@@ -7903,8 +9194,8 @@ async function splitVideo(opts) {
|
|
|
7903
9194
|
await runFfmpeg(args, { verbose, logger });
|
|
7904
9195
|
const files = [];
|
|
7905
9196
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
7906
|
-
const fname =
|
|
7907
|
-
if (
|
|
9197
|
+
const fname = path50.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
9198
|
+
if (fs46.existsSync(fname)) files.push(fname);
|
|
7908
9199
|
}
|
|
7909
9200
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
7910
9201
|
return files;
|
|
@@ -7918,9 +9209,9 @@ function registerMediaCommands(program2) {
|
|
|
7918
9209
|
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(
|
|
7919
9210
|
async (opts) => {
|
|
7920
9211
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7921
|
-
if (!
|
|
7922
|
-
|
|
7923
|
-
const ext =
|
|
9212
|
+
if (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
9213
|
+
fs47.mkdirSync(path51.dirname(path51.resolve(opts.output)), { recursive: true });
|
|
9214
|
+
const ext = path51.extname(opts.input).toLowerCase();
|
|
7924
9215
|
const kind = extToKind(ext);
|
|
7925
9216
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
7926
9217
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -7960,7 +9251,7 @@ function registerMediaCommands(program2) {
|
|
|
7960
9251
|
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(
|
|
7961
9252
|
async (opts) => {
|
|
7962
9253
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7963
|
-
if (!
|
|
9254
|
+
if (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7964
9255
|
const chunks = await splitVideo({
|
|
7965
9256
|
input: opts.input,
|
|
7966
9257
|
outputDir: opts.outputDir,
|
|
@@ -7976,7 +9267,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
7976
9267
|
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(
|
|
7977
9268
|
async (opts) => {
|
|
7978
9269
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7979
|
-
if (!
|
|
9270
|
+
if (!fs47.existsSync(opts.inputDir))
|
|
7980
9271
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
7981
9272
|
const result = await batchOptimize({
|
|
7982
9273
|
inputDir: opts.inputDir,
|
|
@@ -8156,6 +9447,7 @@ function registerCommands(program2) {
|
|
|
8156
9447
|
registerCloudflareCommands(program2);
|
|
8157
9448
|
registerElevenLabsCommands(program2);
|
|
8158
9449
|
registerFalCommands(program2);
|
|
9450
|
+
registerAtlasCloudCommands(program2);
|
|
8159
9451
|
registerMediaCommands(program2);
|
|
8160
9452
|
registerDocCommands(program2);
|
|
8161
9453
|
}
|
|
@@ -8169,7 +9461,7 @@ loadEnv();
|
|
|
8169
9461
|
var require3 = createRequire2(import.meta.url);
|
|
8170
9462
|
var pkg = require3("../package.json");
|
|
8171
9463
|
var program = new Command().name("multix").description(
|
|
8172
|
-
"AI multimodal CLI \u2014 image/video/audio/3D generation, editing, transcription, and media processing via OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, ffmpeg, and ImageMagick"
|
|
9464
|
+
"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, ffmpeg, and ImageMagick"
|
|
8173
9465
|
).version(pkg.version);
|
|
8174
9466
|
registerCommands(program);
|
|
8175
9467
|
program.parseAsync(process.argv).catch((err2) => {
|