@mrgoonie/multix 0.0.8 → 0.1.0-beta.1
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 +15 -0
- package/README.md +79 -8
- package/dist/cli.js +1053 -181
- package/package.json +4 -2
- package/skill/SKILL.md +33 -6
package/dist/cli.js
CHANGED
|
@@ -110,11 +110,14 @@ async function httpJson(opts) {
|
|
|
110
110
|
}
|
|
111
111
|
return response.json();
|
|
112
112
|
}
|
|
113
|
-
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
113
|
+
async function downloadFile(url, dest, timeoutMs = DEFAULT_TIMEOUT_MS, headers = {}) {
|
|
114
114
|
fs4.mkdirSync(path4.dirname(dest), { recursive: true });
|
|
115
115
|
let response;
|
|
116
116
|
try {
|
|
117
|
-
response = await globalThis.fetch(url, {
|
|
117
|
+
response = await globalThis.fetch(url, {
|
|
118
|
+
headers,
|
|
119
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
120
|
+
});
|
|
118
121
|
} catch (cause) {
|
|
119
122
|
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
120
123
|
throw new HttpError(0, msg, url);
|
|
@@ -414,9 +417,9 @@ var BytePlusClient = class {
|
|
|
414
417
|
}
|
|
415
418
|
apiKey;
|
|
416
419
|
baseUrl;
|
|
417
|
-
async request(
|
|
420
|
+
async request(path46, opts = {}) {
|
|
418
421
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
419
|
-
const url = new URL(base +
|
|
422
|
+
const url = new URL(base + path46);
|
|
420
423
|
if (opts.query) {
|
|
421
424
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
422
425
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -434,14 +437,14 @@ var BytePlusClient = class {
|
|
|
434
437
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
435
438
|
});
|
|
436
439
|
}
|
|
437
|
-
get(
|
|
438
|
-
return this.request(
|
|
440
|
+
get(path46, query, logger) {
|
|
441
|
+
return this.request(path46, { method: "GET", query, logger });
|
|
439
442
|
}
|
|
440
|
-
post(
|
|
441
|
-
return this.request(
|
|
443
|
+
post(path46, body, logger) {
|
|
444
|
+
return this.request(path46, { method: "POST", body, logger });
|
|
442
445
|
}
|
|
443
|
-
delete(
|
|
444
|
-
return this.request(
|
|
446
|
+
delete(path46, logger) {
|
|
447
|
+
return this.request(path46, { method: "DELETE", logger });
|
|
445
448
|
}
|
|
446
449
|
};
|
|
447
450
|
function createBytePlusClient() {
|
|
@@ -990,10 +993,10 @@ function toRef(kind, resolved, role) {
|
|
|
990
993
|
return { kind, url: resolved.dataUrl, mime: resolved.mime, role };
|
|
991
994
|
}
|
|
992
995
|
async function resolveRefSpec(kind, spec, logger) {
|
|
993
|
-
const { path:
|
|
994
|
-
if (kind === "image") return toRef(kind, await resolveImageInput(
|
|
995
|
-
if (kind === "video") return toRef(kind, await resolveVideoInput(
|
|
996
|
-
return toRef(kind, await resolveAudioInput(
|
|
996
|
+
const { path: path46, role } = parseRefSpec(spec);
|
|
997
|
+
if (kind === "image") return toRef(kind, await resolveImageInput(path46, logger), role);
|
|
998
|
+
if (kind === "video") return toRef(kind, await resolveVideoInput(path46, logger), role);
|
|
999
|
+
return toRef(kind, await resolveAudioInput(path46, logger), role);
|
|
997
1000
|
}
|
|
998
1001
|
async function resolveRefSpecs(specs, logger) {
|
|
999
1002
|
const out = [];
|
|
@@ -1325,8 +1328,8 @@ function requireElevenLabsKey() {
|
|
|
1325
1328
|
function authHeaders(apiKey, extra) {
|
|
1326
1329
|
return { "xi-api-key": apiKey, ...extra ?? {} };
|
|
1327
1330
|
}
|
|
1328
|
-
function apiGet(
|
|
1329
|
-
const url = new URL(`${ELEVENLABS_BASE_URL}/${
|
|
1331
|
+
function apiGet(endpoint2, apiKey, query) {
|
|
1332
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
|
|
1330
1333
|
if (query) {
|
|
1331
1334
|
for (const [k, v] of Object.entries(query)) {
|
|
1332
1335
|
if (v !== void 0 && v !== null) url.searchParams.set(k, String(v));
|
|
@@ -1334,8 +1337,8 @@ function apiGet(endpoint, apiKey, query) {
|
|
|
1334
1337
|
}
|
|
1335
1338
|
return httpJson({ url: url.toString(), headers: authHeaders(apiKey) });
|
|
1336
1339
|
}
|
|
1337
|
-
function apiPostJson(
|
|
1338
|
-
const url = new URL(`${ELEVENLABS_BASE_URL}/${
|
|
1340
|
+
function apiPostJson(endpoint2, payload, apiKey, opts = {}) {
|
|
1341
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
|
|
1339
1342
|
if (opts.query) {
|
|
1340
1343
|
for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
1341
1344
|
}
|
|
@@ -1347,8 +1350,8 @@ function apiPostJson(endpoint, payload, apiKey, opts = {}) {
|
|
|
1347
1350
|
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2
|
|
1348
1351
|
});
|
|
1349
1352
|
}
|
|
1350
|
-
async function apiDelete(
|
|
1351
|
-
const url = `${ELEVENLABS_BASE_URL}/${
|
|
1353
|
+
async function apiDelete(endpoint2, apiKey) {
|
|
1354
|
+
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
1352
1355
|
const res = await globalThis.fetch(url, {
|
|
1353
1356
|
method: "DELETE",
|
|
1354
1357
|
headers: authHeaders(apiKey),
|
|
@@ -1359,8 +1362,8 @@ async function apiDelete(endpoint, apiKey) {
|
|
|
1359
1362
|
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
1360
1363
|
}
|
|
1361
1364
|
}
|
|
1362
|
-
async function apiPostBinary(
|
|
1363
|
-
const url = new URL(`${ELEVENLABS_BASE_URL}/${
|
|
1365
|
+
async function apiPostBinary(endpoint2, body, apiKey, opts = {}) {
|
|
1366
|
+
const url = new URL(`${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`);
|
|
1364
1367
|
if (opts.query) {
|
|
1365
1368
|
for (const [k, v] of Object.entries(opts.query)) url.searchParams.set(k, v);
|
|
1366
1369
|
}
|
|
@@ -1394,8 +1397,8 @@ async function apiPostBinary(endpoint, body, apiKey, opts = {}) {
|
|
|
1394
1397
|
}
|
|
1395
1398
|
return Buffer.from(await res.arrayBuffer());
|
|
1396
1399
|
}
|
|
1397
|
-
async function apiPostMultipart(
|
|
1398
|
-
const url = `${ELEVENLABS_BASE_URL}/${
|
|
1400
|
+
async function apiPostMultipart(endpoint2, form, apiKey, opts = {}) {
|
|
1401
|
+
const url = `${ELEVENLABS_BASE_URL}/${endpoint2.replace(/^\//, "")}`;
|
|
1399
1402
|
let res;
|
|
1400
1403
|
try {
|
|
1401
1404
|
res = await globalThis.fetch(url, {
|
|
@@ -2076,7 +2079,7 @@ function requiresProcessingWait(mimeType) {
|
|
|
2076
2079
|
var BASE = "https://generativelanguage.googleapis.com";
|
|
2077
2080
|
var FILES_BASE = `${BASE}/upload/v1beta/files`;
|
|
2078
2081
|
var GENERATE_BASE = `${BASE}/v1beta/models`;
|
|
2079
|
-
function
|
|
2082
|
+
function requireGeminiApiKey() {
|
|
2080
2083
|
const key = resolveKey("GEMINI_API_KEY");
|
|
2081
2084
|
if (!key)
|
|
2082
2085
|
throw new ConfigError(
|
|
@@ -2084,11 +2087,14 @@ function requireApiKey() {
|
|
|
2084
2087
|
);
|
|
2085
2088
|
return key;
|
|
2086
2089
|
}
|
|
2087
|
-
function
|
|
2090
|
+
function geminiAuthHeaders(apiKey) {
|
|
2088
2091
|
return { "x-goog-api-key": apiKey };
|
|
2089
2092
|
}
|
|
2093
|
+
function unwrapGeminiFileRef(response) {
|
|
2094
|
+
return "file" in response ? response.file : response;
|
|
2095
|
+
}
|
|
2090
2096
|
async function uploadFile(filePath, opts = {}) {
|
|
2091
|
-
const apiKey =
|
|
2097
|
+
const apiKey = requireGeminiApiKey();
|
|
2092
2098
|
const { timeoutMs = 3e5, logger } = opts;
|
|
2093
2099
|
const mimeType = getMimeType(filePath);
|
|
2094
2100
|
const fileBytes = fs12.readFileSync(filePath);
|
|
@@ -2137,8 +2143,8 @@ Content-Type: ${mimeType}\r
|
|
|
2137
2143
|
await sleep(2e3);
|
|
2138
2144
|
fileRef = await httpJson({
|
|
2139
2145
|
url: `${BASE}/v1beta/${fileRef.name}`,
|
|
2140
|
-
headers:
|
|
2141
|
-
}).then(
|
|
2146
|
+
headers: geminiAuthHeaders(apiKey)
|
|
2147
|
+
}).then(unwrapGeminiFileRef);
|
|
2142
2148
|
logger?.debug(`Processing... state=${fileRef.state}`);
|
|
2143
2149
|
}
|
|
2144
2150
|
if (fileRef.state === "FAILED")
|
|
@@ -2150,11 +2156,11 @@ Content-Type: ${mimeType}\r
|
|
|
2150
2156
|
return fileRef;
|
|
2151
2157
|
}
|
|
2152
2158
|
async function generateContent(req) {
|
|
2153
|
-
const apiKey =
|
|
2159
|
+
const apiKey = requireGeminiApiKey();
|
|
2154
2160
|
return httpJson({
|
|
2155
2161
|
url: `${GENERATE_BASE}/${req.model}:generateContent`,
|
|
2156
2162
|
method: "POST",
|
|
2157
|
-
headers:
|
|
2163
|
+
headers: geminiAuthHeaders(apiKey),
|
|
2158
2164
|
body: {
|
|
2159
2165
|
contents: req.contents,
|
|
2160
2166
|
...req.generationConfig ? { generationConfig: req.generationConfig } : {}
|
|
@@ -2196,10 +2202,12 @@ function sleep(ms) {
|
|
|
2196
2202
|
// src/providers/gemini/models.ts
|
|
2197
2203
|
var IMAGE_MODEL_DEFAULT = "gemini-3.1-flash-image-preview";
|
|
2198
2204
|
var IMAGE_MODEL_FALLBACK = "gemini-2.5-flash-image";
|
|
2205
|
+
var IMAGE_MODEL_LITE = "gemini-3.1-flash-lite-image";
|
|
2199
2206
|
var VIDEO_MODEL_DEFAULT = "veo-3.1-generate-preview";
|
|
2200
|
-
var
|
|
2207
|
+
var TEXT_MODEL_DEFAULT = "gemini-3.5-flash";
|
|
2208
|
+
var ANALYSIS_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
|
|
2201
2209
|
var TTS_MODEL_DEFAULT = "gemini-3.1-flash-tts-preview";
|
|
2202
|
-
var DOC_MODEL_DEFAULT =
|
|
2210
|
+
var DOC_MODEL_DEFAULT = TEXT_MODEL_DEFAULT;
|
|
2203
2211
|
var IMAGEN_MODELS = /* @__PURE__ */ new Set([
|
|
2204
2212
|
"imagen-4.0-generate-001",
|
|
2205
2213
|
"imagen-4.0-ultra-generate-001",
|
|
@@ -2210,6 +2218,7 @@ var GEMINI_IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
|
2210
2218
|
"gemini-3-pro-image-preview",
|
|
2211
2219
|
"gemini-2.5-flash-image",
|
|
2212
2220
|
"gemini-2.5-flash-image-preview",
|
|
2221
|
+
IMAGE_MODEL_LITE,
|
|
2213
2222
|
...IMAGEN_MODELS
|
|
2214
2223
|
]);
|
|
2215
2224
|
var ASPECT_RATIOS = [
|
|
@@ -2225,6 +2234,15 @@ var ASPECT_RATIOS = [
|
|
|
2225
2234
|
"21:9"
|
|
2226
2235
|
];
|
|
2227
2236
|
var IMAGE_SIZES = ["1K", "2K", "4K"];
|
|
2237
|
+
function resolveImageOptions(model, aspectRatio, requestedSize) {
|
|
2238
|
+
if (model !== IMAGE_MODEL_LITE) {
|
|
2239
|
+
return { aspectRatio, ...requestedSize ? { imageSize: requestedSize } : {} };
|
|
2240
|
+
}
|
|
2241
|
+
if (requestedSize && requestedSize !== "1K") {
|
|
2242
|
+
throw new Error(`${IMAGE_MODEL_LITE} supports only 1K image output.`);
|
|
2243
|
+
}
|
|
2244
|
+
return { aspectRatio, imageSize: "1K" };
|
|
2245
|
+
}
|
|
2228
2246
|
function getDefaultModel(task) {
|
|
2229
2247
|
if (task === "generate") {
|
|
2230
2248
|
return process.env.IMAGE_GEN_MODEL ?? process.env.GEMINI_IMAGE_GEN_MODEL ?? IMAGE_MODEL_DEFAULT;
|
|
@@ -2709,6 +2727,13 @@ function sleep2(ms) {
|
|
|
2709
2727
|
init_errors();
|
|
2710
2728
|
import fs18 from "fs";
|
|
2711
2729
|
import path22 from "path";
|
|
2730
|
+
function buildImageGenerationConfig(model, aspectRatio, size) {
|
|
2731
|
+
const imageOptions = resolveImageOptions(model, aspectRatio, size);
|
|
2732
|
+
return {
|
|
2733
|
+
responseModalities: ["IMAGE"],
|
|
2734
|
+
imageConfig: imageOptions
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2712
2737
|
function registerGenerateCommand(parent) {
|
|
2713
2738
|
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").option("-v, --verbose", "Verbose logging").action(
|
|
2714
2739
|
async (opts) => {
|
|
@@ -2725,12 +2750,11 @@ function registerGenerateCommand(parent) {
|
|
|
2725
2750
|
}
|
|
2726
2751
|
logger.debug(`Generating ${numImages} image(s) with model: ${model}`);
|
|
2727
2752
|
logger.debug(`Aspect ratio: ${opts.aspectRatio}${opts.size ? `, size: ${opts.size}` : ""}`);
|
|
2728
|
-
const
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
};
|
|
2753
|
+
const generationConfig = buildImageGenerationConfig(
|
|
2754
|
+
model,
|
|
2755
|
+
opts.aspectRatio,
|
|
2756
|
+
opts.size
|
|
2757
|
+
);
|
|
2734
2758
|
const savedFiles = [];
|
|
2735
2759
|
for (let i = 0; i < numImages; i++) {
|
|
2736
2760
|
try {
|
|
@@ -2782,6 +2806,10 @@ import fs19 from "fs";
|
|
|
2782
2806
|
import path23 from "path";
|
|
2783
2807
|
var SOFT_LIMIT_BYTES = 5 * 1024 * 1024;
|
|
2784
2808
|
var HARD_LIMIT_BYTES = 7 * 1024 * 1024;
|
|
2809
|
+
function buildImageToImageGenerationConfig(model, aspectRatio, size) {
|
|
2810
|
+
const imageOptions = resolveImageOptions(model, aspectRatio, size);
|
|
2811
|
+
return imageOptions.imageSize ? { responseModalities: ["IMAGE"], imageConfig: imageOptions } : void 0;
|
|
2812
|
+
}
|
|
2785
2813
|
function registerGeminiImageToImageCommand(parent) {
|
|
2786
2814
|
parent.command("image-to-image").alias("i2i").description(
|
|
2787
2815
|
"Edit/compose images with Gemini 2.5 Flash Image (Nano Banana) using one or more reference images"
|
|
@@ -2790,13 +2818,26 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
2790
2818
|
"Reference image (URL or local path); repeatable for multi-ref",
|
|
2791
2819
|
collect4,
|
|
2792
2820
|
[]
|
|
2793
|
-
).option("-m, --model <id>", `Gemini image model (default ${IMAGE_MODEL_FALLBACK})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
2821
|
+
).option("-m, --model <id>", `Gemini image model (default ${IMAGE_MODEL_FALLBACK})`).option("--aspect-ratio <ratio>", `Aspect ratio (${ASPECT_RATIOS.join("|")})`, "1:1").option("--size <sz>", `Image size (${IMAGE_SIZES.join("|")})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
2794
2822
|
async (opts) => {
|
|
2795
2823
|
if (!opts.ref || opts.ref.length === 0) {
|
|
2796
2824
|
throw new Error("At least one --ref is required for image-to-image.");
|
|
2797
2825
|
}
|
|
2798
2826
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2799
2827
|
const model = opts.model ?? IMAGE_MODEL_FALLBACK;
|
|
2828
|
+
if (model === IMAGE_MODEL_LITE && opts.ref.length > 1) {
|
|
2829
|
+
logger.warn(
|
|
2830
|
+
`${IMAGE_MODEL_LITE} is not optimized for multiple reference images; use one when possible.`
|
|
2831
|
+
);
|
|
2832
|
+
}
|
|
2833
|
+
if (!ASPECT_RATIOS.includes(opts.aspectRatio)) {
|
|
2834
|
+
throw new Error(
|
|
2835
|
+
`Invalid aspect ratio: ${opts.aspectRatio}. Valid: ${ASPECT_RATIOS.join(", ")}`
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2838
|
+
if (opts.size && !IMAGE_SIZES.includes(opts.size)) {
|
|
2839
|
+
throw new Error(`Invalid size: ${opts.size}. Valid: ${IMAGE_SIZES.join(", ")}`);
|
|
2840
|
+
}
|
|
2800
2841
|
const parts = [];
|
|
2801
2842
|
for (const r of opts.ref) {
|
|
2802
2843
|
const resolved = await resolveImageInput(r, {
|
|
@@ -2820,10 +2861,15 @@ function registerGeminiImageToImageCommand(parent) {
|
|
|
2820
2861
|
}
|
|
2821
2862
|
parts.push({ text: opts.prompt });
|
|
2822
2863
|
logger.debug(`Calling ${model} with ${opts.ref.length} reference image(s)`);
|
|
2864
|
+
const generationConfig = buildImageToImageGenerationConfig(
|
|
2865
|
+
model,
|
|
2866
|
+
opts.aspectRatio,
|
|
2867
|
+
opts.size
|
|
2868
|
+
);
|
|
2823
2869
|
const req = {
|
|
2824
2870
|
model,
|
|
2825
2871
|
contents: [{ parts }],
|
|
2826
|
-
generationConfig
|
|
2872
|
+
...generationConfig ? { generationConfig } : {}
|
|
2827
2873
|
};
|
|
2828
2874
|
const resp = await generateContent(req);
|
|
2829
2875
|
const images = extractImages(resp);
|
|
@@ -2984,16 +3030,186 @@ Generated video: ${dest}`);
|
|
|
2984
3030
|
);
|
|
2985
3031
|
}
|
|
2986
3032
|
|
|
2987
|
-
// src/providers/gemini/commands/
|
|
3033
|
+
// src/providers/gemini/commands/omni-video.ts
|
|
2988
3034
|
init_errors();
|
|
2989
3035
|
import fs21 from "fs";
|
|
3036
|
+
import path25 from "path";
|
|
3037
|
+
|
|
3038
|
+
// src/providers/gemini/interactions.ts
|
|
3039
|
+
init_errors();
|
|
3040
|
+
init_http_client();
|
|
3041
|
+
var BASE4 = "https://generativelanguage.googleapis.com/v1beta";
|
|
3042
|
+
var OMNI_MODEL = "gemini-omni-flash-preview";
|
|
3043
|
+
function buildOmniInteractionPayload(request) {
|
|
3044
|
+
return {
|
|
3045
|
+
model: OMNI_MODEL,
|
|
3046
|
+
input: buildOmniInput(request),
|
|
3047
|
+
response_format: { type: "video", delivery: "uri" },
|
|
3048
|
+
store: true,
|
|
3049
|
+
...request.previousInteractionId ? { previous_interaction_id: request.previousInteractionId } : {}
|
|
3050
|
+
};
|
|
3051
|
+
}
|
|
3052
|
+
async function createOmniInteraction(request) {
|
|
3053
|
+
const apiKey = requireGeminiApiKey();
|
|
3054
|
+
const raw = await httpJson({
|
|
3055
|
+
url: `${BASE4}/interactions`,
|
|
3056
|
+
method: "POST",
|
|
3057
|
+
headers: geminiAuthHeaders(apiKey),
|
|
3058
|
+
body: buildOmniInteractionPayload(request)
|
|
3059
|
+
});
|
|
3060
|
+
if (!raw.id)
|
|
3061
|
+
throw new ProviderError("Omni response did not include an interaction ID.", "gemini");
|
|
3062
|
+
return { interactionId: raw.id, video: extractOmniVideo(raw) };
|
|
3063
|
+
}
|
|
3064
|
+
function buildOmniInput(request) {
|
|
3065
|
+
const parts = [{ type: "text", text: request.prompt }];
|
|
3066
|
+
for (const image of request.images ?? []) {
|
|
3067
|
+
parts.push({ type: "image", data: image.data, mime_type: image.mimeType });
|
|
3068
|
+
}
|
|
3069
|
+
if (request.videoFileUri) parts.push({ type: "document", uri: request.videoFileUri });
|
|
3070
|
+
return parts;
|
|
3071
|
+
}
|
|
3072
|
+
function extractOmniVideo(raw) {
|
|
3073
|
+
for (const step of raw.steps ?? []) {
|
|
3074
|
+
if (step.type !== "model_output") continue;
|
|
3075
|
+
for (const content of step.content ?? []) {
|
|
3076
|
+
if (content.type !== "video") continue;
|
|
3077
|
+
const mimeType = content.mime_type ?? "video/mp4";
|
|
3078
|
+
if (content.data) return { kind: "inline", data: content.data, mimeType };
|
|
3079
|
+
if (content.uri) return { kind: "uri", uri: content.uri, mimeType };
|
|
3080
|
+
}
|
|
3081
|
+
}
|
|
3082
|
+
throw new ProviderError("Omni response did not include a generated video.", "gemini");
|
|
3083
|
+
}
|
|
3084
|
+
async function waitForGeminiFile(fileUri, timeoutMs = 3e5) {
|
|
3085
|
+
const apiKey = requireGeminiApiKey();
|
|
3086
|
+
const name = fileNameFromUri(fileUri);
|
|
3087
|
+
const deadline = Date.now() + timeoutMs;
|
|
3088
|
+
while (Date.now() < deadline) {
|
|
3089
|
+
const response = await httpJson({
|
|
3090
|
+
url: `${BASE4}/${name}`,
|
|
3091
|
+
headers: geminiAuthHeaders(apiKey)
|
|
3092
|
+
});
|
|
3093
|
+
const file = unwrapGeminiFile(response);
|
|
3094
|
+
if (file.state === "ACTIVE") return file;
|
|
3095
|
+
if (file.state === "FAILED") throw new ProviderError("Generated Omni video failed.", "gemini");
|
|
3096
|
+
await sleep4(5e3);
|
|
3097
|
+
}
|
|
3098
|
+
throw new ProviderError("Timed out waiting for generated Omni video.", "gemini");
|
|
3099
|
+
}
|
|
3100
|
+
async function downloadGeminiFile(fileUri, destination) {
|
|
3101
|
+
const apiKey = requireGeminiApiKey();
|
|
3102
|
+
await downloadFile(fileUri, destination, void 0, geminiAuthHeaders(apiKey));
|
|
3103
|
+
}
|
|
3104
|
+
function fileNameFromUri(fileUri) {
|
|
3105
|
+
const pathname = new URL(fileUri).pathname;
|
|
3106
|
+
const match = pathname.match(/\/(files\/[^/:]+)(?::download)?$/);
|
|
3107
|
+
if (!match?.[1]) throw new ProviderError(`Unexpected Gemini file URI: ${fileUri}`, "gemini");
|
|
3108
|
+
return match[1];
|
|
3109
|
+
}
|
|
3110
|
+
function unwrapGeminiFile(response) {
|
|
3111
|
+
return "file" in response ? response.file : response;
|
|
3112
|
+
}
|
|
3113
|
+
function selectGeminiDownloadUri(file, fallbackUri) {
|
|
3114
|
+
return file.downloadUri ?? fallbackUri;
|
|
3115
|
+
}
|
|
3116
|
+
function sleep4(ms) {
|
|
3117
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// src/providers/gemini/commands/omni-video.ts
|
|
3121
|
+
function registerOmniVideoCommand(parent) {
|
|
3122
|
+
parent.command("omni-video").description("Generate or edit video with Gemini Omni Flash [PAID PREVIEW]").requiredOption("--prompt <text>", "Video prompt or concise edit instruction").option("--image <paths...>", "Local image guidance files").option("--video <path>", "One local video to edit (not available in EEA/CH/UK)").option("--previous-interaction-id <id>", "Continue a prior Gemini-stored interaction").option("--upload-timeout <ms>", "Files API processing timeout in ms", "300000").option("--output <path>", "Copy generated video to this path").option("-v, --verbose", "Verbose logging").addHelpText(
|
|
3123
|
+
"after",
|
|
3124
|
+
"\nPreview, paid-tier API. Outputs 3-10 second 720p videos. Audio references, video extension/interpolation, voice editing, and multiple videos are unsupported. Follow-up edits require provider-side stored interactions; no local history is kept."
|
|
3125
|
+
).action(async (opts) => {
|
|
3126
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
3127
|
+
validateInputs(opts);
|
|
3128
|
+
const images = (opts.image ?? []).map(readImage);
|
|
3129
|
+
const videoFile = opts.video ? await uploadFile(opts.video, { timeoutMs: parseTimeout(opts.uploadTimeout), logger }) : void 0;
|
|
3130
|
+
const result = await createOmniInteraction({
|
|
3131
|
+
prompt: opts.prompt,
|
|
3132
|
+
images,
|
|
3133
|
+
videoFileUri: videoFile?.uri,
|
|
3134
|
+
previousInteractionId: opts.previousInteractionId
|
|
3135
|
+
});
|
|
3136
|
+
const destination = path25.join(getOutputDir(), `gemini_omni_${Date.now()}.mp4`);
|
|
3137
|
+
if (result.video.kind === "inline") {
|
|
3138
|
+
fs21.writeFileSync(destination, Buffer.from(result.video.data, "base64"));
|
|
3139
|
+
} else {
|
|
3140
|
+
const generated = await waitForGeminiFile(result.video.uri);
|
|
3141
|
+
await downloadGeminiFile(selectGeminiDownloadUri(generated, result.video.uri), destination);
|
|
3142
|
+
}
|
|
3143
|
+
const metadataPath = `${destination}.json`;
|
|
3144
|
+
fs21.writeFileSync(
|
|
3145
|
+
metadataPath,
|
|
3146
|
+
`${JSON.stringify(buildOmniVideoMetadata(opts, result.interactionId), null, 2)}
|
|
3147
|
+
`
|
|
3148
|
+
);
|
|
3149
|
+
if (opts.output) {
|
|
3150
|
+
fs21.mkdirSync(path25.dirname(path25.resolve(opts.output)), { recursive: true });
|
|
3151
|
+
fs21.copyFileSync(destination, opts.output);
|
|
3152
|
+
fs21.copyFileSync(metadataPath, `${opts.output}.json`);
|
|
3153
|
+
}
|
|
3154
|
+
logger.success(`Saved video: ${destination}`);
|
|
3155
|
+
logger.success(`Saved metadata: ${metadataPath}`);
|
|
3156
|
+
if (opts.output) logger.success(`Copied to: ${opts.output}`);
|
|
3157
|
+
console.log(`Interaction ID: ${result.interactionId}`);
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
function buildOmniVideoMetadata(opts, interactionId) {
|
|
3161
|
+
return {
|
|
3162
|
+
provider: "gemini",
|
|
3163
|
+
model: "gemini-omni-flash-preview",
|
|
3164
|
+
prompt: opts.prompt,
|
|
3165
|
+
inputFiles: [...opts.image ?? [], ...opts.video ? [opts.video] : []].map(
|
|
3166
|
+
(filePath) => path25.basename(filePath)
|
|
3167
|
+
),
|
|
3168
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3169
|
+
interactionId
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
function validateInputs(opts) {
|
|
3173
|
+
if (opts.video && opts.image?.length) {
|
|
3174
|
+
throw new ValidationError("Use either --video or --image guidance, not both.");
|
|
3175
|
+
}
|
|
3176
|
+
if (opts.video) {
|
|
3177
|
+
requireLocalFile(opts.video, "video");
|
|
3178
|
+
if (!getMimeType(opts.video).startsWith("video/")) {
|
|
3179
|
+
throw new ValidationError(`Unsupported video file: ${opts.video}`);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
for (const imagePath of opts.image ?? []) {
|
|
3183
|
+
requireLocalFile(imagePath, "image");
|
|
3184
|
+
if (!getMimeType(imagePath).startsWith("image/")) {
|
|
3185
|
+
throw new ValidationError(`Unsupported image file: ${imagePath}`);
|
|
3186
|
+
}
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
function requireLocalFile(filePath, label) {
|
|
3190
|
+
if (/^https?:\/\//i.test(filePath)) throw new ValidationError(`${label} URLs are not supported.`);
|
|
3191
|
+
if (!fs21.existsSync(filePath)) throw new ValidationError(`${label} file not found: ${filePath}`);
|
|
3192
|
+
}
|
|
3193
|
+
function readImage(filePath) {
|
|
3194
|
+
return { data: fs21.readFileSync(filePath).toString("base64"), mimeType: getMimeType(filePath) };
|
|
3195
|
+
}
|
|
3196
|
+
function parseTimeout(value) {
|
|
3197
|
+
const timeout = Number.parseInt(value, 10);
|
|
3198
|
+
if (!Number.isFinite(timeout) || timeout <= 0)
|
|
3199
|
+
throw new ValidationError("--upload-timeout must be positive.");
|
|
3200
|
+
return timeout;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
// src/providers/gemini/commands/transcribe.ts
|
|
3204
|
+
init_errors();
|
|
3205
|
+
import fs22 from "fs";
|
|
2990
3206
|
function registerTranscribeCommand2(parent) {
|
|
2991
3207
|
parent.command("transcribe").description("Transcribe audio or video files with Gemini").requiredOption("--files <paths...>", "Audio/video file paths to transcribe").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(
|
|
2992
3208
|
async (opts) => {
|
|
2993
3209
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
2994
3210
|
const model = opts.model ?? getDefaultModel("transcribe");
|
|
2995
3211
|
for (const f of opts.files) {
|
|
2996
|
-
if (!
|
|
3212
|
+
if (!fs22.existsSync(f)) throw new ValidationError(`File not found: ${f}`);
|
|
2997
3213
|
}
|
|
2998
3214
|
const results = [];
|
|
2999
3215
|
for (let i = 0; i < opts.files.length; i++) {
|
|
@@ -3046,13 +3262,14 @@ function registerGeminiCommands(program2) {
|
|
|
3046
3262
|
registerGeminiImageToImageCommand(gemini);
|
|
3047
3263
|
registerGenerateVideoCommand(gemini);
|
|
3048
3264
|
registerGeminiImageToVideoCommand(gemini);
|
|
3265
|
+
registerOmniVideoCommand(gemini);
|
|
3049
3266
|
registerGeminiGenerateSpeechCommand(gemini);
|
|
3050
3267
|
}
|
|
3051
3268
|
|
|
3052
3269
|
// src/providers/leonardo/commands/generate.ts
|
|
3053
3270
|
init_http_client();
|
|
3054
|
-
import
|
|
3055
|
-
import
|
|
3271
|
+
import fs23 from "fs";
|
|
3272
|
+
import path26 from "path";
|
|
3056
3273
|
|
|
3057
3274
|
// src/providers/leonardo/client.ts
|
|
3058
3275
|
init_errors();
|
|
@@ -3126,11 +3343,11 @@ var LeonardoClient = class {
|
|
|
3126
3343
|
}
|
|
3127
3344
|
apiKey;
|
|
3128
3345
|
baseUrl;
|
|
3129
|
-
async request(
|
|
3346
|
+
async request(path46, opts = {}) {
|
|
3130
3347
|
const base = this.baseUrl.replace(/\/$/, "");
|
|
3131
|
-
const v2Override =
|
|
3348
|
+
const v2Override = path46.startsWith("v2:");
|
|
3132
3349
|
const finalBase = v2Override ? base.replace(/\/v1$/, "/v2") : base;
|
|
3133
|
-
const finalPath = v2Override ?
|
|
3350
|
+
const finalPath = v2Override ? path46.slice(3) : path46;
|
|
3134
3351
|
const url = new URL(finalBase + finalPath);
|
|
3135
3352
|
if (opts.query) {
|
|
3136
3353
|
for (const [k, v] of Object.entries(opts.query)) {
|
|
@@ -3149,14 +3366,14 @@ var LeonardoClient = class {
|
|
|
3149
3366
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
3150
3367
|
});
|
|
3151
3368
|
}
|
|
3152
|
-
get(
|
|
3153
|
-
return this.request(
|
|
3369
|
+
get(path46, query, logger) {
|
|
3370
|
+
return this.request(path46, { method: "GET", query, logger });
|
|
3154
3371
|
}
|
|
3155
|
-
post(
|
|
3156
|
-
return this.request(
|
|
3372
|
+
post(path46, body, logger) {
|
|
3373
|
+
return this.request(path46, { method: "POST", body, logger });
|
|
3157
3374
|
}
|
|
3158
|
-
delete(
|
|
3159
|
-
return this.request(
|
|
3375
|
+
delete(path46, logger) {
|
|
3376
|
+
return this.request(path46, { method: "DELETE", logger });
|
|
3160
3377
|
}
|
|
3161
3378
|
};
|
|
3162
3379
|
function createLeonardoClient() {
|
|
@@ -3167,7 +3384,7 @@ function createLeonardoClient() {
|
|
|
3167
3384
|
function extFromUrl(url, fallback = ".png") {
|
|
3168
3385
|
try {
|
|
3169
3386
|
const u = new URL(url);
|
|
3170
|
-
const e =
|
|
3387
|
+
const e = path26.extname(u.pathname);
|
|
3171
3388
|
return e.length > 0 ? e : fallback;
|
|
3172
3389
|
} catch {
|
|
3173
3390
|
return fallback;
|
|
@@ -3252,7 +3469,7 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
3252
3469
|
if (!img) continue;
|
|
3253
3470
|
const ext = extFromUrl(img.url);
|
|
3254
3471
|
const filename = autoFilename(generationId, i + 1, ext);
|
|
3255
|
-
const filepath =
|
|
3472
|
+
const filepath = path26.join(outDir, filename);
|
|
3256
3473
|
try {
|
|
3257
3474
|
await downloadFile(img.url, filepath);
|
|
3258
3475
|
saved.push(filepath);
|
|
@@ -3262,9 +3479,9 @@ function registerLeonardoGenerateCommand(parent) {
|
|
|
3262
3479
|
}
|
|
3263
3480
|
}
|
|
3264
3481
|
if (opts.output && saved[0]) {
|
|
3265
|
-
const dest =
|
|
3266
|
-
|
|
3267
|
-
|
|
3482
|
+
const dest = path26.resolve(opts.output);
|
|
3483
|
+
fs23.mkdirSync(path26.dirname(dest), { recursive: true });
|
|
3484
|
+
fs23.copyFileSync(saved[0], dest);
|
|
3268
3485
|
logger.success(`Copied to ${dest}`);
|
|
3269
3486
|
}
|
|
3270
3487
|
console.log(`
|
|
@@ -3276,11 +3493,11 @@ Generated ${saved.length} image(s):`);
|
|
|
3276
3493
|
|
|
3277
3494
|
// src/providers/leonardo/commands/image-to-image.ts
|
|
3278
3495
|
init_http_client();
|
|
3279
|
-
import
|
|
3280
|
-
import
|
|
3496
|
+
import fs24 from "fs";
|
|
3497
|
+
import path27 from "path";
|
|
3281
3498
|
function extFromUrl2(url, fallback = ".png") {
|
|
3282
3499
|
try {
|
|
3283
|
-
return
|
|
3500
|
+
return path27.extname(new URL(url).pathname) || fallback;
|
|
3284
3501
|
} catch {
|
|
3285
3502
|
return fallback;
|
|
3286
3503
|
}
|
|
@@ -3342,7 +3559,7 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
3342
3559
|
if (!img) continue;
|
|
3343
3560
|
const ext = extFromUrl2(img.url);
|
|
3344
3561
|
const filename = `leonardo-i2i-${generationId.slice(0, 8)}-${String(i + 1).padStart(2, "0")}${ext}`;
|
|
3345
|
-
const filepath =
|
|
3562
|
+
const filepath = path27.join(outDir, filename);
|
|
3346
3563
|
try {
|
|
3347
3564
|
await downloadFile(img.url, filepath);
|
|
3348
3565
|
saved.push(filepath);
|
|
@@ -3352,9 +3569,9 @@ function registerLeonardoImageToImageCommand(parent) {
|
|
|
3352
3569
|
}
|
|
3353
3570
|
}
|
|
3354
3571
|
if (opts.output && saved[0]) {
|
|
3355
|
-
const dest =
|
|
3356
|
-
|
|
3357
|
-
|
|
3572
|
+
const dest = path27.resolve(opts.output);
|
|
3573
|
+
fs24.mkdirSync(path27.dirname(dest), { recursive: true });
|
|
3574
|
+
fs24.copyFileSync(saved[0], dest);
|
|
3358
3575
|
logger.success(`Copied to ${dest}`);
|
|
3359
3576
|
}
|
|
3360
3577
|
console.log(`
|
|
@@ -3366,7 +3583,7 @@ Generated ${saved.length} image(s):`);
|
|
|
3366
3583
|
|
|
3367
3584
|
// src/providers/leonardo/commands/video.ts
|
|
3368
3585
|
init_http_client();
|
|
3369
|
-
import
|
|
3586
|
+
import path28 from "path";
|
|
3370
3587
|
function v2DimsFor(resolution) {
|
|
3371
3588
|
if (resolution === "RESOLUTION_1080") return { width: 1920, height: 1080 };
|
|
3372
3589
|
if (resolution === "RESOLUTION_480") return { width: 854, height: 480 };
|
|
@@ -3477,7 +3694,7 @@ async function pollAndDownload(client, jobId, opts) {
|
|
|
3477
3694
|
logger.error(`No video URL in completed generation: ${JSON.stringify(gen)}`);
|
|
3478
3695
|
process.exit(1);
|
|
3479
3696
|
}
|
|
3480
|
-
const outPath = opts.output ??
|
|
3697
|
+
const outPath = opts.output ?? path28.join(getOutputDir(), `${opts.basename}-${jobId.slice(0, 8)}.mp4`);
|
|
3481
3698
|
await downloadFile(videoUrl, outPath);
|
|
3482
3699
|
logger.success(`Saved ${outPath}`);
|
|
3483
3700
|
await maybeDownloadThumb(gen, outPath, {
|
|
@@ -3584,7 +3801,7 @@ function registerLeonardoModelsCommand(parent) {
|
|
|
3584
3801
|
|
|
3585
3802
|
// src/providers/leonardo/commands/status.ts
|
|
3586
3803
|
init_http_client();
|
|
3587
|
-
import
|
|
3804
|
+
import path29 from "path";
|
|
3588
3805
|
var VIDEO_KEYS = ["motionMP4URL", "url"];
|
|
3589
3806
|
var IMAGE_EXT_RE2 = /\.(jpe?g|png|webp|gif)(\?|$)/i;
|
|
3590
3807
|
var VIDEO_EXT_RE = /\.(mp4|webm|mov|m4v)(\?|$)/i;
|
|
@@ -3649,7 +3866,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
3649
3866
|
const outDir = getOutputDir();
|
|
3650
3867
|
let idx = 0;
|
|
3651
3868
|
for (const url of videos) {
|
|
3652
|
-
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output :
|
|
3869
|
+
const dest = opts.output && videos.length === 1 && images.length === 0 ? opts.output : path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.mp4`);
|
|
3653
3870
|
await downloadFile(url, dest);
|
|
3654
3871
|
logger.success(`Saved ${dest}`);
|
|
3655
3872
|
await maybeDownloadThumb(gen, dest, {
|
|
@@ -3662,7 +3879,7 @@ function registerLeonardoStatusCommand(parent) {
|
|
|
3662
3879
|
for (const url of images) {
|
|
3663
3880
|
const m = url.match(IMAGE_EXT_RE2);
|
|
3664
3881
|
const ext = m?.[1] ? m[1].toLowerCase().replace("jpeg", "jpg") : "png";
|
|
3665
|
-
const dest =
|
|
3882
|
+
const dest = path29.join(outDir, `leonardo-${generationId.slice(0, 8)}-${idx}.${ext}`);
|
|
3666
3883
|
await downloadFile(url, dest);
|
|
3667
3884
|
logger.success(`Saved ${dest}`);
|
|
3668
3885
|
idx++;
|
|
@@ -3750,19 +3967,19 @@ function requireMinimaxKey() {
|
|
|
3750
3967
|
}
|
|
3751
3968
|
return key;
|
|
3752
3969
|
}
|
|
3753
|
-
function
|
|
3970
|
+
function authHeaders2(apiKey) {
|
|
3754
3971
|
return {
|
|
3755
3972
|
Authorization: `Bearer ${apiKey}`,
|
|
3756
3973
|
"Content-Type": "application/json"
|
|
3757
3974
|
};
|
|
3758
3975
|
}
|
|
3759
|
-
async function apiPost(
|
|
3760
|
-
const url = `${MINIMAX_BASE_URL}/${
|
|
3976
|
+
async function apiPost(endpoint2, payload, apiKey, opts = {}) {
|
|
3977
|
+
const url = `${MINIMAX_BASE_URL}/${endpoint2}`;
|
|
3761
3978
|
opts.logger?.debug(`POST ${url}`);
|
|
3762
3979
|
const data = await httpJson({
|
|
3763
3980
|
url,
|
|
3764
3981
|
method: "POST",
|
|
3765
|
-
headers:
|
|
3982
|
+
headers: authHeaders2(apiKey),
|
|
3766
3983
|
body: payload,
|
|
3767
3984
|
timeoutMs: opts.timeoutMs ?? 12e4
|
|
3768
3985
|
});
|
|
@@ -3775,10 +3992,10 @@ async function apiPost(endpoint, payload, apiKey, opts = {}) {
|
|
|
3775
3992
|
}
|
|
3776
3993
|
return data;
|
|
3777
3994
|
}
|
|
3778
|
-
async function apiGet2(
|
|
3779
|
-
const url = new URL(`${MINIMAX_BASE_URL}/${
|
|
3995
|
+
async function apiGet2(endpoint2, params, apiKey) {
|
|
3996
|
+
const url = new URL(`${MINIMAX_BASE_URL}/${endpoint2}`);
|
|
3780
3997
|
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
|
|
3781
|
-
return httpJson({ url: url.toString(), headers:
|
|
3998
|
+
return httpJson({ url: url.toString(), headers: authHeaders2(apiKey) });
|
|
3782
3999
|
}
|
|
3783
4000
|
async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
3784
4001
|
const { intervalMs = 1e4, timeoutMs = 6e5, logger } = opts;
|
|
@@ -3794,7 +4011,7 @@ async function pollAsyncTask(taskId, taskType, apiKey, opts = {}) {
|
|
|
3794
4011
|
if (status === "Failed" || status === "Error") {
|
|
3795
4012
|
throw new ProviderError(`Task ${taskId} failed: ${JSON.stringify(result)}`, "minimax");
|
|
3796
4013
|
}
|
|
3797
|
-
await
|
|
4014
|
+
await sleep5(intervalMs);
|
|
3798
4015
|
elapsed += intervalMs;
|
|
3799
4016
|
}
|
|
3800
4017
|
throw new ProviderError(`Task ${taskId} timed out after ${timeoutMs / 1e3}s`, "minimax");
|
|
@@ -3813,14 +4030,14 @@ async function downloadMinimaxFile(fileId, apiKey, dest, logger) {
|
|
|
3813
4030
|
await downloadFile(downloadUrl, dest);
|
|
3814
4031
|
return dest;
|
|
3815
4032
|
}
|
|
3816
|
-
function
|
|
4033
|
+
function sleep5(ms) {
|
|
3817
4034
|
return new Promise((r) => setTimeout(r, ms));
|
|
3818
4035
|
}
|
|
3819
4036
|
|
|
3820
4037
|
// src/providers/minimax/generators/music.ts
|
|
3821
4038
|
init_http_client();
|
|
3822
|
-
import
|
|
3823
|
-
import
|
|
4039
|
+
import fs25 from "fs";
|
|
4040
|
+
import path30 from "path";
|
|
3824
4041
|
async function generateMinimaxMusic(opts) {
|
|
3825
4042
|
const {
|
|
3826
4043
|
apiKey,
|
|
@@ -3865,14 +4082,14 @@ async function generateMinimaxMusic(opts) {
|
|
|
3865
4082
|
}
|
|
3866
4083
|
const durationMs = resp.extra_info?.music_duration ?? 0;
|
|
3867
4084
|
const outDir = getOutputDir();
|
|
3868
|
-
const dest =
|
|
4085
|
+
const dest = path30.join(outDir, `minimax_music_${Date.now()}.${outputFormat}`);
|
|
3869
4086
|
try {
|
|
3870
4087
|
if (audioData.startsWith("http")) {
|
|
3871
4088
|
const bytes = await fetchBytes(audioData);
|
|
3872
|
-
|
|
4089
|
+
fs25.writeFileSync(dest, bytes);
|
|
3873
4090
|
} else {
|
|
3874
4091
|
const audioBytes = Buffer.from(audioData, "hex");
|
|
3875
|
-
|
|
4092
|
+
fs25.writeFileSync(dest, audioBytes);
|
|
3876
4093
|
}
|
|
3877
4094
|
} catch (e) {
|
|
3878
4095
|
return {
|
|
@@ -3883,8 +4100,8 @@ async function generateMinimaxMusic(opts) {
|
|
|
3883
4100
|
const durSec = durationMs / 1e3;
|
|
3884
4101
|
logger?.success(`Saved: ${dest} (${durSec.toFixed(1)}s)`);
|
|
3885
4102
|
if (output) {
|
|
3886
|
-
|
|
3887
|
-
|
|
4103
|
+
fs25.mkdirSync(path30.dirname(path30.resolve(output)), { recursive: true });
|
|
4104
|
+
fs25.copyFileSync(dest, output);
|
|
3888
4105
|
}
|
|
3889
4106
|
return { status: "success", generatedAudio: dest, durationMs, model };
|
|
3890
4107
|
}
|
|
@@ -3958,8 +4175,8 @@ Generated music: ${result.generatedAudio}`);
|
|
|
3958
4175
|
init_errors();
|
|
3959
4176
|
|
|
3960
4177
|
// src/providers/minimax/generators/speech.ts
|
|
3961
|
-
import
|
|
3962
|
-
import
|
|
4178
|
+
import fs26 from "fs";
|
|
4179
|
+
import path31 from "path";
|
|
3963
4180
|
async function generateMinimaxSpeech(opts) {
|
|
3964
4181
|
const {
|
|
3965
4182
|
apiKey,
|
|
@@ -4006,12 +4223,12 @@ async function generateMinimaxSpeech(opts) {
|
|
|
4006
4223
|
const audioBytes = Buffer.from(audioHex, "hex");
|
|
4007
4224
|
const ext = ["mp3", "wav", "flac"].includes(outputFormat) ? outputFormat : "mp3";
|
|
4008
4225
|
const outDir = getOutputDir();
|
|
4009
|
-
const dest =
|
|
4010
|
-
|
|
4226
|
+
const dest = path31.join(outDir, `minimax_speech_${Date.now()}.${ext}`);
|
|
4227
|
+
fs26.writeFileSync(dest, audioBytes);
|
|
4011
4228
|
logger?.success(`Saved: ${dest} (${(audioBytes.length / 1024).toFixed(1)} KB)`);
|
|
4012
4229
|
if (output) {
|
|
4013
|
-
|
|
4014
|
-
|
|
4230
|
+
fs26.mkdirSync(path31.dirname(path31.resolve(output)), { recursive: true });
|
|
4231
|
+
fs26.copyFileSync(dest, output);
|
|
4015
4232
|
}
|
|
4016
4233
|
return { status: "success", generatedAudio: dest, model };
|
|
4017
4234
|
}
|
|
@@ -4058,8 +4275,8 @@ Generated audio: ${result.generatedAudio}`);
|
|
|
4058
4275
|
init_errors();
|
|
4059
4276
|
|
|
4060
4277
|
// src/providers/minimax/generators/video.ts
|
|
4061
|
-
import
|
|
4062
|
-
import
|
|
4278
|
+
import fs27 from "fs";
|
|
4279
|
+
import path32 from "path";
|
|
4063
4280
|
async function generateMinimaxVideo(opts) {
|
|
4064
4281
|
const {
|
|
4065
4282
|
apiKey,
|
|
@@ -4105,18 +4322,18 @@ async function generateMinimaxVideo(opts) {
|
|
|
4105
4322
|
return { status: "error", error: `No file_id in poll result: ${JSON.stringify(pollResult)}` };
|
|
4106
4323
|
}
|
|
4107
4324
|
const outDir = getOutputDir();
|
|
4108
|
-
const dest =
|
|
4325
|
+
const dest = path32.join(outDir, `minimax_video_${Date.now()}.mp4`);
|
|
4109
4326
|
try {
|
|
4110
4327
|
await downloadMinimaxFile(fileId, apiKey, dest, logger);
|
|
4111
4328
|
} catch (e) {
|
|
4112
4329
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
4113
4330
|
}
|
|
4114
4331
|
const elapsed = (Date.now() - start) / 1e3;
|
|
4115
|
-
const stat =
|
|
4332
|
+
const stat = fs27.statSync(dest);
|
|
4116
4333
|
const fileSizeMb = stat.size / (1024 * 1024);
|
|
4117
4334
|
if (output) {
|
|
4118
|
-
|
|
4119
|
-
|
|
4335
|
+
fs27.mkdirSync(path32.dirname(path32.resolve(output)), { recursive: true });
|
|
4336
|
+
fs27.copyFileSync(dest, output);
|
|
4120
4337
|
}
|
|
4121
4338
|
await maybeDownloadThumb(pollResult, dest, {
|
|
4122
4339
|
skip: thumb === false,
|
|
@@ -4179,8 +4396,8 @@ Generated video: ${result.generatedVideo}`);
|
|
|
4179
4396
|
// src/providers/minimax/generators/image.ts
|
|
4180
4397
|
init_errors();
|
|
4181
4398
|
init_http_client();
|
|
4182
|
-
import
|
|
4183
|
-
import
|
|
4399
|
+
import fs28 from "fs";
|
|
4400
|
+
import path33 from "path";
|
|
4184
4401
|
async function generateMinimaxImage(opts) {
|
|
4185
4402
|
const {
|
|
4186
4403
|
apiKey,
|
|
@@ -4218,8 +4435,8 @@ async function generateMinimaxImage(opts) {
|
|
|
4218
4435
|
try {
|
|
4219
4436
|
const bytes = await fetchBytes(url);
|
|
4220
4437
|
const fname = `minimax_image_${Date.now()}_${i}.png`;
|
|
4221
|
-
const dest =
|
|
4222
|
-
|
|
4438
|
+
const dest = path33.join(outDir, fname);
|
|
4439
|
+
fs28.writeFileSync(dest, bytes);
|
|
4223
4440
|
savedFiles.push(dest);
|
|
4224
4441
|
logger?.success(`Saved: ${dest}`);
|
|
4225
4442
|
} catch (e) {
|
|
@@ -4230,8 +4447,8 @@ async function generateMinimaxImage(opts) {
|
|
|
4230
4447
|
}
|
|
4231
4448
|
}
|
|
4232
4449
|
if (output && savedFiles[0]) {
|
|
4233
|
-
|
|
4234
|
-
|
|
4450
|
+
fs28.mkdirSync(path33.dirname(path33.resolve(output)), { recursive: true });
|
|
4451
|
+
fs28.copyFileSync(savedFiles[0], output);
|
|
4235
4452
|
}
|
|
4236
4453
|
return { status: "success", generatedImages: savedFiles, model };
|
|
4237
4454
|
}
|
|
@@ -4267,8 +4484,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
4267
4484
|
// src/providers/minimax/commands/image-to-image.ts
|
|
4268
4485
|
init_errors();
|
|
4269
4486
|
init_http_client();
|
|
4270
|
-
import
|
|
4271
|
-
import
|
|
4487
|
+
import fs29 from "fs";
|
|
4488
|
+
import path34 from "path";
|
|
4272
4489
|
function registerMinimaxImageToImageCommand(parent) {
|
|
4273
4490
|
parent.command("image-to-image").alias("i2i").description(
|
|
4274
4491
|
"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."
|
|
@@ -4323,8 +4540,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
4323
4540
|
if (!url) continue;
|
|
4324
4541
|
try {
|
|
4325
4542
|
const bytes = await fetchBytes(url);
|
|
4326
|
-
const dest =
|
|
4327
|
-
|
|
4543
|
+
const dest = path34.join(outDir, `minimax-i2i-${ts}-${i + 1}.png`);
|
|
4544
|
+
fs29.writeFileSync(dest, bytes);
|
|
4328
4545
|
saved.push(dest);
|
|
4329
4546
|
logger.success(`Saved: ${dest}`);
|
|
4330
4547
|
} catch (e) {
|
|
@@ -4335,8 +4552,8 @@ function registerMinimaxImageToImageCommand(parent) {
|
|
|
4335
4552
|
}
|
|
4336
4553
|
}
|
|
4337
4554
|
if (opts.output && saved[0]) {
|
|
4338
|
-
|
|
4339
|
-
|
|
4555
|
+
fs29.mkdirSync(path34.dirname(path34.resolve(opts.output)), { recursive: true });
|
|
4556
|
+
fs29.copyFileSync(saved[0], opts.output);
|
|
4340
4557
|
logger.success(`Copied to: ${opts.output}`);
|
|
4341
4558
|
}
|
|
4342
4559
|
console.log(`
|
|
@@ -4356,9 +4573,636 @@ function registerMinimaxCommands(program2) {
|
|
|
4356
4573
|
registerMinimaxGenerateMusicCommand(minimax);
|
|
4357
4574
|
}
|
|
4358
4575
|
|
|
4576
|
+
// src/providers/openai/commands/generate-speech.ts
|
|
4577
|
+
init_errors();
|
|
4578
|
+
|
|
4579
|
+
// src/providers/openai/models.ts
|
|
4580
|
+
init_errors();
|
|
4581
|
+
var OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
4582
|
+
var DEFAULT_OPENAI_IMAGE_MODEL = "gpt-image-2";
|
|
4583
|
+
var DEFAULT_OPENAI_TTS_MODEL = "gpt-4o-mini-tts";
|
|
4584
|
+
var DEFAULT_OPENAI_STT_MODEL = "gpt-4o-transcribe";
|
|
4585
|
+
var OPENAI_IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp"];
|
|
4586
|
+
var OPENAI_IMAGE_QUALITIES = ["low", "medium", "high"];
|
|
4587
|
+
var OPENAI_IMAGE_SIZES = ["1024x1024", "1024x1536", "1536x1024", "auto"];
|
|
4588
|
+
var OPENAI_TTS_OUTPUT_FORMATS = ["mp3", "opus", "aac", "flac", "wav", "pcm"];
|
|
4589
|
+
var OPENAI_TTS_VOICES = [
|
|
4590
|
+
"alloy",
|
|
4591
|
+
"ash",
|
|
4592
|
+
"ballad",
|
|
4593
|
+
"cedar",
|
|
4594
|
+
"coral",
|
|
4595
|
+
"echo",
|
|
4596
|
+
"fable",
|
|
4597
|
+
"marin",
|
|
4598
|
+
"nova",
|
|
4599
|
+
"onyx",
|
|
4600
|
+
"sage",
|
|
4601
|
+
"shimmer",
|
|
4602
|
+
"verse"
|
|
4603
|
+
];
|
|
4604
|
+
var OPENAI_TRANSCRIPT_FORMATS = ["json", "text", "diarized_json"];
|
|
4605
|
+
function isOneOf(value, values) {
|
|
4606
|
+
return values.includes(value);
|
|
4607
|
+
}
|
|
4608
|
+
function extFromImageFormat(format) {
|
|
4609
|
+
if (format === "jpeg") return "jpg";
|
|
4610
|
+
return format;
|
|
4611
|
+
}
|
|
4612
|
+
function extFromAudioFormat(format) {
|
|
4613
|
+
return format;
|
|
4614
|
+
}
|
|
4615
|
+
function parseImageFormat(value) {
|
|
4616
|
+
if (!isOneOf(value, OPENAI_IMAGE_OUTPUT_FORMATS)) {
|
|
4617
|
+
throw new ValidationError(`--format must be one of: ${OPENAI_IMAGE_OUTPUT_FORMATS.join(", ")}`);
|
|
4618
|
+
}
|
|
4619
|
+
return value;
|
|
4620
|
+
}
|
|
4621
|
+
function parseImageQuality(value) {
|
|
4622
|
+
if (!isOneOf(value, OPENAI_IMAGE_QUALITIES)) {
|
|
4623
|
+
throw new ValidationError(`--quality must be one of: ${OPENAI_IMAGE_QUALITIES.join(", ")}`);
|
|
4624
|
+
}
|
|
4625
|
+
return value;
|
|
4626
|
+
}
|
|
4627
|
+
function parseImageSize(value) {
|
|
4628
|
+
if (!isOneOf(value, OPENAI_IMAGE_SIZES)) {
|
|
4629
|
+
throw new ValidationError(`--size must be one of: ${OPENAI_IMAGE_SIZES.join(", ")}`);
|
|
4630
|
+
}
|
|
4631
|
+
return value;
|
|
4632
|
+
}
|
|
4633
|
+
function parseDriver(value) {
|
|
4634
|
+
if (!isOneOf(value, ["api", "codex", "auto"])) {
|
|
4635
|
+
throw new ValidationError("--driver must be one of: api, codex, auto");
|
|
4636
|
+
}
|
|
4637
|
+
return value;
|
|
4638
|
+
}
|
|
4639
|
+
function parseNumImages(value) {
|
|
4640
|
+
if (!/^\d+$/.test(value)) {
|
|
4641
|
+
throw new ValidationError("--num-images must be an integer from 1 to 10");
|
|
4642
|
+
}
|
|
4643
|
+
const parsed = Number.parseInt(value, 10);
|
|
4644
|
+
if (parsed < 1 || parsed > 10) {
|
|
4645
|
+
throw new ValidationError("--num-images must be an integer from 1 to 10");
|
|
4646
|
+
}
|
|
4647
|
+
return parsed;
|
|
4648
|
+
}
|
|
4649
|
+
function parseTtsFormat(value) {
|
|
4650
|
+
if (!isOneOf(value, OPENAI_TTS_OUTPUT_FORMATS)) {
|
|
4651
|
+
throw new ValidationError(
|
|
4652
|
+
`--output-format must be one of: ${OPENAI_TTS_OUTPUT_FORMATS.join(", ")}`
|
|
4653
|
+
);
|
|
4654
|
+
}
|
|
4655
|
+
return value;
|
|
4656
|
+
}
|
|
4657
|
+
function validateTtsVoice(value) {
|
|
4658
|
+
if (!isOneOf(value, OPENAI_TTS_VOICES)) {
|
|
4659
|
+
throw new ValidationError(`--voice must be one of: ${OPENAI_TTS_VOICES.join(", ")}`);
|
|
4660
|
+
}
|
|
4661
|
+
}
|
|
4662
|
+
function parseTranscriptFormat(value) {
|
|
4663
|
+
if (!isOneOf(value, OPENAI_TRANSCRIPT_FORMATS)) {
|
|
4664
|
+
throw new ValidationError(`--format must be one of: ${OPENAI_TRANSCRIPT_FORMATS.join(", ")}`);
|
|
4665
|
+
}
|
|
4666
|
+
return value;
|
|
4667
|
+
}
|
|
4668
|
+
function resolveTranscriptionRequestFormat(cliFormat, model) {
|
|
4669
|
+
if (cliFormat === "diarized_json") {
|
|
4670
|
+
if (model !== "gpt-4o-transcribe-diarize") {
|
|
4671
|
+
throw new ValidationError("--format diarized_json requires gpt-4o-transcribe-diarize");
|
|
4672
|
+
}
|
|
4673
|
+
return "diarized_json";
|
|
4674
|
+
}
|
|
4675
|
+
return "json";
|
|
4676
|
+
}
|
|
4677
|
+
function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
4678
|
+
if (names.length !== refs.length) {
|
|
4679
|
+
throw new ValidationError(
|
|
4680
|
+
"--known-speaker-name and --known-speaker-reference must have equal counts"
|
|
4681
|
+
);
|
|
4682
|
+
}
|
|
4683
|
+
if (names.length > 4) {
|
|
4684
|
+
throw new ValidationError("OpenAI diarization supports up to 4 known speakers");
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4688
|
+
// src/providers/openai/openai-audio.ts
|
|
4689
|
+
init_http_client();
|
|
4690
|
+
import fs31 from "fs";
|
|
4691
|
+
import path36 from "path";
|
|
4692
|
+
|
|
4693
|
+
// src/providers/openai/client.ts
|
|
4694
|
+
import fs30 from "fs";
|
|
4695
|
+
import path35 from "path";
|
|
4696
|
+
init_errors();
|
|
4697
|
+
init_http_client();
|
|
4698
|
+
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
4699
|
+
function requireOpenAIKey() {
|
|
4700
|
+
const key = resolveKey("OPENAI_API_KEY");
|
|
4701
|
+
if (!key) {
|
|
4702
|
+
throw new ConfigError(
|
|
4703
|
+
"OPENAI_API_KEY is not set. Get one at https://platform.openai.com/api-keys"
|
|
4704
|
+
);
|
|
4705
|
+
}
|
|
4706
|
+
return key;
|
|
4707
|
+
}
|
|
4708
|
+
function endpoint(pathname) {
|
|
4709
|
+
return `${OPENAI_BASE_URL}/${pathname.replace(/^\//, "")}`;
|
|
4710
|
+
}
|
|
4711
|
+
function authHeaders3(apiKey, extra) {
|
|
4712
|
+
return { Authorization: `Bearer ${apiKey}`, ...extra ?? {} };
|
|
4713
|
+
}
|
|
4714
|
+
function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
|
|
4715
|
+
return httpJson({
|
|
4716
|
+
url: endpoint(pathname),
|
|
4717
|
+
method: "POST",
|
|
4718
|
+
headers: authHeaders3(apiKey, { "Content-Type": "application/json" }),
|
|
4719
|
+
body,
|
|
4720
|
+
timeoutMs
|
|
4721
|
+
});
|
|
4722
|
+
}
|
|
4723
|
+
async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
|
|
4724
|
+
const url = endpoint(pathname);
|
|
4725
|
+
let res;
|
|
4726
|
+
try {
|
|
4727
|
+
res = await globalThis.fetch(url, {
|
|
4728
|
+
method: "POST",
|
|
4729
|
+
headers: authHeaders3(apiKey),
|
|
4730
|
+
body: form,
|
|
4731
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4732
|
+
});
|
|
4733
|
+
} catch (cause) {
|
|
4734
|
+
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
4735
|
+
}
|
|
4736
|
+
if (!res.ok) {
|
|
4737
|
+
const text = await res.text().catch(() => "");
|
|
4738
|
+
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
4739
|
+
}
|
|
4740
|
+
return res.json();
|
|
4741
|
+
}
|
|
4742
|
+
async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
|
|
4743
|
+
const url = endpoint(pathname);
|
|
4744
|
+
let res;
|
|
4745
|
+
try {
|
|
4746
|
+
res = await globalThis.fetch(url, {
|
|
4747
|
+
method: "POST",
|
|
4748
|
+
headers: authHeaders3(apiKey, { "Content-Type": "application/json" }),
|
|
4749
|
+
body: JSON.stringify(body),
|
|
4750
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
4751
|
+
});
|
|
4752
|
+
} catch (cause) {
|
|
4753
|
+
throw new HttpError(0, cause instanceof Error ? cause.message : String(cause), url);
|
|
4754
|
+
}
|
|
4755
|
+
if (!res.ok) {
|
|
4756
|
+
const text = await res.text().catch(() => "");
|
|
4757
|
+
throw new HttpError(res.status, text.slice(0, 500), url);
|
|
4758
|
+
}
|
|
4759
|
+
return Buffer.from(await res.arrayBuffer());
|
|
4760
|
+
}
|
|
4761
|
+
function fileFromPath(filePath) {
|
|
4762
|
+
const abs = path35.resolve(filePath);
|
|
4763
|
+
if (!fs30.existsSync(abs)) throw new ValidationError(`File not found: ${abs}`);
|
|
4764
|
+
const bytes = fs30.readFileSync(abs);
|
|
4765
|
+
return new File([new Uint8Array(bytes)], path35.basename(abs));
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
// src/providers/openai/openai-audio.ts
|
|
4769
|
+
function buildSpeechBody(opts) {
|
|
4770
|
+
const body = {
|
|
4771
|
+
model: opts.model ?? DEFAULT_OPENAI_TTS_MODEL,
|
|
4772
|
+
input: opts.text,
|
|
4773
|
+
voice: opts.voice,
|
|
4774
|
+
response_format: opts.outputFormat
|
|
4775
|
+
};
|
|
4776
|
+
if (opts.instructions) body.instructions = opts.instructions;
|
|
4777
|
+
return body;
|
|
4778
|
+
}
|
|
4779
|
+
async function buildTranscriptionForm(opts) {
|
|
4780
|
+
const model = opts.model ?? DEFAULT_OPENAI_STT_MODEL;
|
|
4781
|
+
const form = new FormData();
|
|
4782
|
+
form.append("file", opts.input);
|
|
4783
|
+
form.append("model", model);
|
|
4784
|
+
form.append("response_format", resolveTranscriptionRequestFormat(opts.cliFormat, model));
|
|
4785
|
+
if (opts.language) form.append("language", opts.language);
|
|
4786
|
+
if (opts.chunkingStrategy) form.append("chunking_strategy", opts.chunkingStrategy);
|
|
4787
|
+
const names = opts.knownSpeakerNames ?? [];
|
|
4788
|
+
const refs = opts.knownSpeakerReferences ?? [];
|
|
4789
|
+
validateKnownSpeakerPairing(names, refs);
|
|
4790
|
+
for (const name of names) form.append("known_speaker_names[]", name);
|
|
4791
|
+
for (const ref of refs) {
|
|
4792
|
+
form.append("known_speaker_references[]", await resolveAudioReferenceDataUrl(ref));
|
|
4793
|
+
}
|
|
4794
|
+
return form;
|
|
4795
|
+
}
|
|
4796
|
+
async function resolveAudioReferenceDataUrl(ref) {
|
|
4797
|
+
const resolved = await resolveAudioInput(ref);
|
|
4798
|
+
if (resolved.kind === "data") return resolved.dataUrl;
|
|
4799
|
+
const bytes = await fetchBytes(resolved.url);
|
|
4800
|
+
return `data:${audioMimeFromReference(resolved.url)};base64,${bytes.toString("base64")}`;
|
|
4801
|
+
}
|
|
4802
|
+
function audioMimeFromReference(ref) {
|
|
4803
|
+
const pathname = new URL(ref).pathname;
|
|
4804
|
+
const ext = path36.extname(pathname).toLowerCase();
|
|
4805
|
+
switch (ext) {
|
|
4806
|
+
case ".mp3":
|
|
4807
|
+
return "audio/mpeg";
|
|
4808
|
+
case ".wav":
|
|
4809
|
+
return "audio/wav";
|
|
4810
|
+
case ".m4a":
|
|
4811
|
+
return "audio/mp4";
|
|
4812
|
+
case ".flac":
|
|
4813
|
+
return "audio/flac";
|
|
4814
|
+
case ".ogg":
|
|
4815
|
+
return "audio/ogg";
|
|
4816
|
+
default:
|
|
4817
|
+
return "application/octet-stream";
|
|
4818
|
+
}
|
|
4819
|
+
}
|
|
4820
|
+
async function generateOpenAISpeech(opts) {
|
|
4821
|
+
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
4822
|
+
const ext = extFromAudioFormat(opts.outputFormat);
|
|
4823
|
+
const dest = path36.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
4824
|
+
fs31.writeFileSync(dest, bytes);
|
|
4825
|
+
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
4826
|
+
if (opts.output) {
|
|
4827
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
|
|
4828
|
+
fs31.copyFileSync(dest, opts.output);
|
|
4829
|
+
}
|
|
4830
|
+
return dest;
|
|
4831
|
+
}
|
|
4832
|
+
async function transcribeOpenAIAudio(opts) {
|
|
4833
|
+
const form = await buildTranscriptionForm({
|
|
4834
|
+
input: fileFromPath(opts.inputPath),
|
|
4835
|
+
model: opts.model,
|
|
4836
|
+
cliFormat: opts.format,
|
|
4837
|
+
language: opts.language,
|
|
4838
|
+
chunkingStrategy: opts.chunkingStrategy ?? (opts.model === "gpt-4o-transcribe-diarize" || opts.format === "diarized_json" ? "auto" : void 0),
|
|
4839
|
+
knownSpeakerNames: opts.knownSpeakerNames,
|
|
4840
|
+
knownSpeakerReferences: opts.knownSpeakerReferences
|
|
4841
|
+
});
|
|
4842
|
+
const data = await postOpenAIForm(
|
|
4843
|
+
"/audio/transcriptions",
|
|
4844
|
+
form,
|
|
4845
|
+
requireOpenAIKey(),
|
|
4846
|
+
6e5
|
|
4847
|
+
);
|
|
4848
|
+
const text = typeof data.text === "string" ? data.text : void 0;
|
|
4849
|
+
const ext = opts.format === "text" ? "txt" : "json";
|
|
4850
|
+
const dest = opts.output ? path36.resolve(opts.output) : path36.join(getOutputDir(), `openai-transcribe-${Date.now()}.${ext}`);
|
|
4851
|
+
fs31.mkdirSync(path36.dirname(dest), { recursive: true });
|
|
4852
|
+
fs31.writeFileSync(dest, opts.format === "text" ? text ?? "" : JSON.stringify(data, null, 2));
|
|
4853
|
+
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
4854
|
+
return { path: dest, text };
|
|
4855
|
+
}
|
|
4856
|
+
|
|
4857
|
+
// src/providers/openai/commands/generate-speech.ts
|
|
4858
|
+
function registerOpenAIGenerateSpeechCommand(parent) {
|
|
4859
|
+
parent.command("generate-speech").description("Generate speech with OpenAI TTS").option("--text <str>", "Text to speak").option("--prompt <str>", "Alias for --text").option("--model <id>", "TTS model", process.env.OPENAI_TTS_MODEL ?? DEFAULT_OPENAI_TTS_MODEL).option("--voice <name>", "OpenAI voice", "marin").option("--instructions <text>", "Voice/style instructions").option("--output-format <fmt>", "mp3|opus|aac|flac|wav|pcm", "mp3").option("--output <path>", "Copy generated audio to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
4860
|
+
const text = opts.text ?? opts.prompt;
|
|
4861
|
+
if (!text) throw new ValidationError("Either --text or --prompt is required");
|
|
4862
|
+
validateTtsVoice(opts.voice);
|
|
4863
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4864
|
+
const dest = await generateOpenAISpeech({
|
|
4865
|
+
text,
|
|
4866
|
+
model: opts.model,
|
|
4867
|
+
voice: opts.voice,
|
|
4868
|
+
outputFormat: parseTtsFormat(opts.outputFormat),
|
|
4869
|
+
instructions: opts.instructions,
|
|
4870
|
+
output: opts.output,
|
|
4871
|
+
logger
|
|
4872
|
+
});
|
|
4873
|
+
console.log(`
|
|
4874
|
+
Generated audio: ${dest}`);
|
|
4875
|
+
});
|
|
4876
|
+
}
|
|
4877
|
+
|
|
4878
|
+
// src/providers/openai/codex-image-driver.ts
|
|
4879
|
+
import fs32 from "fs";
|
|
4880
|
+
import os2 from "os";
|
|
4881
|
+
import path37 from "path";
|
|
4882
|
+
import { execa } from "execa";
|
|
4883
|
+
init_errors();
|
|
4884
|
+
var defaultRunner = async (cmd, args, opts) => {
|
|
4885
|
+
const res = await execa(cmd, args, {
|
|
4886
|
+
cwd: opts?.cwd,
|
|
4887
|
+
env: opts?.env,
|
|
4888
|
+
reject: false,
|
|
4889
|
+
timeout: opts?.timeoutMs
|
|
4890
|
+
});
|
|
4891
|
+
return { stdout: res.stdout, stderr: res.stderr, exitCode: res.exitCode ?? 0 };
|
|
4892
|
+
};
|
|
4893
|
+
async function checkCodexAuthenticated(runner = defaultRunner) {
|
|
4894
|
+
try {
|
|
4895
|
+
const res = await runner("codex", ["login", "status"], { timeoutMs: 15e3 });
|
|
4896
|
+
return res.exitCode === 0 && /Logged in using ChatGPT/i.test(`${res.stdout}
|
|
4897
|
+
${res.stderr}`);
|
|
4898
|
+
} catch {
|
|
4899
|
+
return false;
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
async function resolveImageDriver(opts) {
|
|
4903
|
+
const hasApiKey = opts.hasApiKey ?? Boolean(resolveKey("OPENAI_API_KEY"));
|
|
4904
|
+
const codexAuthCheck = opts.codexAuthCheck ?? (() => checkCodexAuthenticated());
|
|
4905
|
+
if (opts.requested === "api") {
|
|
4906
|
+
if (!hasApiKey) throw new ConfigError("OPENAI_API_KEY is required for --driver api");
|
|
4907
|
+
return "api";
|
|
4908
|
+
}
|
|
4909
|
+
if (opts.requested === "codex") {
|
|
4910
|
+
if (!await codexAuthCheck()) throw new ConfigError("Codex driver requires `codex login`");
|
|
4911
|
+
return "codex";
|
|
4912
|
+
}
|
|
4913
|
+
if (hasApiKey) return "api";
|
|
4914
|
+
if (await codexAuthCheck()) return "codex";
|
|
4915
|
+
throw new ConfigError(
|
|
4916
|
+
"Set OPENAI_API_KEY or run `codex login` for the experimental image driver"
|
|
4917
|
+
);
|
|
4918
|
+
}
|
|
4919
|
+
function sanitizeCodexEnv() {
|
|
4920
|
+
const keep = ["PATH", "HOME", "CODEX_HOME", "TMPDIR", "TEMP", "LANG", "LC_ALL"];
|
|
4921
|
+
const env = {};
|
|
4922
|
+
for (const key of keep) {
|
|
4923
|
+
if (process.env[key]) env[key] = process.env[key];
|
|
4924
|
+
}
|
|
4925
|
+
return env;
|
|
4926
|
+
}
|
|
4927
|
+
async function runCodexImageGeneration(opts) {
|
|
4928
|
+
const runner = opts.runner ?? defaultRunner;
|
|
4929
|
+
const ext = extFromImageFormat(opts.outputFormat);
|
|
4930
|
+
const dest = opts.output ? path37.resolve(opts.output) : path37.join(getOutputDir(), `codex-image-${Date.now()}.${ext}`);
|
|
4931
|
+
fs32.mkdirSync(path37.dirname(dest), { recursive: true });
|
|
4932
|
+
const tmp = fs32.mkdtempSync(path37.join(os2.tmpdir(), "multix-codex-image-"));
|
|
4933
|
+
const schema = path37.join(tmp, "schema.json");
|
|
4934
|
+
const result = path37.join(tmp, "result.json");
|
|
4935
|
+
fs32.writeFileSync(
|
|
4936
|
+
schema,
|
|
4937
|
+
JSON.stringify({
|
|
4938
|
+
type: "object",
|
|
4939
|
+
properties: { status: { type: "string" }, path: { type: "string" } },
|
|
4940
|
+
required: ["status", "path"],
|
|
4941
|
+
additionalProperties: false
|
|
4942
|
+
})
|
|
4943
|
+
);
|
|
4944
|
+
const args = [
|
|
4945
|
+
"exec",
|
|
4946
|
+
"--ephemeral",
|
|
4947
|
+
"--skip-git-repo-check",
|
|
4948
|
+
"--cd",
|
|
4949
|
+
tmp,
|
|
4950
|
+
"--output-schema",
|
|
4951
|
+
schema,
|
|
4952
|
+
"--output-last-message",
|
|
4953
|
+
result
|
|
4954
|
+
];
|
|
4955
|
+
for (const ref of opts.refs ?? []) args.push("--image", ref);
|
|
4956
|
+
args.push(buildCodexPrompt(opts.prompt, dest));
|
|
4957
|
+
const res = await runner("codex", args, {
|
|
4958
|
+
cwd: tmp,
|
|
4959
|
+
env: sanitizeCodexEnv(),
|
|
4960
|
+
timeoutMs: 6e5
|
|
4961
|
+
});
|
|
4962
|
+
if (res.exitCode !== 0) {
|
|
4963
|
+
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
4964
|
+
}
|
|
4965
|
+
if (!fs32.existsSync(dest) || fs32.statSync(dest).size === 0) {
|
|
4966
|
+
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
4967
|
+
}
|
|
4968
|
+
return { path: dest };
|
|
4969
|
+
}
|
|
4970
|
+
function buildCodexPrompt(userPrompt, outputPath) {
|
|
4971
|
+
return [
|
|
4972
|
+
`Create exactly one image at ${outputPath}.`,
|
|
4973
|
+
"Do not modify any repository files.",
|
|
4974
|
+
"Use the attached reference images if present.",
|
|
4975
|
+
`Image request: ${userPrompt}`,
|
|
4976
|
+
`Reply with JSON only: {"status":"ok","path":"${outputPath}"}.`
|
|
4977
|
+
].join(" ");
|
|
4978
|
+
}
|
|
4979
|
+
|
|
4980
|
+
// src/providers/openai/openai-image.ts
|
|
4981
|
+
init_errors();
|
|
4982
|
+
init_http_client();
|
|
4983
|
+
import fs33 from "fs";
|
|
4984
|
+
import path38 from "path";
|
|
4985
|
+
function buildImageGenerationBody(opts) {
|
|
4986
|
+
return {
|
|
4987
|
+
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
4988
|
+
prompt: opts.prompt,
|
|
4989
|
+
size: opts.size ?? "1024x1024",
|
|
4990
|
+
quality: opts.quality ?? "medium",
|
|
4991
|
+
output_format: opts.outputFormat,
|
|
4992
|
+
n: opts.numImages ?? 1
|
|
4993
|
+
};
|
|
4994
|
+
}
|
|
4995
|
+
async function buildImageEditForm(opts) {
|
|
4996
|
+
const form = new FormData();
|
|
4997
|
+
form.append("model", opts.model);
|
|
4998
|
+
form.append("prompt", opts.prompt);
|
|
4999
|
+
form.append("size", opts.size);
|
|
5000
|
+
form.append("quality", opts.quality);
|
|
5001
|
+
form.append("output_format", opts.outputFormat);
|
|
5002
|
+
for (const ref of opts.refs) {
|
|
5003
|
+
form.append("image[]", new File([new Uint8Array(ref.bytes)], ref.filename, { type: ref.mime }));
|
|
5004
|
+
}
|
|
5005
|
+
return form;
|
|
5006
|
+
}
|
|
5007
|
+
function saveOpenAIImages(opts) {
|
|
5008
|
+
const items = opts.data ?? [];
|
|
5009
|
+
if (items.length === 0) throw new ProviderError("No images in OpenAI response", "openai");
|
|
5010
|
+
const outDir = getOutputDir();
|
|
5011
|
+
const ext = extFromImageFormat(opts.format);
|
|
5012
|
+
const ts = Date.now();
|
|
5013
|
+
const saved = [];
|
|
5014
|
+
for (let i = 0; i < items.length; i++) {
|
|
5015
|
+
const b64 = items[i]?.b64_json;
|
|
5016
|
+
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
5017
|
+
const dest = path38.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
5018
|
+
fs33.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
5019
|
+
saved.push(dest);
|
|
5020
|
+
opts.logger?.success(`Saved: ${dest}`);
|
|
5021
|
+
}
|
|
5022
|
+
if (opts.output && saved[0]) {
|
|
5023
|
+
fs33.mkdirSync(path38.dirname(path38.resolve(opts.output)), { recursive: true });
|
|
5024
|
+
fs33.copyFileSync(saved[0], opts.output);
|
|
5025
|
+
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
5026
|
+
}
|
|
5027
|
+
return saved;
|
|
5028
|
+
}
|
|
5029
|
+
async function resolveImageRefBytes(ref) {
|
|
5030
|
+
if (isHttpUrl(ref)) {
|
|
5031
|
+
const bytes2 = await fetchBytes(ref);
|
|
5032
|
+
const pathname = new URL(ref).pathname;
|
|
5033
|
+
const filename = path38.basename(pathname) || "reference.png";
|
|
5034
|
+
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
5035
|
+
}
|
|
5036
|
+
const file = fileFromPath(ref);
|
|
5037
|
+
const bytes = Buffer.from(await file.arrayBuffer());
|
|
5038
|
+
return { filename: file.name, bytes, mime: file.type || sniffImageMime2(bytes, file.name) };
|
|
5039
|
+
}
|
|
5040
|
+
async function generateOpenAIImage(opts) {
|
|
5041
|
+
const data = await postOpenAIJson(
|
|
5042
|
+
"/images/generations",
|
|
5043
|
+
buildImageGenerationBody(opts),
|
|
5044
|
+
requireOpenAIKey()
|
|
5045
|
+
);
|
|
5046
|
+
return saveOpenAIImages({
|
|
5047
|
+
data: data.data,
|
|
5048
|
+
task: "image",
|
|
5049
|
+
format: opts.outputFormat,
|
|
5050
|
+
output: opts.output,
|
|
5051
|
+
logger: opts.logger
|
|
5052
|
+
});
|
|
5053
|
+
}
|
|
5054
|
+
async function editOpenAIImage(opts) {
|
|
5055
|
+
const refs = await Promise.all(opts.refs.map((ref) => resolveImageRefBytes(ref)));
|
|
5056
|
+
const form = await buildImageEditForm({
|
|
5057
|
+
prompt: opts.prompt,
|
|
5058
|
+
refs,
|
|
5059
|
+
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
5060
|
+
size: opts.size ?? "1024x1024",
|
|
5061
|
+
quality: opts.quality ?? "medium",
|
|
5062
|
+
outputFormat: opts.outputFormat
|
|
5063
|
+
});
|
|
5064
|
+
const data = await postOpenAIForm("/images/edits", form, requireOpenAIKey());
|
|
5065
|
+
return saveOpenAIImages({
|
|
5066
|
+
data: data.data,
|
|
5067
|
+
task: "image-edit",
|
|
5068
|
+
format: opts.outputFormat,
|
|
5069
|
+
output: opts.output,
|
|
5070
|
+
logger: opts.logger
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
function sniffImageMime2(buf, name) {
|
|
5074
|
+
if (buf[0] === 255 && buf[1] === 216 && buf[2] === 255) return "image/jpeg";
|
|
5075
|
+
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
5076
|
+
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
5077
|
+
return "image/webp";
|
|
5078
|
+
const ext = path38.extname(name).toLowerCase();
|
|
5079
|
+
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
5080
|
+
if (ext === ".webp") return "image/webp";
|
|
5081
|
+
return "image/png";
|
|
5082
|
+
}
|
|
5083
|
+
|
|
5084
|
+
// src/providers/openai/commands/generate.ts
|
|
5085
|
+
function registerOpenAIGenerateCommand(parent) {
|
|
5086
|
+
parent.command("generate").description("Generate images with OpenAI Image API or experimental Codex driver").requiredOption("--prompt <text>", "Text prompt").option("--driver <driver>", "api|codex|auto", "auto").option(
|
|
5087
|
+
"-m, --model <id>",
|
|
5088
|
+
"Image model",
|
|
5089
|
+
resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
|
|
5090
|
+
).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option("--format <fmt>", "png|jpeg|webp", "png").option("-n, --num-images <n>", "Number of images for API driver", "1").option("--output <path>", "Save first image to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
5091
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5092
|
+
const outputFormat = parseImageFormat(opts.format);
|
|
5093
|
+
const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
|
|
5094
|
+
const saved = driver === "codex" ? [
|
|
5095
|
+
(await runCodexImageGeneration({
|
|
5096
|
+
prompt: opts.prompt,
|
|
5097
|
+
outputFormat,
|
|
5098
|
+
output: opts.output
|
|
5099
|
+
})).path
|
|
5100
|
+
] : await generateOpenAIImage({
|
|
5101
|
+
prompt: opts.prompt,
|
|
5102
|
+
model: opts.model,
|
|
5103
|
+
size: parseImageSize(opts.size),
|
|
5104
|
+
quality: parseImageQuality(opts.quality),
|
|
5105
|
+
outputFormat,
|
|
5106
|
+
numImages: parseNumImages(opts.numImages),
|
|
5107
|
+
output: opts.output,
|
|
5108
|
+
logger
|
|
5109
|
+
});
|
|
5110
|
+
console.log(`
|
|
5111
|
+
Generated ${saved.length} image(s):`);
|
|
5112
|
+
for (const file of saved) console.log(` ${file}`);
|
|
5113
|
+
});
|
|
5114
|
+
}
|
|
5115
|
+
|
|
5116
|
+
// src/providers/openai/commands/image-to-image.ts
|
|
5117
|
+
init_errors();
|
|
5118
|
+
function registerOpenAIImageToImageCommand(parent) {
|
|
5119
|
+
parent.command("image-to-image").alias("i2i").description("Edit images with OpenAI Image API or experimental Codex driver").requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
5120
|
+
"--ref <path>",
|
|
5121
|
+
"Reference image path or URL; repeatable",
|
|
5122
|
+
collect5,
|
|
5123
|
+
[]
|
|
5124
|
+
).option("--driver <driver>", "api|codex|auto", "auto").option(
|
|
5125
|
+
"-m, --model <id>",
|
|
5126
|
+
"Image model",
|
|
5127
|
+
resolveKey("OPENAI_IMAGE_MODEL") ?? DEFAULT_OPENAI_IMAGE_MODEL
|
|
5128
|
+
).option("--size <size>", "1024x1024|1024x1536|1536x1024|auto", "1024x1024").option("--quality <q>", "low|medium|high", "medium").option("--format <fmt>", "png|jpeg|webp", "png").option("--output <path>", "Save first image to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
5129
|
+
if (!opts.ref || opts.ref.length === 0) {
|
|
5130
|
+
throw new ValidationError("At least one --ref is required for image-to-image.");
|
|
5131
|
+
}
|
|
5132
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5133
|
+
const outputFormat = parseImageFormat(opts.format);
|
|
5134
|
+
const driver = await resolveImageDriver({ requested: parseDriver(opts.driver) });
|
|
5135
|
+
const saved = driver === "codex" ? [
|
|
5136
|
+
(await runCodexImageGeneration({
|
|
5137
|
+
prompt: opts.prompt,
|
|
5138
|
+
refs: opts.ref,
|
|
5139
|
+
outputFormat,
|
|
5140
|
+
output: opts.output
|
|
5141
|
+
})).path
|
|
5142
|
+
] : await editOpenAIImage({
|
|
5143
|
+
prompt: opts.prompt,
|
|
5144
|
+
refs: opts.ref,
|
|
5145
|
+
model: opts.model,
|
|
5146
|
+
size: parseImageSize(opts.size),
|
|
5147
|
+
quality: parseImageQuality(opts.quality),
|
|
5148
|
+
outputFormat,
|
|
5149
|
+
output: opts.output,
|
|
5150
|
+
logger
|
|
5151
|
+
});
|
|
5152
|
+
console.log(`
|
|
5153
|
+
Generated ${saved.length} image(s):`);
|
|
5154
|
+
for (const file of saved) console.log(` ${file}`);
|
|
5155
|
+
});
|
|
5156
|
+
}
|
|
5157
|
+
function collect5(value, prev) {
|
|
5158
|
+
return [...prev, value];
|
|
5159
|
+
}
|
|
5160
|
+
|
|
5161
|
+
// src/providers/openai/commands/transcribe.ts
|
|
5162
|
+
function registerOpenAITranscribeCommand(parent) {
|
|
5163
|
+
parent.command("transcribe").description("Transcribe audio/video with OpenAI STT").requiredOption("--input <path>", "Audio or video file").option("--model <id>", "STT model", process.env.OPENAI_STT_MODEL ?? DEFAULT_OPENAI_STT_MODEL).option("--format <fmt>", "json|text|diarized_json", "text").option("--language <code>", "ISO language code").option("--chunking-strategy <strategy>", "e.g. auto").option(
|
|
5164
|
+
"--known-speaker-name <name>",
|
|
5165
|
+
"Known speaker name; repeatable",
|
|
5166
|
+
collect6,
|
|
5167
|
+
[]
|
|
5168
|
+
).option(
|
|
5169
|
+
"--known-speaker-reference <path>",
|
|
5170
|
+
"2-10s reference audio; repeatable",
|
|
5171
|
+
collect6,
|
|
5172
|
+
[]
|
|
5173
|
+
).option("--output <path>", "Save transcript to this path").option("-v, --verbose", "Verbose logging").action(async (opts) => {
|
|
5174
|
+
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5175
|
+
const result = await transcribeOpenAIAudio({
|
|
5176
|
+
inputPath: opts.input,
|
|
5177
|
+
model: opts.model,
|
|
5178
|
+
format: parseTranscriptFormat(opts.format),
|
|
5179
|
+
language: opts.language,
|
|
5180
|
+
chunkingStrategy: opts.chunkingStrategy,
|
|
5181
|
+
knownSpeakerNames: opts.knownSpeakerName,
|
|
5182
|
+
knownSpeakerReferences: opts.knownSpeakerReference,
|
|
5183
|
+
output: opts.output,
|
|
5184
|
+
logger
|
|
5185
|
+
});
|
|
5186
|
+
if (opts.format === "text" && result.text) console.log(`
|
|
5187
|
+
${result.text.trim()}`);
|
|
5188
|
+
});
|
|
5189
|
+
}
|
|
5190
|
+
function collect6(value, prev) {
|
|
5191
|
+
return [...prev, value];
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
// src/providers/openai/commands/index.ts
|
|
5195
|
+
function registerOpenAICommands(program2) {
|
|
5196
|
+
const openai = program2.command("openai").description("OpenAI image generation/editing, TTS, STT, and experimental Codex image driver");
|
|
5197
|
+
registerOpenAIGenerateCommand(openai);
|
|
5198
|
+
registerOpenAIImageToImageCommand(openai);
|
|
5199
|
+
registerOpenAIGenerateSpeechCommand(openai);
|
|
5200
|
+
registerOpenAITranscribeCommand(openai);
|
|
5201
|
+
}
|
|
5202
|
+
|
|
4359
5203
|
// src/providers/openrouter/client.ts
|
|
4360
|
-
import
|
|
4361
|
-
import
|
|
5204
|
+
import fs34 from "fs";
|
|
5205
|
+
import path39 from "path";
|
|
4362
5206
|
init_errors();
|
|
4363
5207
|
init_http_client();
|
|
4364
5208
|
|
|
@@ -4523,8 +5367,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
4523
5367
|
if (!url) continue;
|
|
4524
5368
|
try {
|
|
4525
5369
|
const bytes = await fetchBytes(url);
|
|
4526
|
-
const dest =
|
|
4527
|
-
|
|
5370
|
+
const dest = path39.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
5371
|
+
fs34.writeFileSync(dest, bytes);
|
|
4528
5372
|
savedFiles.push(dest);
|
|
4529
5373
|
logger?.success(`Saved: ${dest}`);
|
|
4530
5374
|
} catch (e) {
|
|
@@ -4535,8 +5379,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
4535
5379
|
}
|
|
4536
5380
|
}
|
|
4537
5381
|
if (output && savedFiles[0]) {
|
|
4538
|
-
|
|
4539
|
-
|
|
5382
|
+
fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
|
|
5383
|
+
fs34.writeFileSync(output, fs34.readFileSync(savedFiles[0]));
|
|
4540
5384
|
}
|
|
4541
5385
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
4542
5386
|
}
|
|
@@ -4571,8 +5415,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
4571
5415
|
}
|
|
4572
5416
|
|
|
4573
5417
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
4574
|
-
import
|
|
4575
|
-
import
|
|
5418
|
+
import fs35 from "fs";
|
|
5419
|
+
import path40 from "path";
|
|
4576
5420
|
init_errors();
|
|
4577
5421
|
init_http_client();
|
|
4578
5422
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -4582,7 +5426,7 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
4582
5426
|
parent.command("image-to-image").alias("i2i").description("Edit/transform images via OpenRouter (default: google/gemini-2.5-flash-image)").requiredOption("--prompt <text>", "Text prompt describing the edit").requiredOption(
|
|
4583
5427
|
"--ref <path>",
|
|
4584
5428
|
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
4585
|
-
|
|
5429
|
+
collect7,
|
|
4586
5430
|
[]
|
|
4587
5431
|
).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--strength <n>", "Recraft init-image strength 0..1 (Recraft models only)").option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
4588
5432
|
async (opts) => {
|
|
@@ -4623,7 +5467,7 @@ Generated ${saved.length} image(s):`);
|
|
|
4623
5467
|
}
|
|
4624
5468
|
);
|
|
4625
5469
|
}
|
|
4626
|
-
function
|
|
5470
|
+
function collect7(value, prev) {
|
|
4627
5471
|
return [...prev, value];
|
|
4628
5472
|
}
|
|
4629
5473
|
function parseStrength(raw) {
|
|
@@ -4662,14 +5506,14 @@ async function runImageToImage(opts) {
|
|
|
4662
5506
|
const url = parsed.urls[i];
|
|
4663
5507
|
if (!url) continue;
|
|
4664
5508
|
const bytes = await fetchBytes(url);
|
|
4665
|
-
const dest =
|
|
4666
|
-
|
|
5509
|
+
const dest = path40.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
5510
|
+
fs35.writeFileSync(dest, bytes);
|
|
4667
5511
|
saved.push(dest);
|
|
4668
5512
|
opts.logger.success(`Saved: ${dest}`);
|
|
4669
5513
|
}
|
|
4670
5514
|
if (opts.output && saved[0]) {
|
|
4671
|
-
|
|
4672
|
-
|
|
5515
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5516
|
+
fs35.copyFileSync(saved[0], opts.output);
|
|
4673
5517
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
4674
5518
|
}
|
|
4675
5519
|
return saved;
|
|
@@ -4715,13 +5559,13 @@ async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
|
4715
5559
|
}
|
|
4716
5560
|
|
|
4717
5561
|
// src/providers/openrouter/commands/video-status.ts
|
|
4718
|
-
import
|
|
4719
|
-
import
|
|
5562
|
+
import fs36 from "fs";
|
|
5563
|
+
import path41 from "path";
|
|
4720
5564
|
async function downloadOpenRouterVideo(opts) {
|
|
4721
5565
|
const { jobId, logger, source } = opts;
|
|
4722
5566
|
const apiKey = requireOpenRouterKey();
|
|
4723
5567
|
const url = videoContentUrl(jobId);
|
|
4724
|
-
const dest =
|
|
5568
|
+
const dest = path41.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
4725
5569
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
4726
5570
|
if (!r.ok) {
|
|
4727
5571
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -4731,14 +5575,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
4731
5575
|
logger.error("Empty response body");
|
|
4732
5576
|
process.exit(1);
|
|
4733
5577
|
}
|
|
4734
|
-
|
|
4735
|
-
const ws =
|
|
5578
|
+
fs36.mkdirSync(path41.dirname(dest), { recursive: true });
|
|
5579
|
+
const ws = fs36.createWriteStream(dest);
|
|
4736
5580
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
4737
5581
|
await pipeline2(r.body, ws);
|
|
4738
5582
|
logger.success(`Saved ${dest}`);
|
|
4739
5583
|
if (opts.output) {
|
|
4740
|
-
|
|
4741
|
-
|
|
5584
|
+
fs36.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
|
|
5585
|
+
fs36.copyFileSync(dest, opts.output);
|
|
4742
5586
|
logger.success(`Copied to ${opts.output}`);
|
|
4743
5587
|
}
|
|
4744
5588
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -4898,10 +5742,10 @@ function registerOpenRouterCommands(program2) {
|
|
|
4898
5742
|
}
|
|
4899
5743
|
|
|
4900
5744
|
// src/commands/check-helpers/binary-check.ts
|
|
4901
|
-
import { execa } from "execa";
|
|
5745
|
+
import { execa as execa2 } from "execa";
|
|
4902
5746
|
async function checkBinary(binary) {
|
|
4903
5747
|
try {
|
|
4904
|
-
const result = await
|
|
5748
|
+
const result = await execa2(binary, ["--version"], { reject: true, all: true });
|
|
4905
5749
|
const version = (result.stdout || result.stderr || "").split("\n")[0]?.trim();
|
|
4906
5750
|
return { available: true, version, binary };
|
|
4907
5751
|
} catch (e) {
|
|
@@ -4982,6 +5826,12 @@ var PROVIDERS = [
|
|
|
4982
5826
|
optional: true,
|
|
4983
5827
|
link: "https://openrouter.ai/settings/keys"
|
|
4984
5828
|
},
|
|
5829
|
+
{
|
|
5830
|
+
name: "OpenAI",
|
|
5831
|
+
envPrimary: "OPENAI_API_KEY",
|
|
5832
|
+
optional: true,
|
|
5833
|
+
link: "https://platform.openai.com/api-keys"
|
|
5834
|
+
},
|
|
4985
5835
|
{
|
|
4986
5836
|
name: "MiniMax",
|
|
4987
5837
|
envPrimary: "MINIMAX_API_KEY",
|
|
@@ -5017,6 +5867,10 @@ function resolveProviderKey(p) {
|
|
|
5017
5867
|
}
|
|
5018
5868
|
return {};
|
|
5019
5869
|
}
|
|
5870
|
+
async function checkCodexImageReadiness(hasAnyProviderKey, verbose = false, checker = checkCodexAuthenticated) {
|
|
5871
|
+
if (hasAnyProviderKey && !verbose) return false;
|
|
5872
|
+
return checker();
|
|
5873
|
+
}
|
|
5020
5874
|
function registerCheckCommand(program2) {
|
|
5021
5875
|
program2.command("check").description("Validate multix setup: tooling, API keys, and live Gemini connectivity").option("-v, --verbose", "Verbose output").action(async (opts) => {
|
|
5022
5876
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
@@ -5053,10 +5907,25 @@ function registerCheckCommand(program2) {
|
|
|
5053
5907
|
logger.warn(`${p.envPrimary} not set (${p.name} features unavailable)`);
|
|
5054
5908
|
}
|
|
5055
5909
|
}
|
|
5910
|
+
const shouldCheckCodex = !anyKey || opts.verbose;
|
|
5911
|
+
let codexReady = false;
|
|
5912
|
+
if (shouldCheckCodex) {
|
|
5913
|
+
logger.header("Codex Image Driver");
|
|
5914
|
+
logger.info("Checking Codex login status...");
|
|
5915
|
+
codexReady = await checkCodexImageReadiness(anyKey, opts.verbose ?? false);
|
|
5916
|
+
if (codexReady) {
|
|
5917
|
+
logger.success("Codex authenticated \u2014 experimental OpenAI image driver available");
|
|
5918
|
+
} else {
|
|
5919
|
+
logger.info("Codex image driver unavailable (run `codex login` to enable it)");
|
|
5920
|
+
}
|
|
5921
|
+
}
|
|
5056
5922
|
if (!anyKey) {
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5923
|
+
if (!codexReady) {
|
|
5924
|
+
logger.header("No Provider Keys Configured");
|
|
5925
|
+
console.log(SETUP_HINTS);
|
|
5926
|
+
process.exit(1);
|
|
5927
|
+
}
|
|
5928
|
+
anyKey = true;
|
|
5060
5929
|
}
|
|
5061
5930
|
const geminiKey = resolved.get("Gemini")?.key;
|
|
5062
5931
|
if (geminiKey) {
|
|
@@ -5079,9 +5948,11 @@ function registerCheckCommand(program2) {
|
|
|
5079
5948
|
console.log(
|
|
5080
5949
|
" Full Gemini multimodal setup (analyze, transcribe, generate, doc convert)"
|
|
5081
5950
|
);
|
|
5951
|
+
} else if (resolved.get("OpenAI")?.key) {
|
|
5952
|
+
console.log(" OpenAI API setup available (images, TTS, STT)");
|
|
5082
5953
|
} else {
|
|
5083
|
-
console.log(" Image generation via
|
|
5084
|
-
console.log(" Add
|
|
5954
|
+
console.log(" Image generation via configured providers or experimental Codex driver");
|
|
5955
|
+
console.log(" Add API keys for full provider features");
|
|
5085
5956
|
}
|
|
5086
5957
|
} else {
|
|
5087
5958
|
logger.error("One or more checks failed \u2014 see above");
|
|
@@ -5092,8 +5963,8 @@ function registerCheckCommand(program2) {
|
|
|
5092
5963
|
|
|
5093
5964
|
// src/commands/doc/convert.ts
|
|
5094
5965
|
init_errors();
|
|
5095
|
-
import
|
|
5096
|
-
import
|
|
5966
|
+
import fs37 from "fs";
|
|
5967
|
+
import path42 from "path";
|
|
5097
5968
|
|
|
5098
5969
|
// src/commands/doc/default-prompt.ts
|
|
5099
5970
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -5114,18 +5985,18 @@ async function convertDocuments(opts) {
|
|
|
5114
5985
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
5115
5986
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
5116
5987
|
for (const f of inputs) {
|
|
5117
|
-
if (!
|
|
5988
|
+
if (!fs37.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
5118
5989
|
}
|
|
5119
5990
|
let outputPath;
|
|
5120
5991
|
if (output) {
|
|
5121
|
-
outputPath =
|
|
5992
|
+
outputPath = path42.resolve(output);
|
|
5122
5993
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
5123
|
-
const base =
|
|
5124
|
-
outputPath =
|
|
5994
|
+
const base = path42.basename(inputs[0], path42.extname(inputs[0]));
|
|
5995
|
+
outputPath = path42.join(getOutputDir(), `${base}-extraction.md`);
|
|
5125
5996
|
} else {
|
|
5126
|
-
outputPath =
|
|
5997
|
+
outputPath = path42.join(getOutputDir(), "document-extraction.md");
|
|
5127
5998
|
}
|
|
5128
|
-
|
|
5999
|
+
fs37.mkdirSync(path42.dirname(outputPath), { recursive: true });
|
|
5129
6000
|
const results = [];
|
|
5130
6001
|
for (let i = 0; i < inputs.length; i++) {
|
|
5131
6002
|
const filePath = inputs[i];
|
|
@@ -5157,7 +6028,7 @@ async function convertDocuments(opts) {
|
|
|
5157
6028
|
if (attempt < 2) {
|
|
5158
6029
|
const wait = 2 ** attempt * 1e3;
|
|
5159
6030
|
logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
|
|
5160
|
-
await
|
|
6031
|
+
await sleep6(wait);
|
|
5161
6032
|
}
|
|
5162
6033
|
}
|
|
5163
6034
|
}
|
|
@@ -5176,7 +6047,7 @@ async function convertDocuments(opts) {
|
|
|
5176
6047
|
`);
|
|
5177
6048
|
for (const r of results) {
|
|
5178
6049
|
lines.push(`
|
|
5179
|
-
## ${
|
|
6050
|
+
## ${path42.basename(r.file)}
|
|
5180
6051
|
`);
|
|
5181
6052
|
if (r.status === "success" && r.markdown) {
|
|
5182
6053
|
lines.push(`${r.markdown}
|
|
@@ -5189,7 +6060,7 @@ async function convertDocuments(opts) {
|
|
|
5189
6060
|
}
|
|
5190
6061
|
lines.push("\n---\n");
|
|
5191
6062
|
}
|
|
5192
|
-
|
|
6063
|
+
fs37.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
5193
6064
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
5194
6065
|
console.log(`
|
|
5195
6066
|
${"=".repeat(50)}`);
|
|
@@ -5205,7 +6076,7 @@ ${"=".repeat(50)}`);
|
|
|
5205
6076
|
const failed = results.filter((r) => r.status === "error").length;
|
|
5206
6077
|
if (failed > 0) process.exit(1);
|
|
5207
6078
|
}
|
|
5208
|
-
function
|
|
6079
|
+
function sleep6(ms) {
|
|
5209
6080
|
return new Promise((r) => setTimeout(r, ms));
|
|
5210
6081
|
}
|
|
5211
6082
|
|
|
@@ -5235,12 +6106,12 @@ function registerDocCommands(program2) {
|
|
|
5235
6106
|
|
|
5236
6107
|
// src/commands/media/index.ts
|
|
5237
6108
|
init_errors();
|
|
5238
|
-
import
|
|
5239
|
-
import
|
|
6109
|
+
import fs40 from "fs";
|
|
6110
|
+
import path45 from "path";
|
|
5240
6111
|
|
|
5241
6112
|
// src/commands/media/batch.ts
|
|
5242
|
-
import
|
|
5243
|
-
import
|
|
6113
|
+
import fs38 from "fs";
|
|
6114
|
+
import path43 from "path";
|
|
5244
6115
|
|
|
5245
6116
|
// src/commands/media/detect-type.ts
|
|
5246
6117
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -5259,10 +6130,10 @@ init_errors();
|
|
|
5259
6130
|
|
|
5260
6131
|
// src/commands/media/ffmpeg-runner.ts
|
|
5261
6132
|
init_errors();
|
|
5262
|
-
import { execa as
|
|
6133
|
+
import { execa as execa3 } from "execa";
|
|
5263
6134
|
async function assertFfmpeg() {
|
|
5264
6135
|
try {
|
|
5265
|
-
await
|
|
6136
|
+
await execa3("ffmpeg", ["-version"], { reject: true });
|
|
5266
6137
|
} catch (e) {
|
|
5267
6138
|
if (e.code === "ENOENT") {
|
|
5268
6139
|
throw new ValidationError(
|
|
@@ -5275,7 +6146,7 @@ async function runFfmpeg(args, opts = {}) {
|
|
|
5275
6146
|
const { verbose, logger } = opts;
|
|
5276
6147
|
logger?.debug(`ffmpeg ${args.join(" ")}`);
|
|
5277
6148
|
try {
|
|
5278
|
-
const result = await
|
|
6149
|
+
const result = await execa3("ffmpeg", args, { reject: true, all: true });
|
|
5279
6150
|
if (verbose && result.stderr) logger?.debug(result.stderr);
|
|
5280
6151
|
} catch (e) {
|
|
5281
6152
|
const err2 = e;
|
|
@@ -5285,7 +6156,7 @@ async function runFfmpeg(args, opts = {}) {
|
|
|
5285
6156
|
}
|
|
5286
6157
|
async function getMediaInfo(filePath) {
|
|
5287
6158
|
try {
|
|
5288
|
-
const result = await
|
|
6159
|
+
const result = await execa3(
|
|
5289
6160
|
"ffprobe",
|
|
5290
6161
|
["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", filePath],
|
|
5291
6162
|
{ reject: true }
|
|
@@ -5348,10 +6219,10 @@ async function optimizeAudio(opts) {
|
|
|
5348
6219
|
|
|
5349
6220
|
// src/commands/media/magick-runner.ts
|
|
5350
6221
|
init_errors();
|
|
5351
|
-
import { execa as
|
|
6222
|
+
import { execa as execa4 } from "execa";
|
|
5352
6223
|
async function assertMagick() {
|
|
5353
6224
|
try {
|
|
5354
|
-
await
|
|
6225
|
+
await execa4("magick", ["-version"], { reject: true });
|
|
5355
6226
|
} catch (e) {
|
|
5356
6227
|
if (e.code === "ENOENT") {
|
|
5357
6228
|
throw new ValidationError(
|
|
@@ -5364,7 +6235,7 @@ async function runMagick(args, opts = {}) {
|
|
|
5364
6235
|
const { verbose, logger } = opts;
|
|
5365
6236
|
logger?.debug(`magick ${args.join(" ")}`);
|
|
5366
6237
|
try {
|
|
5367
|
-
const result = await
|
|
6238
|
+
const result = await execa4("magick", args, { reject: true, all: true });
|
|
5368
6239
|
if (verbose && result.stderr) logger?.debug(result.stderr);
|
|
5369
6240
|
} catch (e) {
|
|
5370
6241
|
const err2 = e;
|
|
@@ -5436,10 +6307,10 @@ async function batchOptimize(opts) {
|
|
|
5436
6307
|
verbose,
|
|
5437
6308
|
logger
|
|
5438
6309
|
} = opts;
|
|
5439
|
-
|
|
5440
|
-
const entries =
|
|
6310
|
+
fs38.mkdirSync(outputDir, { recursive: true });
|
|
6311
|
+
const entries = fs38.readdirSync(inputDir);
|
|
5441
6312
|
const mediaFiles = entries.filter((f) => {
|
|
5442
|
-
const ext =
|
|
6313
|
+
const ext = path43.extname(f).toLowerCase();
|
|
5443
6314
|
return ALL_EXTS.has(ext);
|
|
5444
6315
|
});
|
|
5445
6316
|
if (mediaFiles.length === 0) {
|
|
@@ -5450,9 +6321,9 @@ async function batchOptimize(opts) {
|
|
|
5450
6321
|
let succeeded = 0;
|
|
5451
6322
|
const errors = [];
|
|
5452
6323
|
for (const file of mediaFiles) {
|
|
5453
|
-
const inputPath =
|
|
5454
|
-
const outputPath =
|
|
5455
|
-
const ext =
|
|
6324
|
+
const inputPath = path43.join(inputDir, file);
|
|
6325
|
+
const outputPath = path43.join(outputDir, file);
|
|
6326
|
+
const ext = path43.extname(file).toLowerCase();
|
|
5456
6327
|
const kind = extToKind(ext);
|
|
5457
6328
|
logger?.debug(`Processing: ${file}`);
|
|
5458
6329
|
try {
|
|
@@ -5489,8 +6360,8 @@ async function batchOptimize(opts) {
|
|
|
5489
6360
|
|
|
5490
6361
|
// src/commands/media/split-video.ts
|
|
5491
6362
|
init_errors();
|
|
5492
|
-
import
|
|
5493
|
-
import
|
|
6363
|
+
import fs39 from "fs";
|
|
6364
|
+
import path44 from "path";
|
|
5494
6365
|
async function splitVideo(opts) {
|
|
5495
6366
|
await assertFfmpeg();
|
|
5496
6367
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -5501,10 +6372,10 @@ async function splitVideo(opts) {
|
|
|
5501
6372
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
5502
6373
|
return [input];
|
|
5503
6374
|
}
|
|
5504
|
-
|
|
5505
|
-
const ext =
|
|
5506
|
-
const baseName =
|
|
5507
|
-
const outputPattern =
|
|
6375
|
+
fs39.mkdirSync(outputDir, { recursive: true });
|
|
6376
|
+
const ext = path44.extname(input) || ".mp4";
|
|
6377
|
+
const baseName = path44.basename(input, ext);
|
|
6378
|
+
const outputPattern = path44.join(outputDir, `${baseName}_chunk_%03d${ext}`);
|
|
5508
6379
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
5509
6380
|
const args = [
|
|
5510
6381
|
"-i",
|
|
@@ -5523,8 +6394,8 @@ async function splitVideo(opts) {
|
|
|
5523
6394
|
await runFfmpeg(args, { verbose, logger });
|
|
5524
6395
|
const files = [];
|
|
5525
6396
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
5526
|
-
const fname =
|
|
5527
|
-
if (
|
|
6397
|
+
const fname = path44.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
6398
|
+
if (fs39.existsSync(fname)) files.push(fname);
|
|
5528
6399
|
}
|
|
5529
6400
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
5530
6401
|
return files;
|
|
@@ -5538,9 +6409,9 @@ function registerMediaCommands(program2) {
|
|
|
5538
6409
|
media.command("optimize").description("Optimize a single media file (video, audio, or image)").requiredOption("--input <file>", "Input media file").requiredOption("--output <file>", "Output file path").option("--target-size <MB>", "Target file size in MB").option("--quality <n>", "Quality: video CRF (0-51) or image (1-100)", "85").option("--max-width <px>", "Max image/video width in pixels", "1920").option("--bitrate <rate>", "Audio bitrate (e.g. 64k)", "64k").option("--resolution <WxH>", "Force video resolution (e.g. 1920x1080)").option("-v, --verbose", "Verbose logging").action(
|
|
5539
6410
|
async (opts) => {
|
|
5540
6411
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5541
|
-
if (!
|
|
5542
|
-
|
|
5543
|
-
const ext =
|
|
6412
|
+
if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
6413
|
+
fs40.mkdirSync(path45.dirname(path45.resolve(opts.output)), { recursive: true });
|
|
6414
|
+
const ext = path45.extname(opts.input).toLowerCase();
|
|
5544
6415
|
const kind = extToKind(ext);
|
|
5545
6416
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
5546
6417
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -5580,7 +6451,7 @@ function registerMediaCommands(program2) {
|
|
|
5580
6451
|
media.command("split").description("Split a long video into fixed-duration chunks").requiredOption("--input <file>", "Input video file").option("--output-dir <dir>", "Output directory for chunks", "./chunks").option("--chunk-duration <sec>", "Chunk duration in seconds", "3600").option("-v, --verbose", "Verbose logging").action(
|
|
5581
6452
|
async (opts) => {
|
|
5582
6453
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5583
|
-
if (!
|
|
6454
|
+
if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
5584
6455
|
const chunks = await splitVideo({
|
|
5585
6456
|
input: opts.input,
|
|
5586
6457
|
outputDir: opts.outputDir,
|
|
@@ -5596,7 +6467,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
5596
6467
|
media.command("batch").description("Batch optimize all media files in a directory").requiredOption("--input-dir <dir>", "Input directory").requiredOption("--output-dir <dir>", "Output directory").option("--quality <n>", "Quality setting", "85").option("--max-width <px>", "Max image/video width", "1920").option("--bitrate <rate>", "Audio bitrate", "64k").option("-v, --verbose", "Verbose logging").action(
|
|
5597
6468
|
async (opts) => {
|
|
5598
6469
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
5599
|
-
if (!
|
|
6470
|
+
if (!fs40.existsSync(opts.inputDir))
|
|
5600
6471
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
5601
6472
|
const result = await batchOptimize({
|
|
5602
6473
|
inputDir: opts.inputDir,
|
|
@@ -5622,6 +6493,7 @@ function registerCommands(program2) {
|
|
|
5622
6493
|
registerCheckCommand(program2);
|
|
5623
6494
|
registerGeminiCommands(program2);
|
|
5624
6495
|
registerMinimaxCommands(program2);
|
|
6496
|
+
registerOpenAICommands(program2);
|
|
5625
6497
|
registerOpenRouterCommands(program2);
|
|
5626
6498
|
registerLeonardoCommands(program2);
|
|
5627
6499
|
registerBytePlusCommands(program2);
|