@mrgoonie/multix 0.15.0 → 0.15.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 +7 -0
- package/README.md +1 -0
- package/dist/cli.js +135 -54
- package/package.json +1 -1
- package/skill/SKILL.md +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.15.1](https://github.com/mrgoonie/multix-cli/compare/v0.15.0...v0.15.1) (2026-10-11)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Performance
|
|
12
|
+
|
|
13
|
+
* **gemini:** stream file uploads with resumable Files API ([#85](https://github.com/mrgoonie/multix-cli/issues/85)) ([b2cc886](https://github.com/mrgoonie/multix-cli/commit/b2cc886cf5bdf960e88aeea5155ae2623547d918))
|
|
14
|
+
|
|
8
15
|
## [0.15.0](https://github.com/mrgoonie/multix-cli/compare/v0.14.1...v0.15.0) (2026-10-11)
|
|
9
16
|
|
|
10
17
|
|
package/README.md
CHANGED
|
@@ -99,6 +99,7 @@ Set at least one provider key. Add to `.env` in your project root or `~/.multix/
|
|
|
99
99
|
| `CLOUDFLARE_AI_TTS_MODEL` | No | Speech model setting; only `@cf/myshell-ai/melotts` is accepted |
|
|
100
100
|
| `MULTIX_OUTPUT_DIR` | No | Override default output dir (`./multix-output`) |
|
|
101
101
|
| `MULTIX_MAX_DOWNLOAD_MB` | No | Size limit per downloaded Replicate output file (default `2048`) |
|
|
102
|
+
| `MULTIX_UPLOAD_MIN_KBPS` | No | Slowest Gemini Files API upload speed to allow for, in KB/s (default `1000`); lower it on slow links |
|
|
102
103
|
| `MULTIX_IMAGE_FORMAT` | No | `webp` (default) or `original` to keep provider image format/quality |
|
|
103
104
|
| `OPENROUTER_IMAGE_MODEL` | No | Default OpenRouter model |
|
|
104
105
|
| `OPENROUTER_FALLBACK_MODELS` | No | Comma-separated fallback model ids |
|
package/dist/cli.js
CHANGED
|
@@ -46,8 +46,8 @@ var init_errors = __esm({
|
|
|
46
46
|
"use strict";
|
|
47
47
|
init_redact();
|
|
48
48
|
MultixError = class extends Error {
|
|
49
|
-
constructor(message, code) {
|
|
50
|
-
super(message);
|
|
49
|
+
constructor(message, code, options) {
|
|
50
|
+
super(message, options);
|
|
51
51
|
this.code = code;
|
|
52
52
|
this.name = "MultixError";
|
|
53
53
|
Error.captureStackTrace?.(
|
|
@@ -64,8 +64,8 @@ var init_errors = __esm({
|
|
|
64
64
|
}
|
|
65
65
|
};
|
|
66
66
|
ProviderError = class extends MultixError {
|
|
67
|
-
constructor(message, provider) {
|
|
68
|
-
super(message, "PROVIDER_ERROR");
|
|
67
|
+
constructor(message, provider, options) {
|
|
68
|
+
super(message, "PROVIDER_ERROR", options);
|
|
69
69
|
this.provider = provider;
|
|
70
70
|
this.name = "ProviderError";
|
|
71
71
|
}
|
|
@@ -78,8 +78,8 @@ var init_errors = __esm({
|
|
|
78
78
|
}
|
|
79
79
|
};
|
|
80
80
|
HttpError = class extends MultixError {
|
|
81
|
-
constructor(status, snippet, url, retryAfterMs) {
|
|
82
|
-
super(`HTTP ${status} from ${redactUrl(url)}: ${snippet}`, "HTTP_ERROR");
|
|
81
|
+
constructor(status, snippet, url, retryAfterMs, options) {
|
|
82
|
+
super(`HTTP ${status} from ${redactUrl(url)}: ${snippet}`, "HTTP_ERROR", options);
|
|
83
83
|
this.status = status;
|
|
84
84
|
this.snippet = snippet;
|
|
85
85
|
this.retryAfterMs = retryAfterMs;
|
|
@@ -4651,11 +4651,20 @@ init_errors();
|
|
|
4651
4651
|
import fs20 from "fs";
|
|
4652
4652
|
import os2 from "os";
|
|
4653
4653
|
import path23 from "path";
|
|
4654
|
+
|
|
4655
|
+
// src/core/limits.ts
|
|
4656
|
+
var FILES_API_MAX_BYTES = 2e9;
|
|
4657
|
+
|
|
4658
|
+
// src/core/stdin-file.ts
|
|
4654
4659
|
var STDIN_ARG = "-";
|
|
4655
4660
|
var STDIN_LABEL = "<stdin>";
|
|
4656
4661
|
var HEAD_BYTES = 4096;
|
|
4657
|
-
var MAX_STDIN_BYTES =
|
|
4662
|
+
var MAX_STDIN_BYTES = FILES_API_MAX_BYTES;
|
|
4658
4663
|
var WAIT_HINT_MS = 2e3;
|
|
4664
|
+
function stdinTooLargeMessage(maxBytes) {
|
|
4665
|
+
const bytes = maxBytes.toLocaleString("en-US");
|
|
4666
|
+
return maxBytes === FILES_API_MAX_BYTES ? `Piped stdin exceeds the Gemini Files API limit of 2 GB (${bytes} bytes)` : `Piped stdin exceeds the ${bytes}-byte limit`;
|
|
4667
|
+
}
|
|
4659
4668
|
var ascii = (buf, start, end) => buf.toString("latin1", start, end);
|
|
4660
4669
|
var HEIF_BRANDS = /* @__PURE__ */ new Set([
|
|
4661
4670
|
"heic",
|
|
@@ -4795,9 +4804,7 @@ async function stdinToTempFile(opts = {}) {
|
|
|
4795
4804
|
clearTimeout(hint);
|
|
4796
4805
|
const buf = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
4797
4806
|
if (size + buf.length > maxBytes) {
|
|
4798
|
-
throw new ValidationError(
|
|
4799
|
-
`Piped stdin exceeds ${Math.floor(maxBytes / 1024 ** 2)} MB; save it to a file instead`
|
|
4800
|
-
);
|
|
4807
|
+
throw new ValidationError(stdinTooLargeMessage(maxBytes));
|
|
4801
4808
|
}
|
|
4802
4809
|
if (head.length < HEAD_BYTES) {
|
|
4803
4810
|
head = Buffer.concat([head, buf.subarray(0, HEAD_BYTES - head.length)]);
|
|
@@ -4859,9 +4866,9 @@ async function resolveFileInputs(files, opts = {}) {
|
|
|
4859
4866
|
}
|
|
4860
4867
|
|
|
4861
4868
|
// src/providers/gemini/client.ts
|
|
4862
|
-
init_errors();
|
|
4863
4869
|
import fs21 from "fs";
|
|
4864
4870
|
import path25 from "path";
|
|
4871
|
+
init_errors();
|
|
4865
4872
|
|
|
4866
4873
|
// src/providers/gemini/key-pool.ts
|
|
4867
4874
|
init_errors();
|
|
@@ -4985,49 +4992,123 @@ function withKey(opts, fn) {
|
|
|
4985
4992
|
function authHeaders2(apiKey) {
|
|
4986
4993
|
return { "x-goog-api-key": apiKey };
|
|
4987
4994
|
}
|
|
4995
|
+
var DEFAULT_TIMEOUT_MS3 = 3e5;
|
|
4996
|
+
var UPLOAD_MIN_KBPS_ENV = "MULTIX_UPLOAD_MIN_KBPS";
|
|
4997
|
+
var DEFAULT_MIN_UPLOAD_BYTES_PER_SEC = 1e6;
|
|
4998
|
+
var UPLOAD_TIMEOUT_SLACK_MS = 6e4;
|
|
4999
|
+
function minUploadBytesPerSec() {
|
|
5000
|
+
const raw = resolveKey(UPLOAD_MIN_KBPS_ENV)?.trim();
|
|
5001
|
+
if (!raw) return DEFAULT_MIN_UPLOAD_BYTES_PER_SEC;
|
|
5002
|
+
const kbps = Number(raw);
|
|
5003
|
+
if (!Number.isFinite(kbps) || kbps < 1) {
|
|
5004
|
+
throw new ConfigError(
|
|
5005
|
+
`${UPLOAD_MIN_KBPS_ENV} must be a number of kilobytes per second, at least 1, got "${raw}"`
|
|
5006
|
+
);
|
|
5007
|
+
}
|
|
5008
|
+
return kbps * 1e3;
|
|
5009
|
+
}
|
|
5010
|
+
function uploadTimeoutMs(size, timeoutMs = DEFAULT_TIMEOUT_MS3, bytesPerSec = DEFAULT_MIN_UPLOAD_BYTES_PER_SEC) {
|
|
5011
|
+
return Math.max(timeoutMs, UPLOAD_TIMEOUT_SLACK_MS + Math.ceil(size / bytesPerSec * 1e3));
|
|
5012
|
+
}
|
|
4988
5013
|
async function uploadFile(filePath, opts = {}) {
|
|
4989
|
-
|
|
5014
|
+
const size = fs21.statSync(filePath).size;
|
|
5015
|
+
if (size > FILES_API_MAX_BYTES) {
|
|
5016
|
+
throw new ValidationError(
|
|
5017
|
+
`File too large for the Gemini Files API: ${path25.basename(filePath)} is ${formatMb3(size)} MB (limit 2 GB = ${FILES_API_MAX_BYTES} bytes)`
|
|
5018
|
+
);
|
|
5019
|
+
}
|
|
5020
|
+
const bytesPerSec = minUploadBytesPerSec();
|
|
5021
|
+
return withKey(opts, (apiKey) => uploadFileWithKey(filePath, apiKey, { ...opts, bytesPerSec }));
|
|
5022
|
+
}
|
|
5023
|
+
function formatMb3(bytes) {
|
|
5024
|
+
return (bytes / 1024 / 1024).toFixed(2);
|
|
5025
|
+
}
|
|
5026
|
+
async function uploadFetch(url, init, timeoutMs, step) {
|
|
5027
|
+
let resp;
|
|
5028
|
+
try {
|
|
5029
|
+
resp = await globalThis.fetch(url, {
|
|
5030
|
+
...init,
|
|
5031
|
+
// With the default "follow", fetch clones the request body (tee) to replay
|
|
5032
|
+
// it on a redirect, and that copy keeps every chunk in memory.
|
|
5033
|
+
redirect: "error",
|
|
5034
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
5035
|
+
});
|
|
5036
|
+
} catch (err2) {
|
|
5037
|
+
const e = err2;
|
|
5038
|
+
const timedOut = e?.name === "TimeoutError";
|
|
5039
|
+
const redirected = e?.cause instanceof Error && e.cause.message === "unexpected redirect";
|
|
5040
|
+
const reason = timedOut ? `timed out after ${Math.round(timeoutMs / 1e3)} s` : e?.cause instanceof Error ? `${e.message}: ${e.cause.message}` : String(e?.message ?? err2);
|
|
5041
|
+
const message = `File upload failed: ${reason}`;
|
|
5042
|
+
if (redirected || timedOut && step === "upload") {
|
|
5043
|
+
throw new ProviderError(message, "gemini", { cause: err2 });
|
|
5044
|
+
}
|
|
5045
|
+
throw new HttpError(0, message, FILES_BASE, void 0, { cause: err2 });
|
|
5046
|
+
}
|
|
5047
|
+
if (!resp.ok) {
|
|
5048
|
+
const txt = await resp.text();
|
|
5049
|
+
throw new HttpError(resp.status, `File upload failed: ${txt.slice(0, 500)}`, FILES_BASE);
|
|
5050
|
+
}
|
|
5051
|
+
return resp;
|
|
5052
|
+
}
|
|
5053
|
+
async function readFinalizedFile(resp) {
|
|
5054
|
+
const status = resp.headers.get("x-goog-upload-status")?.trim().toLowerCase();
|
|
5055
|
+
let file;
|
|
5056
|
+
let readError;
|
|
5057
|
+
try {
|
|
5058
|
+
file = (await resp.json())?.file;
|
|
5059
|
+
} catch (err2) {
|
|
5060
|
+
readError = err2;
|
|
5061
|
+
}
|
|
5062
|
+
if (status !== "final" || typeof file?.uri !== "string" || typeof file?.name !== "string") {
|
|
5063
|
+
const detail = status !== "final" ? `upload status is "${status?.slice(0, 40) ?? "missing"}"` : readError ? "unreadable response body" : "no file in the response";
|
|
5064
|
+
throw new ProviderError(`File upload failed: upload was not finalized (${detail})`, "gemini", {
|
|
5065
|
+
cause: readError
|
|
5066
|
+
});
|
|
5067
|
+
}
|
|
5068
|
+
return file;
|
|
4990
5069
|
}
|
|
4991
5070
|
async function uploadFileWithKey(filePath, apiKey, opts) {
|
|
4992
|
-
const { timeoutMs =
|
|
5071
|
+
const { timeoutMs = DEFAULT_TIMEOUT_MS3, logger, bytesPerSec } = opts;
|
|
4993
5072
|
const mimeType = getMimeType(filePath);
|
|
4994
|
-
const fileBytes = fs21.readFileSync(filePath);
|
|
4995
5073
|
const displayName = path25.basename(filePath);
|
|
4996
|
-
const
|
|
4997
|
-
|
|
4998
|
-
const
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
--${boundary}--`)
|
|
5012
|
-
]);
|
|
5013
|
-
logger?.debug(`Uploading ${displayName} (${(fileBytes.length / 1024 / 1024).toFixed(2)} MB)`);
|
|
5014
|
-
const resp = await globalThis.fetch(FILES_BASE, {
|
|
5015
|
-
method: "POST",
|
|
5016
|
-
headers: {
|
|
5017
|
-
"x-goog-api-key": apiKey,
|
|
5018
|
-
"Content-Type": `multipart/related; boundary=${boundary}`,
|
|
5019
|
-
"Content-Length": String(body.length),
|
|
5020
|
-
"X-Goog-Upload-Protocol": "multipart"
|
|
5074
|
+
const file = await fs21.openAsBlob(filePath);
|
|
5075
|
+
logger?.debug(`Uploading ${displayName} (${formatMb3(file.size)} MB)`);
|
|
5076
|
+
const start = await uploadFetch(
|
|
5077
|
+
FILES_BASE,
|
|
5078
|
+
{
|
|
5079
|
+
method: "POST",
|
|
5080
|
+
headers: {
|
|
5081
|
+
"x-goog-api-key": apiKey,
|
|
5082
|
+
"Content-Type": "application/json",
|
|
5083
|
+
"X-Goog-Upload-Protocol": "resumable",
|
|
5084
|
+
"X-Goog-Upload-Command": "start",
|
|
5085
|
+
"X-Goog-Upload-Header-Content-Length": String(file.size),
|
|
5086
|
+
"X-Goog-Upload-Header-Content-Type": mimeType
|
|
5087
|
+
},
|
|
5088
|
+
body: JSON.stringify({ file: { display_name: displayName } })
|
|
5021
5089
|
},
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5026
|
-
|
|
5027
|
-
|
|
5090
|
+
timeoutMs,
|
|
5091
|
+
"start"
|
|
5092
|
+
);
|
|
5093
|
+
const uploadUrl = start.headers.get("x-goog-upload-url");
|
|
5094
|
+
await start.body?.cancel();
|
|
5095
|
+
if (!uploadUrl) {
|
|
5096
|
+
throw new ProviderError("File upload failed: the Files API returned no upload URL", "gemini");
|
|
5028
5097
|
}
|
|
5029
|
-
const
|
|
5030
|
-
|
|
5098
|
+
const resp = await uploadFetch(
|
|
5099
|
+
uploadUrl,
|
|
5100
|
+
{
|
|
5101
|
+
method: "POST",
|
|
5102
|
+
headers: {
|
|
5103
|
+
"X-Goog-Upload-Offset": "0",
|
|
5104
|
+
"X-Goog-Upload-Command": "upload, finalize"
|
|
5105
|
+
},
|
|
5106
|
+
body: file
|
|
5107
|
+
},
|
|
5108
|
+
uploadTimeoutMs(file.size, timeoutMs, bytesPerSec),
|
|
5109
|
+
"upload"
|
|
5110
|
+
);
|
|
5111
|
+
let fileRef = await readFinalizedFile(resp);
|
|
5031
5112
|
if (requiresProcessingWait(mimeType)) {
|
|
5032
5113
|
const deadline = Date.now() + timeoutMs;
|
|
5033
5114
|
while (fileRef.state === "PROCESSING" && Date.now() < deadline) {
|
|
@@ -9474,7 +9555,7 @@ import path45 from "path";
|
|
|
9474
9555
|
import fs37 from "fs";
|
|
9475
9556
|
import path44 from "path";
|
|
9476
9557
|
init_errors();
|
|
9477
|
-
var
|
|
9558
|
+
var DEFAULT_TIMEOUT_MS4 = 24e4;
|
|
9478
9559
|
function requireOpenAIKey() {
|
|
9479
9560
|
const key = resolveKey("OPENAI_API_KEY");
|
|
9480
9561
|
if (!key) {
|
|
@@ -9490,7 +9571,7 @@ function endpoint(pathname) {
|
|
|
9490
9571
|
function authHeaders4(apiKey, extra) {
|
|
9491
9572
|
return { Authorization: `Bearer ${apiKey}`, ...extra ?? {} };
|
|
9492
9573
|
}
|
|
9493
|
-
function postOpenAIJson(pathname, body, apiKey, timeoutMs =
|
|
9574
|
+
function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS4) {
|
|
9494
9575
|
return httpJson({
|
|
9495
9576
|
url: endpoint(pathname),
|
|
9496
9577
|
method: "POST",
|
|
@@ -9499,7 +9580,7 @@ function postOpenAIJson(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS3)
|
|
|
9499
9580
|
timeoutMs
|
|
9500
9581
|
});
|
|
9501
9582
|
}
|
|
9502
|
-
async function postOpenAIForm(pathname, form, apiKey, timeoutMs =
|
|
9583
|
+
async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS4) {
|
|
9503
9584
|
const url = endpoint(pathname);
|
|
9504
9585
|
let res;
|
|
9505
9586
|
try {
|
|
@@ -9518,7 +9599,7 @@ async function postOpenAIForm(pathname, form, apiKey, timeoutMs = DEFAULT_TIMEOU
|
|
|
9518
9599
|
}
|
|
9519
9600
|
return res.json();
|
|
9520
9601
|
}
|
|
9521
|
-
async function postOpenAIBinary(pathname, body, apiKey, timeoutMs =
|
|
9602
|
+
async function postOpenAIBinary(pathname, body, apiKey, timeoutMs = DEFAULT_TIMEOUT_MS4) {
|
|
9522
9603
|
const url = endpoint(pathname);
|
|
9523
9604
|
let res;
|
|
9524
9605
|
try {
|
|
@@ -11441,7 +11522,7 @@ import fs45 from "fs";
|
|
|
11441
11522
|
import path52 from "path";
|
|
11442
11523
|
var DATA_URI_MAX_BYTES = 256 * 1024;
|
|
11443
11524
|
var UPLOAD_MAX_BYTES = 100 * 1024 * 1024;
|
|
11444
|
-
function
|
|
11525
|
+
function formatMb4(bytes) {
|
|
11445
11526
|
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
11446
11527
|
}
|
|
11447
11528
|
async function resolveFileInput(value, client, logger) {
|
|
@@ -11457,7 +11538,7 @@ async function resolveFileInput(value, client, logger) {
|
|
|
11457
11538
|
if (stat.size === 0) throw new ValidationError(`Input file is empty: ${value}`);
|
|
11458
11539
|
if (stat.size >= UPLOAD_MAX_BYTES) {
|
|
11459
11540
|
throw new ValidationError(
|
|
11460
|
-
`Input file ${value} is ${
|
|
11541
|
+
`Input file ${value} is ${formatMb4(stat.size)}; Replicate accepts uploads under 100MB. Host it and pass an https URL instead.`
|
|
11461
11542
|
);
|
|
11462
11543
|
}
|
|
11463
11544
|
const filename = path52.basename(filePath);
|
|
@@ -11467,7 +11548,7 @@ async function resolveFileInput(value, client, logger) {
|
|
|
11467
11548
|
logger.debug(`Inlining ${filename} (${stat.size} bytes) as a data URI`);
|
|
11468
11549
|
return `data:${contentType};base64,${bytes.toString("base64")}`;
|
|
11469
11550
|
}
|
|
11470
|
-
logger.info(`Uploading ${filename} (${
|
|
11551
|
+
logger.info(`Uploading ${filename} (${formatMb4(stat.size)}) to Replicate Files...`);
|
|
11471
11552
|
const file = await client.uploadFile({
|
|
11472
11553
|
data: new Blob([bytes], { type: contentType }),
|
|
11473
11554
|
filename,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrgoonie/multix",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"description": "AI multimodal CLI — OpenAI, Gemini, MiniMax, OpenRouter, Leonardo, BytePlus, ElevenLabs, fal.ai, Atlas Cloud, Replicate, HeyGen, ffmpeg, ImageMagick, doc-to-md",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/skill/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: multix
|
|
3
3
|
description: AI multimodal CLI — generate and edit images (OpenAI, Codex driver, Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream, Cloudflare Workers AI FLUX.1 Schnell, fal.ai FLUX, Replicate FLUX), generate video (Gemini Veo, MiniMax Hailuo, OpenRouter via Veo/Seedance/Kling, Leonardo via Motion/Veo/Kling/Hailuo/Seedance/LTX, BytePlus Seedance, Cloudflare AI Gateway via Replicate, fal.ai queue models, Replicate direct API models incl. P-Video, HeyGen Video Agent and scripted avatar videos), video translation and lipsync (HeyGen), TTS/STT/music (OpenAI, Gemini Flash TTS incl. Gemini 3.8 with delivery-style control, MiniMax, Cloudflare Workers AI MeloTTS, ElevenLabs), 3D models (BytePlus Hyper3D/Hitem3d), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick, and self-update the CLI. Use whenever the user wants to create, edit, or transform images/video/audio/3D/documents from a CLI, mentions any of: OpenAI, Codex image driver, image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, Seedance, Kling, Cloudflare Workers AI, AI Gateway, fal.ai, Atlas Cloud, Replicate, replicate.com models, Replicate predictions, HeyGen, avatar video, talking head, Video Agent, video translation, dubbing, lipsync, video generation, text-to-video, image-to-video, i2v, ElevenLabs voice cloning, updating/upgrading multix, or asks for batch media optimization.
|
|
4
|
-
version: 0.15.
|
|
4
|
+
version: 0.15.1 # x-release-please-version
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -387,6 +387,7 @@ Requires `cwebp` (libwebp: `brew install webp`, `apt install webp`) on `PATH`.
|
|
|
387
387
|
|---|---|
|
|
388
388
|
| `MULTIX_OUTPUT_DIR` | Override default output dir |
|
|
389
389
|
| `MULTIX_MAX_DOWNLOAD_MB` | Size limit per downloaded Replicate output file (default 2048) |
|
|
390
|
+
| `MULTIX_UPLOAD_MIN_KBPS` | Slowest Gemini Files API upload speed to allow for, in KB/s (default 1000); lower it on slow links |
|
|
390
391
|
| `OPENAI_IMAGE_MODEL` / `OPENAI_TTS_MODEL` / `OPENAI_STT_MODEL` | OpenAI model defaults |
|
|
391
392
|
| `OPENROUTER_IMAGE_MODEL` | Default OpenRouter image model |
|
|
392
393
|
| `OPENROUTER_FALLBACK_MODELS` | CSV; applies to `generate` AND `i2i` |
|