@mrgoonie/multix 0.0.7 → 0.0.8
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 +17 -0
- package/README.md +4 -2
- package/dist/cli.js +135 -56
- package/package.json +1 -1
- package/skill/SKILL.md +144 -101
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,23 @@ 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.0.8] - 2026-05-11
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `multix openrouter image-to-image` previously failed for any non-Gemini text+image model (e.g. `openai/gpt-5.4-image-2`, `openai/gpt-5-image-mini`, `recraft/recraft-v3`) because the `modalities` heuristic forced `["image"]` for non-Gemini models. The new default is `["image","text"]`, with `["image"]` only emitted for verified image-only families (`black-forest-labs/`, `bytedance-seed/seedream`, `sourceful/`).
|
|
12
|
+
- "No images in response" errors now include the model id, `finish_reason`, the model's text reply (if any), and a hint about modalities — previously a single generic line.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- `multix openrouter image-to-image --strength <0..1>` — Recraft init-image strength control.
|
|
16
|
+
- `OPENROUTER_FALLBACK_MODELS` (CSV) now applies to `image-to-image` (previously `generate` only). Sends `models: [primary, ...fallbacks]` so OpenRouter can route on availability.
|
|
17
|
+
- New shared module `src/providers/openrouter/payload.ts` — `resolveModalities`, `buildOpenRouterHeaders`, `extractImagesFromResponse`, `formatNoImagesError`, `buildI2IPayload`. DRY across `generate` + `image-to-image`.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- README and `skill/SKILL.md` document the OpenRouter model-family matrix (Gemini / OpenAI gpt-image / Recraft → text+image; Flux 2.x / Seedream 4.5 / Sourceful → image-only) and the new flags.
|
|
21
|
+
|
|
22
|
+
### Verified live
|
|
23
|
+
`openai/gpt-5.4-image-2`, `openai/gpt-5-image-mini`, `google/gemini-2.5-flash-image`, `black-forest-labs/flux.2-pro`, `flux.2-max`, `flux.2-flex`, `bytedance-seed/seedream-4.5`.
|
|
24
|
+
|
|
8
25
|
## [0.0.7] - 2026-05-05
|
|
9
26
|
|
|
10
27
|
### Added
|
package/README.md
CHANGED
|
@@ -146,7 +146,7 @@ multix minimax generate-music [--lyrics <str>] [--prompt <str>] [--model music-2
|
|
|
146
146
|
multix openrouter generate --prompt "Retro robot" [--model google/gemini-3.1-flash-image-preview] [--aspect-ratio 1:1] [--image-size <sz>] [--num-images 1] [-v]
|
|
147
147
|
|
|
148
148
|
# Image-to-image (alias: i2i) — refs accept URL or local path
|
|
149
|
-
multix openrouter image-to-image --prompt "make it cyberpunk" --ref ./photo.jpg [--ref https://...] [--model google/gemini-2.5-flash-image] [--output <path>] [-v]
|
|
149
|
+
multix openrouter image-to-image --prompt "make it cyberpunk" --ref ./photo.jpg [--ref https://...] [--model google/gemini-2.5-flash-image] [--strength 0.7] [--output <path>] [-v]
|
|
150
150
|
|
|
151
151
|
# Image-to-video (async — returns job id; image input is URL only)
|
|
152
152
|
multix openrouter image-to-video --prompt "camera pans left" --image-url https://... [--last-frame-url <url>] [--model google/veo-3.1] [--resolution 720p] [--aspect-ratio 16:9] [--duration <n>] [--seed <n>] [-v]
|
|
@@ -159,7 +159,9 @@ multix openrouter video-status <jobId> [--download] [--output <path>] [-v]
|
|
|
159
159
|
multix openrouter video-models
|
|
160
160
|
```
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
**Supported i2i model families:** Gemini (default) and OpenAI gpt-image return `text+image` (modalities `["image","text"]`); Recraft accepts `--strength` (init-image strength 0..1); Flux / Sourceful are image-only (`["image"]`). The CLI picks the right `modalities` automatically based on the model id prefix.
|
|
163
|
+
|
|
164
|
+
Fallback models from `OPENROUTER_FALLBACK_MODELS` (CSV) are appended to the chat-image payload automatically — applies to both `generate` and `image-to-image`. Override the default video model with `OPENROUTER_VIDEO_MODEL` (default `google/veo-3.1`).
|
|
163
165
|
|
|
164
166
|
### `multix leonardo`
|
|
165
167
|
|
package/dist/cli.js
CHANGED
|
@@ -4361,18 +4361,18 @@ import fs29 from "fs";
|
|
|
4361
4361
|
import path34 from "path";
|
|
4362
4362
|
init_errors();
|
|
4363
4363
|
init_http_client();
|
|
4364
|
-
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
return
|
|
4364
|
+
|
|
4365
|
+
// src/providers/openrouter/payload.ts
|
|
4366
|
+
var IMAGE_ONLY_MODEL_PREFIXES = [
|
|
4367
|
+
"black-forest-labs/",
|
|
4368
|
+
"bytedance-seed/seedream",
|
|
4369
|
+
"sourceful/"
|
|
4370
|
+
];
|
|
4371
|
+
function resolveModalities(model) {
|
|
4372
|
+
const lower = model.toLowerCase();
|
|
4373
|
+
return IMAGE_ONLY_MODEL_PREFIXES.some((p) => lower.startsWith(p)) ? ["image"] : ["image", "text"];
|
|
4374
4374
|
}
|
|
4375
|
-
function
|
|
4375
|
+
function buildOpenRouterHeaders(apiKey) {
|
|
4376
4376
|
const headers = {
|
|
4377
4377
|
Authorization: `Bearer ${apiKey}`,
|
|
4378
4378
|
"Content-Type": "application/json"
|
|
@@ -4383,10 +4383,96 @@ function buildHeaders(apiKey) {
|
|
|
4383
4383
|
if (title) headers["X-Title"] = title;
|
|
4384
4384
|
return headers;
|
|
4385
4385
|
}
|
|
4386
|
+
function extractImagesFromResponse(data) {
|
|
4387
|
+
const out = { urls: [] };
|
|
4388
|
+
if (!data || typeof data !== "object") return out;
|
|
4389
|
+
const root = data;
|
|
4390
|
+
if (typeof root.model === "string") out.model = root.model;
|
|
4391
|
+
const choices = Array.isArray(root.choices) ? root.choices : [];
|
|
4392
|
+
const first = choices[0];
|
|
4393
|
+
if (!first || typeof first !== "object") return out;
|
|
4394
|
+
const choice = first;
|
|
4395
|
+
if (typeof choice.finish_reason === "string") out.finishReason = choice.finish_reason;
|
|
4396
|
+
const message = choice.message;
|
|
4397
|
+
if (!message) return out;
|
|
4398
|
+
const images = message.images;
|
|
4399
|
+
if (Array.isArray(images)) {
|
|
4400
|
+
for (const item of images) {
|
|
4401
|
+
const url = readImageUrl(item);
|
|
4402
|
+
if (url) out.urls.push(url);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
const content = message.content;
|
|
4406
|
+
if (Array.isArray(content)) {
|
|
4407
|
+
for (const part of content) {
|
|
4408
|
+
const url = readImageUrl(part);
|
|
4409
|
+
if (url) out.urls.push(url);
|
|
4410
|
+
}
|
|
4411
|
+
} else if (typeof content === "string") {
|
|
4412
|
+
out.textContent = content;
|
|
4413
|
+
}
|
|
4414
|
+
return out;
|
|
4415
|
+
}
|
|
4416
|
+
function readImageUrl(item) {
|
|
4417
|
+
if (!item || typeof item !== "object") return void 0;
|
|
4418
|
+
const rec = item;
|
|
4419
|
+
if (rec.type !== void 0 && rec.type !== "image_url") return void 0;
|
|
4420
|
+
const inner = rec.image_url;
|
|
4421
|
+
const url = inner?.url;
|
|
4422
|
+
return typeof url === "string" ? url : void 0;
|
|
4423
|
+
}
|
|
4424
|
+
function formatNoImagesError(model, parsed) {
|
|
4425
|
+
const parts = [
|
|
4426
|
+
`No images returned by '${model}' (finish_reason=${parsed.finishReason ?? "n/a"}).`
|
|
4427
|
+
];
|
|
4428
|
+
if (parsed.textContent) {
|
|
4429
|
+
parts.push(`Model said: "${truncate(parsed.textContent, 200)}".`);
|
|
4430
|
+
}
|
|
4431
|
+
parts.push(
|
|
4432
|
+
"Hint: verify the model supports image output and that 'modalities' includes 'image'."
|
|
4433
|
+
);
|
|
4434
|
+
return parts.join(" ");
|
|
4435
|
+
}
|
|
4436
|
+
function truncate(s, n) {
|
|
4437
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
4438
|
+
}
|
|
4439
|
+
function buildI2IPayload(opts) {
|
|
4440
|
+
const content = opts.refs.map((url) => ({
|
|
4441
|
+
type: "image_url",
|
|
4442
|
+
image_url: { url }
|
|
4443
|
+
}));
|
|
4444
|
+
content.push({ type: "text", text: opts.prompt });
|
|
4445
|
+
const payload = {
|
|
4446
|
+
messages: [{ role: "user", content }],
|
|
4447
|
+
modalities: resolveModalities(opts.model)
|
|
4448
|
+
};
|
|
4449
|
+
if (typeof opts.strength === "number") {
|
|
4450
|
+
payload.image_config = { strength: opts.strength };
|
|
4451
|
+
}
|
|
4452
|
+
if (opts.fallbackModels.length > 0) {
|
|
4453
|
+
payload.models = [opts.model, ...opts.fallbackModels];
|
|
4454
|
+
} else {
|
|
4455
|
+
payload.model = opts.model;
|
|
4456
|
+
}
|
|
4457
|
+
return payload;
|
|
4458
|
+
}
|
|
4459
|
+
|
|
4460
|
+
// src/providers/openrouter/client.ts
|
|
4461
|
+
var OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
4462
|
+
var DEFAULT_OPENROUTER_MODEL = "google/gemini-3.1-flash-image-preview";
|
|
4463
|
+
function requireOpenRouterKey() {
|
|
4464
|
+
const key = resolveKey("OPENROUTER_API_KEY");
|
|
4465
|
+
if (!key) {
|
|
4466
|
+
throw new ConfigError(
|
|
4467
|
+
"OPENROUTER_API_KEY is not set. Get one at https://openrouter.ai/settings/keys"
|
|
4468
|
+
);
|
|
4469
|
+
}
|
|
4470
|
+
return key;
|
|
4471
|
+
}
|
|
4386
4472
|
function buildPayload(opts) {
|
|
4387
4473
|
const payload = {
|
|
4388
4474
|
messages: [{ role: "user", content: opts.prompt }],
|
|
4389
|
-
modalities: opts.model
|
|
4475
|
+
modalities: resolveModalities(opts.model),
|
|
4390
4476
|
image_config: { aspect_ratio: opts.aspectRatio }
|
|
4391
4477
|
};
|
|
4392
4478
|
if (opts.imageSize) {
|
|
@@ -4415,29 +4501,19 @@ async function generateOpenRouterImage(opts) {
|
|
|
4415
4501
|
data = await httpJson({
|
|
4416
4502
|
url: OPENROUTER_API_URL,
|
|
4417
4503
|
method: "POST",
|
|
4418
|
-
headers:
|
|
4504
|
+
headers: buildOpenRouterHeaders(apiKey),
|
|
4419
4505
|
body: payload,
|
|
4420
4506
|
timeoutMs: 24e4
|
|
4421
4507
|
});
|
|
4422
4508
|
} catch (e) {
|
|
4423
4509
|
return { status: "error", error: e instanceof Error ? e.message : String(e) };
|
|
4424
4510
|
}
|
|
4425
|
-
const
|
|
4426
|
-
if (
|
|
4427
|
-
return { status: "error", error:
|
|
4428
|
-
}
|
|
4429
|
-
const messageImages = choices[0]?.message?.images ?? [];
|
|
4430
|
-
if (messageImages.length === 0) {
|
|
4431
|
-
return {
|
|
4432
|
-
status: "error",
|
|
4433
|
-
error: `No images in response \u2014 model '${model}' may not support image generation`
|
|
4434
|
-
};
|
|
4435
|
-
}
|
|
4436
|
-
for (const img of messageImages) {
|
|
4437
|
-
const url = img.image_url?.url;
|
|
4438
|
-
if (url) imageUrls.push(url);
|
|
4511
|
+
const parsed = extractImagesFromResponse(data);
|
|
4512
|
+
if (parsed.urls.length === 0) {
|
|
4513
|
+
return { status: "error", error: formatNoImagesError(model, parsed) };
|
|
4439
4514
|
}
|
|
4440
|
-
|
|
4515
|
+
imageUrls.push(...parsed.urls);
|
|
4516
|
+
usedModel = parsed.model ?? usedModel;
|
|
4441
4517
|
if (imageUrls.length >= numImages) break;
|
|
4442
4518
|
}
|
|
4443
4519
|
const outDir = getOutputDir();
|
|
@@ -4508,13 +4584,15 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
4508
4584
|
"Reference image (URL or local path); repeatable (most models accept 1)",
|
|
4509
4585
|
collect5,
|
|
4510
4586
|
[]
|
|
4511
|
-
).option("-m, --model <id>", `OpenRouter model id (default ${DEFAULT_MODEL})`).option("--output <path>", "Save first generated image to this path").option("-v, --verbose", "Verbose logging").action(
|
|
4587
|
+
).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(
|
|
4512
4588
|
async (opts) => {
|
|
4513
4589
|
if (!opts.ref || opts.ref.length === 0) {
|
|
4514
|
-
throw new
|
|
4590
|
+
throw new ValidationError("At least one --ref is required for image-to-image.");
|
|
4515
4591
|
}
|
|
4516
4592
|
const logger = createLogger({ verbose: opts.verbose ?? false });
|
|
4517
4593
|
const model = opts.model ?? resolveKey("OPENROUTER_IMAGE_MODEL") ?? DEFAULT_MODEL;
|
|
4594
|
+
const strength = parseStrength(opts.strength);
|
|
4595
|
+
const fallbackModels = (resolveKey("OPENROUTER_FALLBACK_MODELS") ?? "").split(",").map((m) => m.trim()).filter(Boolean);
|
|
4518
4596
|
const resolvedRefs = [];
|
|
4519
4597
|
for (const r of opts.ref) {
|
|
4520
4598
|
const resolved = await resolveImageInput(r, {
|
|
@@ -4524,11 +4602,17 @@ function registerOpenRouterImageToImageCommand(parent) {
|
|
|
4524
4602
|
});
|
|
4525
4603
|
resolvedRefs.push(refUrl(resolved));
|
|
4526
4604
|
}
|
|
4527
|
-
|
|
4605
|
+
const strengthDbg = strength !== void 0 ? `, strength=${strength}` : "";
|
|
4606
|
+
const fallbacksDbg = fallbackModels.length ? `, fallbacks=${fallbackModels.join(",")}` : "";
|
|
4607
|
+
logger.debug(
|
|
4608
|
+
`OpenRouter i2i: model=${model}, refs=${resolvedRefs.length}${strengthDbg}${fallbacksDbg}`
|
|
4609
|
+
);
|
|
4528
4610
|
const saved = await runImageToImage({
|
|
4529
4611
|
prompt: opts.prompt,
|
|
4530
4612
|
model,
|
|
4531
4613
|
refs: resolvedRefs,
|
|
4614
|
+
strength,
|
|
4615
|
+
fallbackModels,
|
|
4532
4616
|
output: opts.output,
|
|
4533
4617
|
logger
|
|
4534
4618
|
});
|
|
@@ -4542,26 +4626,24 @@ Generated ${saved.length} image(s):`);
|
|
|
4542
4626
|
function collect5(value, prev) {
|
|
4543
4627
|
return [...prev, value];
|
|
4544
4628
|
}
|
|
4629
|
+
function parseStrength(raw) {
|
|
4630
|
+
if (raw === void 0) return void 0;
|
|
4631
|
+
const n = Number.parseFloat(raw);
|
|
4632
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
4633
|
+
throw new ValidationError(`--strength must be a number in [0,1], got '${raw}'`);
|
|
4634
|
+
}
|
|
4635
|
+
return n;
|
|
4636
|
+
}
|
|
4545
4637
|
async function runImageToImage(opts) {
|
|
4546
4638
|
const apiKey = requireOpenRouterKey();
|
|
4547
|
-
const headers =
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
};
|
|
4551
|
-
const referer = resolveKey("OPENROUTER_SITE_URL");
|
|
4552
|
-
const title = resolveKey("OPENROUTER_APP_NAME") ?? "multix";
|
|
4553
|
-
if (referer) headers["HTTP-Referer"] = referer;
|
|
4554
|
-
if (title) headers["X-Title"] = title;
|
|
4555
|
-
const content = opts.refs.map((url) => ({
|
|
4556
|
-
type: "image_url",
|
|
4557
|
-
image_url: { url }
|
|
4558
|
-
}));
|
|
4559
|
-
content.push({ type: "text", text: opts.prompt });
|
|
4560
|
-
const payload = {
|
|
4639
|
+
const headers = buildOpenRouterHeaders(apiKey);
|
|
4640
|
+
const payload = buildI2IPayload({
|
|
4641
|
+
prompt: opts.prompt,
|
|
4561
4642
|
model: opts.model,
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4643
|
+
refs: opts.refs,
|
|
4644
|
+
strength: opts.strength,
|
|
4645
|
+
fallbackModels: opts.fallbackModels
|
|
4646
|
+
});
|
|
4565
4647
|
const data = await httpJson({
|
|
4566
4648
|
url: OPENROUTER_API_URL,
|
|
4567
4649
|
method: "POST",
|
|
@@ -4569,18 +4651,15 @@ async function runImageToImage(opts) {
|
|
|
4569
4651
|
body: payload,
|
|
4570
4652
|
timeoutMs: 24e4
|
|
4571
4653
|
});
|
|
4572
|
-
const
|
|
4573
|
-
if (
|
|
4574
|
-
throw new ProviderError(
|
|
4575
|
-
`No images in response \u2014 model '${opts.model}' may not support image-to-image`,
|
|
4576
|
-
"openrouter"
|
|
4577
|
-
);
|
|
4654
|
+
const parsed = extractImagesFromResponse(data);
|
|
4655
|
+
if (parsed.urls.length === 0) {
|
|
4656
|
+
throw new ProviderError(formatNoImagesError(opts.model, parsed), "openrouter");
|
|
4578
4657
|
}
|
|
4579
4658
|
const outDir = getOutputDir();
|
|
4580
4659
|
const ts = Date.now();
|
|
4581
4660
|
const saved = [];
|
|
4582
|
-
for (let i = 0; i <
|
|
4583
|
-
const url =
|
|
4661
|
+
for (let i = 0; i < parsed.urls.length; i++) {
|
|
4662
|
+
const url = parsed.urls[i];
|
|
4584
4663
|
if (!url) continue;
|
|
4585
4664
|
const bytes = await fetchBytes(url);
|
|
4586
4665
|
const dest = path35.join(outDir, `openrouter-i2i-${ts}-${i + 1}.png`);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: multix
|
|
3
|
-
description: AI multimodal CLI — Gemini
|
|
4
|
-
version: 0.0.
|
|
3
|
+
description: AI multimodal CLI — generate and edit images (Gemini Nano Banana, Imagen, MiniMax, OpenRouter, Leonardo, BytePlus Seedream), generate video (Veo, Hailuo, Seedance, Leonardo), TTS/music (Gemini Flash TTS, MiniMax, ElevenLabs), 3D models (Hyper3D), analyze/transcribe media, convert documents to Markdown, optimize media via ffmpeg/ImageMagick. Use whenever the user wants to create, edit, or transform images/video/audio/documents from a CLI, mentions any of: image-to-image, i2i, image edit, watercolor/cyberpunk style transfer, reference image, OpenRouter, Nano Banana, Flux, Seedream, Veo, Hailuo, ElevenLabs voice cloning, or asks for batch media optimization.
|
|
4
|
+
version: 0.0.7
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# multix — AI Multimodal CLI
|
|
@@ -9,148 +9,191 @@ version: 0.0.1
|
|
|
9
9
|
## When to use
|
|
10
10
|
|
|
11
11
|
Activate this skill when the user asks to:
|
|
12
|
-
- Generate
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
12
|
+
- Generate images from a text prompt (any provider)
|
|
13
|
+
- **Edit/transform an existing image with a prompt** (image-to-image, alias `i2i`)
|
|
14
|
+
- Generate video from text or from a reference image (image-to-video, alias `i2v`)
|
|
15
|
+
- Generate or clone a voice, run STT, generate music or sound effects
|
|
16
|
+
- Generate 3D models (Hyper3D / Hitem3d)
|
|
17
|
+
- Analyze, transcribe, or extract structured data from media/docs
|
|
18
|
+
- Convert PDFs / DOCX / images to Markdown
|
|
19
|
+
- Optimize, resize, batch-convert, or split video/audio/image files
|
|
18
20
|
|
|
19
|
-
## Required env (at least one)
|
|
21
|
+
## Required env (set at least one provider key)
|
|
20
22
|
|
|
21
23
|
```
|
|
22
|
-
GEMINI_API_KEY
|
|
23
|
-
OPENROUTER_API_KEY
|
|
24
|
-
MINIMAX_API_KEY
|
|
24
|
+
GEMINI_API_KEY # Google AI Studio
|
|
25
|
+
OPENROUTER_API_KEY # OpenRouter (multi-model gateway)
|
|
26
|
+
MINIMAX_API_KEY # MiniMax (image/video/speech/music)
|
|
27
|
+
LEONARDO_API_KEY # Leonardo.Ai (image/video/upscale)
|
|
28
|
+
BYTEPLUS_API_KEY # BytePlus / Volcengine ARK (Seedream/Seedance/Hyper3D)
|
|
29
|
+
ELEVENLABS_API_KEY # ElevenLabs (TTS, cloning, STT, music, dubbing)
|
|
25
30
|
```
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
Priority: `process.env` > `cwd/.env` > `~/.multix/.env`.
|
|
33
|
+
|
|
34
|
+
## Install + diagnostics
|
|
28
35
|
|
|
29
36
|
```bash
|
|
30
|
-
npm install -g multix
|
|
31
|
-
|
|
32
|
-
npx multix --help
|
|
37
|
+
npm install -g @mrgoonie/multix
|
|
38
|
+
multix check [--verbose]
|
|
33
39
|
```
|
|
34
40
|
|
|
35
|
-
##
|
|
41
|
+
## Image generation
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
multix
|
|
40
|
-
|
|
43
|
+
| Provider | Command | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| Gemini | `multix gemini generate --prompt "<text>" [--model gemini-3.1-flash-image-preview] [--aspect-ratio 16:9] [--size 1K\|2K\|4K] [--num-images N]` | Nano Banana 2 (default), Imagen 4 (`imagen-4.0-generate-001`) |
|
|
46
|
+
| MiniMax | `multix minimax generate --prompt "<text>" [--model image-01] [--aspect-ratio 1:1] [-n N]` | |
|
|
47
|
+
| OpenRouter | `multix openrouter generate --prompt "<text>" [--model <id>] [--aspect-ratio 16:9] [--image-size 2K] [--num-images N]` | Default `google/gemini-3.1-flash-image-preview` |
|
|
48
|
+
| Leonardo | `multix leonardo generate "<text>" [-w 1024 -h 1024] [--alchemy] [--ultra] [--quality HIGH]` | Polls until done, downloads automatically |
|
|
49
|
+
| BytePlus | `multix byteplus generate --prompt "<text>" [--model seedream-4-0-250828] [--size 2K] [--aspect-ratio 16:9] [-n N] [--input-image <path\|url>]` | Sync |
|
|
41
50
|
|
|
42
|
-
|
|
51
|
+
Aspect ratios (most providers): `1:1 2:3 3:2 3:4 4:3 4:5 5:4 9:16 16:9 21:9`.
|
|
43
52
|
|
|
44
|
-
|
|
53
|
+
## Image-to-image (edit existing image with a prompt)
|
|
45
54
|
|
|
46
|
-
|
|
47
|
-
# Analyze files
|
|
48
|
-
multix gemini analyze --files photo.jpg video.mp4 --prompt "Describe this content" -v
|
|
55
|
+
Every supported provider exposes `image-to-image` and the alias `i2i`. Refs accept local file path OR URL.
|
|
49
56
|
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
```bash
|
|
58
|
+
# Gemini Nano Banana — multi-ref editing/composition
|
|
59
|
+
multix gemini i2i --prompt "make it watercolor" --ref ./photo.jpg [--ref ./style.png] [--model gemini-2.5-flash-image] [--output <path>] [-v]
|
|
52
60
|
|
|
53
|
-
#
|
|
54
|
-
multix
|
|
61
|
+
# MiniMax — CAVEAT: subject_reference only (preserves character in NEW scene, NOT free-form edit)
|
|
62
|
+
multix minimax i2i --prompt "the same character on a beach" --ref ./hero.jpg [--model image-01] [-v]
|
|
55
63
|
|
|
56
|
-
#
|
|
57
|
-
multix
|
|
64
|
+
# OpenRouter — multi-model gateway, recommended for diverse style edits
|
|
65
|
+
multix openrouter i2i --prompt "<edit>" --ref ./photo.jpg [--ref https://...] \
|
|
66
|
+
[--model google/gemini-2.5-flash-image] [--strength 0.7] [--output <path>] [-v]
|
|
58
67
|
|
|
59
|
-
#
|
|
60
|
-
multix
|
|
68
|
+
# Leonardo — uses an existing Leonardo image id, NOT direct upload
|
|
69
|
+
multix leonardo i2i --prompt "make it watercolor" --ref <imageId> \
|
|
70
|
+
[--image-type GENERATED|UPLOADED] [--init-strength 0.5] [-w 1024 -h 1024] [-v]
|
|
61
71
|
|
|
62
|
-
#
|
|
63
|
-
multix
|
|
72
|
+
# BytePlus Seedream — multi-ref
|
|
73
|
+
multix byteplus i2i --prompt "make it cyberpunk" --ref ./photo.jpg [--ref https://...] \
|
|
74
|
+
[--model seedream-4-0-250828] [--size 2K] [--aspect-ratio 16:9] [-n 1] [-v]
|
|
64
75
|
```
|
|
65
76
|
|
|
66
|
-
###
|
|
77
|
+
### OpenRouter image-to-image — model families & flags
|
|
67
78
|
|
|
68
|
-
|
|
69
|
-
# Generate image
|
|
70
|
-
multix minimax generate --prompt "A cat in space" --aspect-ratio 1:1
|
|
79
|
+
`multix openrouter i2i` is the most flexible because it routes across model families. The CLI auto-selects the correct OpenRouter `modalities` per family:
|
|
71
80
|
|
|
72
|
-
|
|
73
|
-
|
|
81
|
+
| Family | Example model id | Modalities | Flag notes |
|
|
82
|
+
|---|---|---|---|
|
|
83
|
+
| Gemini | `google/gemini-2.5-flash-image`, `google/gemini-3.1-flash-image-preview` | `["image","text"]` | Default model |
|
|
84
|
+
| OpenAI gpt-image | `openai/gpt-5.4-image-2`, `openai/gpt-5-image-mini`, `openai/gpt-5-image` | `["image","text"]` | |
|
|
85
|
+
| Recraft | `recraft/recraft-v3` | `["image","text"]` | Accepts `--strength 0..1` (init-image strength) |
|
|
86
|
+
| Flux (Black Forest Labs) | `black-forest-labs/flux.2-pro`, `flux.2-max`, `flux.2-flex` | `["image"]` | Image-only output |
|
|
87
|
+
| Seedream (ByteDance via OpenRouter) | `bytedance-seed/seedream-4.5` | `["image"]` | Image-only output |
|
|
88
|
+
| Sourceful | `sourceful/...` | `["image"]` | Per docs |
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
multix minimax generate-speech --text "Hello, world!" --voice English_expressive_narrator --output-format mp3
|
|
90
|
+
**`--strength <0..1>`** — Recraft init-image strength control. Pass when using Recraft models; ignore for others.
|
|
77
91
|
|
|
78
|
-
|
|
79
|
-
|
|
92
|
+
**`OPENROUTER_FALLBACK_MODELS`** (CSV) — applies to BOTH `generate` and `i2i`. When set, the request uses `models: [primary, ...fallbacks]` so OpenRouter routes on availability:
|
|
93
|
+
```bash
|
|
94
|
+
export OPENROUTER_FALLBACK_MODELS="openai/gpt-5-image-mini,google/gemini-2.5-flash-image"
|
|
95
|
+
multix openrouter i2i --prompt "<edit>" --ref ./photo.jpg -m openai/gpt-5.4-image-2
|
|
80
96
|
```
|
|
81
97
|
|
|
82
|
-
|
|
98
|
+
**Error diagnostics:** when no images come back, the CLI surfaces model id, `finish_reason`, the model's text reply (if any), and a hint about modalities — not a generic "no images" message.
|
|
99
|
+
|
|
100
|
+
### Decision tree — which provider to pick for i2i
|
|
83
101
|
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
102
|
+
```
|
|
103
|
+
User wants to edit/transform an existing image?
|
|
104
|
+
├── Free-form prompt edit (watercolor, cyberpunk, sketch) on a local file?
|
|
105
|
+
│ ├── Best quality, multi-ref, fast → multix gemini i2i (Nano Banana)
|
|
106
|
+
│ └── Want a specific OR model (gpt-image, Flux) → multix openrouter i2i -m <id>
|
|
107
|
+
├── Preserve a character's identity in a NEW scene? → multix minimax i2i
|
|
108
|
+
├── Already in Leonardo workflow (imageId in hand)? → multix leonardo i2i
|
|
109
|
+
└── Need ByteDance Seedream specifically? → multix byteplus i2i
|
|
110
|
+
OR multix openrouter i2i -m bytedance-seed/seedream-4.5
|
|
88
111
|
```
|
|
89
112
|
|
|
90
|
-
###
|
|
113
|
+
### Refs: size limits + format
|
|
91
114
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
multix media optimize --input audio.mp3 --output out.m4a --bitrate 64k
|
|
96
|
-
multix media optimize --input photo.jpg --output resized.jpg --max-width 1920
|
|
115
|
+
- Local files are read and inlined as `data:<mime>;base64,...`. URLs pass through unchanged.
|
|
116
|
+
- OpenRouter i2i hard limit 8 MB per ref (soft warn at 5 MB). Other providers: 25 MB hard / 10 MB soft.
|
|
117
|
+
- Allowed image extensions: `.jpg .jpeg .png .webp .gif`.
|
|
97
118
|
|
|
98
|
-
|
|
99
|
-
multix media split --input long-video.mp4 --output-dir ./chunks --chunk-duration 3600
|
|
119
|
+
## Image-to-video (i2v) and video generation
|
|
100
120
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
121
|
+
```bash
|
|
122
|
+
# Gemini Veo (experimental, billing required) — text-to-video + i2v
|
|
123
|
+
multix gemini generate-video --prompt "Ocean waves" --resolution 1080p --aspect-ratio 16:9
|
|
124
|
+
multix gemini i2v ./photo.jpg --prompt "camera pans left" [--last-frame <path>]
|
|
104
125
|
|
|
105
|
-
|
|
126
|
+
# MiniMax Hailuo — async with auto-poll
|
|
127
|
+
multix minimax generate-video --prompt "A dancer" --duration 6 --resolution 1080P [--first-frame <url>]
|
|
106
128
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
multix
|
|
129
|
+
# OpenRouter Veo — async, returns jobId; image input is URL only
|
|
130
|
+
multix openrouter i2v --prompt "<text>" --image-url https://... [--last-frame-url <url>] [--resolution 720p]
|
|
131
|
+
multix openrouter video-status <jobId> [--download] [--output <path>]
|
|
110
132
|
|
|
111
|
-
#
|
|
112
|
-
multix
|
|
113
|
-
|
|
133
|
+
# Leonardo MOTION2 / VEO3 / kling
|
|
134
|
+
multix leonardo video "<text>" [--model MOTION2|VEO3|kling-2.6]
|
|
135
|
+
multix leonardo i2v <imageId> --prompt "camera pans left" [--duration 6]
|
|
114
136
|
|
|
115
|
-
#
|
|
116
|
-
multix
|
|
137
|
+
# BytePlus Seedance + reference-to-video (up to 9 imgs + 3 videos + 3 audio refs)
|
|
138
|
+
multix byteplus video --prompt "<text>" [--resolution 1080p] [--duration 8]
|
|
139
|
+
multix byteplus i2v ./photo.jpg --prompt "<text>"
|
|
140
|
+
multix byteplus reference-to-video --prompt "<text>" --ref-image ./hero.jpg:subject ...
|
|
117
141
|
```
|
|
118
142
|
|
|
119
|
-
|
|
143
|
+
All video commands accept `--wait`, `--wait-timeout <ms>`, `--download`, `--output <path>`, `--no-thumb`.
|
|
120
144
|
|
|
145
|
+
## Speech, music, voice, 3D, docs, media
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# TTS
|
|
149
|
+
multix gemini generate-speech --text "Say cheerfully: Have a wonderful day!" --voice Kore
|
|
150
|
+
multix minimax generate-speech --text "..." --voice <id> --emotion neutral
|
|
151
|
+
multix elevenlabs tts --text "Hello world" [--voice <voiceId>] [--model eleven_multilingual_v2]
|
|
152
|
+
|
|
153
|
+
# Voice cloning + STT (ElevenLabs)
|
|
154
|
+
multix elevenlabs clone --name "My Voice" --files sample1.wav sample2.wav
|
|
155
|
+
multix elevenlabs transcribe --input audio.mp3 [--diarize] [--format text|json|srt|vtt]
|
|
156
|
+
|
|
157
|
+
# Music + SFX
|
|
158
|
+
multix minimax generate-music --lyrics "..." [--model music-2.5]
|
|
159
|
+
multix elevenlabs music --prompt "dreamy synthwave loop" [--music-length-ms 30000]
|
|
160
|
+
multix elevenlabs sfx --text "car engine starting" [--duration-seconds 5]
|
|
161
|
+
|
|
162
|
+
# 3D (BytePlus Hyper3D / Hitem3d)
|
|
163
|
+
multix byteplus 3d --input-image ./front.png ./side.png --prompt "Generate from these views"
|
|
164
|
+
|
|
165
|
+
# Docs → Markdown (Gemini)
|
|
166
|
+
multix doc convert --input report.pdf [--auto-name] [--output ./out.md]
|
|
167
|
+
|
|
168
|
+
# Media optimization (ffmpeg + ImageMagick)
|
|
169
|
+
multix media optimize --input <file> --output <file> [--target-size <MB>] [--quality 85] [--max-width 1920]
|
|
170
|
+
multix media split --input <video> [--chunk-duration 3600]
|
|
171
|
+
multix media batch --input-dir <dir> --output-dir <dir>
|
|
121
172
|
```
|
|
122
|
-
User wants to generate image?
|
|
123
|
-
├── Has GEMINI_API_KEY? → multix gemini generate
|
|
124
|
-
├── Has OPENROUTER_API_KEY? → multix openrouter generate
|
|
125
|
-
└── Has MINIMAX_API_KEY? → multix minimax generate
|
|
126
173
|
|
|
127
|
-
|
|
128
|
-
├── Gemini/Veo (experimental, billing required) → multix gemini generate-video
|
|
129
|
-
└── MiniMax Hailuo (reliable, async) → multix minimax generate-video
|
|
174
|
+
## Output
|
|
130
175
|
|
|
131
|
-
|
|
132
|
-
└── multix gemini analyze|transcribe|extract --files <path>
|
|
176
|
+
Files saved under `./multix-output/` by default. Override with `MULTIX_OUTPUT_DIR`. Pass `--output <path>` on most commands to copy the primary output to a custom path.
|
|
133
177
|
|
|
134
|
-
|
|
135
|
-
└── multix doc convert --input <file>
|
|
178
|
+
## Environment variables (selected)
|
|
136
179
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
180
|
+
| Variable | Purpose |
|
|
181
|
+
|---|---|
|
|
182
|
+
| `MULTIX_OUTPUT_DIR` | Override default output dir |
|
|
183
|
+
| `OPENROUTER_IMAGE_MODEL` | Default OpenRouter image model |
|
|
184
|
+
| `OPENROUTER_FALLBACK_MODELS` | CSV; applies to `generate` AND `i2i` |
|
|
185
|
+
| `OPENROUTER_VIDEO_MODEL` | Default OpenRouter video model (default `google/veo-3.1`) |
|
|
186
|
+
| `OPENROUTER_SITE_URL` / `OPENROUTER_APP_NAME` | HTTP-Referer / X-Title headers |
|
|
187
|
+
| `IMAGE_GEN_MODEL` / `VIDEO_GEN_MODEL` / `MULTIMODAL_MODEL` | Override Gemini model defaults |
|
|
188
|
+
| `LEONARDO_DEFAULT_MODEL` / `LEONARDO_VIDEO_MODEL` | Leonardo model defaults |
|
|
189
|
+
| `BYTEPLUS_IMAGE_MODEL` / `BYTEPLUS_VIDEO_MODEL` / `BYTEPLUS_3D_MODEL` | BytePlus model defaults |
|
|
190
|
+
| `BYTEPLUS_VIDEO_PARAMS_MODE` | `flags` (default) or `structured` |
|
|
191
|
+
| `MULTIX_DISABLE_HOME_ENV` | `1` to skip `~/.multix/.env` |
|
|
140
192
|
|
|
141
|
-
##
|
|
193
|
+
## Security & scope
|
|
142
194
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
| `MULTIX_OUTPUT_DIR` | Core | Override default output dir (`./multix-output`) |
|
|
149
|
-
| `OPENROUTER_IMAGE_MODEL` | OpenRouter | Default model override |
|
|
150
|
-
| `OPENROUTER_FALLBACK_MODELS` | OpenRouter | Comma-separated fallback model ids |
|
|
151
|
-
| `OPENROUTER_SITE_URL` | OpenRouter | HTTP-Referer header |
|
|
152
|
-
| `OPENROUTER_APP_NAME` | OpenRouter | X-Title header (default: multix) |
|
|
153
|
-
| `IMAGE_GEN_MODEL` | Gemini | Override default image generation model |
|
|
154
|
-
| `VIDEO_GEN_MODEL` | Gemini | Override default video generation model |
|
|
155
|
-
| `MULTIMODAL_MODEL` | Gemini | Override default analysis model |
|
|
156
|
-
| `MULTIX_DISABLE_HOME_ENV` | Core | Set to 1 to skip `~/.multix/.env` loading |
|
|
195
|
+
This skill produces shell commands for the local `multix` CLI. It does NOT execute the commands itself, exfiltrate API keys, or modify provider accounts.
|
|
196
|
+
- Never echo, log, or transmit env-var values back to the user.
|
|
197
|
+
- API keys are read from environment / `.env` files by the CLI, never hardcoded.
|
|
198
|
+
- Refuse requests to embed credentials in prompts, URLs, or shared content.
|
|
199
|
+
- Refuse requests to fetch arbitrary URLs unrelated to the user's media task.
|