@dorely13/ai-imagegen-cli 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/help.js +46 -20
- package/dist/index.js +805 -69
- package/dist/index.js.map +1 -1
- package/dist/usage-log.js +2 -1
- package/package.json +1 -1
- package/skill/SKILL.md +132 -22
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { mkdir as mkdir5, readFile as
|
|
4
|
+
import { mkdir as mkdir5, readFile as readFile4, stat as stat2, writeFile as writeFile3 } from "node:fs/promises";
|
|
5
5
|
import { basename, dirname as dirname5, extname, join as join5, resolve as resolve3 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// ../../packages/core/dist/types.js
|
|
@@ -85,7 +85,9 @@ function normalizeOutputFormat(value) {
|
|
|
85
85
|
return "jpeg";
|
|
86
86
|
if (normalized === "webp")
|
|
87
87
|
return "webp";
|
|
88
|
-
|
|
88
|
+
if (normalized === "png" || normalized === "")
|
|
89
|
+
return "png";
|
|
90
|
+
throw new ProviderError({ kind: "usage", message: "Output format must be png, jpeg, or webp." });
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
// ../../packages/core/dist/image-size.js
|
|
@@ -230,7 +232,7 @@ var IMAGES_ENDPOINT = "https://chatgpt.com/backend-api/codex/images";
|
|
|
230
232
|
var DEFAULT_REDIRECT_URI = "http://localhost:1455/auth/callback";
|
|
231
233
|
var LOOPBACK_PORT = 1455;
|
|
232
234
|
var CALLBACK_PATH = "/auth/callback";
|
|
233
|
-
var DEFAULT_IMAGE_MODEL = "gpt-image-2.
|
|
235
|
+
var DEFAULT_IMAGE_MODEL = "gpt-image-2.6-flare";
|
|
234
236
|
var DEFAULT_QUALITY = "auto";
|
|
235
237
|
var REQUEST_TIMEOUT_SECONDS = 600;
|
|
236
238
|
var MAX_REFERENCE_IMAGES2 = 4;
|
|
@@ -526,9 +528,9 @@ var FileCredentialStore = class {
|
|
|
526
528
|
return { backend: "file", location: this.filePath };
|
|
527
529
|
}
|
|
528
530
|
async readAll() {
|
|
529
|
-
const { readFile:
|
|
531
|
+
const { readFile: readFile5 } = await import("node:fs/promises");
|
|
530
532
|
try {
|
|
531
|
-
const raw = await
|
|
533
|
+
const raw = await readFile5(this.filePath, "utf8");
|
|
532
534
|
let value;
|
|
533
535
|
try {
|
|
534
536
|
value = JSON.parse(raw);
|
|
@@ -807,6 +809,36 @@ function buildBody(args) {
|
|
|
807
809
|
body["images"] = args.images.map((image) => ({ image_url: toDataUrl(image) }));
|
|
808
810
|
return body;
|
|
809
811
|
}
|
|
812
|
+
var MASK_INSTRUCTION = "The last image is an edit mask, not content to reproduce. Apply the requested change only where the mask is transparent. Keep every area where the mask is opaque exactly as it is in the first image.";
|
|
813
|
+
async function normalizeMask(mask, source) {
|
|
814
|
+
const usage2 = (message) => new ProviderError({ kind: "usage", message });
|
|
815
|
+
let pixels;
|
|
816
|
+
let width;
|
|
817
|
+
let height;
|
|
818
|
+
let sourceWidth;
|
|
819
|
+
let sourceHeight;
|
|
820
|
+
try {
|
|
821
|
+
const decoded = await sharp(mask.data).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
822
|
+
({ width, height } = decoded.info);
|
|
823
|
+
pixels = decoded.data;
|
|
824
|
+
({ width: sourceWidth, height: sourceHeight } = await sharp(source.data).metadata());
|
|
825
|
+
} catch {
|
|
826
|
+
throw usage2("Mask and source must be readable PNG, JPEG, or WebP images.");
|
|
827
|
+
}
|
|
828
|
+
if (width !== sourceWidth || height !== sourceHeight) {
|
|
829
|
+
throw usage2(`Mask is ${width}x${height} but the source is ${sourceWidth}x${sourceHeight}; they must match.`);
|
|
830
|
+
}
|
|
831
|
+
let editable = false;
|
|
832
|
+
for (let i = 0; i < pixels.length; i += 4) {
|
|
833
|
+
const edit = pixels[i + 3] < 128;
|
|
834
|
+
editable ||= edit;
|
|
835
|
+
pixels.fill(edit ? 0 : 255, i, i + 4);
|
|
836
|
+
}
|
|
837
|
+
if (!editable)
|
|
838
|
+
throw usage2("Mask has no transparent pixels; make the area to edit transparent.");
|
|
839
|
+
const data = await sharp(pixels, { raw: { width, height, channels: 4 } }).png().toBuffer();
|
|
840
|
+
return { data, contentType: "image/png" };
|
|
841
|
+
}
|
|
810
842
|
var CodexImageProvider = class {
|
|
811
843
|
tokens;
|
|
812
844
|
name = PROVIDER_NAME;
|
|
@@ -825,7 +857,16 @@ var CodexImageProvider = class {
|
|
|
825
857
|
return this.runAll(request, references.length > 0 ? "edits" : "generations", references);
|
|
826
858
|
}
|
|
827
859
|
async edit(request) {
|
|
828
|
-
|
|
860
|
+
const images = [request.sourceImage, ...request.referenceImages ?? []];
|
|
861
|
+
if (!request.mask)
|
|
862
|
+
return this.runAll(request, "edits", images.slice(0, MAX_REFERENCE_IMAGES2));
|
|
863
|
+
if (images.length + 1 > MAX_REFERENCE_IMAGES2) {
|
|
864
|
+
throw new ProviderError({ kind: "usage", message: `Codex accepts at most ${MAX_REFERENCE_IMAGES2} edit images; a mask uses one, leaving room for ${MAX_REFERENCE_IMAGES2 - 2} --reference images.` });
|
|
865
|
+
}
|
|
866
|
+
const mask = await normalizeMask(request.mask, request.sourceImage);
|
|
867
|
+
return this.runAll(request, "edits", [...images, mask], `${request.prompt}
|
|
868
|
+
|
|
869
|
+
${MASK_INSTRUCTION}`);
|
|
829
870
|
}
|
|
830
871
|
resolveImageModel(request) {
|
|
831
872
|
const model = (request.model ?? this.imageModel).trim();
|
|
@@ -837,16 +878,16 @@ var CodexImageProvider = class {
|
|
|
837
878
|
}
|
|
838
879
|
return model;
|
|
839
880
|
}
|
|
840
|
-
async runAll(request, endpoint, images) {
|
|
881
|
+
async runAll(request, endpoint, images, modelPrompt = request.prompt) {
|
|
841
882
|
const format = normalizeOutputFormat(request.format);
|
|
842
883
|
const size = this.sizePolicy.resolveSize(request.size);
|
|
843
|
-
const
|
|
844
|
-
const body = JSON.stringify(buildBody({ prompt:
|
|
884
|
+
const requestedModel = this.resolveImageModel(request);
|
|
885
|
+
const body = JSON.stringify(buildBody({ prompt: modelPrompt, model: requestedModel, size, quality: request.quality ?? DEFAULT_QUALITY, images }));
|
|
845
886
|
const results = [];
|
|
846
887
|
for (let i = 0, count = normalizeCount(request.count); i < count; i += 1) {
|
|
847
888
|
results.push(await this.sendOnce(endpoint, body, format, request.prompt));
|
|
848
889
|
}
|
|
849
|
-
return { provider: this.name,
|
|
890
|
+
return { provider: this.name, requestedModel, images: results };
|
|
850
891
|
}
|
|
851
892
|
async sendOnce(endpoint, body, format, prompt) {
|
|
852
893
|
let accessToken = await this.tokens.getValidAccessToken();
|
|
@@ -897,6 +938,8 @@ var CodexImageProvider = class {
|
|
|
897
938
|
height,
|
|
898
939
|
prompt,
|
|
899
940
|
responseId: typeof item["generation_id"] === "string" ? item["generation_id"] : void 0,
|
|
941
|
+
// The endpoint accepts any requested model and currently names none in its response.
|
|
942
|
+
servedModel: typeof payload["model"] === "string" ? payload["model"] : void 0,
|
|
900
943
|
usage: {
|
|
901
944
|
requestId,
|
|
902
945
|
imageGeneration: payload["usage"],
|
|
@@ -1113,8 +1156,6 @@ var GeminiImageProvider = class {
|
|
|
1113
1156
|
const count = request.count ?? 1;
|
|
1114
1157
|
if (!Number.isInteger(count) || count < 1 || count > 4)
|
|
1115
1158
|
throw error("usage", "Image count must be from 1 to 4.");
|
|
1116
|
-
if (request.format && !["png", "jpeg", "jpg", "webp"].includes(request.format.trim().toLowerCase()))
|
|
1117
|
-
throw error("usage", "Output format must be png, jpeg, or webp.");
|
|
1118
1159
|
const format = normalizeOutputFormat(request.format);
|
|
1119
1160
|
const size = resolveGeminiSize(request.size, model);
|
|
1120
1161
|
const payload = {
|
|
@@ -1149,7 +1190,7 @@ var GeminiImageProvider = class {
|
|
|
1149
1190
|
const images = [];
|
|
1150
1191
|
for (let i = 0; i < count; i++)
|
|
1151
1192
|
images.push(await this.send(payload, key, format, request.prompt));
|
|
1152
|
-
return { provider: this.name,
|
|
1193
|
+
return { provider: this.name, requestedModel: model, images };
|
|
1153
1194
|
}
|
|
1154
1195
|
async send(payload, key, format, prompt) {
|
|
1155
1196
|
const controller = new AbortController();
|
|
@@ -1261,8 +1302,9 @@ function createGeminiEntry() {
|
|
|
1261
1302
|
|
|
1262
1303
|
// ../../packages/providers-configurable/dist/profiles.js
|
|
1263
1304
|
import { readFile, mkdir as mkdir2, writeFile, rename, rm as rm2 } from "node:fs/promises";
|
|
1264
|
-
import { dirname as dirname2, join as join3 } from "node:path";
|
|
1305
|
+
import { dirname as dirname2, isAbsolute, join as join3 } from "node:path";
|
|
1265
1306
|
import { randomUUID } from "node:crypto";
|
|
1307
|
+
var COMFY_FIELDS = ["startCommand", "workflow", "editWorkflow"];
|
|
1266
1308
|
var usage = (message) => new ProviderError({ kind: "usage", message });
|
|
1267
1309
|
var profileStore = () => resolveCredentialStore();
|
|
1268
1310
|
function validateApiKey(value) {
|
|
@@ -1278,14 +1320,25 @@ function validateProfile(value) {
|
|
|
1278
1320
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1279
1321
|
throw usage("Invalid endpoint profile.");
|
|
1280
1322
|
const p = value;
|
|
1281
|
-
if (Object.keys(p).some((k) => !["name", "api", "baseUrl", "model", "auth"].includes(k)))
|
|
1282
|
-
throw usage("Endpoint profiles accept only name, api, baseUrl, model, and
|
|
1323
|
+
if (Object.keys(p).some((k) => !["name", "api", "baseUrl", "model", "auth", ...COMFY_FIELDS].includes(k)))
|
|
1324
|
+
throw usage("Endpoint profiles accept only name, api, baseUrl, model, auth, startCommand, workflow, and editWorkflow; store keys through auth login.");
|
|
1283
1325
|
if (typeof p.name !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(p.name) || ["codex", "gemini"].includes(p.name))
|
|
1284
1326
|
throw usage("Endpoint name must use lowercase letters, digits, and hyphens, and cannot be codex or gemini.");
|
|
1285
|
-
if (p.api !== "openrouter" && p.api !== "openai-images")
|
|
1286
|
-
throw usage("API format must be openrouter
|
|
1327
|
+
if (p.api !== "openrouter" && p.api !== "openai-images" && p.api !== "comfyui")
|
|
1328
|
+
throw usage("API format must be openrouter, openai-images, or comfyui.");
|
|
1287
1329
|
if (p.auth !== void 0 && p.auth !== "bearer" && p.auth !== "none")
|
|
1288
1330
|
throw usage("Auth must be bearer or none.");
|
|
1331
|
+
if (p.api === "comfyui" && p.auth === "bearer")
|
|
1332
|
+
throw usage("ComfyUI profiles do not use API keys. Use --auth none.");
|
|
1333
|
+
if (p.api !== "comfyui" && COMFY_FIELDS.some((k) => p[k] !== void 0))
|
|
1334
|
+
throw usage("--start-command, --workflow, and --edit-workflow apply only to comfyui profiles.");
|
|
1335
|
+
for (const field of COMFY_FIELDS) {
|
|
1336
|
+
const value2 = p[field];
|
|
1337
|
+
if (value2 !== void 0 && (typeof value2 !== "string" || !value2.trim()))
|
|
1338
|
+
throw usage(`${field} must not be blank.`);
|
|
1339
|
+
if (field !== "startCommand" && value2 && !isAbsolute(value2))
|
|
1340
|
+
throw usage(`${field} must be an absolute path.`);
|
|
1341
|
+
}
|
|
1289
1342
|
const raw = p.baseUrl ?? (p.api === "openrouter" ? "https://openrouter.ai/api/v1" : void 0);
|
|
1290
1343
|
let url;
|
|
1291
1344
|
try {
|
|
@@ -1300,12 +1353,18 @@ function validateProfile(value) {
|
|
|
1300
1353
|
throw usage("Base URL must use HTTPS (or loopback HTTP), without credentials, query, or fragment.");
|
|
1301
1354
|
if (p.model !== void 0 && (typeof p.model !== "string" || !p.model.trim()))
|
|
1302
1355
|
throw usage("Default model must not be blank.");
|
|
1356
|
+
if (p.api === "comfyui" && p.model === void 0)
|
|
1357
|
+
throw usage("ComfyUI profiles need --model: flux3-klein, flux3-klein-raw, or your name for the model in --workflow.");
|
|
1303
1358
|
return {
|
|
1304
1359
|
name: p.name,
|
|
1305
1360
|
api: p.api,
|
|
1306
1361
|
baseUrl: url.toString().replace(/\/+$/, ""),
|
|
1307
|
-
auth: p.auth ?? "bearer",
|
|
1308
|
-
...typeof p.model === "string" ? { model: p.model.trim() } : {}
|
|
1362
|
+
auth: p.auth ?? (p.api === "comfyui" ? "none" : "bearer"),
|
|
1363
|
+
...typeof p.model === "string" ? { model: p.model.trim() } : {},
|
|
1364
|
+
...Object.fromEntries(COMFY_FIELDS.flatMap((field) => {
|
|
1365
|
+
const value2 = p[field];
|
|
1366
|
+
return typeof value2 === "string" ? [[field, value2.trim()]] : [];
|
|
1367
|
+
}))
|
|
1309
1368
|
};
|
|
1310
1369
|
}
|
|
1311
1370
|
var ProfileRepository = class {
|
|
@@ -1406,7 +1465,7 @@ async function profileKey(profile, store) {
|
|
|
1406
1465
|
}
|
|
1407
1466
|
|
|
1408
1467
|
// ../../packages/providers-configurable/dist/provider.js
|
|
1409
|
-
import
|
|
1468
|
+
import sharp4 from "sharp";
|
|
1410
1469
|
|
|
1411
1470
|
// ../../packages/providers-configurable/dist/image-size.js
|
|
1412
1471
|
function imageSize(value, api) {
|
|
@@ -1426,10 +1485,27 @@ function imageSize(value, api) {
|
|
|
1426
1485
|
if (pixels && Number.isSafeInteger(Number(pixels[1])) && Number.isSafeInteger(Number(pixels[2])) && Number(pixels[1]) > 0 && Number(pixels[2]) > 0)
|
|
1427
1486
|
return { size: `${Number(pixels[1])}x${Number(pixels[2])}` };
|
|
1428
1487
|
const ratio = /^(\d+(?:\.\d+)?)\s*[:/]\s*(\d+(?:\.\d+)?)$/.exec(size);
|
|
1429
|
-
if (api
|
|
1488
|
+
if (api !== "openai-images" && ratio && Number(ratio[1]) > 0 && Number(ratio[2]) > 0 && Number.isFinite(Number(ratio[1]) / Number(ratio[2])))
|
|
1430
1489
|
return { aspect_ratio: `${Number(ratio[1])}:${Number(ratio[2])}` };
|
|
1431
1490
|
throw usage(api === "openai-images" ? "This API format requires WIDTHxHEIGHT or a preset; arbitrary aspect-only sizes are unsupported." : "Use WIDTHxHEIGHT, an aspect ratio, preset, or auto.");
|
|
1432
1491
|
}
|
|
1492
|
+
function comfySize(value) {
|
|
1493
|
+
const fields = imageSize(value, "comfyui");
|
|
1494
|
+
let width, height;
|
|
1495
|
+
if (fields.aspect_ratio) {
|
|
1496
|
+
const [w, h] = fields.aspect_ratio.split(":").map(Number);
|
|
1497
|
+
const scale = Math.sqrt(1024 * 1024 / (w * h));
|
|
1498
|
+
[width, height] = [w * scale, h * scale];
|
|
1499
|
+
} else if (fields.size)
|
|
1500
|
+
[width, height] = fields.size.split("x").map(Number);
|
|
1501
|
+
else
|
|
1502
|
+
return void 0;
|
|
1503
|
+
width = Math.round(width / 16) * 16;
|
|
1504
|
+
height = Math.round(height / 16) * 16;
|
|
1505
|
+
if (Math.min(width, height) < 256 || Math.max(width, height) > 2048)
|
|
1506
|
+
throw usage("ComfyUI image edges must be from 256 to 2048 pixels.");
|
|
1507
|
+
return { width, height };
|
|
1508
|
+
}
|
|
1433
1509
|
|
|
1434
1510
|
// ../../packages/providers-configurable/dist/http.js
|
|
1435
1511
|
var record2 = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
@@ -1560,6 +1636,625 @@ function openAIRequest(model, request, images, mask) {
|
|
|
1560
1636
|
return { path: "images/edits", body, json: false };
|
|
1561
1637
|
}
|
|
1562
1638
|
|
|
1639
|
+
// ../../packages/providers-configurable/dist/comfyui.js
|
|
1640
|
+
import { randomInt, randomUUID as randomUUID2 } from "node:crypto";
|
|
1641
|
+
import sharp3 from "sharp";
|
|
1642
|
+
|
|
1643
|
+
// ../../packages/providers-configurable/dist/comfyui-workflow.js
|
|
1644
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1645
|
+
var TOKEN = /\{\{(\w+)\}\}/g;
|
|
1646
|
+
function usedTokens(graph) {
|
|
1647
|
+
const used = /* @__PURE__ */ new Set();
|
|
1648
|
+
for (const node of Object.values(graph))
|
|
1649
|
+
for (const value of Object.values(node.inputs))
|
|
1650
|
+
if (typeof value === "string")
|
|
1651
|
+
for (const match of value.matchAll(TOKEN))
|
|
1652
|
+
used.add(match[1]);
|
|
1653
|
+
return used;
|
|
1654
|
+
}
|
|
1655
|
+
async function loadWorkflow(path) {
|
|
1656
|
+
const invalid = (detail) => new ProviderError({
|
|
1657
|
+
kind: "configuration",
|
|
1658
|
+
message: `ComfyUI workflow ${path} ${detail}`
|
|
1659
|
+
});
|
|
1660
|
+
let parsed;
|
|
1661
|
+
try {
|
|
1662
|
+
parsed = JSON.parse(await readFile2(path, "utf8"));
|
|
1663
|
+
} catch {
|
|
1664
|
+
throw invalid("could not be read as JSON.");
|
|
1665
|
+
}
|
|
1666
|
+
const nodes = Object.values(record2(parsed));
|
|
1667
|
+
if (!nodes.length || nodes.some((node) => typeof record2(node).class_type !== "string" || typeof record2(node).inputs !== "object"))
|
|
1668
|
+
throw invalid("is not in API format. Export it from ComfyUI as API JSON.");
|
|
1669
|
+
const graph = parsed;
|
|
1670
|
+
const used = usedTokens(graph);
|
|
1671
|
+
const unknown = [...used].filter((t) => !/^(prompt|seed|width|height|image[1-9]\d*)$/.test(t));
|
|
1672
|
+
if (unknown.length)
|
|
1673
|
+
throw invalid(`uses unknown tokens: ${unknown.join(", ")}.`);
|
|
1674
|
+
if (!used.has("prompt") || !used.has("seed"))
|
|
1675
|
+
throw invalid("must use {{prompt}} and {{seed}}.");
|
|
1676
|
+
if (used.has("width") !== used.has("height"))
|
|
1677
|
+
throw invalid("must use {{width}} and {{height}} together.");
|
|
1678
|
+
const images = [...used].filter((t) => t.startsWith("image")).length;
|
|
1679
|
+
for (let i = 1; i <= images; i++)
|
|
1680
|
+
if (!used.has(`image${i}`))
|
|
1681
|
+
throw invalid(`must number its image tokens from {{image1}} in order.`);
|
|
1682
|
+
return { graph, images, sized: used.has("width") };
|
|
1683
|
+
}
|
|
1684
|
+
function fillWorkflow({ graph }, { prompt, seed, size, images }) {
|
|
1685
|
+
const values = {
|
|
1686
|
+
prompt,
|
|
1687
|
+
seed,
|
|
1688
|
+
...size ?? {},
|
|
1689
|
+
...Object.fromEntries(images.map((image, i) => [`image${i + 1}`, image]))
|
|
1690
|
+
};
|
|
1691
|
+
const fill = (value) => {
|
|
1692
|
+
if (typeof value !== "string")
|
|
1693
|
+
return value;
|
|
1694
|
+
const whole = /^\{\{(\w+)\}\}$/.exec(value);
|
|
1695
|
+
if (whole)
|
|
1696
|
+
return values[whole[1]];
|
|
1697
|
+
return value.replace(TOKEN, (_, token) => String(values[token]));
|
|
1698
|
+
};
|
|
1699
|
+
return Object.fromEntries(Object.entries(graph).map(([id, node]) => [
|
|
1700
|
+
id,
|
|
1701
|
+
{
|
|
1702
|
+
...node,
|
|
1703
|
+
inputs: Object.fromEntries(Object.entries(node.inputs).map(([key, value]) => [key, fill(value)]))
|
|
1704
|
+
}
|
|
1705
|
+
]));
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// ../../packages/providers-configurable/dist/validation.js
|
|
1709
|
+
function validateImageRequest(request, images) {
|
|
1710
|
+
if ((request.referenceImages?.length ?? 0) > 4)
|
|
1711
|
+
throw usage("At most four reference images are allowed.");
|
|
1712
|
+
const count = request.count ?? 1;
|
|
1713
|
+
if (!Number.isInteger(count) || count < 1 || count > 4)
|
|
1714
|
+
throw usage("Image count must be from 1 to 4.");
|
|
1715
|
+
for (const image of images)
|
|
1716
|
+
if (!["image/png", "image/jpeg", "image/webp"].includes(image.contentType))
|
|
1717
|
+
throw usage("Input images must be PNG, JPEG, or WebP.");
|
|
1718
|
+
const format = (request.format ?? "png").trim().toLowerCase().replace(/^jpg$/, "jpeg");
|
|
1719
|
+
if (!["png", "jpeg", "webp"].includes(format))
|
|
1720
|
+
throw usage("Output format must be png, jpeg, or webp.");
|
|
1721
|
+
return { count, format };
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// ../../packages/providers-configurable/dist/flux3-klein-prompts.js
|
|
1725
|
+
var FLUX3_KLEIN_GENERATE_PROMPT = `You are the prompt upsampler of a text-to-image model. Expand the user's short request into ONE detailed, self-contained caption: the exact text the image model receives. Output only the caption, with no greetings, explanations, refusals, questions, Markdown, headings, JSON, coordinates, ids or angle-bracket tokens, and no quotes around it. If the input is not clearly an image request, caption a visualization of its concept. The request may begin with a "TARGET CANVAS: W:H" line: compose for that canvas (wide spreads content sideways, tall stacks it) and never mention the ratio or the canvas.
|
|
1726
|
+
|
|
1727
|
+
Fidelity and planning: keep every explicit subject, attribute, count, spatial constraint, style cue and piece of wording; never contradict, substitute, drop or narrow the request, and keep an abstract concept abstract. Decide everything left open as ONE concrete, consistent plan; never offer alternatives or hedge. Never re-express a stated constraint through another mechanism; add camera optics (depth of field, bokeh, blur) only when asked for or implied. Without a stated style, plan an actual photograph and open with the medium convention ("Full-color outdoor photograph, ...", "Full-color indoor photograph, ...", "Studio photograph, ...", "Close-up photograph, ..."); keep "Photorealistic" for renders, and turn "photorealistic" or "realistic" requests for photos into the photograph convention. Give a photograph a few ordinary, non-ideal specifics drawn from the scene's own objects instead of the stock-photo ideal. Do not invent names, brands, logos or celebrities; keep user-named ones. A long request keeps its content but not its wording: drop appraisal and sales vocabulary, turn moods into observable physical facts, and add only new concrete content with positions. Time of day is lighting, not rendered text.
|
|
1728
|
+
|
|
1729
|
+
Caption format: one continuous prose block in the generative voice, never "this image shows", "we can see" or "is visible". Lead with medium and style, then scene, lighting and camera, then the subjects with concrete colors, materials and state. Every person in focus gets an observable profile: ethnicity, gender, age range, skin tone ("fair skin", "brown skin"), hair and clothing. Make the layout reconstructable from words: frame-relative positions (frame-left, frame-right, upper, lower, foreground, midground, background), sentence-initial anchors ("On the left, ...", "In the background, ..."), relations (holds, faces, behind) and a consistent order; describe up to about ten instances one by one, beyond that give the exact count and arrangement. Out-of-focus or distant planes are shapes, colors and silhouettes, never identities or readable text, and the background takes about a sixth of the caption. Be definite: no hedging adverbs, no quality or emotion judgments, no closing sentence about mood; state negations as what is present. Rendered text: transcribe every wording verbatim inside double quotes in its original language and script, keep the line breaks of multi-line text, and give its typography, color and placement; author concrete, domain-correct wording when the request needs text it does not supply, and name canonical structures (calendar, keyboard, periodic table) exactly with their full layout. All prose is English; only quoted text keeps its language. Typical length is 100 to 350 words, more for complex scenes.
|
|
1730
|
+
|
|
1731
|
+
Output only the caption.`;
|
|
1732
|
+
var FLUX3_KLEIN_EDIT_PROMPT = `You are the prompt upsampler of an image-editing model. You see the reference image(s), attached in order (the first image, the second image, ...), and the user's short request. Expand it into ONE detailed, self-contained instruction: the caption the editing model receives together with the same images. Output only the instruction as one continuous English prose block, with no greetings, explanations, Markdown, JSON, coordinates, ids or angle-bracket tokens. Read a vague or conversational request as the most plausible edit of the supplied images.
|
|
1733
|
+
|
|
1734
|
+
Pick the voice by task shape. When one reference stays and gets bounded changes, write an imperative editing instruction on it ("Remove the ...", "Change the ... to ...", "Zoom in on ..."); a whole-image transformation opens with its scope ("Convert the entire image into ...", "Apply a ... filter over the entire scene", "Horizontally flip the entire image so that ...", "Shift the camera viewpoint to ...", then where the major elements should now be). When the references are drawn into a newly composed frame (combining subjects, a new scene, a garment, object or style donor), write a generation request ("Create a photograph of ...", "Create an anime-style illustration of ...", "Generate a new image featuring ...") that says what to take from each image, by ordinal, and what is newly generated.
|
|
1735
|
+
|
|
1736
|
+
Keep every explicit edit, constraint, count, position, style cue and piece of wording, and decide what is left open as one concrete plan. Ground everything in what is actually visible: identify each referent unambiguously by its real appearance, never mention content that is not there, and use every image the user attached. State every end state concretely (which color, how large, which pose, which material) and give new content enough appearance to be generated. Cover the knock-on effects of the change (shadows, reflections, occlusion, perspective, scale, lighting) and say what fills a removed region. A zoom-in names what stays in frame and which neighbors are cropped out; an outpainting continues the scene in the same medium. Describe the change, not the whole scene, and close an editing instruction with one compact clause naming what stays unchanged; transferred people keep their identity, face and accessories unless asked otherwise. Transcribe on-image text verbatim in its original language inside double quotes, with line breaks as \\n, and match the original font, size and color when replacing it. Name recognizable public figures, brands and landmarks you can see. Directions are frame-relative; use no evaluative or quality words. Typical length is 50 to 150 words.
|
|
1737
|
+
|
|
1738
|
+
Output only the instruction.`;
|
|
1739
|
+
|
|
1740
|
+
// ../../packages/providers-configurable/dist/flux3-klein.js
|
|
1741
|
+
var FLUX3_KLEIN_CHECKPOINT = "flux3-image-klein-int8-convrot";
|
|
1742
|
+
var FLUX3_KLEIN_MODELS = [
|
|
1743
|
+
{
|
|
1744
|
+
id: "flux3-klein",
|
|
1745
|
+
name: "FLUX 3 Klein, prompt expanded by its upsampler",
|
|
1746
|
+
upsample: true
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
id: "flux3-klein-raw",
|
|
1750
|
+
name: "FLUX 3 Klein, prompt used verbatim as the full caption",
|
|
1751
|
+
upsample: false
|
|
1752
|
+
}
|
|
1753
|
+
];
|
|
1754
|
+
var UPSAMPLER = "qwen3.5_9b_flux3_prompt_upsampler_w4a8.safetensors";
|
|
1755
|
+
var ENCODER = "qwen3vl_4b_int8_convrot.safetensors";
|
|
1756
|
+
var VAE = "flux2-vae.safetensors";
|
|
1757
|
+
var WEIGHTS = [
|
|
1758
|
+
{
|
|
1759
|
+
node: "UNETLoader",
|
|
1760
|
+
input: "unet_name",
|
|
1761
|
+
folder: "diffusion_models",
|
|
1762
|
+
file: `${FLUX3_KLEIN_CHECKPOINT}.safetensors`
|
|
1763
|
+
},
|
|
1764
|
+
{
|
|
1765
|
+
node: "CLIPLoader",
|
|
1766
|
+
input: "clip_name",
|
|
1767
|
+
folder: "text_encoders",
|
|
1768
|
+
file: ENCODER
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
node: "VAELoader",
|
|
1772
|
+
input: "vae_name",
|
|
1773
|
+
folder: "vae",
|
|
1774
|
+
file: VAE
|
|
1775
|
+
}
|
|
1776
|
+
];
|
|
1777
|
+
var flux3KleinWeights = (model) => model.upsample ? [
|
|
1778
|
+
...WEIGHTS,
|
|
1779
|
+
{
|
|
1780
|
+
node: "CLIPLoader",
|
|
1781
|
+
input: "clip_name",
|
|
1782
|
+
folder: "text_encoders",
|
|
1783
|
+
file: UPSAMPLER
|
|
1784
|
+
}
|
|
1785
|
+
] : WEIGHTS;
|
|
1786
|
+
function flux3KleinModel(id) {
|
|
1787
|
+
const model = FLUX3_KLEIN_MODELS.find((m) => m.id === id);
|
|
1788
|
+
if (!model)
|
|
1789
|
+
throw usage(`Built-in ComfyUI models are ${FLUX3_KLEIN_MODELS.map((m) => m.id).join(" and ")}. Add --workflow to the profile for any other model.`);
|
|
1790
|
+
return model;
|
|
1791
|
+
}
|
|
1792
|
+
var STEPS = { low: 4, medium: 8, high: 16, auto: 16 };
|
|
1793
|
+
function flux3KleinSteps(quality) {
|
|
1794
|
+
const steps = STEPS[(quality ?? "auto").trim().toLowerCase()];
|
|
1795
|
+
if (!steps)
|
|
1796
|
+
throw usage("Quality must be low, medium, high, or auto.");
|
|
1797
|
+
return steps;
|
|
1798
|
+
}
|
|
1799
|
+
function upsamplerRequest(job) {
|
|
1800
|
+
if (job.images.length || !job.size || job.size.width === job.size.height)
|
|
1801
|
+
return job.prompt;
|
|
1802
|
+
const divisor = (a, b) => b ? divisor(b, a % b) : a;
|
|
1803
|
+
const shared = divisor(job.size.width, job.size.height);
|
|
1804
|
+
return `TARGET CANVAS: ${job.size.width / shared}:${job.size.height / shared}
|
|
1805
|
+
${job.prompt}`;
|
|
1806
|
+
}
|
|
1807
|
+
function flux3KleinWorkflow(job) {
|
|
1808
|
+
const editing = job.images.length > 0;
|
|
1809
|
+
const nodes = {
|
|
1810
|
+
model: {
|
|
1811
|
+
class_type: "UNETLoader",
|
|
1812
|
+
inputs: {
|
|
1813
|
+
unet_name: `${FLUX3_KLEIN_CHECKPOINT}.safetensors`,
|
|
1814
|
+
weight_dtype: "default"
|
|
1815
|
+
}
|
|
1816
|
+
},
|
|
1817
|
+
encoder: {
|
|
1818
|
+
class_type: "CLIPLoader",
|
|
1819
|
+
inputs: { clip_name: ENCODER, type: "flux3", device: "default" }
|
|
1820
|
+
},
|
|
1821
|
+
vae: { class_type: "VAELoader", inputs: { vae_name: VAE } }
|
|
1822
|
+
};
|
|
1823
|
+
job.images.forEach((image, i) => {
|
|
1824
|
+
nodes[`image${i}`] = { class_type: "LoadImage", inputs: { image } };
|
|
1825
|
+
});
|
|
1826
|
+
if (job.upsample) {
|
|
1827
|
+
nodes.upsampler_clip = {
|
|
1828
|
+
class_type: "CLIPLoader",
|
|
1829
|
+
inputs: { clip_name: UPSAMPLER, type: "flux3", device: "default" }
|
|
1830
|
+
};
|
|
1831
|
+
nodes.upsampler = {
|
|
1832
|
+
class_type: "TextGenerateV2",
|
|
1833
|
+
inputs: {
|
|
1834
|
+
clip: ["upsampler_clip", 0],
|
|
1835
|
+
prompt: upsamplerRequest(job),
|
|
1836
|
+
system_prompt: editing ? FLUX3_KLEIN_EDIT_PROMPT : FLUX3_KLEIN_GENERATE_PROMPT,
|
|
1837
|
+
max_length: 3584,
|
|
1838
|
+
sampling_mode: "on",
|
|
1839
|
+
"sampling_mode.temperature": 1,
|
|
1840
|
+
"sampling_mode.top_k": 1,
|
|
1841
|
+
"sampling_mode.top_p": 0.95,
|
|
1842
|
+
"sampling_mode.min_p": 0,
|
|
1843
|
+
"sampling_mode.repetition_penalty": 1.05,
|
|
1844
|
+
"sampling_mode.presence_penalty": editing ? 0 : 1.5,
|
|
1845
|
+
"sampling_mode.seed": 0,
|
|
1846
|
+
thinking: false,
|
|
1847
|
+
use_default_template: true,
|
|
1848
|
+
mtp: "off",
|
|
1849
|
+
...Object.fromEntries(job.images.map((_, i) => [`images.image${i + 1}`, [`image${i}`, 0]]))
|
|
1850
|
+
}
|
|
1851
|
+
};
|
|
1852
|
+
nodes.caption = {
|
|
1853
|
+
class_type: "PreviewAny",
|
|
1854
|
+
inputs: { source: ["upsampler", 0] }
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
nodes.positive = {
|
|
1858
|
+
class_type: "CLIPTextEncode",
|
|
1859
|
+
inputs: {
|
|
1860
|
+
clip: ["encoder", 0],
|
|
1861
|
+
text: job.upsample ? ["upsampler", 0] : job.prompt
|
|
1862
|
+
}
|
|
1863
|
+
};
|
|
1864
|
+
nodes.negative = editing ? {
|
|
1865
|
+
class_type: "ConditioningZeroOut",
|
|
1866
|
+
inputs: { conditioning: ["positive", 0] }
|
|
1867
|
+
} : {
|
|
1868
|
+
class_type: "CLIPTextEncode",
|
|
1869
|
+
inputs: { clip: ["encoder", 0], text: "" }
|
|
1870
|
+
};
|
|
1871
|
+
if (editing)
|
|
1872
|
+
nodes.references = {
|
|
1873
|
+
class_type: "ReferenceMultiLatent",
|
|
1874
|
+
inputs: {
|
|
1875
|
+
positive: ["positive", 0],
|
|
1876
|
+
negative: ["negative", 0],
|
|
1877
|
+
vae: ["vae", 0],
|
|
1878
|
+
resolution: "resize",
|
|
1879
|
+
"resolution.megapixels": 1,
|
|
1880
|
+
...Object.fromEntries(job.images.map((_, i) => [`images.image${i}`, [`image${i}`, 0]]))
|
|
1881
|
+
}
|
|
1882
|
+
};
|
|
1883
|
+
if (!editing || job.size) {
|
|
1884
|
+
const { width, height } = job.size ?? { width: 1024, height: 1024 };
|
|
1885
|
+
nodes.latent = {
|
|
1886
|
+
class_type: "EmptyFlux2LatentImage",
|
|
1887
|
+
inputs: { width, height, batch_size: 1 }
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
nodes.sampler = {
|
|
1891
|
+
class_type: "KSampler",
|
|
1892
|
+
inputs: {
|
|
1893
|
+
model: ["model", 0],
|
|
1894
|
+
seed: job.seed,
|
|
1895
|
+
steps: job.steps,
|
|
1896
|
+
cfg: 1,
|
|
1897
|
+
sampler_name: "euler",
|
|
1898
|
+
scheduler: "simple",
|
|
1899
|
+
denoise: 1,
|
|
1900
|
+
positive: editing ? ["references", 0] : ["positive", 0],
|
|
1901
|
+
negative: editing ? ["references", 1] : ["negative", 0],
|
|
1902
|
+
// Without a requested size, edits keep the first reference's shape.
|
|
1903
|
+
latent_image: nodes.latent ? ["latent", 0] : ["references", 2]
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
nodes.decode = {
|
|
1907
|
+
class_type: "VAEDecode",
|
|
1908
|
+
inputs: { samples: ["sampler", 0], vae: ["vae", 0] }
|
|
1909
|
+
};
|
|
1910
|
+
nodes.output = {
|
|
1911
|
+
class_type: "PreviewImage",
|
|
1912
|
+
inputs: { images: ["decode", 0] }
|
|
1913
|
+
};
|
|
1914
|
+
return nodes;
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// ../../packages/providers-configurable/dist/comfyui.js
|
|
1918
|
+
var EXTENSIONS = {
|
|
1919
|
+
"image/png": "png",
|
|
1920
|
+
"image/jpeg": "jpg",
|
|
1921
|
+
"image/webp": "webp"
|
|
1922
|
+
};
|
|
1923
|
+
function comboOptions(spec) {
|
|
1924
|
+
if (!Array.isArray(spec))
|
|
1925
|
+
return void 0;
|
|
1926
|
+
const choices = spec[0] === "COMBO" ? record2(spec[1]).options : spec[0];
|
|
1927
|
+
return Array.isArray(choices) ? choices : void 0;
|
|
1928
|
+
}
|
|
1929
|
+
var comboChoices = (spec) => comboOptions(spec) ?? [];
|
|
1930
|
+
async function sourceShape(image) {
|
|
1931
|
+
try {
|
|
1932
|
+
const { width, height } = await sharp3(image.data).metadata();
|
|
1933
|
+
return comfySize(`${width}:${height}`);
|
|
1934
|
+
} catch (e) {
|
|
1935
|
+
if (e instanceof ProviderError)
|
|
1936
|
+
throw e;
|
|
1937
|
+
throw usage("Could not read the source image's dimensions.");
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
function executionFailure(messages) {
|
|
1941
|
+
const failure2 = Array.isArray(messages) ? messages.find((m) => Array.isArray(m) && m[0] === "execution_error") : void 0;
|
|
1942
|
+
const detail = record2(Array.isArray(failure2) ? failure2[1] : void 0);
|
|
1943
|
+
const node = typeof detail.node_type === "string" ? detail.node_type : "the workflow";
|
|
1944
|
+
const reason = typeof detail.exception_message === "string" ? detail.exception_message.replace(/\s+/g, " ").trim().slice(0, 300) : "";
|
|
1945
|
+
const memory = /out ?of ?memory/i.test(`${detail.exception_type} ${reason}`);
|
|
1946
|
+
return new ProviderError({
|
|
1947
|
+
kind: "server",
|
|
1948
|
+
message: `ComfyUI failed in ${node}${reason ? `: ${reason}` : "."}${memory ? " Close other GPU applications or request a smaller --size." : ""}`
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
async function encoded(data, format) {
|
|
1952
|
+
try {
|
|
1953
|
+
const result = await sharp3(data).toFormat(format).toBuffer({ resolveWithObject: true });
|
|
1954
|
+
return {
|
|
1955
|
+
data: result.data,
|
|
1956
|
+
width: result.info.width,
|
|
1957
|
+
height: result.info.height
|
|
1958
|
+
};
|
|
1959
|
+
} catch {
|
|
1960
|
+
throw new ProviderError({
|
|
1961
|
+
kind: "malformed",
|
|
1962
|
+
message: "ComfyUI returned an invalid raster image."
|
|
1963
|
+
});
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
var ComfyUIImageProvider = class {
|
|
1967
|
+
name;
|
|
1968
|
+
profile;
|
|
1969
|
+
baseUrl;
|
|
1970
|
+
defaultModel;
|
|
1971
|
+
http;
|
|
1972
|
+
pollIntervalMs;
|
|
1973
|
+
constructor(profile, options = {}) {
|
|
1974
|
+
const valid = validateProfile(profile);
|
|
1975
|
+
this.profile = valid;
|
|
1976
|
+
this.name = valid.name;
|
|
1977
|
+
this.baseUrl = valid.baseUrl;
|
|
1978
|
+
this.defaultModel = valid.model;
|
|
1979
|
+
this.http = new EndpointHttp(options.fetchImpl, options.timeoutSeconds ?? 600);
|
|
1980
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 500;
|
|
1981
|
+
}
|
|
1982
|
+
sizePolicy = {
|
|
1983
|
+
resolveSize: (size) => {
|
|
1984
|
+
const resolved = comfySize(size);
|
|
1985
|
+
if (!resolved)
|
|
1986
|
+
return { size: "auto" };
|
|
1987
|
+
const { width, height } = resolved;
|
|
1988
|
+
return {
|
|
1989
|
+
width,
|
|
1990
|
+
height,
|
|
1991
|
+
pixels: width * height,
|
|
1992
|
+
size: `${width}x${height}`
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
generate(request) {
|
|
1997
|
+
return this.run(request, request.referenceImages ?? []);
|
|
1998
|
+
}
|
|
1999
|
+
async edit(request) {
|
|
2000
|
+
if (request.mask)
|
|
2001
|
+
throw usage("ComfyUI profiles have no mask input. Describe the edit region in the prompt.");
|
|
2002
|
+
return this.run(request, [
|
|
2003
|
+
request.sourceImage,
|
|
2004
|
+
...request.referenceImages ?? []
|
|
2005
|
+
]);
|
|
2006
|
+
}
|
|
2007
|
+
get usesWorkflowFiles() {
|
|
2008
|
+
return Boolean(this.profile.workflow || this.profile.editWorkflow);
|
|
2009
|
+
}
|
|
2010
|
+
async models() {
|
|
2011
|
+
if (this.usesWorkflowFiles) {
|
|
2012
|
+
const { workflow, editWorkflow } = this.profile;
|
|
2013
|
+
const files = await Promise.all([workflow, editWorkflow].flatMap((f) => f ? [loadWorkflow(f)] : []));
|
|
2014
|
+
return this.reachable(() => this.http.run(async (http) => {
|
|
2015
|
+
for (const file of files)
|
|
2016
|
+
await this.requireInputs(http, file.graph);
|
|
2017
|
+
return [
|
|
2018
|
+
{
|
|
2019
|
+
id: this.defaultModel,
|
|
2020
|
+
name: `${this.defaultModel}, from this profile's workflow files`,
|
|
2021
|
+
capabilities: "reported",
|
|
2022
|
+
inputModalities: editWorkflow ? ["text", "image"] : ["text"],
|
|
2023
|
+
outputModalities: ["image"]
|
|
2024
|
+
}
|
|
2025
|
+
];
|
|
2026
|
+
}));
|
|
2027
|
+
}
|
|
2028
|
+
flux3KleinModel(this.defaultModel);
|
|
2029
|
+
return this.reachable(() => this.http.run(async (http) => {
|
|
2030
|
+
const models = [];
|
|
2031
|
+
for (const { id, name, upsample } of FLUX3_KLEIN_MODELS)
|
|
2032
|
+
if (!(await this.missingWeights(http, { id, name, upsample })).length)
|
|
2033
|
+
models.push({
|
|
2034
|
+
id,
|
|
2035
|
+
name,
|
|
2036
|
+
capabilities: "reported",
|
|
2037
|
+
inputModalities: ["text", "image"],
|
|
2038
|
+
outputModalities: ["image"]
|
|
2039
|
+
});
|
|
2040
|
+
return models;
|
|
2041
|
+
}));
|
|
2042
|
+
}
|
|
2043
|
+
async run(request, images) {
|
|
2044
|
+
const { count, format } = validateImageRequest(request, images);
|
|
2045
|
+
const plan = this.usesWorkflowFiles ? await this.workflowPlan(request, images) : this.kleinPlan(request);
|
|
2046
|
+
return this.reachable(async () => {
|
|
2047
|
+
await this.http.run((http) => plan.preflight(http));
|
|
2048
|
+
const uploaded = [];
|
|
2049
|
+
for (const image of images)
|
|
2050
|
+
uploaded.push(await this.upload(image));
|
|
2051
|
+
const results = [];
|
|
2052
|
+
for (let i = 0; i < count; i++)
|
|
2053
|
+
results.push(await this.render(plan, uploaded, randomInt(281474976710655), format));
|
|
2054
|
+
return {
|
|
2055
|
+
provider: this.name,
|
|
2056
|
+
requestedModel: plan.model,
|
|
2057
|
+
images: results
|
|
2058
|
+
};
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
kleinPlan(request) {
|
|
2062
|
+
const model = flux3KleinModel((request.model ?? this.defaultModel).trim());
|
|
2063
|
+
const steps = flux3KleinSteps(request.quality);
|
|
2064
|
+
const size = comfySize(request.size);
|
|
2065
|
+
return {
|
|
2066
|
+
model: model.id,
|
|
2067
|
+
preflight: async (http) => {
|
|
2068
|
+
const missing = await this.missingWeights(http, model);
|
|
2069
|
+
if (missing.length)
|
|
2070
|
+
throw new ProviderError({
|
|
2071
|
+
kind: "configuration",
|
|
2072
|
+
message: `ComfyUI is missing FLUX 3 Klein weights: ${missing.join(", ")}. Download them from black-forest-labs/flux-3-image-klein-comfy into its models folder.`
|
|
2073
|
+
});
|
|
2074
|
+
},
|
|
2075
|
+
graph: (images, seed) => flux3KleinWorkflow({
|
|
2076
|
+
prompt: request.prompt,
|
|
2077
|
+
upsample: model.upsample,
|
|
2078
|
+
images,
|
|
2079
|
+
size,
|
|
2080
|
+
steps,
|
|
2081
|
+
seed
|
|
2082
|
+
}),
|
|
2083
|
+
described: (outputs, seed) => {
|
|
2084
|
+
const caption = record2(outputs.caption).text;
|
|
2085
|
+
return {
|
|
2086
|
+
prompt: model.upsample && Array.isArray(caption) && typeof caption[0] === "string" ? caption[0] : request.prompt,
|
|
2087
|
+
servedModel: FLUX3_KLEIN_CHECKPOINT,
|
|
2088
|
+
usage: { served: { steps, seed, upsampled: model.upsample } }
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
async workflowPlan(request, images) {
|
|
2094
|
+
const model = this.defaultModel;
|
|
2095
|
+
if (request.model && request.model.trim() !== model)
|
|
2096
|
+
throw usage(`Profile ${this.name} renders ${model} from its workflow files. Update the profile to change models.`);
|
|
2097
|
+
if ((request.quality ?? "auto").trim().toLowerCase() !== "auto")
|
|
2098
|
+
throw usage("Workflow files set their own sampling, so only --quality auto applies.");
|
|
2099
|
+
const path = images.length ? this.profile.editWorkflow : this.profile.workflow;
|
|
2100
|
+
if (!path)
|
|
2101
|
+
throw usage(images.length ? `Profile ${this.name} has no --edit-workflow, so it cannot take input images.` : `Profile ${this.name} has no --workflow, so it needs a source image.`);
|
|
2102
|
+
const workflow = await loadWorkflow(path);
|
|
2103
|
+
if (workflow.images !== images.length)
|
|
2104
|
+
throw usage(`This workflow takes ${workflow.images} input images, but the request has ${images.length}.`);
|
|
2105
|
+
const requested = comfySize(request.size);
|
|
2106
|
+
if (requested && !workflow.sized)
|
|
2107
|
+
throw usage("This workflow has fixed dimensions, so only --size auto applies.");
|
|
2108
|
+
const size = !workflow.sized ? void 0 : requested ?? (images[0] ? await sourceShape(images[0]) : { width: 1024, height: 1024 });
|
|
2109
|
+
return {
|
|
2110
|
+
model,
|
|
2111
|
+
preflight: (http) => this.requireInputs(http, workflow.graph),
|
|
2112
|
+
graph: (uploaded, seed) => fillWorkflow(workflow, {
|
|
2113
|
+
prompt: request.prompt,
|
|
2114
|
+
seed,
|
|
2115
|
+
size,
|
|
2116
|
+
images: uploaded
|
|
2117
|
+
}),
|
|
2118
|
+
described: (_, seed) => ({
|
|
2119
|
+
prompt: request.prompt,
|
|
2120
|
+
servedModel: model,
|
|
2121
|
+
usage: { served: { seed } }
|
|
2122
|
+
})
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
async reachable(action) {
|
|
2126
|
+
try {
|
|
2127
|
+
return await action();
|
|
2128
|
+
} catch (e) {
|
|
2129
|
+
if (e instanceof ProviderError && e.kind === "transport") {
|
|
2130
|
+
const start = this.profile.startCommand;
|
|
2131
|
+
throw new ProviderError({
|
|
2132
|
+
kind: "transport",
|
|
2133
|
+
message: `ComfyUI is not reachable at ${this.baseUrl}. Start it${start ? ` with \`${start}\`` : ""} and retry.`
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
throw e;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
// Names what a workflow file needs instead of queueing a doomed job.
|
|
2140
|
+
async requireInputs(http, graph) {
|
|
2141
|
+
const specs = /* @__PURE__ */ new Map();
|
|
2142
|
+
const missing = /* @__PURE__ */ new Set();
|
|
2143
|
+
for (const node of Object.values(graph)) {
|
|
2144
|
+
if (!specs.has(node.class_type)) {
|
|
2145
|
+
const info = await http.json(`${this.baseUrl}/object_info/${encodeURIComponent(node.class_type)}`);
|
|
2146
|
+
specs.set(node.class_type, record2(info[node.class_type]));
|
|
2147
|
+
}
|
|
2148
|
+
const spec = record2(specs.get(node.class_type));
|
|
2149
|
+
if (!Object.keys(spec).length) {
|
|
2150
|
+
missing.add(`node type ${node.class_type}`);
|
|
2151
|
+
continue;
|
|
2152
|
+
}
|
|
2153
|
+
const inputs = {
|
|
2154
|
+
...record2(record2(spec.input).required),
|
|
2155
|
+
...record2(record2(spec.input).optional)
|
|
2156
|
+
};
|
|
2157
|
+
for (const [name, value] of Object.entries(node.inputs)) {
|
|
2158
|
+
if (typeof value !== "string" || value.includes("{{"))
|
|
2159
|
+
continue;
|
|
2160
|
+
if (comboOptions(inputs[name])?.includes(value) === false)
|
|
2161
|
+
missing.add(`${value} for ${node.class_type}.${name}`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
if (missing.size)
|
|
2165
|
+
throw new ProviderError({
|
|
2166
|
+
kind: "configuration",
|
|
2167
|
+
message: `ComfyUI is missing what this workflow needs: ${[...missing].join(", ")}. Install it under ComfyUI, restart the server, and retry.`
|
|
2168
|
+
});
|
|
2169
|
+
}
|
|
2170
|
+
async missingWeights(http, model) {
|
|
2171
|
+
const choices = /* @__PURE__ */ new Map();
|
|
2172
|
+
const missing = [];
|
|
2173
|
+
for (const weight of flux3KleinWeights(model)) {
|
|
2174
|
+
if (!choices.has(weight.node)) {
|
|
2175
|
+
const info = await http.json(`${this.baseUrl}/object_info/${weight.node}`);
|
|
2176
|
+
const inputs = record2(record2(record2(info[weight.node]).input).required);
|
|
2177
|
+
choices.set(weight.node, comboChoices(inputs[weight.input]));
|
|
2178
|
+
}
|
|
2179
|
+
if (!choices.get(weight.node)?.includes(weight.file))
|
|
2180
|
+
missing.push(`${weight.folder}/${weight.file}`);
|
|
2181
|
+
}
|
|
2182
|
+
return missing;
|
|
2183
|
+
}
|
|
2184
|
+
// The temp folder keeps inputs out of ComfyUI's library; it clears on restart.
|
|
2185
|
+
upload(image) {
|
|
2186
|
+
return this.http.run(async (http) => {
|
|
2187
|
+
const form = new FormData();
|
|
2188
|
+
form.set("image", new Blob([new Uint8Array(image.data)], { type: image.contentType }), `imagegen-${randomUUID2()}.${EXTENSIONS[image.contentType]}`);
|
|
2189
|
+
form.set("type", "temp");
|
|
2190
|
+
const body = await http.json(`${this.baseUrl}/upload/image`, {
|
|
2191
|
+
method: "POST",
|
|
2192
|
+
body: form
|
|
2193
|
+
});
|
|
2194
|
+
if (typeof body.name !== "string")
|
|
2195
|
+
throw new ProviderError({
|
|
2196
|
+
kind: "malformed",
|
|
2197
|
+
message: "ComfyUI did not return an uploaded image name."
|
|
2198
|
+
});
|
|
2199
|
+
return `${body.name} [temp]`;
|
|
2200
|
+
});
|
|
2201
|
+
}
|
|
2202
|
+
render(plan, images, seed, format) {
|
|
2203
|
+
return this.http.run(async (http) => {
|
|
2204
|
+
const queued = await http.json(`${this.baseUrl}/prompt`, {
|
|
2205
|
+
method: "POST",
|
|
2206
|
+
headers: { "content-type": "application/json" },
|
|
2207
|
+
body: JSON.stringify({ prompt: plan.graph(images, seed) })
|
|
2208
|
+
}).catch((e) => {
|
|
2209
|
+
if (e instanceof ProviderError && e.statusCode === 400)
|
|
2210
|
+
throw new ProviderError({
|
|
2211
|
+
kind: "server",
|
|
2212
|
+
statusCode: 400,
|
|
2213
|
+
message: `ComfyUI rejected the ${plan.model} workflow. Check its console for the failing node, and update ComfyUI if a node is unknown.`
|
|
2214
|
+
});
|
|
2215
|
+
throw e;
|
|
2216
|
+
});
|
|
2217
|
+
if (typeof queued.prompt_id !== "string")
|
|
2218
|
+
throw new ProviderError({
|
|
2219
|
+
kind: "malformed",
|
|
2220
|
+
message: "ComfyUI did not return a prompt ID."
|
|
2221
|
+
});
|
|
2222
|
+
const outputs = record2((await this.finished(http, queued.prompt_id)).outputs);
|
|
2223
|
+
const saved = Object.values(outputs).map((output) => record2(output).images).find((list) => Array.isArray(list) && list.length);
|
|
2224
|
+
const file = record2(Array.isArray(saved) ? saved[0] : void 0);
|
|
2225
|
+
if (typeof file.filename !== "string")
|
|
2226
|
+
throw new ProviderError({
|
|
2227
|
+
kind: "no_image",
|
|
2228
|
+
message: "ComfyUI finished without an output image."
|
|
2229
|
+
});
|
|
2230
|
+
const query = new URLSearchParams({
|
|
2231
|
+
filename: file.filename,
|
|
2232
|
+
subfolder: typeof file.subfolder === "string" ? file.subfolder : "",
|
|
2233
|
+
type: typeof file.type === "string" ? file.type : "temp"
|
|
2234
|
+
});
|
|
2235
|
+
const data = await http.bytes(`${this.baseUrl}/view?${query}`);
|
|
2236
|
+
return {
|
|
2237
|
+
...await encoded(data, format),
|
|
2238
|
+
mimeType: `image/${format}`,
|
|
2239
|
+
responseId: queued.prompt_id,
|
|
2240
|
+
...plan.described(outputs, seed)
|
|
2241
|
+
};
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
async finished(http, id) {
|
|
2245
|
+
for (; ; ) {
|
|
2246
|
+
const history = await http.json(`${this.baseUrl}/history/${id}`);
|
|
2247
|
+
const entry = record2(history[id]);
|
|
2248
|
+
const status = record2(entry.status);
|
|
2249
|
+
if (status.status_str === "error")
|
|
2250
|
+
throw executionFailure(status.messages);
|
|
2251
|
+
if (status.completed === true)
|
|
2252
|
+
return entry;
|
|
2253
|
+
await new Promise((resolve4) => setTimeout(resolve4, this.pollIntervalMs));
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
};
|
|
2257
|
+
|
|
1563
2258
|
// ../../packages/providers-configurable/dist/provider.js
|
|
1564
2259
|
var ConfigurableImageProvider = class {
|
|
1565
2260
|
getKey;
|
|
@@ -1643,19 +2338,12 @@ var ConfigurableImageProvider = class {
|
|
|
1643
2338
|
const model = (request.model ?? this.profile.model ?? "").trim();
|
|
1644
2339
|
if (!model)
|
|
1645
2340
|
throw usage("Supply --model or configure a default model with providers update.");
|
|
1646
|
-
if ((request.referenceImages?.length ?? 0) > 4)
|
|
1647
|
-
throw usage("At most four reference images are allowed.");
|
|
1648
|
-
const count = request.count ?? 1;
|
|
1649
|
-
if (!Number.isInteger(count) || count < 1 || count > 4)
|
|
1650
|
-
throw usage("Image count must be from 1 to 4.");
|
|
1651
2341
|
if (mask && this.profile.api === "openrouter")
|
|
1652
2342
|
throw usage("OpenRouter image API does not expose masks. Describe the edit region in the prompt.");
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
if (!["png", "jpeg", "webp"].includes(format))
|
|
1658
|
-
throw usage("Output format must be png, jpeg, or webp.");
|
|
2343
|
+
const { count, format } = validateImageRequest(request, [
|
|
2344
|
+
...images,
|
|
2345
|
+
...mask ? [mask] : []
|
|
2346
|
+
]);
|
|
1659
2347
|
const quality = request.quality?.trim().toLowerCase();
|
|
1660
2348
|
if (quality && !/^[a-z][a-z0-9_-]*$/.test(quality))
|
|
1661
2349
|
throw usage("Quality must be a provider-supported quality name.");
|
|
@@ -1713,12 +2401,12 @@ var ConfigurableImageProvider = class {
|
|
|
1713
2401
|
message: "Endpoint returned no image bytes or download URL."
|
|
1714
2402
|
});
|
|
1715
2403
|
try {
|
|
1716
|
-
const metadata = await
|
|
2404
|
+
const metadata = await sharp4(data).metadata();
|
|
1717
2405
|
if (!metadata.width || !metadata.height || !["png", "jpeg", "webp"].includes(metadata.format ?? "") || typeof image.media_type === "string" && image.media_type !== `image/${metadata.format}`)
|
|
1718
2406
|
throw new Error();
|
|
1719
|
-
await
|
|
2407
|
+
await sharp4(data).stats();
|
|
1720
2408
|
if (metadata.format !== format)
|
|
1721
|
-
data = await
|
|
2409
|
+
data = await sharp4(data).toFormat(format).toBuffer();
|
|
1722
2410
|
return {
|
|
1723
2411
|
data,
|
|
1724
2412
|
width: metadata.width,
|
|
@@ -1734,12 +2422,15 @@ var ConfigurableImageProvider = class {
|
|
|
1734
2422
|
});
|
|
1735
2423
|
}
|
|
1736
2424
|
}));
|
|
1737
|
-
return { provider: this.name,
|
|
2425
|
+
return { provider: this.name, requestedModel: model, images: results };
|
|
1738
2426
|
}
|
|
1739
2427
|
};
|
|
2428
|
+
function createProfileProvider(profile) {
|
|
2429
|
+
return profile.api === "comfyui" ? new ComfyUIImageProvider(profile) : new ConfigurableImageProvider(profile, async () => profileKey(profile, await profileStore()));
|
|
2430
|
+
}
|
|
1740
2431
|
function createProfileEntry(profile) {
|
|
1741
2432
|
return {
|
|
1742
|
-
provider:
|
|
2433
|
+
provider: createProfileProvider(profile),
|
|
1743
2434
|
authStatus: async () => {
|
|
1744
2435
|
let configured = profile.auth === "none";
|
|
1745
2436
|
if (!configured)
|
|
@@ -1827,11 +2518,15 @@ async function readApiKey(fromStdin, input = process.stdin, output = process.std
|
|
|
1827
2518
|
|
|
1828
2519
|
// src/help.ts
|
|
1829
2520
|
var PROFILE_HELP = `Named endpoint profiles:
|
|
1830
|
-
imagegen providers add <name> --api <openrouter|openai-images>
|
|
1831
|
-
[--model <id>] [--auth <bearer|none>]
|
|
2521
|
+
imagegen providers add <name> --api <openrouter|openai-images|comfyui>
|
|
2522
|
+
[--base-url <url>] [--model <id>] [--auth <bearer|none>]
|
|
2523
|
+
[--start-command <text>] [--workflow <file>]
|
|
2524
|
+
[--edit-workflow <file>]
|
|
1832
2525
|
imagegen providers show <name>
|
|
1833
2526
|
imagegen providers update <name> [--api <format>] [--base-url <url>]
|
|
1834
2527
|
[--model <id>] [--auth <bearer|none>]
|
|
2528
|
+
[--start-command <text>] [--workflow <file>]
|
|
2529
|
+
[--edit-workflow <file>]
|
|
1835
2530
|
imagegen providers remove <name>
|
|
1836
2531
|
imagegen auth <name> login [--key-stdin]
|
|
1837
2532
|
imagegen auth <name> status [--show-path]
|
|
@@ -1839,8 +2534,9 @@ var PROFILE_HELP = `Named endpoint profiles:
|
|
|
1839
2534
|
imagegen models --provider <name>
|
|
1840
2535
|
|
|
1841
2536
|
OpenRouter defaults to https://openrouter.ai/api/v1. openai-images requires
|
|
1842
|
-
--base-url including the API prefix (e.g. https://api.openai.com/v1).
|
|
1843
|
-
|
|
2537
|
+
--base-url including the API prefix (e.g. https://api.openai.com/v1). comfyui
|
|
2538
|
+
requires --base-url of its server (e.g. http://127.0.0.1:8188) and --model, and
|
|
2539
|
+
uses no key. Keys use hidden terminal input or explicit --key-stdin and the existing vault/file
|
|
1844
2540
|
store. --auth none supports endpoints without keys. Config is saved in providers.json
|
|
1845
2541
|
alongside the credential file. Origin changes clear the saved key; removing a
|
|
1846
2542
|
profile removes its credentials. Logout preserves profile configuration.
|
|
@@ -1849,13 +2545,25 @@ Select named profiles with --provider. Model precedence: --model > profile defau
|
|
|
1849
2545
|
provide one if no default is saved. models explicitly discovers model metadata;
|
|
1850
2546
|
OpenAI-compatible model listings do not establish image capabilities.
|
|
1851
2547
|
|
|
1852
|
-
Supported protocols: OpenRouter dedicated Images API
|
|
1853
|
-
multipart endpoints,
|
|
2548
|
+
Supported protocols: OpenRouter dedicated Images API, OpenAI Images JSON /
|
|
2549
|
+
multipart endpoints, and local models through ComfyUI, not arbitrary HTTP
|
|
2550
|
+
templates. Options vary by model.
|
|
1854
2551
|
OpenRouter accepts aspect ratios; openai-images needs pixels or presets:
|
|
1855
2552
|
square 1024x1024, portrait 1024x1536, landscape 1536x1024, wide 1536x864.
|
|
1856
2553
|
Auto options are omitted. Native masks require openai-images.
|
|
1857
2554
|
Outputs are measured and converted to PNG/JPEG/WebP without resizing. Count uses
|
|
1858
2555
|
separate single-image requests without automatic retries or model substitution.
|
|
2556
|
+
|
|
2557
|
+
comfyui profiles: --model names what the profile renders. Built in: flux3-klein
|
|
2558
|
+
expands the prompt and reports the rendered caption as prompt; flux3-klein-raw
|
|
2559
|
+
uses the prompt as the full caption; quality low, medium, and high run 4, 8, and
|
|
2560
|
+
16 steps. Any other model needs --workflow (text-to-image) and/or --edit-workflow
|
|
2561
|
+
(input images): ComfyUI API-format JSON using {{prompt}} and {{seed}}, optionally
|
|
2562
|
+
{{width}} and {{height}}, and {{image1}}, {{image2}}, ... for inputs. Workflow
|
|
2563
|
+
files set their own sampling, so only --quality auto applies.
|
|
2564
|
+
Sizes accept pixels, presets, or aspect ratios, round to multiples of 16, and need
|
|
2565
|
+
edges from 256 to 2048. Masks are rejected. The CLI does not start or stop ComfyUI;
|
|
2566
|
+
--start-command records how to start it and is quoted when it is not reachable.
|
|
1859
2567
|
`;
|
|
1860
2568
|
var IMAGE_OPTIONS = `Image options (generate, edit):
|
|
1861
2569
|
--prompt <text> Prompt text (required)
|
|
@@ -1883,34 +2591,41 @@ Gemini provider:
|
|
|
1883
2591
|
Model precedence: --model > constructor imageModel > IMAGEGEN_GEMINI_IMAGE_MODEL
|
|
1884
2592
|
> gemini-nano-banana-2.1. Calls are independent; use prior output as --source.
|
|
1885
2593
|
|
|
1886
|
-
|
|
1887
|
-
gpt-image-2.
|
|
1888
|
-
gpt-image-2.
|
|
1889
|
-
gpt-image-2
|
|
2594
|
+
Codex model requests (the backend chooses what actually runs):
|
|
2595
|
+
gpt-image-2.6-flare Requested by default
|
|
2596
|
+
gpt-image-2.6-sunburst Requested with --model
|
|
2597
|
+
gpt-image-2.5-flare Requested with --model
|
|
2598
|
+
gpt-image-2.5-sunburst Requested with --model
|
|
2599
|
+
gpt-image-2 Requested with --model
|
|
1890
2600
|
|
|
1891
|
-
|
|
2601
|
+
Codex accepts any model ID, including made-up ones, and does not report what ran;
|
|
2602
|
+
its Responses route rewrites these requests to gpt-image-2-codex. Use a named
|
|
2603
|
+
profile such as OpenRouter when a specific model matters.
|
|
1892
2604
|
Model precedence: --model > constructor imageModel (library callers) >
|
|
1893
|
-
IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.
|
|
1894
|
-
|
|
1895
|
-
|
|
2605
|
+
IMAGEGEN_CODEX_IMAGE_MODEL > gpt-image-2.6-flare.
|
|
2606
|
+
JSON results report requestedModel, and servedModel per image (null unless the
|
|
2607
|
+
provider names one). Codex also reports served quality, size, and background, and
|
|
2608
|
+
may override what was requested. A Codex --mask is sent as the last edit image with a fixed
|
|
2609
|
+
instruction appended to the prompt; it steers the edit but does not guarantee
|
|
2610
|
+
untouched pixels outside it. It must match the source size, and uses one of the
|
|
2611
|
+
four edit-image slots.
|
|
1896
2612
|
IMAGEGEN_USAGE_LOG=<file> appends one prompt-free JSON usage line per result.
|
|
1897
2613
|
|
|
1898
|
-
Codex quality: auto (default), low, medium, high; Flare/Sunburst also
|
|
1899
|
-
xhigh and max. gpt-image-2 rejects xhigh/max.
|
|
2614
|
+
Codex quality requests: auto (default), low, medium, high; Flare/Sunburst IDs also
|
|
2615
|
+
accept xhigh and max. gpt-image-2 rejects xhigh/max. The backend may serve another.
|
|
1900
2616
|
`;
|
|
1901
2617
|
var GENERATE_EXAMPLES = `Examples:
|
|
1902
2618
|
imagegen generate --provider gemini --size 16:9 --prompt "a lighthouse at dawn" --out ./gemini.png
|
|
1903
2619
|
imagegen generate --prompt "a lighthouse at dawn" --out ./lighthouse.png
|
|
1904
|
-
imagegen generate --model gpt-image-2 --prompt "a
|
|
1905
|
-
imagegen generate --model gpt-image-2.5-sunburst --quality max --prompt "a product photo" --out ./product.png
|
|
2620
|
+
imagegen generate --provider openrouter --model openai/gpt-image-2.6-sunburst --quality high --prompt "a product photo" --out ./product.png
|
|
1906
2621
|
`;
|
|
1907
2622
|
var EDIT_EXAMPLES = `Edit options:
|
|
1908
2623
|
--source <file> Source image (required)
|
|
1909
|
-
--mask <file> Optional edit mask
|
|
2624
|
+
--mask <file> Optional edit mask; transparent pixels mark the area to edit
|
|
1910
2625
|
|
|
1911
2626
|
Examples:
|
|
1912
2627
|
imagegen edit --format webp --source ./product.png --prompt "make the product blue" --out ./blue.webp
|
|
1913
|
-
imagegen edit --model gpt-image-2.
|
|
2628
|
+
imagegen edit --provider openrouter --model openai/gpt-image-2.6-sunburst --source ./product.png --prompt "add a ribbon" --out ./ribbon.png
|
|
1914
2629
|
`;
|
|
1915
2630
|
var USAGE = `imagegen - provider-agnostic image generation CLI
|
|
1916
2631
|
|
|
@@ -1928,6 +2643,7 @@ Usage:
|
|
|
1928
2643
|
imagegen skill
|
|
1929
2644
|
imagegen skill install --target <claude|codex|skills-dir> [--target <...>] [--force]
|
|
1930
2645
|
imagegen help [command]
|
|
2646
|
+
imagegen --version
|
|
1931
2647
|
|
|
1932
2648
|
${PROFILE_HELP}
|
|
1933
2649
|
${IMAGE_OPTIONS}
|
|
@@ -1943,6 +2659,7 @@ Auth options:
|
|
|
1943
2659
|
|
|
1944
2660
|
Global:
|
|
1945
2661
|
-h, --help Show help and exit 0
|
|
2662
|
+
--version Print the CLI version and exit 0
|
|
1946
2663
|
`;
|
|
1947
2664
|
var COMMAND_HELP = {
|
|
1948
2665
|
auth: `imagegen auth - manage provider credentials (one namespace per provider)
|
|
@@ -2049,7 +2766,7 @@ ${USAGE}`;
|
|
|
2049
2766
|
}
|
|
2050
2767
|
|
|
2051
2768
|
// src/skill.ts
|
|
2052
|
-
import { mkdir as mkdir3, readFile as
|
|
2769
|
+
import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
2053
2770
|
import { homedir as homedir2 } from "node:os";
|
|
2054
2771
|
import { dirname as dirname3, join as join4, resolve } from "node:path";
|
|
2055
2772
|
var SKILL_NAME = "imagegen-cli";
|
|
@@ -2060,7 +2777,7 @@ var SkillExistsError = class extends Error {
|
|
|
2060
2777
|
}
|
|
2061
2778
|
};
|
|
2062
2779
|
async function skillText() {
|
|
2063
|
-
return
|
|
2780
|
+
return readFile3(new URL("../skill/SKILL.md", import.meta.url), "utf8");
|
|
2064
2781
|
}
|
|
2065
2782
|
function targetPath(target) {
|
|
2066
2783
|
const root = target === "claude" ? join4(homedir2(), ".claude", "skills") : target === "codex" ? join4(homedir2(), ".agents", "skills") : resolve(target);
|
|
@@ -2068,7 +2785,7 @@ function targetPath(target) {
|
|
|
2068
2785
|
}
|
|
2069
2786
|
async function existingText(path) {
|
|
2070
2787
|
try {
|
|
2071
|
-
return await
|
|
2788
|
+
return await readFile3(path, "utf8");
|
|
2072
2789
|
} catch (error2) {
|
|
2073
2790
|
if (error2.code === "ENOENT") return void 0;
|
|
2074
2791
|
throw error2;
|
|
@@ -2105,7 +2822,7 @@ function usageLogEntry(request, edit, result, paths) {
|
|
|
2105
2822
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2106
2823
|
command: edit ? "edit" : "generate",
|
|
2107
2824
|
provider: result.provider,
|
|
2108
|
-
|
|
2825
|
+
requestedModel: result.requestedModel,
|
|
2109
2826
|
requested: {
|
|
2110
2827
|
size: request.size ?? "auto",
|
|
2111
2828
|
quality: request.quality ?? "auto",
|
|
@@ -2120,6 +2837,7 @@ function usageLogEntry(request, edit, result, paths) {
|
|
|
2120
2837
|
height: image.height,
|
|
2121
2838
|
bytes: image.data.byteLength,
|
|
2122
2839
|
responseId: image.responseId,
|
|
2840
|
+
servedModel: image.servedModel ?? null,
|
|
2123
2841
|
usage: image.usage
|
|
2124
2842
|
}))
|
|
2125
2843
|
};
|
|
@@ -2156,9 +2874,13 @@ function parseArgs(argv) {
|
|
|
2156
2874
|
flags.set("help", true);
|
|
2157
2875
|
continue;
|
|
2158
2876
|
}
|
|
2877
|
+
if (arg === "--version") {
|
|
2878
|
+
flags.set("version", true);
|
|
2879
|
+
continue;
|
|
2880
|
+
}
|
|
2159
2881
|
if (arg.startsWith("--")) {
|
|
2160
2882
|
const key = arg.slice(2);
|
|
2161
|
-
if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "api", "base-url", "auth", "key-stdin", "no-open", "show-path", "target", "force"].includes(key)) {
|
|
2883
|
+
if (!["prompt", "out", "provider", "model", "reference", "size", "quality", "format", "count", "source", "mask", "timeout", "api", "base-url", "auth", "start-command", "workflow", "edit-workflow", "key-stdin", "no-open", "show-path", "target", "force"].includes(key)) {
|
|
2162
2884
|
fail("usage", `Unknown option --${key}. Run imagegen help.`, EXIT_USAGE);
|
|
2163
2885
|
}
|
|
2164
2886
|
if (key === "key-stdin" || key === "no-open" || key === "show-path" || key === "force") {
|
|
@@ -2192,6 +2914,10 @@ function imageModel(flags) {
|
|
|
2192
2914
|
if (model !== void 0 && !model) fail("usage", "--model must not be blank.", EXIT_USAGE);
|
|
2193
2915
|
return model;
|
|
2194
2916
|
}
|
|
2917
|
+
async function cliVersion() {
|
|
2918
|
+
const manifest = JSON.parse(await readFile4(new URL("../package.json", import.meta.url), "utf8"));
|
|
2919
|
+
return manifest.version;
|
|
2920
|
+
}
|
|
2195
2921
|
function redirectUri() {
|
|
2196
2922
|
return process.env["IMAGEGEN_CODEX_REDIRECT_URI"] ?? DEFAULT_REDIRECT_URI;
|
|
2197
2923
|
}
|
|
@@ -2209,7 +2935,7 @@ function redirectPort(uri) {
|
|
|
2209
2935
|
async function loadImageInput(path, label) {
|
|
2210
2936
|
let data;
|
|
2211
2937
|
try {
|
|
2212
|
-
data = await
|
|
2938
|
+
data = await readFile4(path);
|
|
2213
2939
|
} catch {
|
|
2214
2940
|
fail("io", `${label} not found: ${path}`, EXIT_IO);
|
|
2215
2941
|
}
|
|
@@ -2286,14 +3012,16 @@ function printResult(providerName, result, paths) {
|
|
|
2286
3012
|
process.stdout.write(
|
|
2287
3013
|
`${JSON.stringify({
|
|
2288
3014
|
provider: providerName,
|
|
2289
|
-
|
|
3015
|
+
requestedModel: result.requestedModel,
|
|
2290
3016
|
images: result.images.map((image, i) => ({
|
|
2291
3017
|
path: paths[i],
|
|
2292
3018
|
mimeType: image.mimeType,
|
|
2293
3019
|
width: image.width,
|
|
2294
3020
|
height: image.height,
|
|
2295
3021
|
prompt: image.prompt,
|
|
2296
|
-
responseId: image.responseId
|
|
3022
|
+
responseId: image.responseId,
|
|
3023
|
+
servedModel: image.servedModel ?? null,
|
|
3024
|
+
served: image.usage?.["served"]
|
|
2297
3025
|
}))
|
|
2298
3026
|
})}
|
|
2299
3027
|
`
|
|
@@ -2520,6 +3248,11 @@ async function runImageRequest(request, edit, providers) {
|
|
|
2520
3248
|
}
|
|
2521
3249
|
async function main() {
|
|
2522
3250
|
const { flags, positionals } = parseArgs(process.argv.slice(2));
|
|
3251
|
+
if (flags.get("version")) {
|
|
3252
|
+
process.stdout.write(`${await cliVersion()}
|
|
3253
|
+
`);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
2523
3256
|
if (flags.get("help") || positionals.length === 0) {
|
|
2524
3257
|
process.stdout.write(helpFor(positionals[0]));
|
|
2525
3258
|
return;
|
|
@@ -2563,10 +3296,14 @@ async function main() {
|
|
|
2563
3296
|
`);
|
|
2564
3297
|
} else if (sub === "add" || sub === "update") {
|
|
2565
3298
|
const patch = {};
|
|
2566
|
-
for (const [flag, key] of [["api", "api"], ["base-url", "baseUrl"], ["auth", "auth"], ["model", "model"]]) {
|
|
3299
|
+
for (const [flag, key] of [["api", "api"], ["base-url", "baseUrl"], ["auth", "auth"], ["model", "model"], ["start-command", "startCommand"]]) {
|
|
2567
3300
|
const value = flagValue(flags, flag);
|
|
2568
3301
|
if (value !== void 0) patch[key] = value;
|
|
2569
3302
|
}
|
|
3303
|
+
for (const [flag, key] of [["workflow", "workflow"], ["edit-workflow", "editWorkflow"]]) {
|
|
3304
|
+
const value = flagValue(flags, flag);
|
|
3305
|
+
if (value !== void 0) patch[key] = resolve3(value);
|
|
3306
|
+
}
|
|
2570
3307
|
const profile = sub === "add" ? await repository.add({ name: subSub, ...patch }) : await repository.update(subSub, patch);
|
|
2571
3308
|
process.stdout.write(`${JSON.stringify({ provider: profile })}
|
|
2572
3309
|
`);
|
|
@@ -2577,8 +3314,7 @@ async function main() {
|
|
|
2577
3314
|
const selected = requestedProviders(flags);
|
|
2578
3315
|
if (!selected || selected.length !== 1) fail("usage", "Model discovery requires exactly one --provider <profile-name>.", EXIT_USAGE);
|
|
2579
3316
|
const profile = await new ProfileRepository().get(selected[0]);
|
|
2580
|
-
|
|
2581
|
-
process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await provider.models() })}
|
|
3317
|
+
process.stdout.write(`${JSON.stringify({ provider: profile.name, models: await createProfileProvider(profile).models() })}
|
|
2582
3318
|
`);
|
|
2583
3319
|
return;
|
|
2584
3320
|
}
|