DMAI-NODES / web /core.mjs
ceroomedia's picture
Publish DMAI NODES 0.1.0 preview under MIT
1758ac6 verified
Raw History Blame
6.42 kB
export const API = "/dmai-nodes/v1";
export const RATIOS = [
"1:1",
"4:5",
"5:4",
"3:4",
"4:3",
"2:3",
"3:2",
"9:16",
"16:9",
"9:21",
"21:9",
"9:19.5",
"19.5:9",
"10:16",
"16:10",
"1:2",
"2:1",
"1:3",
"3:1",
];
export const clone = (value) => structuredClone(value);
export const uid = () => crypto.randomUUID();
export function number(value, min, max, label, integer = false) {
if (
typeof value !== "number" ||
!Number.isFinite(value) ||
value < min ||
value > max ||
(integer && !Number.isInteger(value))
)
throw new Error(
`${label} must be ${integer ? "a whole number" : "a number"} from ${min} to ${max}.`,
);
return value;
}
export function parseObject(raw, label) {
let value;
try {
value = JSON.parse(raw);
} catch {
throw new Error(
`${label} contains invalid JSON. Your saved value has been preserved.`,
);
}
if (
!value ||
typeof value !== "object" ||
Array.isArray(value) ||
value.schema_version !== 1
)
throw new Error(
`${label} must be a version 1 object. Your saved value has been preserved.`,
);
return value;
}
export function readPrompt(raw) {
const value = parseObject(raw, "Prompt configuration");
if (
typeof value.prompt !== "string" ||
typeof value.negative_prompt !== "string"
)
throw new Error("Prompt and negative prompt must be text.");
number(value.width, 64, 8192, "Width", true);
number(value.height, 64, 8192, "Height", true);
number(value.count, 1, 8, "Image count", true);
if (value.width % 64 || value.height % 64)
throw new Error("Dimensions must be multiples of 64.");
return value;
}
export function dimensions(ratio, resolution) {
if (!RATIOS.includes(ratio) || !["1K", "2K"].includes(resolution))
throw new Error("Choose a supported ratio and resolution.");
const [w, h] = ratio.split(":").map(Number),
long = resolution === "1K" ? 1024 : 2048;
return w >= h
? [long, Math.max(64, Math.round((long * h) / w / 64) * 64)]
: [Math.max(64, Math.round((long * w) / h / 64) * 64), long];
}
export function customDimensions(width, height) {
number(width, 64, 8192, "Width", true);
number(height, 64, 8192, "Height", true);
return [width, height].map((n) => Math.round(n / 64) * 64);
}
export function readManifest(raw) {
const value = parseObject(raw, "LoRA configuration");
if (!Array.isArray(value.entries))
throw new Error("LoRA entries must be a list.");
const ids = new Set();
for (const e of value.entries) {
if (!e || typeof e.id !== "string" || !e.id || ids.has(e.id))
throw new Error("Each LoRA needs a unique entry ID.");
ids.add(e.id);
if (typeof e.name !== "string" || !e.name || typeof e.enabled !== "boolean")
throw new Error(
"Each LoRA needs a filename and a boolean enabled value.",
);
number(e.strength_model, -20, 20, "Model strength");
number(e.strength_clip, -20, 20, "CLIP strength");
}
return value;
}
export function moveEntry(entries, id, to) {
const from = entries.findIndex((e) => e.id === id);
if (from < 0 || !Number.isInteger(to) || to < 0 || to >= entries.length)
return entries.slice();
const next = entries.slice(),
[entry] = next.splice(from, 1);
next.splice(to, 0, entry);
return next;
}
export function readEngine(raw) {
const value = parseObject(raw, "Engine configuration");
if (
typeof value.profile_id !== "string" ||
!["enhanced", "manual"].includes(value.mode) ||
typeof value.preset_id !== "string"
)
throw new Error("Engine profile, mode or preset is invalid.");
if (!value.settings || !value.models || !Array.isArray(value.presets))
throw new Error("Engine settings, models and presets are required.");
if (typeof value.seed === "string") {
if (
!/^\d{1,20}$/.test(value.seed) ||
BigInt(value.seed) > 18446744073709551615n
)
throw new Error("Seed must be an unsigned 64-bit integer.");
} else number(value.seed, 0, Number.MAX_SAFE_INTEGER, "Seed", true);
validateSettings(value.settings);
for (const key of ["diffusion_model", "text_encoder", "vae", "checkpoint"])
if (typeof value.models[key] !== "string")
throw new Error(`Model filename ${key} must be text.`);
return value;
}
export function validateSettings(s) {
number(s.steps, 1, 150, "Steps", true);
number(s.cfg, 0, 30, "CFG");
number(s.denoise, 0, 1, "Denoise");
if (
typeof s.sampler !== "string" ||
!s.sampler ||
typeof s.scheduler !== "string" ||
!s.scheduler ||
!["none", "krea2t"].includes(s.enhancer)
)
throw new Error("Sampler, scheduler or enhancer is invalid.");
return clone(s);
}
export function mergePresets(existing, incoming, builtins) {
if (existing.length + incoming.length > 20)
throw new Error(
"An engine can store at most 20 imported presets. Remove an imported preset first.",
);
const ids = new Set([...existing, ...builtins].map((p) => p.id));
for (const p of incoming) {
if (ids.has(p.id))
throw new Error(
`Preset ID ${p.id} already exists. Use a new ID before importing.`,
);
ids.add(p.id);
}
return [...existing, ...incoming].map(clone);
}
export function selectionBody(selection, watermark) {
if (selection.all) {
if (!Number.isInteger(watermark) || watermark < 0)
throw new Error("Refresh the gallery before selecting all images.");
return { all: true, before: watermark };
}
const ids = [...selection.ids];
if (!ids.length) throw new Error("Select at least one image.");
return { ids };
}
export async function envelope(api, path, options = {}) {
const response = await api.fetchApi(`${API}${path}`, options);
let data;
try {
data = await response.json();
} catch {
throw new Error(
`DMAI NODES returned an invalid response (${response.status}).`,
);
}
if (!response.ok || data?.ok !== true)
throw new Error(
data?.error?.message || `Request failed (${response.status}).`,
);
return data.data;
}
export async function binary(api, path, options = {}) {
const response = await api.fetchApi(`${API}${path}`, options);
if (!response.ok) {
let message;
try {
message = (await response.json())?.error?.message;
} catch {}
throw new Error(message || `Download failed (${response.status}).`);
}
return response.blob();
}