@mrgoonie/multix 0.9.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 +14 -0
- package/README.md +51 -1
- package/dist/cli.js +2409 -906
- package/package.json +3 -2
- package/skill/SKILL.md +36 -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 });
|
|
@@ -3141,14 +4485,214 @@ function registerFalCommands(program2) {
|
|
|
3141
4485
|
registerFalResultCommand(fal);
|
|
3142
4486
|
}
|
|
3143
4487
|
|
|
3144
|
-
// src/
|
|
4488
|
+
// src/core/stdin-file.ts
|
|
3145
4489
|
init_errors();
|
|
3146
4490
|
import fs20 from "fs";
|
|
4491
|
+
import os2 from "os";
|
|
4492
|
+
import path23 from "path";
|
|
4493
|
+
var STDIN_ARG = "-";
|
|
4494
|
+
var STDIN_LABEL = "<stdin>";
|
|
4495
|
+
var HEAD_BYTES = 4096;
|
|
4496
|
+
var MAX_STDIN_BYTES = 2 * 1024 ** 3;
|
|
4497
|
+
var WAIT_HINT_MS = 2e3;
|
|
4498
|
+
var ascii = (buf, start, end) => buf.toString("latin1", start, end);
|
|
4499
|
+
var HEIF_BRANDS = /* @__PURE__ */ new Set([
|
|
4500
|
+
"heic",
|
|
4501
|
+
"heix",
|
|
4502
|
+
"hevc",
|
|
4503
|
+
"hevx",
|
|
4504
|
+
"heim",
|
|
4505
|
+
"heis",
|
|
4506
|
+
"hevm",
|
|
4507
|
+
"hevs",
|
|
4508
|
+
"mif1",
|
|
4509
|
+
"msf1"
|
|
4510
|
+
]);
|
|
4511
|
+
var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
|
|
4512
|
+
function isMpegAudioFrame(head) {
|
|
4513
|
+
if (head.length < 4 || head[0] !== 255) return false;
|
|
4514
|
+
const b1 = head[1] ?? 0;
|
|
4515
|
+
const b2 = head[2] ?? 0;
|
|
4516
|
+
if ((b1 & 224) !== 224) return false;
|
|
4517
|
+
const version = b1 >> 3 & 3;
|
|
4518
|
+
const layer = b1 >> 1 & 3;
|
|
4519
|
+
const bitrate = b2 >> 4;
|
|
4520
|
+
const sampleRate = b2 >> 2 & 3;
|
|
4521
|
+
return version !== 1 && layer !== 0 && bitrate !== 15 && bitrate !== 0 && sampleRate !== 3;
|
|
4522
|
+
}
|
|
4523
|
+
function detectExtension(head) {
|
|
4524
|
+
if (head.length >= 3 && head[0] === 239 && head[1] === 187 && head[2] === 191) return ".txt";
|
|
4525
|
+
if (head.length >= 2 && (head[0] === 255 && head[1] === 254 || head[0] === 254 && head[1] === 255)) {
|
|
4526
|
+
return void 0;
|
|
4527
|
+
}
|
|
4528
|
+
if (head.length >= 8 && head.readUInt32BE(0) === 2303741511) return ".png";
|
|
4529
|
+
if (head.length >= 3 && head[0] === 255 && head[1] === 216 && head[2] === 255) return ".jpg";
|
|
4530
|
+
if (ascii(head, 0, 4) === "GIF8") return ".gif";
|
|
4531
|
+
if (ascii(head, 0, 5) === "%PDF-") return ".pdf";
|
|
4532
|
+
if (ascii(head, 0, 2) === "BM" && head.length >= 26 && head.readUInt32LE(14) >= 12) return ".bmp";
|
|
4533
|
+
if (ascii(head, 0, 4) === "RIFF") {
|
|
4534
|
+
const kind = ascii(head, 8, 12);
|
|
4535
|
+
if (kind === "WEBP") return ".webp";
|
|
4536
|
+
if (kind === "WAVE") return ".wav";
|
|
4537
|
+
if (kind === "AVI ") return ".avi";
|
|
4538
|
+
}
|
|
4539
|
+
if (ascii(head, 0, 4) === "OggS") return ".ogg";
|
|
4540
|
+
if (ascii(head, 0, 4) === "fLaC") return ".flac";
|
|
4541
|
+
if (ascii(head, 0, 4) === "FORM" && ascii(head, 8, 12) === "AIFF") return ".aiff";
|
|
4542
|
+
if (ascii(head, 0, 3) === "ID3" && (head[3] ?? 0) >= 2 && (head[3] ?? 0) <= 4 && head[4] !== 255) {
|
|
4543
|
+
return ".mp3";
|
|
4544
|
+
}
|
|
4545
|
+
if (ascii(head, 4, 8) === "ftyp") {
|
|
4546
|
+
const brand = ascii(head, 8, 12);
|
|
4547
|
+
if (HEIF_BRANDS.has(brand)) return ".heic";
|
|
4548
|
+
if (AVIF_BRANDS.has(brand)) return void 0;
|
|
4549
|
+
if (brand.startsWith("M4A") || brand.startsWith("M4B")) return ".m4a";
|
|
4550
|
+
if (brand === "qt ") return ".mov";
|
|
4551
|
+
if (brand.startsWith("3g")) return ".3gpp";
|
|
4552
|
+
return ".mp4";
|
|
4553
|
+
}
|
|
4554
|
+
if (head.length >= 4 && head.readUInt32BE(0) === 440786851) {
|
|
4555
|
+
return head.includes("webm", 0, "latin1") ? ".webm" : void 0;
|
|
4556
|
+
}
|
|
4557
|
+
if (head.length >= 2 && head[0] === 255 && ((head[1] ?? 0) & 246) === 240) return ".aac";
|
|
4558
|
+
if (isMpegAudioFrame(head)) return ".mp3";
|
|
4559
|
+
if (looksLikeText(head)) return ".txt";
|
|
4560
|
+
return void 0;
|
|
4561
|
+
}
|
|
4562
|
+
function looksLikeText(head) {
|
|
4563
|
+
if (head.length === 0) return false;
|
|
4564
|
+
for (const byte of head) {
|
|
4565
|
+
if (byte < 32 && byte !== 9 && byte !== 10 && byte !== 12 && byte !== 13 && byte !== 27) {
|
|
4566
|
+
return false;
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4569
|
+
try {
|
|
4570
|
+
new TextDecoder("utf-8", { fatal: true }).decode(head, { stream: true });
|
|
4571
|
+
return true;
|
|
4572
|
+
} catch {
|
|
4573
|
+
return false;
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
function normalizeExt(ext) {
|
|
4577
|
+
const clean = ext.trim().toLowerCase();
|
|
4578
|
+
if (!/^\.?[a-z0-9]{1,10}$/.test(clean)) {
|
|
4579
|
+
throw new ValidationError(`Invalid --stdin-ext: ${ext} (expected e.g. .mp3 or txt)`);
|
|
4580
|
+
}
|
|
4581
|
+
return clean.startsWith(".") ? clean : `.${clean}`;
|
|
4582
|
+
}
|
|
4583
|
+
var SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
4584
|
+
function trackTempDir(dir) {
|
|
4585
|
+
let done = false;
|
|
4586
|
+
const remove = () => {
|
|
4587
|
+
try {
|
|
4588
|
+
fs20.rmSync(dir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
4589
|
+
} catch {
|
|
4590
|
+
}
|
|
4591
|
+
};
|
|
4592
|
+
const onSignal = (signal) => {
|
|
4593
|
+
dispose();
|
|
4594
|
+
process.kill(process.pid, signal);
|
|
4595
|
+
};
|
|
4596
|
+
const dispose = () => {
|
|
4597
|
+
if (done) return;
|
|
4598
|
+
done = true;
|
|
4599
|
+
process.removeListener("exit", dispose);
|
|
4600
|
+
for (const s of SIGNALS) process.removeListener(s, onSignal);
|
|
4601
|
+
remove();
|
|
4602
|
+
};
|
|
4603
|
+
process.once("exit", dispose);
|
|
4604
|
+
for (const s of SIGNALS) process.once(s, onSignal);
|
|
4605
|
+
return dispose;
|
|
4606
|
+
}
|
|
4607
|
+
async function stdinToTempFile(opts = {}) {
|
|
4608
|
+
const input = opts.input ?? process.stdin;
|
|
4609
|
+
if (input.isTTY) {
|
|
4610
|
+
throw new ValidationError(
|
|
4611
|
+
"`--files -` reads piped input, but stdin is a terminal. Example: cat clip.mp3 | multix gemini transcribe --files -"
|
|
4612
|
+
);
|
|
4613
|
+
}
|
|
4614
|
+
const forcedExt = opts.ext ? normalizeExt(opts.ext) : void 0;
|
|
4615
|
+
const maxBytes = opts.maxBytes ?? MAX_STDIN_BYTES;
|
|
4616
|
+
const dir = fs20.mkdtempSync(path23.join(os2.tmpdir(), "multix-stdin-"));
|
|
4617
|
+
const cleanup = trackTempDir(dir);
|
|
4618
|
+
const rawPath = path23.join(dir, "input");
|
|
4619
|
+
const hint = setTimeout(() => {
|
|
4620
|
+
process.stderr.write("Waiting for data on stdin (`--files -`)... press Ctrl+C to cancel\n");
|
|
4621
|
+
}, WAIT_HINT_MS);
|
|
4622
|
+
hint.unref?.();
|
|
4623
|
+
let head = Buffer.alloc(0);
|
|
4624
|
+
let size = 0;
|
|
4625
|
+
try {
|
|
4626
|
+
const fd = fs20.openSync(rawPath, "w", 384);
|
|
4627
|
+
try {
|
|
4628
|
+
for await (const chunk of input) {
|
|
4629
|
+
clearTimeout(hint);
|
|
4630
|
+
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
4631
|
+
if (size + buf.length > maxBytes) {
|
|
4632
|
+
throw new ValidationError(
|
|
4633
|
+
`Piped stdin exceeds ${Math.floor(maxBytes / 1024 ** 2)} MB; save it to a file instead`
|
|
4634
|
+
);
|
|
4635
|
+
}
|
|
4636
|
+
if (head.length < HEAD_BYTES) {
|
|
4637
|
+
head = Buffer.concat([head, buf.subarray(0, HEAD_BYTES - head.length)]);
|
|
4638
|
+
}
|
|
4639
|
+
let offset = 0;
|
|
4640
|
+
while (offset < buf.length) offset += fs20.writeSync(fd, buf, offset);
|
|
4641
|
+
size += buf.length;
|
|
4642
|
+
}
|
|
4643
|
+
} finally {
|
|
4644
|
+
clearTimeout(hint);
|
|
4645
|
+
fs20.closeSync(fd);
|
|
4646
|
+
}
|
|
4647
|
+
if (size === 0) throw new ValidationError("`--files -` was given but stdin was empty");
|
|
4648
|
+
const detectedExt = forcedExt ?? detectExtension(head);
|
|
4649
|
+
const finalPath = path23.join(dir, `stdin${detectedExt ?? ".bin"}`);
|
|
4650
|
+
fs20.renameSync(rawPath, finalPath);
|
|
4651
|
+
return { path: finalPath, size, detected: detectedExt !== void 0, cleanup };
|
|
4652
|
+
} catch (error) {
|
|
4653
|
+
cleanup();
|
|
4654
|
+
throw error;
|
|
4655
|
+
}
|
|
4656
|
+
}
|
|
4657
|
+
function labelError(message, input) {
|
|
4658
|
+
return input.path === input.label ? message : message.split(input.path).join(input.label);
|
|
4659
|
+
}
|
|
4660
|
+
async function resolveFileInputs(files, opts = {}) {
|
|
4661
|
+
const stdinCount = files.filter((f) => f === STDIN_ARG).length;
|
|
4662
|
+
if (stdinCount > 1) throw new ValidationError("`-` (stdin) can only be passed once to --files");
|
|
4663
|
+
if (opts.stdinExt && stdinCount === 0) {
|
|
4664
|
+
throw new ValidationError("--stdin-ext only applies when --files includes `-`");
|
|
4665
|
+
}
|
|
4666
|
+
for (const f of files) {
|
|
4667
|
+
if (f !== STDIN_ARG && !fs20.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
4668
|
+
}
|
|
4669
|
+
if (stdinCount === 0) {
|
|
4670
|
+
return { inputs: files.map((f) => ({ path: f, label: f })), cleanup: () => {
|
|
4671
|
+
} };
|
|
4672
|
+
}
|
|
4673
|
+
const stdin = await stdinToTempFile({
|
|
4674
|
+
input: opts.input,
|
|
4675
|
+
ext: opts.stdinExt,
|
|
4676
|
+
maxBytes: opts.maxBytes
|
|
4677
|
+
});
|
|
4678
|
+
if (!stdin.detected) {
|
|
4679
|
+
stdin.cleanup();
|
|
4680
|
+
throw new ValidationError(
|
|
4681
|
+
"Could not detect the type of piped stdin; pass --stdin-ext (e.g. --stdin-ext mp3)"
|
|
4682
|
+
);
|
|
4683
|
+
}
|
|
4684
|
+
return {
|
|
4685
|
+
inputs: files.map(
|
|
4686
|
+
(f) => f === STDIN_ARG ? { path: stdin.path, label: STDIN_LABEL } : { path: f, label: f }
|
|
4687
|
+
),
|
|
4688
|
+
cleanup: stdin.cleanup
|
|
4689
|
+
};
|
|
4690
|
+
}
|
|
3147
4691
|
|
|
3148
4692
|
// src/providers/gemini/client.ts
|
|
3149
4693
|
init_errors();
|
|
3150
|
-
import
|
|
3151
|
-
import
|
|
4694
|
+
import fs21 from "fs";
|
|
4695
|
+
import path25 from "path";
|
|
3152
4696
|
|
|
3153
4697
|
// src/providers/gemini/key-pool.ts
|
|
3154
4698
|
init_errors();
|
|
@@ -3214,7 +4758,7 @@ async function withGeminiKey(fn, opts = {}) {
|
|
|
3214
4758
|
}
|
|
3215
4759
|
|
|
3216
4760
|
// src/providers/gemini/task-resolver.ts
|
|
3217
|
-
import
|
|
4761
|
+
import path24 from "path";
|
|
3218
4762
|
var MIME_MAP = {
|
|
3219
4763
|
// Audio
|
|
3220
4764
|
".mp3": "audio/mp3",
|
|
@@ -3255,7 +4799,7 @@ var MIME_MAP = {
|
|
|
3255
4799
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
3256
4800
|
};
|
|
3257
4801
|
function getMimeType(filePath) {
|
|
3258
|
-
const ext =
|
|
4802
|
+
const ext = path24.extname(filePath).toLowerCase();
|
|
3259
4803
|
return MIME_MAP[ext] ?? "application/octet-stream";
|
|
3260
4804
|
}
|
|
3261
4805
|
function requiresProcessingWait(mimeType) {
|
|
@@ -3278,8 +4822,8 @@ async function uploadFile(filePath, opts = {}) {
|
|
|
3278
4822
|
async function uploadFileWithKey(filePath, apiKey, opts) {
|
|
3279
4823
|
const { timeoutMs = 3e5, logger } = opts;
|
|
3280
4824
|
const mimeType = getMimeType(filePath);
|
|
3281
|
-
const fileBytes =
|
|
3282
|
-
const displayName =
|
|
4825
|
+
const fileBytes = fs21.readFileSync(filePath);
|
|
4826
|
+
const displayName = path25.basename(filePath);
|
|
3283
4827
|
const boundary = `multix_${Date.now()}`;
|
|
3284
4828
|
const metaJson = JSON.stringify({ file: { displayName, mimeType } });
|
|
3285
4829
|
const body = Buffer.concat([
|
|
@@ -3396,12 +4940,12 @@ function extractImages(resp) {
|
|
|
3396
4940
|
).map((p) => ({ mimeType: p.inlineData.mimeType, data: p.inlineData.data }));
|
|
3397
4941
|
}
|
|
3398
4942
|
function inlinePart(filePath) {
|
|
3399
|
-
const data =
|
|
4943
|
+
const data = fs21.readFileSync(filePath).toString("base64");
|
|
3400
4944
|
const mimeType = getMimeType(filePath);
|
|
3401
4945
|
return { inlineData: { mimeType, data } };
|
|
3402
4946
|
}
|
|
3403
4947
|
function shouldUseFileApi(filePath) {
|
|
3404
|
-
const stat =
|
|
4948
|
+
const stat = fs21.statSync(filePath);
|
|
3405
4949
|
return stat.size > 20 * 1024 * 1024;
|
|
3406
4950
|
}
|
|
3407
4951
|
function sleep2(ms) {
|
|
@@ -3455,13 +4999,13 @@ function getDefaultModel(task) {
|
|
|
3455
4999
|
}
|
|
3456
5000
|
|
|
3457
5001
|
// src/providers/gemini/result-formatter.ts
|
|
3458
|
-
import
|
|
3459
|
-
import
|
|
5002
|
+
import fs22 from "fs";
|
|
5003
|
+
import path26 from "path";
|
|
3460
5004
|
function formatResults(results, format, outputPath) {
|
|
3461
5005
|
const content = renderFormat(results, format);
|
|
3462
5006
|
if (outputPath) {
|
|
3463
|
-
|
|
3464
|
-
|
|
5007
|
+
fs22.mkdirSync(path26.dirname(path26.resolve(outputPath)), { recursive: true });
|
|
5008
|
+
fs22.writeFileSync(outputPath, content, "utf8");
|
|
3465
5009
|
} else {
|
|
3466
5010
|
process.stdout.write(content);
|
|
3467
5011
|
}
|
|
@@ -3523,17 +5067,17 @@ ${r.response}
|
|
|
3523
5067
|
|
|
3524
5068
|
// src/providers/gemini/commands/analyze.ts
|
|
3525
5069
|
function registerAnalyzeCommand(parent) {
|
|
3526
|
-
parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
|
|
5070
|
+
parent.command("analyze").description("Analyze files with Gemini (images, video, audio, documents)").requiredOption("--files <paths...>", "Input file paths to analyze (use - to read piped stdin)").option("--stdin-ext <ext>", "Extension for piped stdin when auto-detection fails (e.g. mp3)").option("--prompt <text>", "Analysis prompt", "Analyze this content").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file instead of stdout").option("-v, --verbose", "Verbose logging").action(
|
|
3527
5071
|
async (opts) => {
|
|
3528
5072
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3529
5073
|
const model = opts.model ?? getDefaultModel("analyze");
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
}
|
|
5074
|
+
const { inputs, cleanup } = await resolveFileInputs(opts.files, {
|
|
5075
|
+
stdinExt: opts.stdinExt
|
|
5076
|
+
});
|
|
3533
5077
|
const results = [];
|
|
3534
|
-
for (let i = 0; i <
|
|
3535
|
-
const filePath =
|
|
3536
|
-
logger.debug(`[${i + 1}/${
|
|
5078
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
5079
|
+
const { path: filePath, label } = inputs[i];
|
|
5080
|
+
logger.debug(`[${i + 1}/${inputs.length}] Processing: ${label}`);
|
|
3537
5081
|
try {
|
|
3538
5082
|
const resp = await withGeminiKey(
|
|
3539
5083
|
async (apiKey) => {
|
|
@@ -3560,17 +5104,21 @@ function registerAnalyzeCommand(parent) {
|
|
|
3560
5104
|
{ logger }
|
|
3561
5105
|
);
|
|
3562
5106
|
results.push({
|
|
3563
|
-
file:
|
|
5107
|
+
file: label,
|
|
3564
5108
|
status: "success",
|
|
3565
5109
|
response: extractText(resp)
|
|
3566
5110
|
});
|
|
3567
5111
|
} catch (e) {
|
|
3568
|
-
const msg =
|
|
3569
|
-
|
|
3570
|
-
|
|
5112
|
+
const msg = labelError(
|
|
5113
|
+
e instanceof Error ? e.message : String(e),
|
|
5114
|
+
inputs[i]
|
|
5115
|
+
);
|
|
5116
|
+
logger.error(`Failed: ${label} \u2014 ${msg}`);
|
|
5117
|
+
results.push({ file: label, status: "error", error: msg });
|
|
3571
5118
|
}
|
|
3572
5119
|
}
|
|
3573
5120
|
formatResults(results, opts.format, opts.output);
|
|
5121
|
+
cleanup();
|
|
3574
5122
|
logger.debug(
|
|
3575
5123
|
`Done: ${results.filter((r) => r.status === "success").length}/${results.length} succeeded`
|
|
3576
5124
|
);
|
|
@@ -3581,21 +5129,19 @@ function registerAnalyzeCommand(parent) {
|
|
|
3581
5129
|
}
|
|
3582
5130
|
|
|
3583
5131
|
// src/providers/gemini/commands/extract.ts
|
|
3584
|
-
init_errors();
|
|
3585
|
-
import fs21 from "fs";
|
|
3586
5132
|
function registerExtractCommand(parent) {
|
|
3587
|
-
parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths").requiredOption("--prompt <text>", "Extraction prompt (required)").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
|
|
5133
|
+
parent.command("extract").description("Extract structured information from files with Gemini").requiredOption("--files <paths...>", "Input file paths (use - to read piped stdin)").option("--stdin-ext <ext>", "Extension for piped stdin when auto-detection fails (e.g. mp3)").requiredOption("--prompt <text>", "Extraction prompt (required)").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
|
|
3588
5134
|
async (opts) => {
|
|
3589
5135
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3590
5136
|
const model = opts.model ?? getDefaultModel("extract");
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
}
|
|
5137
|
+
const { inputs, cleanup } = await resolveFileInputs(opts.files, {
|
|
5138
|
+
stdinExt: opts.stdinExt
|
|
5139
|
+
});
|
|
3594
5140
|
const generationConfig = opts.format === "json" ? { responseMimeType: "application/json" } : void 0;
|
|
3595
5141
|
const results = [];
|
|
3596
|
-
for (let i = 0; i <
|
|
3597
|
-
const filePath =
|
|
3598
|
-
logger.debug(`[${i + 1}/${
|
|
5142
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
5143
|
+
const { path: filePath, label } = inputs[i];
|
|
5144
|
+
logger.debug(`[${i + 1}/${inputs.length}] Extracting: ${label}`);
|
|
3599
5145
|
try {
|
|
3600
5146
|
const resp = await withGeminiKey(
|
|
3601
5147
|
async (apiKey) => {
|
|
@@ -3621,14 +5167,18 @@ function registerExtractCommand(parent) {
|
|
|
3621
5167
|
},
|
|
3622
5168
|
{ logger }
|
|
3623
5169
|
);
|
|
3624
|
-
results.push({ file:
|
|
5170
|
+
results.push({ file: label, status: "success", response: extractText(resp) });
|
|
3625
5171
|
} catch (e) {
|
|
3626
|
-
const msg =
|
|
3627
|
-
|
|
3628
|
-
|
|
5172
|
+
const msg = labelError(
|
|
5173
|
+
e instanceof Error ? e.message : String(e),
|
|
5174
|
+
inputs[i]
|
|
5175
|
+
);
|
|
5176
|
+
logger.error(`Failed: ${label} \u2014 ${msg}`);
|
|
5177
|
+
results.push({ file: label, status: "error", error: msg });
|
|
3629
5178
|
}
|
|
3630
5179
|
}
|
|
3631
5180
|
formatResults(results, opts.format, opts.output);
|
|
5181
|
+
cleanup();
|
|
3632
5182
|
const failed = results.filter((r) => r.status === "error").length;
|
|
3633
5183
|
if (failed > 0) process.exit(1);
|
|
3634
5184
|
}
|
|
@@ -3639,8 +5189,8 @@ function registerExtractCommand(parent) {
|
|
|
3639
5189
|
init_errors();
|
|
3640
5190
|
|
|
3641
5191
|
// src/providers/gemini/generators/speech.ts
|
|
3642
|
-
import
|
|
3643
|
-
import
|
|
5192
|
+
import fs23 from "fs";
|
|
5193
|
+
import path27 from "path";
|
|
3644
5194
|
|
|
3645
5195
|
// src/providers/gemini/voices.ts
|
|
3646
5196
|
var GEMINI_TTS_VOICES = [
|
|
@@ -3806,12 +5356,12 @@ function extractWavData(wav) {
|
|
|
3806
5356
|
}
|
|
3807
5357
|
function saveSpeech(outBytes, outputFormat, output, logger) {
|
|
3808
5358
|
const outDir = getOutputDir();
|
|
3809
|
-
const dest =
|
|
3810
|
-
|
|
5359
|
+
const dest = path27.join(outDir, `gemini_speech_${Date.now()}.${outputFormat}`);
|
|
5360
|
+
fs23.writeFileSync(dest, outBytes);
|
|
3811
5361
|
logger?.success(`Saved: ${dest} (${(outBytes.length / 1024).toFixed(1)} KB)`);
|
|
3812
5362
|
if (output) {
|
|
3813
|
-
|
|
3814
|
-
|
|
5363
|
+
fs23.mkdirSync(path27.dirname(path27.resolve(output)), { recursive: true });
|
|
5364
|
+
fs23.copyFileSync(dest, output);
|
|
3815
5365
|
logger?.success(`Copied to: ${output}`);
|
|
3816
5366
|
}
|
|
3817
5367
|
return dest;
|
|
@@ -3924,84 +5474,24 @@ function collectSpeakers(value, prev) {
|
|
|
3924
5474
|
|
|
3925
5475
|
// src/providers/gemini/commands/generate-video.ts
|
|
3926
5476
|
init_errors();
|
|
3927
|
-
import
|
|
3928
|
-
import
|
|
3929
|
-
|
|
3930
|
-
// src/providers/gemini/veo.ts
|
|
3931
|
-
import fs23 from "fs";
|
|
3932
|
-
|
|
3933
|
-
// src/core/async-poll.ts
|
|
3934
|
-
init_errors();
|
|
3935
|
-
var PollTimeoutError2 = class extends MultixError {
|
|
3936
|
-
constructor(message, lastStatus) {
|
|
3937
|
-
super(message, "POLL_TIMEOUT");
|
|
3938
|
-
this.lastStatus = lastStatus;
|
|
3939
|
-
this.name = "PollTimeoutError";
|
|
3940
|
-
}
|
|
3941
|
-
lastStatus;
|
|
3942
|
-
};
|
|
3943
|
-
function isTransientHttpError(error) {
|
|
3944
|
-
if (!(error instanceof HttpError)) return false;
|
|
3945
|
-
return error.status === 0 || error.status === 429 || error.status >= 500;
|
|
3946
|
-
}
|
|
3947
|
-
var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3948
|
-
async function pollUntil2(opts) {
|
|
3949
|
-
const now = opts.now ?? Date.now;
|
|
3950
|
-
const sleep5 = opts.sleep ?? defaultSleep;
|
|
3951
|
-
const random = opts.random ?? Math.random;
|
|
3952
|
-
const isTransient = opts.isTransientError ?? isTransientHttpError;
|
|
3953
|
-
const maxInterval = opts.maxIntervalMs ?? 15e3;
|
|
3954
|
-
const minInterval = opts.minIntervalMs ?? 500;
|
|
3955
|
-
const factor = opts.backoffFactor ?? 1.5;
|
|
3956
|
-
const jitter = opts.jitter ?? true;
|
|
3957
|
-
const deadline = now() + opts.timeoutMs;
|
|
3958
|
-
let interval = opts.intervalMs ?? 3e3;
|
|
3959
|
-
let attempt = 0;
|
|
3960
|
-
let last;
|
|
3961
|
-
let lastError;
|
|
3962
|
-
for (; ; ) {
|
|
3963
|
-
attempt++;
|
|
3964
|
-
let retryAfterMs;
|
|
3965
|
-
try {
|
|
3966
|
-
last = await opts.fetchStatus({ remainingMs: Math.max(1, deadline - now()), attempt });
|
|
3967
|
-
lastError = void 0;
|
|
3968
|
-
opts.onPoll?.(last, attempt);
|
|
3969
|
-
if (opts.isTerminal(last)) return last;
|
|
3970
|
-
} catch (error) {
|
|
3971
|
-
if (!isTransient(error)) throw error;
|
|
3972
|
-
lastError = error;
|
|
3973
|
-
if (error instanceof HttpError) retryAfterMs = error.retryAfterMs;
|
|
3974
|
-
}
|
|
3975
|
-
const remaining = deadline - now();
|
|
3976
|
-
if (remaining <= 0) {
|
|
3977
|
-
const hint = opts.resumeHint ? ` ${opts.resumeHint}` : "";
|
|
3978
|
-
const cause = lastError instanceof Error ? ` Last error: ${lastError.message}.` : "";
|
|
3979
|
-
throw new PollTimeoutError2(
|
|
3980
|
-
`Timed out after ${opts.timeoutMs}ms waiting for job.${cause}${hint}`,
|
|
3981
|
-
last
|
|
3982
|
-
);
|
|
3983
|
-
}
|
|
3984
|
-
let delay = retryAfterMs ?? interval;
|
|
3985
|
-
if (jitter && retryAfterMs === void 0) delay *= 0.8 + random() * 0.4;
|
|
3986
|
-
await sleep5(Math.min(Math.max(delay, minInterval), remaining));
|
|
3987
|
-
interval = Math.min(interval * factor, maxInterval);
|
|
3988
|
-
}
|
|
3989
|
-
}
|
|
5477
|
+
import fs25 from "fs";
|
|
5478
|
+
import path28 from "path";
|
|
3990
5479
|
|
|
3991
5480
|
// src/providers/gemini/veo.ts
|
|
5481
|
+
import fs24 from "fs";
|
|
3992
5482
|
init_errors();
|
|
3993
5483
|
var API_HOST = "generativelanguage.googleapis.com";
|
|
3994
5484
|
var BASE2 = `https://${API_HOST}/v1beta`;
|
|
3995
5485
|
var VEO_IMAGE_TYPES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/webp"]);
|
|
3996
5486
|
var VEO_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
3997
5487
|
function veoImageFromFile(imgPath) {
|
|
3998
|
-
if (!
|
|
5488
|
+
if (!fs24.existsSync(imgPath)) throw new ValidationError(`Image not found: ${imgPath}`);
|
|
3999
5489
|
const detected = getMimeType(imgPath);
|
|
4000
5490
|
const mimeType = detected === "application/octet-stream" ? "image/jpeg" : detected;
|
|
4001
5491
|
if (!VEO_IMAGE_TYPES.has(mimeType)) {
|
|
4002
5492
|
throw new ValidationError(`Veo accepts JPEG, PNG or WebP images: ${imgPath} (${mimeType})`);
|
|
4003
5493
|
}
|
|
4004
|
-
return { bytesBase64Encoded:
|
|
5494
|
+
return { bytesBase64Encoded: fs24.readFileSync(imgPath).toString("base64"), mimeType };
|
|
4005
5495
|
}
|
|
4006
5496
|
function veoFramesFromPaths(paths = []) {
|
|
4007
5497
|
if (paths.length > 2) {
|
|
@@ -4050,7 +5540,7 @@ async function submitVeo(apiKey, model, body) {
|
|
|
4050
5540
|
}
|
|
4051
5541
|
async function waitForVeo(apiKey, operation, logger, opts = {}) {
|
|
4052
5542
|
if (operation.done) return checkVeoResult(operation);
|
|
4053
|
-
const finalOp = await
|
|
5543
|
+
const finalOp = await pollUntil({
|
|
4054
5544
|
fetchStatus: ({ remainingMs }) => httpJson({
|
|
4055
5545
|
url: `${BASE2}/${operation.name}`,
|
|
4056
5546
|
headers: headers(apiKey),
|
|
@@ -4126,7 +5616,7 @@ function registerGenerateVideoCommand(parent) {
|
|
|
4126
5616
|
logger.debug(`Submitting video generation with ${model}...`);
|
|
4127
5617
|
logger.info("This may take 30s\u20136 minutes. Veo requires billing.");
|
|
4128
5618
|
const frames = veoFramesFromPaths(opts.referenceImages);
|
|
4129
|
-
const dest =
|
|
5619
|
+
const dest = path28.join(getOutputDir(), `veo_generated_${Date.now()}.mp4`);
|
|
4130
5620
|
const finalOp = await runVeo(
|
|
4131
5621
|
model,
|
|
4132
5622
|
{
|
|
@@ -4140,8 +5630,8 @@ function registerGenerateVideoCommand(parent) {
|
|
|
4140
5630
|
);
|
|
4141
5631
|
logger.success(`Saved video: ${dest}`);
|
|
4142
5632
|
if (opts.output) {
|
|
4143
|
-
|
|
4144
|
-
|
|
5633
|
+
fs25.mkdirSync(path28.dirname(path28.resolve(opts.output)), { recursive: true });
|
|
5634
|
+
fs25.copyFileSync(dest, opts.output);
|
|
4145
5635
|
logger.success(`Copied to: ${opts.output}`);
|
|
4146
5636
|
}
|
|
4147
5637
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -4157,8 +5647,8 @@ Generated video: ${dest}`);
|
|
|
4157
5647
|
|
|
4158
5648
|
// src/providers/gemini/commands/generate.ts
|
|
4159
5649
|
init_errors();
|
|
4160
|
-
import
|
|
4161
|
-
import
|
|
5650
|
+
import fs26 from "fs";
|
|
5651
|
+
import path29 from "path";
|
|
4162
5652
|
function registerGenerateCommand(parent) {
|
|
4163
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(
|
|
4164
5654
|
async (opts) => {
|
|
@@ -4203,8 +5693,8 @@ function registerGenerateCommand(parent) {
|
|
|
4203
5693
|
const outDir = getOutputDir();
|
|
4204
5694
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
4205
5695
|
const fname = `gemini_generated_${Date.now()}_${i}.${ext}`;
|
|
4206
|
-
const dest =
|
|
4207
|
-
|
|
5696
|
+
const dest = path29.join(outDir, fname);
|
|
5697
|
+
fs26.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
4208
5698
|
savedFiles.push(dest);
|
|
4209
5699
|
logger.success(`Saved: ${dest}`);
|
|
4210
5700
|
}
|
|
@@ -4219,8 +5709,8 @@ function registerGenerateCommand(parent) {
|
|
|
4219
5709
|
process.exit(1);
|
|
4220
5710
|
}
|
|
4221
5711
|
if (opts.output && savedFiles[0]) {
|
|
4222
|
-
|
|
4223
|
-
|
|
5712
|
+
fs26.mkdirSync(path29.dirname(path29.resolve(opts.output)), { recursive: true });
|
|
5713
|
+
fs26.copyFileSync(savedFiles[0], opts.output);
|
|
4224
5714
|
logger.success(`Copied to: ${opts.output}`);
|
|
4225
5715
|
}
|
|
4226
5716
|
const finalImages = finalizeGeneratedImages(savedFiles, { ...opts, logger });
|
|
@@ -4232,8 +5722,8 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4232
5722
|
}
|
|
4233
5723
|
|
|
4234
5724
|
// src/providers/gemini/commands/image-to-image.ts
|
|
4235
|
-
import
|
|
4236
|
-
import
|
|
5725
|
+
import fs27 from "fs";
|
|
5726
|
+
import path30 from "path";
|
|
4237
5727
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
4238
5728
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
4239
5729
|
function registerGeminiImageToImageCommand(parent) {
|
|
@@ -4294,14 +5784,14 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
4294
5784
|
const img = images[i];
|
|
4295
5785
|
if (!img) continue;
|
|
4296
5786
|
const ext = img.mimeType.includes("png") ? "png" : "jpg";
|
|
4297
|
-
const dest =
|
|
4298
|
-
|
|
5787
|
+
const dest = path30.join(outDir, `gemini-i2i-${ts}-${i + 1}.${ext}`);
|
|
5788
|
+
fs27.writeFileSync(dest, Buffer.from(img.data, "base64"));
|
|
4299
5789
|
saved.push(dest);
|
|
4300
5790
|
logger.success(`Saved: ${dest}`);
|
|
4301
5791
|
}
|
|
4302
5792
|
if (opts.output && saved[0]) {
|
|
4303
|
-
|
|
4304
|
-
|
|
5793
|
+
fs27.mkdirSync(path30.dirname(path30.resolve(opts.output)), { recursive: true });
|
|
5794
|
+
fs27.copyFileSync(saved[0], opts.output);
|
|
4305
5795
|
logger.success(`Copied to: ${opts.output}`);
|
|
4306
5796
|
}
|
|
4307
5797
|
const finalImages = finalizeGeneratedImages(saved, { ...opts, logger });
|
|
@@ -4329,7 +5819,7 @@ function sniffImageMime(buf, url) {
|
|
|
4329
5819
|
if (buf.length >= 12 && buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
4330
5820
|
return "image/webp";
|
|
4331
5821
|
}
|
|
4332
|
-
const ext =
|
|
5822
|
+
const ext = path30.extname(new URL(url).pathname).toLowerCase();
|
|
4333
5823
|
if (ext === ".png") return "image/png";
|
|
4334
5824
|
if (ext === ".webp") return "image/webp";
|
|
4335
5825
|
if (ext === ".gif") return "image/gif";
|
|
@@ -4338,8 +5828,8 @@ function sniffImageMime(buf, url) {
|
|
|
4338
5828
|
|
|
4339
5829
|
// src/providers/gemini/commands/image-to-video.ts
|
|
4340
5830
|
init_errors();
|
|
4341
|
-
import
|
|
4342
|
-
import
|
|
5831
|
+
import fs28 from "fs";
|
|
5832
|
+
import path31 from "path";
|
|
4343
5833
|
function registerGeminiImageToVideoCommand(parent) {
|
|
4344
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(
|
|
4345
5835
|
async (imagePath, opts) => {
|
|
@@ -4359,7 +5849,7 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
4359
5849
|
logger.info(
|
|
4360
5850
|
"Submitting image-to-video. This may take 30s\u20136 minutes. Veo requires billing."
|
|
4361
5851
|
);
|
|
4362
|
-
const dest =
|
|
5852
|
+
const dest = path31.join(getOutputDir(), `veo_i2v_${Date.now()}.mp4`);
|
|
4363
5853
|
const finalOp = await runVeo(
|
|
4364
5854
|
model,
|
|
4365
5855
|
{
|
|
@@ -4374,8 +5864,8 @@ function registerGeminiImageToVideoCommand(parent) {
|
|
|
4374
5864
|
);
|
|
4375
5865
|
logger.success(`Saved video: ${dest}`);
|
|
4376
5866
|
if (opts.output) {
|
|
4377
|
-
|
|
4378
|
-
|
|
5867
|
+
fs28.mkdirSync(path31.dirname(path31.resolve(opts.output)), { recursive: true });
|
|
5868
|
+
fs28.copyFileSync(dest, opts.output);
|
|
4379
5869
|
logger.success(`Copied to: ${opts.output}`);
|
|
4380
5870
|
}
|
|
4381
5871
|
await maybeDownloadThumb(finalOp.response, dest, {
|
|
@@ -4390,20 +5880,21 @@ Generated video: ${dest}`);
|
|
|
4390
5880
|
}
|
|
4391
5881
|
|
|
4392
5882
|
// src/providers/gemini/commands/transcribe.ts
|
|
4393
|
-
init_errors();
|
|
4394
|
-
import fs28 from "fs";
|
|
4395
5883
|
function registerTranscribeCommand2(parent) {
|
|
4396
|
-
parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption(
|
|
5884
|
+
parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption(
|
|
5885
|
+
"--files <paths...>",
|
|
5886
|
+
"Audio/video file paths to transcribe (use - to read piped stdin)"
|
|
5887
|
+
).option("--stdin-ext <ext>", "Extension for piped stdin when auto-detection fails (e.g. mp3)").option("--prompt <text>", "Transcription prompt", "Generate a transcript with timestamps").option("--model <id>", "Gemini model to use").option("--format <fmt>", "Output format: text|json|csv|markdown", "text").option("--output <path>", "Write results to file").option("-v, --verbose", "Verbose logging").action(
|
|
4397
5888
|
async (opts) => {
|
|
4398
5889
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4399
5890
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
}
|
|
5891
|
+
const { inputs, cleanup } = await resolveFileInputs(opts.files, {
|
|
5892
|
+
stdinExt: opts.stdinExt
|
|
5893
|
+
});
|
|
4403
5894
|
const results = [];
|
|
4404
|
-
for (let i = 0; i <
|
|
4405
|
-
const filePath =
|
|
4406
|
-
logger.debug(`[${i + 1}/${
|
|
5895
|
+
for (let i = 0; i < inputs.length; i++) {
|
|
5896
|
+
const { path: filePath, label } = inputs[i];
|
|
5897
|
+
logger.debug(`[${i + 1}/${inputs.length}] Transcribing: ${label}`);
|
|
4407
5898
|
try {
|
|
4408
5899
|
const resp = await withGeminiKey(
|
|
4409
5900
|
async (apiKey) => {
|
|
@@ -4430,17 +5921,21 @@ function registerTranscribeCommand2(parent) {
|
|
|
4430
5921
|
{ logger }
|
|
4431
5922
|
);
|
|
4432
5923
|
results.push({
|
|
4433
|
-
file:
|
|
5924
|
+
file: label,
|
|
4434
5925
|
status: "success",
|
|
4435
5926
|
response: extractText(resp)
|
|
4436
5927
|
});
|
|
4437
5928
|
} catch (e) {
|
|
4438
|
-
const msg =
|
|
4439
|
-
|
|
4440
|
-
|
|
5929
|
+
const msg = labelError(
|
|
5930
|
+
e instanceof Error ? e.message : String(e),
|
|
5931
|
+
inputs[i]
|
|
5932
|
+
);
|
|
5933
|
+
logger.error(`Failed: ${label} \u2014 ${msg}`);
|
|
5934
|
+
results.push({ file: label, status: "error", error: msg });
|
|
4441
5935
|
}
|
|
4442
5936
|
}
|
|
4443
5937
|
formatResults(results, opts.format, opts.output);
|
|
5938
|
+
cleanup();
|
|
4444
5939
|
const failed = results.filter((r) => r.status === "error").length;
|
|
4445
5940
|
if (failed > 0) process.exit(1);
|
|
4446
5941
|
}
|
|
@@ -4464,7 +5959,7 @@ function registerGeminiCommands(program2) {
|
|
|
4464
5959
|
|
|
4465
5960
|
// src/providers/leonardo/commands/generate.ts
|
|
4466
5961
|
import fs29 from "fs";
|
|
4467
|
-
import
|
|
5962
|
+
import path32 from "path";
|
|
4468
5963
|
|
|
4469
5964
|
// src/providers/leonardo/client.ts
|
|
4470
5965
|
init_errors();
|
|
@@ -4537,11 +6032,11 @@ var LeonardoClient = class {
|
|
|
4537
6032
|
}
|
|
4538
6033
|
apiKey;
|
|
4539
6034
|
baseUrl;
|
|
4540
|
-
async request(
|
|
6035
|
+
async request(path52, opts = {}) {
|
|
4541
6036
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
4542
|
-
const v2Override =
|
|
6037
|
+
const v2Override = path52.startsWith("v2:");
|
|
4543
6038
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
4544
|
-
const finalPath = v2Override ?
|
|
6039
|
+
const finalPath = v2Override ? path52.slice(3) : path52;
|
|
4545
6040
|
const url = new URL(finalBase + finalPath);
|
|
4546
6041
|
if (opts.query) {
|
|
4547
6042
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -4560,14 +6055,14 @@ var LeonardoClient = class {
|
|
|
4560
6055
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
4561
6056
|
});
|
|
4562
6057
|
}
|
|
4563
|
-
get(
|
|
4564
|
-
return this.request(
|
|
6058
|
+
get(path52, query, logger) {
|
|
6059
|
+
return this.request(path52, { method: "GET", query, logger });
|
|
4565
6060
|
}
|
|
4566
|
-
post(
|
|
4567
|
-
return this.request(
|
|
6061
|
+
post(path52, body, logger) {
|
|
6062
|
+
return this.request(path52, { method: "POST", body, logger });
|
|
4568
6063
|
}
|
|
4569
|
-
delete(
|
|
4570
|
-
return this.request(
|
|
6064
|
+
delete(path52, logger) {
|
|
6065
|
+
return this.request(path52, { method: "DELETE", logger });
|
|
4571
6066
|
}
|
|
4572
6067
|
};
|
|
4573
6068
|
function createLeonardoClient() {
|
|
@@ -4578,7 +6073,7 @@ function createLeonardoClient() {
|
|
|
4578
6073
|
function extFromUrl2(url, fallback = ".png") {
|
|
4579
6074
|
try {
|
|
4580
6075
|
const u = new URL(url);
|
|
4581
|
-
const e =
|
|
6076
|
+
const e = path32.extname(u.pathname);
|
|
4582
6077
|
return e.length > 0 ? e : fallback;
|
|
4583
6078
|
} catch {
|
|
4584
6079
|
return fallback;
|
|
@@ -4663,7 +6158,7 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
4663
6158
|
if (!img) continue;
|
|
4664
6159
|
const ext = extFromUrl2(img.url);
|
|
4665
6160
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
4666
|
-
const filepath =
|
|
6161
|
+
const filepath = path32.join(outDir, filename);
|
|
4667
6162
|
try {
|
|
4668
6163
|
await downloadFile(img.url, filepath);
|
|
4669
6164
|
saved.push(filepath);
|
|
@@ -4673,8 +6168,8 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
4673
6168
|
}
|
|
4674
6169
|
}
|
|
4675
6170
|
if (opts.output && saved[0]) {
|
|
4676
|
-
const dest =
|
|
4677
|
-
fs29.mkdirSync(
|
|
6171
|
+
const dest = path32.resolve(opts.output);
|
|
6172
|
+
fs29.mkdirSync(path32.dirname(dest), { recursive: true });
|
|
4678
6173
|
fs29.copyFileSync(saved[0], dest);
|
|
4679
6174
|
logger.success(`Copied to ${dest}`);
|
|
4680
6175
|
}
|
|
@@ -4688,10 +6183,10 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4688
6183
|
|
|
4689
6184
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
4690
6185
|
import fs30 from "fs";
|
|
4691
|
-
import
|
|
6186
|
+
import path33 from "path";
|
|
4692
6187
|
function extFromUrl3(url, fallback = ".png") {
|
|
4693
6188
|
try {
|
|
4694
|
-
return
|
|
6189
|
+
return path33.extname(new URL(url).pathname) || fallback;
|
|
4695
6190
|
} catch {
|
|
4696
6191
|
return fallback;
|
|
4697
6192
|
}
|
|
@@ -4753,7 +6248,7 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
4753
6248
|
if (!img) continue;
|
|
4754
6249
|
const ext = extFromUrl3(img.url);
|
|
4755
6250
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
4756
|
-
const filepath =
|
|
6251
|
+
const filepath = path33.join(outDir, filename);
|
|
4757
6252
|
try {
|
|
4758
6253
|
await downloadFile(img.url, filepath);
|
|
4759
6254
|
saved.push(filepath);
|
|
@@ -4763,8 +6258,8 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
4763
6258
|
}
|
|
4764
6259
|
}
|
|
4765
6260
|
if (opts.output && saved[0]) {
|
|
4766
|
-
const dest =
|
|
4767
|
-
fs30.mkdirSync(
|
|
6261
|
+
const dest = path33.resolve(opts.output);
|
|
6262
|
+
fs30.mkdirSync(path33.dirname(dest), { recursive: true });
|
|
4768
6263
|
fs30.copyFileSync(saved[0], dest);
|
|
4769
6264
|
logger.success(`Copied to ${dest}`);
|
|
4770
6265
|
}
|
|
@@ -4777,7 +6272,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
4777
6272
|
}
|
|
4778
6273
|
|
|
4779
6274
|
// src/providers/leonardo/commands/video.ts
|
|
4780
|
-
import
|
|
6275
|
+
import path34 from "path";
|
|
4781
6276
|
function v2DimsFor(resolution) {
|
|
4782
6277
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
4783
6278
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -4862,7 +6357,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
4862
6357
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
4863
6358
|
});
|
|
4864
6359
|
} catch (e) {
|
|
4865
|
-
if (e instanceof
|
|
6360
|
+
if (e instanceof PollTimeoutError2) {
|
|
4866
6361
|
logger.error(`Timed out \u2014 multix leonardo status ${jobId}`);
|
|
4867
6362
|
process.exit(1);
|
|
4868
6363
|
}
|
|
@@ -4888,7 +6383,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
4888
6383
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
4889
6384
|
process.exit(1);
|
|
4890
6385
|
}
|
|
4891
|
-
const outPath = opts.output ??
|
|
6386
|
+
const outPath = opts.output ?? path34.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
4892
6387
|
await downloadFile(videoUrl2, outPath);
|
|
4893
6388
|
logger.success(`Saved ${outPath}`);
|
|
4894
6389
|
await maybeDownloadThumb(gen, outPath, {
|
|
@@ -4994,7 +6489,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
4994
6489
|
}
|
|
4995
6490
|
|
|
4996
6491
|
// src/providers/leonardo/commands/status.ts
|
|
4997
|
-
import
|
|
6492
|
+
import path35 from "path";
|
|
4998
6493
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
4999
6494
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
5000
6495
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -5034,7 +6529,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
5034
6529
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.generations_by_pk?.status}`)
|
|
5035
6530
|
}) : await fetchOnce();
|
|
5036
6531
|
} catch (e) {
|
|
5037
|
-
if (e instanceof
|
|
6532
|
+
if (e instanceof PollTimeoutError2) {
|
|
5038
6533
|
logger.error(`Timed out waiting for generation ${generationId}`);
|
|
5039
6534
|
process.exit(1);
|
|
5040
6535
|
}
|
|
@@ -5059,7 +6554,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
5059
6554
|
const outDir = getOutputDir();
|
|
5060
6555
|
let idx = 0;
|
|
5061
6556
|
for (const url of videos) {
|
|
5062
|
-
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`);
|
|
5063
6558
|
await downloadFile(url, dest);
|
|
5064
6559
|
logger.success(`Saved ${dest}`);
|
|
5065
6560
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -5072,7 +6567,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
5072
6567
|
for (const url of images) {
|
|
5073
6568
|
const m = url.match(IMAGE_EXT_RE2);
|
|
5074
6569
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
5075
|
-
const dest =
|
|
6570
|
+
const dest = path35.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
5076
6571
|
await downloadFile(url, dest);
|
|
5077
6572
|
logger.success(`Saved ${dest}`);
|
|
5078
6573
|
idx++;
|
|
@@ -5230,7 +6725,7 @@ function sleep3(ms) {
|
|
|
5230
6725
|
|
|
5231
6726
|
// src/providers/minimax/generators/music.ts
|
|
5232
6727
|
import fs31 from "fs";
|
|
5233
|
-
import
|
|
6728
|
+
import path36 from "path";
|
|
5234
6729
|
async function generateMinimaxMusic(opts) {
|
|
5235
6730
|
const {
|
|
5236
6731
|
apiKey,
|
|
@@ -5275,7 +6770,7 @@ async function generateMinimaxMusic(opts) {
|
|
|
5275
6770
|
}
|
|
5276
6771
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
5277
6772
|
const outDir = getOutputDir();
|
|
5278
|
-
const dest =
|
|
6773
|
+
const dest = path36.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
5279
6774
|
try {
|
|
5280
6775
|
if (audioData.startsWith("http")) {
|
|
5281
6776
|
const bytes = await fetchBytes(audioData);
|
|
@@ -5293,7 +6788,7 @@ async function generateMinimaxMusic(opts) {
|
|
|
5293
6788
|
const durSec = durationMs / 1e3;
|
|
5294
6789
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
5295
6790
|
if (output) {
|
|
5296
|
-
fs31.mkdirSync(
|
|
6791
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(output)), { recursive: true });
|
|
5297
6792
|
fs31.copyFileSync(dest, output);
|
|
5298
6793
|
}
|
|
5299
6794
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
@@ -5369,7 +6864,7 @@ init_errors();
|
|
|
5369
6864
|
|
|
5370
6865
|
// src/providers/minimax/generators/speech.ts
|
|
5371
6866
|
import fs32 from "fs";
|
|
5372
|
-
import
|
|
6867
|
+
import path37 from "path";
|
|
5373
6868
|
async function generateMinimaxSpeech(opts) {
|
|
5374
6869
|
const {
|
|
5375
6870
|
apiKey,
|
|
@@ -5416,11 +6911,11 @@ async function generateMinimaxSpeech(opts) {
|
|
|
5416
6911
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
5417
6912
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
5418
6913
|
const outDir = getOutputDir();
|
|
5419
|
-
const dest =
|
|
6914
|
+
const dest = path37.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
5420
6915
|
fs32.writeFileSync(dest, audioBytes);
|
|
5421
6916
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
5422
6917
|
if (output) {
|
|
5423
|
-
fs32.mkdirSync(
|
|
6918
|
+
fs32.mkdirSync(path37.dirname(path37.resolve(output)), { recursive: true });
|
|
5424
6919
|
fs32.copyFileSync(dest, output);
|
|
5425
6920
|
}
|
|
5426
6921
|
return { status: "success", generatedAudio: dest, model };
|
|
@@ -5469,7 +6964,7 @@ init_errors();
|
|
|
5469
6964
|
|
|
5470
6965
|
// src/providers/minimax/generators/video.ts
|
|
5471
6966
|
import fs33 from "fs";
|
|
5472
|
-
import
|
|
6967
|
+
import path38 from "path";
|
|
5473
6968
|
async function generateMinimaxVideo(opts) {
|
|
5474
6969
|
const {
|
|
5475
6970
|
apiKey,
|
|
@@ -5515,7 +7010,7 @@ async function generateMinimaxVideo(opts) {
|
|
|
5515
7010
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
5516
7011
|
}
|
|
5517
7012
|
const outDir = getOutputDir();
|
|
5518
|
-
const dest =
|
|
7013
|
+
const dest = path38.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
5519
7014
|
try {
|
|
5520
7015
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
5521
7016
|
} catch (e) {
|
|
@@ -5525,7 +7020,7 @@ async function generateMinimaxVideo(opts) {
|
|
|
5525
7020
|
const stat = fs33.statSync(dest);
|
|
5526
7021
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
5527
7022
|
if (output) {
|
|
5528
|
-
fs33.mkdirSync(
|
|
7023
|
+
fs33.mkdirSync(path38.dirname(path38.resolve(output)), { recursive: true });
|
|
5529
7024
|
fs33.copyFileSync(dest, output);
|
|
5530
7025
|
}
|
|
5531
7026
|
await maybeDownloadThumb(pollResult, dest, {
|
|
@@ -5589,7 +7084,7 @@ Generated video: ${result.generatedVideo}`);
|
|
|
5589
7084
|
// src/providers/minimax/generators/image.ts
|
|
5590
7085
|
init_errors();
|
|
5591
7086
|
import fs34 from "fs";
|
|
5592
|
-
import
|
|
7087
|
+
import path39 from "path";
|
|
5593
7088
|
async function generateMinimaxImage(opts) {
|
|
5594
7089
|
const {
|
|
5595
7090
|
apiKey,
|
|
@@ -5627,7 +7122,7 @@ async function generateMinimaxImage(opts) {
|
|
|
5627
7122
|
try {
|
|
5628
7123
|
const bytes = await fetchBytes(url);
|
|
5629
7124
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
5630
|
-
const dest =
|
|
7125
|
+
const dest = path39.join(outDir, fname);
|
|
5631
7126
|
fs34.writeFileSync(dest, bytes);
|
|
5632
7127
|
savedFiles.push(dest);
|
|
5633
7128
|
logger?.success(`Saved: ${dest}`);
|
|
@@ -5639,7 +7134,7 @@ async function generateMinimaxImage(opts) {
|
|
|
5639
7134
|
}
|
|
5640
7135
|
}
|
|
5641
7136
|
if (output && savedFiles[0]) {
|
|
5642
|
-
fs34.mkdirSync(
|
|
7137
|
+
fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
|
|
5643
7138
|
fs34.copyFileSync(savedFiles[0], output);
|
|
5644
7139
|
}
|
|
5645
7140
|
return { status: "success", generatedImages: savedFiles, model };
|
|
@@ -5680,7 +7175,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
5680
7175
|
// src/providers/minimax/commands/image-to-image.ts
|
|
5681
7176
|
init_errors();
|
|
5682
7177
|
import fs35 from "fs";
|
|
5683
|
-
import
|
|
7178
|
+
import path40 from "path";
|
|
5684
7179
|
function registerMinimaxImageToImageCommand(parent) {
|
|
5685
7180
|
parent.command("image-to-image").alias("i2i").description(
|
|
5686
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."
|
|
@@ -5735,7 +7230,7 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5735
7230
|
if (!url) continue;
|
|
5736
7231
|
try {
|
|
5737
7232
|
const bytes = await fetchBytes(url);
|
|
5738
|
-
const dest =
|
|
7233
|
+
const dest = path40.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
5739
7234
|
fs35.writeFileSync(dest, bytes);
|
|
5740
7235
|
saved.push(dest);
|
|
5741
7236
|
logger.success(`Saved: ${dest}`);
|
|
@@ -5747,7 +7242,7 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
5747
7242
|
}
|
|
5748
7243
|
}
|
|
5749
7244
|
if (opts.output && saved[0]) {
|
|
5750
|
-
fs35.mkdirSync(
|
|
7245
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5751
7246
|
fs35.copyFileSync(saved[0], opts.output);
|
|
5752
7247
|
logger.success(`Copied to: ${opts.output}`);
|
|
5753
7248
|
}
|
|
@@ -5883,11 +7378,11 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
5883
7378
|
|
|
5884
7379
|
// src/providers/openai/openai-audio.ts
|
|
5885
7380
|
import fs37 from "fs";
|
|
5886
|
-
import
|
|
7381
|
+
import path42 from "path";
|
|
5887
7382
|
|
|
5888
7383
|
// src/providers/openai/client.ts
|
|
5889
7384
|
import fs36 from "fs";
|
|
5890
|
-
import
|
|
7385
|
+
import path41 from "path";
|
|
5891
7386
|
init_errors();
|
|
5892
7387
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
5893
7388
|
function requireOpenAIKey() {
|
|
@@ -5953,10 +7448,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
5953
7448
|
return Buffer.from(await res.arrayBuffer());
|
|
5954
7449
|
}
|
|
5955
7450
|
function fileFromPath(filePath) {
|
|
5956
|
-
const abs =
|
|
7451
|
+
const abs = path41.resolve(filePath);
|
|
5957
7452
|
if (!fs36.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
5958
7453
|
const bytes = fs36.readFileSync(abs);
|
|
5959
|
-
return new File([new Uint8Array(bytes)],
|
|
7454
|
+
return new File([new Uint8Array(bytes)], path41.basename(abs));
|
|
5960
7455
|
}
|
|
5961
7456
|
|
|
5962
7457
|
// src/providers/openai/openai-audio.ts
|
|
@@ -5995,7 +7490,7 @@ async function resolveAudioReferenceDataUrl(ref) {
|
|
|
5995
7490
|
}
|
|
5996
7491
|
function audioMimeFromReference(ref) {
|
|
5997
7492
|
const pathname = new URL(ref).pathname;
|
|
5998
|
-
const ext =
|
|
7493
|
+
const ext = path42.extname(pathname).toLowerCase();
|
|
5999
7494
|
switch (ext) {
|
|
6000
7495
|
case ".mp3":
|
|
6001
7496
|
return "audio/mpeg";
|
|
@@ -6014,11 +7509,11 @@ function audioMimeFromReference(ref) {
|
|
|
6014
7509
|
async function generateOpenAISpeech(opts) {
|
|
6015
7510
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
6016
7511
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
6017
|
-
const dest =
|
|
7512
|
+
const dest = path42.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
6018
7513
|
fs37.writeFileSync(dest, bytes);
|
|
6019
7514
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
6020
7515
|
if (opts.output) {
|
|
6021
|
-
fs37.mkdirSync(
|
|
7516
|
+
fs37.mkdirSync(path42.dirname(path42.resolve(opts.output)), { recursive: true });
|
|
6022
7517
|
fs37.copyFileSync(dest, opts.output);
|
|
6023
7518
|
}
|
|
6024
7519
|
return dest;
|
|
@@ -6041,8 +7536,8 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
6041
7536
|
);
|
|
6042
7537
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
6043
7538
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
6044
|
-
const dest = opts.output ?
|
|
6045
|
-
fs37.mkdirSync(
|
|
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 });
|
|
6046
7541
|
fs37.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
6047
7542
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
6048
7543
|
return { path: dest, text };
|
|
@@ -6071,8 +7566,8 @@ Generated audio: ${dest}`);
|
|
|
6071
7566
|
|
|
6072
7567
|
// src/providers/openai/codex-image-driver.ts
|
|
6073
7568
|
import fs38 from "fs";
|
|
6074
|
-
import
|
|
6075
|
-
import
|
|
7569
|
+
import os3 from "os";
|
|
7570
|
+
import path43 from "path";
|
|
6076
7571
|
import { execa } from "execa";
|
|
6077
7572
|
init_errors();
|
|
6078
7573
|
var defaultRunner = async (cmd, args, opts) => {
|
|
@@ -6121,11 +7616,11 @@ function sanitizeCodexEnv() {
|
|
|
6121
7616
|
async function runCodexImageGeneration(opts) {
|
|
6122
7617
|
const runner = opts.runner ?? defaultRunner;
|
|
6123
7618
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
6124
|
-
const dest = opts.output ?
|
|
6125
|
-
fs38.mkdirSync(
|
|
6126
|
-
const tmp = fs38.mkdtempSync(
|
|
6127
|
-
const schema =
|
|
6128
|
-
const result =
|
|
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");
|
|
6129
7624
|
fs38.writeFileSync(
|
|
6130
7625
|
schema,
|
|
6131
7626
|
JSON.stringify({
|
|
@@ -6174,7 +7669,7 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
6174
7669
|
// src/providers/openai/openai-image.ts
|
|
6175
7670
|
init_errors();
|
|
6176
7671
|
import fs39 from "fs";
|
|
6177
|
-
import
|
|
7672
|
+
import path44 from "path";
|
|
6178
7673
|
function buildImageGenerationBody(opts) {
|
|
6179
7674
|
return {
|
|
6180
7675
|
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
@@ -6207,13 +7702,13 @@ function saveOpenAIImages(opts) {
|
|
|
6207
7702
|
for (let i = 0; i < items.length; i++) {
|
|
6208
7703
|
const b64 = items[i]?.b64_json;
|
|
6209
7704
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
6210
|
-
const dest =
|
|
7705
|
+
const dest = path44.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
6211
7706
|
fs39.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
6212
7707
|
saved.push(dest);
|
|
6213
7708
|
opts.logger?.success(`Saved: ${dest}`);
|
|
6214
7709
|
}
|
|
6215
7710
|
if (opts.output && saved[0]) {
|
|
6216
|
-
fs39.mkdirSync(
|
|
7711
|
+
fs39.mkdirSync(path44.dirname(path44.resolve(opts.output)), { recursive: true });
|
|
6217
7712
|
fs39.copyFileSync(saved[0], opts.output);
|
|
6218
7713
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
6219
7714
|
}
|
|
@@ -6223,7 +7718,7 @@ async function resolveImageRefBytes(ref) {
|
|
|
6223
7718
|
if (isHttpUrl(ref)) {
|
|
6224
7719
|
const bytes2 = await fetchBytes(ref);
|
|
6225
7720
|
const pathname = new URL(ref).pathname;
|
|
6226
|
-
const filename =
|
|
7721
|
+
const filename = path44.basename(pathname) || "reference.png";
|
|
6227
7722
|
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
6228
7723
|
}
|
|
6229
7724
|
const file = fileFromPath(ref);
|
|
@@ -6268,7 +7763,7 @@ function sniffImageMime2(buf, name) {
|
|
|
6268
7763
|
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
6269
7764
|
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
6270
7765
|
return "image/webp";
|
|
6271
|
-
const ext =
|
|
7766
|
+
const ext = path44.extname(name).toLowerCase();
|
|
6272
7767
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
6273
7768
|
if (ext === ".webp") return "image/webp";
|
|
6274
7769
|
return "image/png";
|
|
@@ -6413,7 +7908,7 @@ function registerOpenAICommands(program2) {
|
|
|
6413
7908
|
|
|
6414
7909
|
// src/providers/openrouter/client.ts
|
|
6415
7910
|
import fs40 from "fs";
|
|
6416
|
-
import
|
|
7911
|
+
import path45 from "path";
|
|
6417
7912
|
init_errors();
|
|
6418
7913
|
|
|
6419
7914
|
// src/providers/openrouter/payload.ts
|
|
@@ -6577,7 +8072,7 @@ async function generateOpenRouterImage(opts) {
|
|
|
6577
8072
|
if (!url) continue;
|
|
6578
8073
|
try {
|
|
6579
8074
|
const bytes = await fetchBytes(url);
|
|
6580
|
-
const dest =
|
|
8075
|
+
const dest = path45.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
6581
8076
|
fs40.writeFileSync(dest, bytes);
|
|
6582
8077
|
savedFiles.push(dest);
|
|
6583
8078
|
logger?.success(`Saved: ${dest}`);
|
|
@@ -6589,7 +8084,7 @@ async function generateOpenRouterImage(opts) {
|
|
|
6589
8084
|
}
|
|
6590
8085
|
}
|
|
6591
8086
|
if (output && savedFiles[0]) {
|
|
6592
|
-
fs40.mkdirSync(
|
|
8087
|
+
fs40.mkdirSync(path45.dirname(path45.resolve(output)), { recursive: true });
|
|
6593
8088
|
fs40.writeFileSync(output, fs40.readFileSync(savedFiles[0]));
|
|
6594
8089
|
}
|
|
6595
8090
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
@@ -6632,7 +8127,7 @@ Generated ${finalImages.length} image(s):`);
|
|
|
6632
8127
|
|
|
6633
8128
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
6634
8129
|
import fs41 from "fs";
|
|
6635
|
-
import
|
|
8130
|
+
import path46 from "path";
|
|
6636
8131
|
init_errors();
|
|
6637
8132
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
6638
8133
|
var SOFT_LIMIT_BYTES2 = 5 * 1024 * 1024;
|
|
@@ -6724,13 +8219,13 @@ async function runImageToImage(opts) {
|
|
|
6724
8219
|
const url = parsed.urls[i];
|
|
6725
8220
|
if (!url) continue;
|
|
6726
8221
|
const bytes = await fetchBytes(url);
|
|
6727
|
-
const dest =
|
|
8222
|
+
const dest = path46.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
6728
8223
|
fs41.writeFileSync(dest, bytes);
|
|
6729
8224
|
saved.push(dest);
|
|
6730
8225
|
opts.logger.success(`Saved: ${dest}`);
|
|
6731
8226
|
}
|
|
6732
8227
|
if (opts.output && saved[0]) {
|
|
6733
|
-
fs41.mkdirSync(
|
|
8228
|
+
fs41.mkdirSync(path46.dirname(path46.resolve(opts.output)), { recursive: true });
|
|
6734
8229
|
fs41.copyFileSync(saved[0], opts.output);
|
|
6735
8230
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
6736
8231
|
}
|
|
@@ -6777,12 +8272,12 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
6777
8272
|
|
|
6778
8273
|
// src/providers/openrouter/commands/video-status.ts
|
|
6779
8274
|
import fs42 from "fs";
|
|
6780
|
-
import
|
|
8275
|
+
import path47 from "path";
|
|
6781
8276
|
async function downloadOpenRouterVideo(opts) {
|
|
6782
8277
|
const { jobId, logger, source } = opts;
|
|
6783
8278
|
const apiKey = requireOpenRouterKey();
|
|
6784
8279
|
const url = videoContentUrl(jobId);
|
|
6785
|
-
const dest =
|
|
8280
|
+
const dest = path47.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
6786
8281
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
6787
8282
|
if (!r.ok) {
|
|
6788
8283
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -6792,13 +8287,13 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
6792
8287
|
logger.error("Empty response body");
|
|
6793
8288
|
process.exit(1);
|
|
6794
8289
|
}
|
|
6795
|
-
fs42.mkdirSync(
|
|
8290
|
+
fs42.mkdirSync(path47.dirname(dest), { recursive: true });
|
|
6796
8291
|
const ws = fs42.createWriteStream(dest);
|
|
6797
8292
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
6798
8293
|
await pipeline2(r.body, ws);
|
|
6799
8294
|
logger.success(`Saved ${dest}`);
|
|
6800
8295
|
if (opts.output) {
|
|
6801
|
-
fs42.mkdirSync(
|
|
8296
|
+
fs42.mkdirSync(path47.dirname(path47.resolve(opts.output)), { recursive: true });
|
|
6802
8297
|
fs42.copyFileSync(dest, opts.output);
|
|
6803
8298
|
logger.success(`Copied to ${opts.output}`);
|
|
6804
8299
|
}
|
|
@@ -6828,7 +8323,7 @@ function registerOpenRouterVideoStatusCommand(parent) {
|
|
|
6828
8323
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
6829
8324
|
}) : await pollVideoJob(jobId);
|
|
6830
8325
|
} catch (e) {
|
|
6831
|
-
if (e instanceof
|
|
8326
|
+
if (e instanceof PollTimeoutError2) {
|
|
6832
8327
|
logger.error(`Timed out waiting for job ${jobId}`);
|
|
6833
8328
|
process.exit(1);
|
|
6834
8329
|
}
|
|
@@ -6912,7 +8407,7 @@ function registerOpenRouterImageToVideoCommand(parent) {
|
|
|
6912
8407
|
onTick: (n, v) => logger.debug(`attempt ${n} \u2014 status: ${v.status}`)
|
|
6913
8408
|
});
|
|
6914
8409
|
} catch (e) {
|
|
6915
|
-
if (e instanceof
|
|
8410
|
+
if (e instanceof PollTimeoutError2) {
|
|
6916
8411
|
logger.error(`Timed out \u2014 multix openrouter video-status ${res.id}`);
|
|
6917
8412
|
process.exit(1);
|
|
6918
8413
|
}
|
|
@@ -6966,7 +8461,7 @@ function registerOpenRouterCommands(program2) {
|
|
|
6966
8461
|
|
|
6967
8462
|
// src/commands/check.ts
|
|
6968
8463
|
import fs43 from "fs";
|
|
6969
|
-
import
|
|
8464
|
+
import os4 from "os";
|
|
6970
8465
|
|
|
6971
8466
|
// src/commands/check-helpers/binary-check.ts
|
|
6972
8467
|
import { execa as execa2 } from "execa";
|
|
@@ -7022,6 +8517,7 @@ To configure multix, set at least one provider API key:
|
|
|
7022
8517
|
Leonardo.Ai : https://app.leonardo.ai/settings/api-keys
|
|
7023
8518
|
BytePlus : https://console.byteplus.com/auth/api-keys
|
|
7024
8519
|
fal.ai : https://fal.ai/dashboard/keys
|
|
8520
|
+
Atlas Cloud : https://www.atlascloud.ai/console/api-keys
|
|
7025
8521
|
|
|
7026
8522
|
Option A \u2014 user global config (recommended):
|
|
7027
8523
|
echo 'GEMINI_API_KEY=your-key' >> ~/.multix/.env
|
|
@@ -7090,6 +8586,12 @@ var PROVIDERS = [
|
|
|
7090
8586
|
envPrimary: "FAL_KEY",
|
|
7091
8587
|
optional: true,
|
|
7092
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"
|
|
7093
8595
|
}
|
|
7094
8596
|
];
|
|
7095
8597
|
function resolveProviderKey(p) {
|
|
@@ -7144,7 +8646,7 @@ function registerCheckCommand(program2) {
|
|
|
7144
8646
|
logger.info(`Env files loaded: ${files.length > 0 ? files.join(", ") : "none"}`);
|
|
7145
8647
|
}
|
|
7146
8648
|
if (!legacyClaudeEnvEnabled()) {
|
|
7147
|
-
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 :
|
|
8649
|
+
const home = process.env.MULTIX_DISABLE_HOME_ENV ? void 0 : os4.homedir();
|
|
7148
8650
|
const legacy = legacyClaudeEnvFiles(process.cwd(), home).filter((f) => fs43.existsSync(f));
|
|
7149
8651
|
if (legacy.length > 0) {
|
|
7150
8652
|
logger.info(
|
|
@@ -7251,7 +8753,7 @@ function registerCheckCommand(program2) {
|
|
|
7251
8753
|
// src/commands/doc/convert.ts
|
|
7252
8754
|
init_errors();
|
|
7253
8755
|
import fs44 from "fs";
|
|
7254
|
-
import
|
|
8756
|
+
import path48 from "path";
|
|
7255
8757
|
|
|
7256
8758
|
// src/commands/doc/default-prompt.ts
|
|
7257
8759
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -7276,14 +8778,14 @@ async function convertDocuments(opts) {
|
|
|
7276
8778
|
}
|
|
7277
8779
|
let outputPath;
|
|
7278
8780
|
if (output) {
|
|
7279
|
-
outputPath =
|
|
8781
|
+
outputPath = path48.resolve(output);
|
|
7280
8782
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
7281
|
-
const base =
|
|
7282
|
-
outputPath =
|
|
8783
|
+
const base = path48.basename(inputs[0], path48.extname(inputs[0]));
|
|
8784
|
+
outputPath = path48.join(getOutputDir(), `${base}-extraction.md`);
|
|
7283
8785
|
} else {
|
|
7284
|
-
outputPath =
|
|
8786
|
+
outputPath = path48.join(getOutputDir(), "document-extraction.md");
|
|
7285
8787
|
}
|
|
7286
|
-
fs44.mkdirSync(
|
|
8788
|
+
fs44.mkdirSync(path48.dirname(outputPath), { recursive: true });
|
|
7287
8789
|
const results = [];
|
|
7288
8790
|
for (let i = 0; i < inputs.length; i++) {
|
|
7289
8791
|
const filePath = inputs[i];
|
|
@@ -7343,7 +8845,7 @@ async function convertDocuments(opts) {
|
|
|
7343
8845
|
`);
|
|
7344
8846
|
for (const r of results) {
|
|
7345
8847
|
lines.push(`
|
|
7346
|
-
## ${
|
|
8848
|
+
## ${path48.basename(r.file)}
|
|
7347
8849
|
`);
|
|
7348
8850
|
if (r.status === "success" && r.markdown) {
|
|
7349
8851
|
lines.push(`${r.markdown}
|
|
@@ -7405,11 +8907,11 @@ function registerDocCommands(program2) {
|
|
|
7405
8907
|
// src/commands/media/index.ts
|
|
7406
8908
|
init_errors();
|
|
7407
8909
|
import fs47 from "fs";
|
|
7408
|
-
import
|
|
8910
|
+
import path51 from "path";
|
|
7409
8911
|
|
|
7410
8912
|
// src/commands/media/batch.ts
|
|
7411
8913
|
import fs45 from "fs";
|
|
7412
|
-
import
|
|
8914
|
+
import path49 from "path";
|
|
7413
8915
|
|
|
7414
8916
|
// src/commands/media/detect-type.ts
|
|
7415
8917
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -7608,7 +9110,7 @@ async function batchOptimize(opts) {
|
|
|
7608
9110
|
fs45.mkdirSync(outputDir, { recursive: true });
|
|
7609
9111
|
const entries = fs45.readdirSync(inputDir);
|
|
7610
9112
|
const mediaFiles = entries.filter((f) => {
|
|
7611
|
-
const ext =
|
|
9113
|
+
const ext = path49.extname(f).toLowerCase();
|
|
7612
9114
|
return ALL_EXTS.has(ext);
|
|
7613
9115
|
});
|
|
7614
9116
|
if (mediaFiles.length === 0) {
|
|
@@ -7619,9 +9121,9 @@ async function batchOptimize(opts) {
|
|
|
7619
9121
|
let succeeded = 0;
|
|
7620
9122
|
const errors = [];
|
|
7621
9123
|
for (const file of mediaFiles) {
|
|
7622
|
-
const inputPath =
|
|
7623
|
-
const outputPath =
|
|
7624
|
-
const ext =
|
|
9124
|
+
const inputPath = path49.join(inputDir, file);
|
|
9125
|
+
const outputPath = path49.join(outputDir, file);
|
|
9126
|
+
const ext = path49.extname(file).toLowerCase();
|
|
7625
9127
|
const kind = extToKind(ext);
|
|
7626
9128
|
logger?.debug(`Processing: ${file}`);
|
|
7627
9129
|
try {
|
|
@@ -7659,7 +9161,7 @@ async function batchOptimize(opts) {
|
|
|
7659
9161
|
// src/commands/media/split-video.ts
|
|
7660
9162
|
init_errors();
|
|
7661
9163
|
import fs46 from "fs";
|
|
7662
|
-
import
|
|
9164
|
+
import path50 from "path";
|
|
7663
9165
|
async function splitVideo(opts) {
|
|
7664
9166
|
await assertFfmpeg();
|
|
7665
9167
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -7671,9 +9173,9 @@ async function splitVideo(opts) {
|
|
|
7671
9173
|
return [input];
|
|
7672
9174
|
}
|
|
7673
9175
|
fs46.mkdirSync(outputDir, { recursive: true });
|
|
7674
|
-
const ext =
|
|
7675
|
-
const baseName =
|
|
7676
|
-
const outputPattern =
|
|
9176
|
+
const ext = path50.extname(input) || ".mp4";
|
|
9177
|
+
const baseName = path50.basename(input, ext);
|
|
9178
|
+
const outputPattern = path50.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
7677
9179
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
7678
9180
|
const args = [
|
|
7679
9181
|
"-i",
|
|
@@ -7692,7 +9194,7 @@ async function splitVideo(opts) {
|
|
|
7692
9194
|
await runFfmpeg(args, { verbose, logger });
|
|
7693
9195
|
const files = [];
|
|
7694
9196
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
7695
|
-
const fname =
|
|
9197
|
+
const fname = path50.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
7696
9198
|
if (fs46.existsSync(fname)) files.push(fname);
|
|
7697
9199
|
}
|
|
7698
9200
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
@@ -7708,8 +9210,8 @@ function registerMediaCommands(program2) {
|
|
|
7708
9210
|
async (opts) => {
|
|
7709
9211
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
7710
9212
|
if (!fs47.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
7711
|
-
fs47.mkdirSync(
|
|
7712
|
-
const ext =
|
|
9213
|
+
fs47.mkdirSync(path51.dirname(path51.resolve(opts.output)), { recursive: true });
|
|
9214
|
+
const ext = path51.extname(opts.input).toLowerCase();
|
|
7713
9215
|
const kind = extToKind(ext);
|
|
7714
9216
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
7715
9217
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -7945,6 +9447,7 @@ function registerCommands(program2) {
|
|
|
7945
9447
|
registerCloudflareCommands(program2);
|
|
7946
9448
|
registerElevenLabsCommands(program2);
|
|
7947
9449
|
registerFalCommands(program2);
|
|
9450
|
+
registerAtlasCloudCommands(program2);
|
|
7948
9451
|
registerMediaCommands(program2);
|
|
7949
9452
|
registerDocCommands(program2);
|
|
7950
9453
|
}
|
|
@@ -7958,7 +9461,7 @@ loadEnv();
|
|
|
7958
9461
|
var require3 = createRequire2(import.meta.url);
|
|
7959
9462
|
var pkg = require3("../package.json");
|
|
7960
9463
|
var program = new Command().name("multix").description(
|
|
7961
|
-
"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"
|
|
7962
9465
|
).version(pkg.version);
|
|
7963
9466
|
registerCommands(program);
|
|
7964
9467
|
program.parseAsync(process.argv).catch((err2) => {
|