@mrgoonie/multix 0.1.0-beta → 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 +8 -0
- package/README.md +23 -5
- package/dist/cli.js +409 -192
- package/package.json +1 -1
- package/skill/SKILL.md +17 -5
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 = [];
|
|
@@ -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,7 +3967,7 @@ 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"
|
|
@@ -3762,7 +3979,7 @@ async function apiPost(endpoint2, payload, apiKey, opts = {}) {
|
|
|
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
|
});
|
|
@@ -3778,7 +3995,7 @@ async function apiPost(endpoint2, payload, apiKey, opts = {}) {
|
|
|
3778
3995
|
async function apiGet2(endpoint2, params, apiKey) {
|
|
3779
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(`
|
|
@@ -4470,12 +4687,12 @@ function validateKnownSpeakerPairing(names = [], refs = []) {
|
|
|
4470
4687
|
|
|
4471
4688
|
// src/providers/openai/openai-audio.ts
|
|
4472
4689
|
init_http_client();
|
|
4473
|
-
import
|
|
4474
|
-
import
|
|
4690
|
+
import fs31 from "fs";
|
|
4691
|
+
import path36 from "path";
|
|
4475
4692
|
|
|
4476
4693
|
// src/providers/openai/client.ts
|
|
4477
|
-
import
|
|
4478
|
-
import
|
|
4694
|
+
import fs30 from "fs";
|
|
4695
|
+
import path35 from "path";
|
|
4479
4696
|
init_errors();
|
|
4480
4697
|
init_http_client();
|
|
4481
4698
|
var DEFAULT_TIMEOUT_MS3 = 24e4;
|
|
@@ -4491,14 +4708,14 @@ function requireOpenAIKey() {
|
|
|
4491
4708
|
function endpoint(pathname) {
|
|
4492
4709
|
return `${OPENAI_BASE_URL}/${pathname.replace(/^\//, "")}`;
|
|
4493
4710
|
}
|
|
4494
|
-
function
|
|
4711
|
+
function authHeaders3(apiKey, extra) {
|
|
4495
4712
|
return { Authorization: `Bearer ${apiKey}`, ...extra ?? {} };
|
|
4496
4713
|
}
|
|
4497
4714
|
function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3) {
|
|
4498
4715
|
return httpJson({
|
|
4499
4716
|
url: endpoint(pathname),
|
|
4500
4717
|
method: "POST",
|
|
4501
|
-
headers:
|
|
4718
|
+
headers: authHeaders3(apiKey, { "Content-Type": "application/json" }),
|
|
4502
4719
|
body,
|
|
4503
4720
|
timeoutMs
|
|
4504
4721
|
});
|
|
@@ -4509,7 +4726,7 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
|
|
|
4509
4726
|
try {
|
|
4510
4727
|
res = await globalThis.fetch(url, {
|
|
4511
4728
|
method: "POST",
|
|
4512
|
-
headers:
|
|
4729
|
+
headers: authHeaders3(apiKey),
|
|
4513
4730
|
body: form,
|
|
4514
4731
|
signal: AbortSignal.timeout(timeoutMs)
|
|
4515
4732
|
});
|
|
@@ -4528,7 +4745,7 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
4528
4745
|
try {
|
|
4529
4746
|
res = await globalThis.fetch(url, {
|
|
4530
4747
|
method: "POST",
|
|
4531
|
-
headers:
|
|
4748
|
+
headers: authHeaders3(apiKey, { "Content-Type": "application/json" }),
|
|
4532
4749
|
body: JSON.stringify(body),
|
|
4533
4750
|
signal: AbortSignal.timeout(timeoutMs)
|
|
4534
4751
|
});
|
|
@@ -4542,10 +4759,10 @@ async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIME
|
|
|
4542
4759
|
return Buffer.from(await res.arrayBuffer());
|
|
4543
4760
|
}
|
|
4544
4761
|
function fileFromPath(filePath) {
|
|
4545
|
-
const abs =
|
|
4546
|
-
if (!
|
|
4547
|
-
const bytes =
|
|
4548
|
-
return new File([new Uint8Array(bytes)],
|
|
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));
|
|
4549
4766
|
}
|
|
4550
4767
|
|
|
4551
4768
|
// src/providers/openai/openai-audio.ts
|
|
@@ -4584,7 +4801,7 @@ async function resolveAudioReferenceDataUrl(ref) {
|
|
|
4584
4801
|
}
|
|
4585
4802
|
function audioMimeFromReference(ref) {
|
|
4586
4803
|
const pathname = new URL(ref).pathname;
|
|
4587
|
-
const ext =
|
|
4804
|
+
const ext = path36.extname(pathname).toLowerCase();
|
|
4588
4805
|
switch (ext) {
|
|
4589
4806
|
case ".mp3":
|
|
4590
4807
|
return "audio/mpeg";
|
|
@@ -4603,12 +4820,12 @@ function audioMimeFromReference(ref) {
|
|
|
4603
4820
|
async function generateOpenAISpeech(opts) {
|
|
4604
4821
|
const bytes = await postOpenAIBinary("/audio/speech", buildSpeechBody(opts), requireOpenAIKey());
|
|
4605
4822
|
const ext = extFromAudioFormat(opts.outputFormat);
|
|
4606
|
-
const dest =
|
|
4607
|
-
|
|
4823
|
+
const dest = path36.join(getOutputDir(), `openai-speech-${Date.now()}.${ext}`);
|
|
4824
|
+
fs31.writeFileSync(dest, bytes);
|
|
4608
4825
|
opts.logger?.success(`Saved: ${dest} (${(bytes.length / 1024).toFixed(1)} KB)`);
|
|
4609
4826
|
if (opts.output) {
|
|
4610
|
-
|
|
4611
|
-
|
|
4827
|
+
fs31.mkdirSync(path36.dirname(path36.resolve(opts.output)), { recursive: true });
|
|
4828
|
+
fs31.copyFileSync(dest, opts.output);
|
|
4612
4829
|
}
|
|
4613
4830
|
return dest;
|
|
4614
4831
|
}
|
|
@@ -4630,9 +4847,9 @@ async function transcribeOpenAIAudio(opts) {
|
|
|
4630
4847
|
);
|
|
4631
4848
|
const text = typeof data.text === "string" ? data.text : void 0;
|
|
4632
4849
|
const ext = opts.format === "text" ? "txt" : "json";
|
|
4633
|
-
const dest = opts.output ?
|
|
4634
|
-
|
|
4635
|
-
|
|
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));
|
|
4636
4853
|
opts.logger?.success(`Saved transcript: ${dest}`);
|
|
4637
4854
|
return { path: dest, text };
|
|
4638
4855
|
}
|
|
@@ -4659,9 +4876,9 @@ Generated audio: ${dest}`);
|
|
|
4659
4876
|
}
|
|
4660
4877
|
|
|
4661
4878
|
// src/providers/openai/codex-image-driver.ts
|
|
4662
|
-
import
|
|
4879
|
+
import fs32 from "fs";
|
|
4663
4880
|
import os2 from "os";
|
|
4664
|
-
import
|
|
4881
|
+
import path37 from "path";
|
|
4665
4882
|
import { execa } from "execa";
|
|
4666
4883
|
init_errors();
|
|
4667
4884
|
var defaultRunner = async (cmd, args, opts) => {
|
|
@@ -4710,12 +4927,12 @@ function sanitizeCodexEnv() {
|
|
|
4710
4927
|
async function runCodexImageGeneration(opts) {
|
|
4711
4928
|
const runner = opts.runner ?? defaultRunner;
|
|
4712
4929
|
const ext = extFromImageFormat(opts.outputFormat);
|
|
4713
|
-
const dest = opts.output ?
|
|
4714
|
-
|
|
4715
|
-
const tmp =
|
|
4716
|
-
const schema =
|
|
4717
|
-
const result =
|
|
4718
|
-
|
|
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(
|
|
4719
4936
|
schema,
|
|
4720
4937
|
JSON.stringify({
|
|
4721
4938
|
type: "object",
|
|
@@ -4745,7 +4962,7 @@ async function runCodexImageGeneration(opts) {
|
|
|
4745
4962
|
if (res.exitCode !== 0) {
|
|
4746
4963
|
throw new ProviderError(`Codex image driver failed: ${res.stderr || res.stdout}`, "openai");
|
|
4747
4964
|
}
|
|
4748
|
-
if (!
|
|
4965
|
+
if (!fs32.existsSync(dest) || fs32.statSync(dest).size === 0) {
|
|
4749
4966
|
throw new ProviderError(`Codex did not create a non-empty image at ${dest}`, "openai");
|
|
4750
4967
|
}
|
|
4751
4968
|
return { path: dest };
|
|
@@ -4763,8 +4980,8 @@ function buildCodexPrompt(userPrompt, outputPath) {
|
|
|
4763
4980
|
// src/providers/openai/openai-image.ts
|
|
4764
4981
|
init_errors();
|
|
4765
4982
|
init_http_client();
|
|
4766
|
-
import
|
|
4767
|
-
import
|
|
4983
|
+
import fs33 from "fs";
|
|
4984
|
+
import path38 from "path";
|
|
4768
4985
|
function buildImageGenerationBody(opts) {
|
|
4769
4986
|
return {
|
|
4770
4987
|
model: opts.model ?? DEFAULT_OPENAI_IMAGE_MODEL,
|
|
@@ -4797,14 +5014,14 @@ function saveOpenAIImages(opts) {
|
|
|
4797
5014
|
for (let i = 0; i < items.length; i++) {
|
|
4798
5015
|
const b64 = items[i]?.b64_json;
|
|
4799
5016
|
if (!b64) throw new ProviderError("No b64_json in OpenAI image response", "openai");
|
|
4800
|
-
const dest =
|
|
4801
|
-
|
|
5017
|
+
const dest = path38.join(outDir, `openai-${opts.task}-${ts}-${i + 1}.${ext}`);
|
|
5018
|
+
fs33.writeFileSync(dest, Buffer.from(b64, "base64"));
|
|
4802
5019
|
saved.push(dest);
|
|
4803
5020
|
opts.logger?.success(`Saved: ${dest}`);
|
|
4804
5021
|
}
|
|
4805
5022
|
if (opts.output && saved[0]) {
|
|
4806
|
-
|
|
4807
|
-
|
|
5023
|
+
fs33.mkdirSync(path38.dirname(path38.resolve(opts.output)), { recursive: true });
|
|
5024
|
+
fs33.copyFileSync(saved[0], opts.output);
|
|
4808
5025
|
opts.logger?.success(`Copied to: ${opts.output}`);
|
|
4809
5026
|
}
|
|
4810
5027
|
return saved;
|
|
@@ -4813,7 +5030,7 @@ async function resolveImageRefBytes(ref) {
|
|
|
4813
5030
|
if (isHttpUrl(ref)) {
|
|
4814
5031
|
const bytes2 = await fetchBytes(ref);
|
|
4815
5032
|
const pathname = new URL(ref).pathname;
|
|
4816
|
-
const filename =
|
|
5033
|
+
const filename = path38.basename(pathname) || "reference.png";
|
|
4817
5034
|
return { filename, bytes: bytes2, mime: sniffImageMime2(bytes2, filename) };
|
|
4818
5035
|
}
|
|
4819
5036
|
const file = fileFromPath(ref);
|
|
@@ -4858,7 +5075,7 @@ function sniffImageMime2(buf, name) {
|
|
|
4858
5075
|
if (buf[0] === 137 && buf[1] === 80 && buf[2] === 78 && buf[3] === 71) return "image/png";
|
|
4859
5076
|
if (buf.slice(0, 4).toString() === "RIFF" && buf.slice(8, 12).toString() === "WEBP")
|
|
4860
5077
|
return "image/webp";
|
|
4861
|
-
const ext =
|
|
5078
|
+
const ext = path38.extname(name).toLowerCase();
|
|
4862
5079
|
if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg";
|
|
4863
5080
|
if (ext === ".webp") return "image/webp";
|
|
4864
5081
|
return "image/png";
|
|
@@ -4984,8 +5201,8 @@ function registerOpenAICommands(program2) {
|
|
|
4984
5201
|
}
|
|
4985
5202
|
|
|
4986
5203
|
// src/providers/openrouter/client.ts
|
|
4987
|
-
import
|
|
4988
|
-
import
|
|
5204
|
+
import fs34 from "fs";
|
|
5205
|
+
import path39 from "path";
|
|
4989
5206
|
init_errors();
|
|
4990
5207
|
init_http_client();
|
|
4991
5208
|
|
|
@@ -5150,8 +5367,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
5150
5367
|
if (!url) continue;
|
|
5151
5368
|
try {
|
|
5152
5369
|
const bytes = await fetchBytes(url);
|
|
5153
|
-
const dest =
|
|
5154
|
-
|
|
5370
|
+
const dest = path39.join(outDir, `openrouter_image_${Date.now()}_${i}.png`);
|
|
5371
|
+
fs34.writeFileSync(dest, bytes);
|
|
5155
5372
|
savedFiles.push(dest);
|
|
5156
5373
|
logger?.success(`Saved: ${dest}`);
|
|
5157
5374
|
} catch (e) {
|
|
@@ -5162,8 +5379,8 @@ async function generateOpenRouterImage(opts) {
|
|
|
5162
5379
|
}
|
|
5163
5380
|
}
|
|
5164
5381
|
if (output && savedFiles[0]) {
|
|
5165
|
-
|
|
5166
|
-
|
|
5382
|
+
fs34.mkdirSync(path39.dirname(path39.resolve(output)), { recursive: true });
|
|
5383
|
+
fs34.writeFileSync(output, fs34.readFileSync(savedFiles[0]));
|
|
5167
5384
|
}
|
|
5168
5385
|
return { status: "success", generatedImages: savedFiles, model: usedModel };
|
|
5169
5386
|
}
|
|
@@ -5198,8 +5415,8 @@ Generated ${result.generatedImages?.length ?? 0} image(s):`);
|
|
|
5198
5415
|
}
|
|
5199
5416
|
|
|
5200
5417
|
// src/providers/openrouter/commands/image-to-image.ts
|
|
5201
|
-
import
|
|
5202
|
-
import
|
|
5418
|
+
import fs35 from "fs";
|
|
5419
|
+
import path40 from "path";
|
|
5203
5420
|
init_errors();
|
|
5204
5421
|
init_http_client();
|
|
5205
5422
|
var DEFAULT_MODEL = "google/gemini-2.5-flash-image";
|
|
@@ -5289,14 +5506,14 @@ async function runImageToImage(opts) {
|
|
|
5289
5506
|
const url = parsed.urls[i];
|
|
5290
5507
|
if (!url) continue;
|
|
5291
5508
|
const bytes = await fetchBytes(url);
|
|
5292
|
-
const dest =
|
|
5293
|
-
|
|
5509
|
+
const dest = path40.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
|
5510
|
+
fs35.writeFileSync(dest, bytes);
|
|
5294
5511
|
saved.push(dest);
|
|
5295
5512
|
opts.logger.success(`Saved: ${dest}`);
|
|
5296
5513
|
}
|
|
5297
5514
|
if (opts.output && saved[0]) {
|
|
5298
|
-
|
|
5299
|
-
|
|
5515
|
+
fs35.mkdirSync(path40.dirname(path40.resolve(opts.output)), { recursive: true });
|
|
5516
|
+
fs35.copyFileSync(saved[0], opts.output);
|
|
5300
5517
|
opts.logger.success(`Copied to: ${opts.output}`);
|
|
5301
5518
|
}
|
|
5302
5519
|
return saved;
|
|
@@ -5305,7 +5522,7 @@ async function runImageToImage(opts) {
|
|
|
5305
5522
|
// src/providers/openrouter/video-client.ts
|
|
5306
5523
|
init_http_client();
|
|
5307
5524
|
var OPENROUTER_VIDEOS_URL = "https://openrouter.ai/api/v1/videos";
|
|
5308
|
-
function
|
|
5525
|
+
function authHeaders4(apiKey) {
|
|
5309
5526
|
const headers = {
|
|
5310
5527
|
Authorization: `Bearer ${apiKey}`,
|
|
5311
5528
|
"Content-Type": "application/json"
|
|
@@ -5320,7 +5537,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
5320
5537
|
return httpJson({
|
|
5321
5538
|
url: OPENROUTER_VIDEOS_URL,
|
|
5322
5539
|
method: "POST",
|
|
5323
|
-
headers:
|
|
5540
|
+
headers: authHeaders4(apiKey),
|
|
5324
5541
|
body,
|
|
5325
5542
|
timeoutMs: 12e4
|
|
5326
5543
|
});
|
|
@@ -5328,7 +5545,7 @@ async function submitVideoJob(body, apiKey = requireOpenRouterKey()) {
|
|
|
5328
5545
|
async function pollVideoJob(jobId, apiKey = requireOpenRouterKey()) {
|
|
5329
5546
|
return httpJson({
|
|
5330
5547
|
url: `${OPENROUTER_VIDEOS_URL}/${jobId}`,
|
|
5331
|
-
headers:
|
|
5548
|
+
headers: authHeaders4(apiKey)
|
|
5332
5549
|
});
|
|
5333
5550
|
}
|
|
5334
5551
|
function videoContentUrl(jobId) {
|
|
@@ -5337,18 +5554,18 @@ function videoContentUrl(jobId) {
|
|
|
5337
5554
|
async function listVideoModels(apiKey = requireOpenRouterKey()) {
|
|
5338
5555
|
return httpJson({
|
|
5339
5556
|
url: `${OPENROUTER_VIDEOS_URL}/models`,
|
|
5340
|
-
headers:
|
|
5557
|
+
headers: authHeaders4(apiKey)
|
|
5341
5558
|
});
|
|
5342
5559
|
}
|
|
5343
5560
|
|
|
5344
5561
|
// src/providers/openrouter/commands/video-status.ts
|
|
5345
|
-
import
|
|
5346
|
-
import
|
|
5562
|
+
import fs36 from "fs";
|
|
5563
|
+
import path41 from "path";
|
|
5347
5564
|
async function downloadOpenRouterVideo(opts) {
|
|
5348
5565
|
const { jobId, logger, source } = opts;
|
|
5349
5566
|
const apiKey = requireOpenRouterKey();
|
|
5350
5567
|
const url = videoContentUrl(jobId);
|
|
5351
|
-
const dest =
|
|
5568
|
+
const dest = path41.join(getOutputDir(), `openrouter_video_${jobId}.mp4`);
|
|
5352
5569
|
const r = await globalThis.fetch(url, { headers: { Authorization: `Bearer ${apiKey}` } });
|
|
5353
5570
|
if (!r.ok) {
|
|
5354
5571
|
logger.error(`Download HTTP ${r.status}`);
|
|
@@ -5358,14 +5575,14 @@ async function downloadOpenRouterVideo(opts) {
|
|
|
5358
5575
|
logger.error("Empty response body");
|
|
5359
5576
|
process.exit(1);
|
|
5360
5577
|
}
|
|
5361
|
-
|
|
5362
|
-
const ws =
|
|
5578
|
+
fs36.mkdirSync(path41.dirname(dest), { recursive: true });
|
|
5579
|
+
const ws = fs36.createWriteStream(dest);
|
|
5363
5580
|
const { pipeline: pipeline2 } = await import("stream/promises");
|
|
5364
5581
|
await pipeline2(r.body, ws);
|
|
5365
5582
|
logger.success(`Saved ${dest}`);
|
|
5366
5583
|
if (opts.output) {
|
|
5367
|
-
|
|
5368
|
-
|
|
5584
|
+
fs36.mkdirSync(path41.dirname(path41.resolve(opts.output)), { recursive: true });
|
|
5585
|
+
fs36.copyFileSync(dest, opts.output);
|
|
5369
5586
|
logger.success(`Copied to ${opts.output}`);
|
|
5370
5587
|
}
|
|
5371
5588
|
await maybeDownloadThumb(source, dest, {
|
|
@@ -5746,8 +5963,8 @@ function registerCheckCommand(program2) {
|
|
|
5746
5963
|
|
|
5747
5964
|
// src/commands/doc/convert.ts
|
|
5748
5965
|
init_errors();
|
|
5749
|
-
import
|
|
5750
|
-
import
|
|
5966
|
+
import fs37 from "fs";
|
|
5967
|
+
import path42 from "path";
|
|
5751
5968
|
|
|
5752
5969
|
// src/commands/doc/default-prompt.ts
|
|
5753
5970
|
var DEFAULT_CONVERT_PROMPT = `Convert this document to clean, well-formatted Markdown.
|
|
@@ -5768,18 +5985,18 @@ async function convertDocuments(opts) {
|
|
|
5768
5985
|
const model = opts.model ?? DOC_MODEL_DEFAULT;
|
|
5769
5986
|
const prompt = opts.prompt ?? DEFAULT_CONVERT_PROMPT;
|
|
5770
5987
|
for (const f of inputs) {
|
|
5771
|
-
if (!
|
|
5988
|
+
if (!fs37.existsSync(f)) throw new ValidationError(`Input file not found: ${f}`);
|
|
5772
5989
|
}
|
|
5773
5990
|
let outputPath;
|
|
5774
5991
|
if (output) {
|
|
5775
|
-
outputPath =
|
|
5992
|
+
outputPath = path42.resolve(output);
|
|
5776
5993
|
} else if (autoName && inputs.length === 1 && inputs[0]) {
|
|
5777
|
-
const base =
|
|
5778
|
-
outputPath =
|
|
5994
|
+
const base = path42.basename(inputs[0], path42.extname(inputs[0]));
|
|
5995
|
+
outputPath = path42.join(getOutputDir(), `${base}-extraction.md`);
|
|
5779
5996
|
} else {
|
|
5780
|
-
outputPath =
|
|
5997
|
+
outputPath = path42.join(getOutputDir(), "document-extraction.md");
|
|
5781
5998
|
}
|
|
5782
|
-
|
|
5999
|
+
fs37.mkdirSync(path42.dirname(outputPath), { recursive: true });
|
|
5783
6000
|
const results = [];
|
|
5784
6001
|
for (let i = 0; i < inputs.length; i++) {
|
|
5785
6002
|
const filePath = inputs[i];
|
|
@@ -5811,7 +6028,7 @@ async function convertDocuments(opts) {
|
|
|
5811
6028
|
if (attempt < 2) {
|
|
5812
6029
|
const wait = 2 ** attempt * 1e3;
|
|
5813
6030
|
logger?.debug(`Retry ${attempt + 1} after ${wait / 1e3}s: ${errorMsg}`);
|
|
5814
|
-
await
|
|
6031
|
+
await sleep6(wait);
|
|
5815
6032
|
}
|
|
5816
6033
|
}
|
|
5817
6034
|
}
|
|
@@ -5830,7 +6047,7 @@ async function convertDocuments(opts) {
|
|
|
5830
6047
|
`);
|
|
5831
6048
|
for (const r of results) {
|
|
5832
6049
|
lines.push(`
|
|
5833
|
-
## ${
|
|
6050
|
+
## ${path42.basename(r.file)}
|
|
5834
6051
|
`);
|
|
5835
6052
|
if (r.status === "success" && r.markdown) {
|
|
5836
6053
|
lines.push(`${r.markdown}
|
|
@@ -5843,7 +6060,7 @@ async function convertDocuments(opts) {
|
|
|
5843
6060
|
}
|
|
5844
6061
|
lines.push("\n---\n");
|
|
5845
6062
|
}
|
|
5846
|
-
|
|
6063
|
+
fs37.writeFileSync(outputPath, lines.join("\n"), "utf8");
|
|
5847
6064
|
const succeeded = results.filter((r) => r.status === "success").length;
|
|
5848
6065
|
console.log(`
|
|
5849
6066
|
${"=".repeat(50)}`);
|
|
@@ -5859,7 +6076,7 @@ ${"=".repeat(50)}`);
|
|
|
5859
6076
|
const failed = results.filter((r) => r.status === "error").length;
|
|
5860
6077
|
if (failed > 0) process.exit(1);
|
|
5861
6078
|
}
|
|
5862
|
-
function
|
|
6079
|
+
function sleep6(ms) {
|
|
5863
6080
|
return new Promise((r) => setTimeout(r, ms));
|
|
5864
6081
|
}
|
|
5865
6082
|
|
|
@@ -5889,12 +6106,12 @@ function registerDocCommands(program2) {
|
|
|
5889
6106
|
|
|
5890
6107
|
// src/commands/media/index.ts
|
|
5891
6108
|
init_errors();
|
|
5892
|
-
import
|
|
5893
|
-
import
|
|
6109
|
+
import fs40 from "fs";
|
|
6110
|
+
import path45 from "path";
|
|
5894
6111
|
|
|
5895
6112
|
// src/commands/media/batch.ts
|
|
5896
|
-
import
|
|
5897
|
-
import
|
|
6113
|
+
import fs38 from "fs";
|
|
6114
|
+
import path43 from "path";
|
|
5898
6115
|
|
|
5899
6116
|
// src/commands/media/detect-type.ts
|
|
5900
6117
|
var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".mpg", ".mpeg"]);
|
|
@@ -6090,10 +6307,10 @@ async function batchOptimize(opts) {
|
|
|
6090
6307
|
verbose,
|
|
6091
6308
|
logger
|
|
6092
6309
|
} = opts;
|
|
6093
|
-
|
|
6094
|
-
const entries =
|
|
6310
|
+
fs38.mkdirSync(outputDir, { recursive: true });
|
|
6311
|
+
const entries = fs38.readdirSync(inputDir);
|
|
6095
6312
|
const mediaFiles = entries.filter((f) => {
|
|
6096
|
-
const ext =
|
|
6313
|
+
const ext = path43.extname(f).toLowerCase();
|
|
6097
6314
|
return ALL_EXTS.has(ext);
|
|
6098
6315
|
});
|
|
6099
6316
|
if (mediaFiles.length === 0) {
|
|
@@ -6104,9 +6321,9 @@ async function batchOptimize(opts) {
|
|
|
6104
6321
|
let succeeded = 0;
|
|
6105
6322
|
const errors = [];
|
|
6106
6323
|
for (const file of mediaFiles) {
|
|
6107
|
-
const inputPath =
|
|
6108
|
-
const outputPath =
|
|
6109
|
-
const ext =
|
|
6324
|
+
const inputPath = path43.join(inputDir, file);
|
|
6325
|
+
const outputPath = path43.join(outputDir, file);
|
|
6326
|
+
const ext = path43.extname(file).toLowerCase();
|
|
6110
6327
|
const kind = extToKind(ext);
|
|
6111
6328
|
logger?.debug(`Processing: ${file}`);
|
|
6112
6329
|
try {
|
|
@@ -6143,8 +6360,8 @@ async function batchOptimize(opts) {
|
|
|
6143
6360
|
|
|
6144
6361
|
// src/commands/media/split-video.ts
|
|
6145
6362
|
init_errors();
|
|
6146
|
-
import
|
|
6147
|
-
import
|
|
6363
|
+
import fs39 from "fs";
|
|
6364
|
+
import path44 from "path";
|
|
6148
6365
|
async function splitVideo(opts) {
|
|
6149
6366
|
await assertFfmpeg();
|
|
6150
6367
|
const { input, outputDir, chunkDuration = 3600, verbose, logger } = opts;
|
|
@@ -6155,10 +6372,10 @@ async function splitVideo(opts) {
|
|
|
6155
6372
|
logger?.info("Video is short enough \u2014 no splitting needed");
|
|
6156
6373
|
return [input];
|
|
6157
6374
|
}
|
|
6158
|
-
|
|
6159
|
-
const ext =
|
|
6160
|
-
const baseName =
|
|
6161
|
-
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}`);
|
|
6162
6379
|
logger?.debug(`Splitting into ~${numChunks} chunks of ${chunkDuration}s each`);
|
|
6163
6380
|
const args = [
|
|
6164
6381
|
"-i",
|
|
@@ -6177,8 +6394,8 @@ async function splitVideo(opts) {
|
|
|
6177
6394
|
await runFfmpeg(args, { verbose, logger });
|
|
6178
6395
|
const files = [];
|
|
6179
6396
|
for (let i = 0; i < numChunks + 1; i++) {
|
|
6180
|
-
const fname =
|
|
6181
|
-
if (
|
|
6397
|
+
const fname = path44.join(outputDir, `${baseName}_chunk_${String(i).padStart(3, "0")}${ext}`);
|
|
6398
|
+
if (fs39.existsSync(fname)) files.push(fname);
|
|
6182
6399
|
}
|
|
6183
6400
|
logger?.success(`Created ${files.length} chunks in ${outputDir}`);
|
|
6184
6401
|
return files;
|
|
@@ -6192,9 +6409,9 @@ function registerMediaCommands(program2) {
|
|
|
6192
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(
|
|
6193
6410
|
async (opts) => {
|
|
6194
6411
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6195
|
-
if (!
|
|
6196
|
-
|
|
6197
|
-
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();
|
|
6198
6415
|
const kind = extToKind(ext);
|
|
6199
6416
|
if (!kind) throw new ValidationError(`Unsupported file type: ${ext}`);
|
|
6200
6417
|
const quality = Number.parseInt(opts.quality, 10);
|
|
@@ -6234,7 +6451,7 @@ function registerMediaCommands(program2) {
|
|
|
6234
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(
|
|
6235
6452
|
async (opts) => {
|
|
6236
6453
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6237
|
-
if (!
|
|
6454
|
+
if (!fs40.existsSync(opts.input)) throw new ValidationError(`Input not found: ${opts.input}`);
|
|
6238
6455
|
const chunks = await splitVideo({
|
|
6239
6456
|
input: opts.input,
|
|
6240
6457
|
outputDir: opts.outputDir,
|
|
@@ -6250,7 +6467,7 @@ Created ${chunks.length} chunk(s) in ${opts.outputDir}`);
|
|
|
6250
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(
|
|
6251
6468
|
async (opts) => {
|
|
6252
6469
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
6253
|
-
if (!
|
|
6470
|
+
if (!fs40.existsSync(opts.inputDir))
|
|
6254
6471
|
throw new ValidationError(`Input dir not found: ${opts.inputDir}`);
|
|
6255
6472
|
const result = await batchOptimize({
|
|
6256
6473
|
inputDir: opts.inputDir,
|