DMAI-NODES / web /engine.mjs
ceroomedia's picture
Release 0.1.1: Prompter Generate and measured workflow progress
6e0df2b verified
Raw History Blame
18.6 kB
import {
Controller,
el,
button,
input,
select,
details,
options,
saveBlob,
statusMessage,
} from "./dom.mjs";
import {
readEngine,
validateSettings,
clone,
mergePresets,
envelope,
uid,
} from "./core.mjs";
export class Engine extends Controller {
constructor(node, context) {
super(node, "config_json", "Generation Engine", "bolt", context);
this.size(365, 480);
this.wrap("onConnectionsChange", () =>
queueMicrotask(() => {
if (!this.disposed && this.value && this.commitControls()) this.build();
}),
);
this.wrap("onExecuted", (output) => {
const report = Array.isArray(output?.dmai_report)
? output.dmai_report[0]
: output?.dmai_report;
if (report)
this.note(
`${report.request?.count ?? report.images?.length ?? ""} images complete`.trim(),
);
});
this.hydrate();
}
hydrate() {
if (this.disposed) return;
const revision = (this.revision = (this.revision ?? 0) + 1);
try {
this.value = readEngine(this.native.value);
this.note();
this.build();
this.guard(async () => {
const data = await this.context.bootstrap();
if (!this.disposed && this.revision === revision) {
this.data = data;
this.build();
}
});
} catch (error) {
this.invalid(error);
}
}
persist() {
this.set(this.value);
}
profile() {
return this.data?.profiles?.find((p) => p.id === this.value.profile_id);
}
presets() {
return [...(this.data?.presets ?? []), ...this.value.presets];
}
chosen() {
return this.presets().find(
(p) =>
p.id === this.value.preset_id && p.model.id === this.value.profile_id,
);
}
effective() {
return this.value.mode === "enhanced"
? (this.chosen()?.settings ?? this.value.settings)
: this.value.settings;
}
linked(name) {
return (
this.node.inputs?.some(
(input) => input.name === name && input.link != null,
) ?? false
);
}
samplingSummary(settings) {
return `${this.linked("sampler") ? "Connected sampler" : settings.sampler} / ${this.linked("sigmas") ? "connected sigmas" : settings.scheduler}`;
}
drafts() {
this.node.properties ??= {};
return (this.node.properties.dmai_engine_drafts ??= {});
}
remember() {
this.drafts()[this.value.profile_id] = clone({
mode: this.value.mode,
preset_id: this.value.preset_id,
settings: this.value.settings,
models: this.value.models,
});
}
manualDrafts() {
this.node.properties ??= {};
return (this.node.properties.dmai_manual_drafts ??= {});
}
commitControls() {
return (this.commitManual?.() ?? true) && (this.commitSeed?.() ?? true);
}
applyMode(mode) {
if (mode === this.value.mode || !this.commitControls()) return;
if (this.value.mode === "manual")
this.manualDrafts()[this.value.profile_id] = clone(this.value.settings);
if (mode === "enhanced") {
const preset =
this.chosen() ??
this.presets().find((p) => p.model.id === this.value.profile_id);
if (!preset) {
this.error(new Error("Import a preset for this profile first."));
return;
}
this.value.preset_id = preset.id;
this.value.settings = clone(preset.settings);
} else
this.value.settings = clone(
this.manualDrafts()[this.value.profile_id] ?? this.effective(),
);
this.value.mode = mode;
this.persist();
this.build();
}
changeProfile(id) {
if (!this.commitControls()) return;
this.remember();
if (this.value.mode === "manual")
this.manualDrafts()[this.value.profile_id] = clone(this.value.settings);
const profile = this.data.profiles.find((p) => p.id === id);
if (!profile) return;
const saved = this.drafts()[id],
preset = this.presets().find((p) => p.model.id === id),
models = {
diffusion_model: "",
text_encoder: "",
vae: "",
checkpoint: "",
};
for (const f of profile.model_fields)
models[f.key] = this.data.models[f.inventory]?.includes(f.default)
? f.default
: "";
this.value.profile_id = id;
Object.assign(
this.value,
saved
? clone(saved)
: {
mode: preset ? "enhanced" : "manual",
preset_id: preset?.id ?? "",
settings: clone(preset?.settings ?? profile.default_settings),
models,
},
);
this.persist();
this.build();
}
build() {
this.body.replaceChildren();
this.commitManual = null;
this.commitSeed = null;
const profile = this.profile(),
s = this.effective();
const profileSelect = select(
"Model",
this.data?.profiles?.map((p) => ({ value: p.id, label: p.label })) ?? [
{ value: this.value.profile_id, label: "Loading model profiles…" },
],
this.value.profile_id,
);
profileSelect.field.classList.add("dmai-model");
profileSelect.control.disabled = !this.data;
profileSelect.control.addEventListener("change", () => {
this.changeProfile(profileSelect.control.value);
profileSelect.control.value = this.value.profile_id;
});
this.body.append(profileSelect.field);
const mode = el("div", "dmai-segmented dmai-mode");
for (const [value, label, glyph] of [
["enhanced", "DMAI Enhanced", "spark"],
["manual", "Manual", "sliders"],
]) {
const b = button(label, glyph, () => this.applyMode(value), true);
b.setAttribute("aria-pressed", String(this.value.mode === value));
b.disabled = !this.data;
mode.append(b);
}
this.body.append(mode);
const presetRow = el("div", "dmai-preset-row"),
presetSelect = select(
"Preset",
[
{
value: "",
label:
this.value.mode === "manual"
? "Custom settings"
: "Choose a preset",
},
...this.presets()
.filter((p) => p.model.id === this.value.profile_id)
.map((p) => ({ value: p.id, label: p.name })),
],
this.value.mode === "enhanced" ? this.value.preset_id : "",
);
presetSelect.control.disabled = !this.data;
presetSelect.control.addEventListener("change", () => {
const p = this.presets().find((p) => p.id === presetSelect.control.value);
if (!p) return;
if (!this.commitControls()) {
presetSelect.control.value =
this.value.mode === "enhanced" ? this.value.preset_id : "";
return;
}
if (this.value.mode === "manual")
this.manualDrafts()[this.value.profile_id] = clone(this.value.settings);
this.value.mode = "enhanced";
this.value.preset_id = p.id;
this.value.settings = clone(p.settings);
this.persist();
this.build();
});
const file = el("input");
file.type = "file";
file.accept = ".json,application/json";
file.hidden = true;
file.addEventListener("change", () =>
this.guard(async () => {
const f = file.files?.[0];
if (f) await this.importFile(f);
file.value = "";
}),
);
const importButton = button("Import preset JSON", "upload", () =>
file.click(),
),
exportButton = button("Export preset JSON", "download", () =>
this.exportPreset(),
);
importButton.disabled = !this.data;
exportButton.disabled = !profile;
presetRow.append(presetSelect.field, importButton, exportButton, file);
this.body.append(presetRow);
const meta = el("div", "dmai-row dmai-preset-meta"),
origin =
this.value.mode === "manual"
? "Manual"
: this.value.presets.some((p) => p.id === this.value.preset_id)
? "Imported"
: "DMAI";
meta.append(
el("span", "dmai-badge", origin),
el(
"span",
"dmai-muted",
`${this.linked("sigmas") ? "External sigmas" : `${s.steps} steps`} · CFG ${s.cfg}`,
),
);
this.body.append(meta);
if (this.value.mode === "manual") this.manual(s, profile);
else
this.body.append(
el(
"div",
"dmai-engine-summary",
`${this.samplingSummary(s)}${s.enhancer === "krea2t" ? " · Enhancer" : ""}`,
),
);
if (this.linked("sampler") || this.linked("sigmas"))
this.body.append(
el(
"p",
"dmai-muted",
this.linked("sigmas")
? "Steps, schedule and denoise come from connected sigmas."
: "Using the connected sampler with these steps and schedule.",
),
);
if (this.linked("latent"))
this.body.append(
el("p", "dmai-muted", "Connected latent supplies image dimensions."),
);
const seedRow = el("div", "dmai-seed"),
seed = input("Seed", "text", this.value.seed);
seed.control.inputMode = "numeric";
this.commitSeed = () => {
if (
!/^\d{1,20}$/.test(seed.control.value) ||
BigInt(seed.control.value) > 18446744073709551615n
) {
seed.control.setAttribute("aria-invalid", "true");
this.error(
new Error("Seed must be an integer from 0 to 18446744073709551615."),
);
return false;
}
const n = Number(seed.control.value);
this.value.seed = Number.isSafeInteger(n) ? n : seed.control.value;
seed.control.removeAttribute("aria-invalid");
this.persist();
return true;
};
seed.control.addEventListener("change", () => {
if (this.commitSeed()) this.note();
});
seedRow.append(
seed.field,
button("Random seed", "dice", () => {
this.value.seed = crypto.getRandomValues(new Uint32Array(1))[0];
seed.control.value = this.value.seed;
this.persist();
}),
);
this.body.append(seedRow);
const advanced = details("Model files");
const externalModels = ["model", "clip", "vae"].every((name) =>
this.linked(name),
);
if (externalModels)
advanced.body.append(
el("p", "dmai-muted", "Using connected model, CLIP and VAE."),
);
else if (profile) {
for (const f of profile.model_fields) {
const list = this.data.models[f.inventory] ?? [],
picker = select(
f.label,
[
{ value: "", label: "Choose an installed file" },
...list.map((name) => ({ value: name, label: name })),
],
this.value.models[f.key],
);
picker.control.addEventListener("change", () => {
this.value.models[f.key] = picker.control.value;
this.persist();
});
advanced.body.append(picker.field);
}
const missing = profile.model_fields.filter(
(f) =>
f.required &&
!(this.data.models[f.inventory] ?? []).includes(
this.value.models[f.key],
),
);
if (missing.length) {
advanced.root.open = true;
advanced.body.append(
el(
"p",
"dmai-warning",
`Choose installed files: ${missing.map((f) => f.label).join(", ")}.`,
),
);
}
if (["model", "clip", "vae"].some((name) => this.linked(name)))
advanced.body.append(
el(
"p",
"dmai-warning",
"Connect model, CLIP and VAE together to use external models.",
),
);
}
advanced.body.append(
button(
"Refresh model list",
"swap",
() =>
this.guard(async () => {
if (!this.commitControls()) return;
this.data = await this.context.bootstrap(true);
this.build();
}),
true,
),
);
if (this.value.presets.length)
advanced.body.append(
button(
"Manage imported presets",
"layers",
() => this.managePresets(),
true,
),
);
if (profile?.default_guidance !== undefined && !this.linked("positive"))
advanced.body.append(
el(
"p",
"dmai-muted",
`Model guidance: ${profile.default_guidance}. Connect conditioning for custom guidance.`,
),
);
this.body.append(advanced.root);
if (this.data && !profile)
this.error(
new Error(
"This model profile is not installed. Choose a supported profile.",
),
);
}
manual(settings, profile) {
const grid = el("div", "dmai-settings");
const controls = {};
for (const [name, label, type, min, max, step] of [
["steps", "Steps", "number", 1, 150, 1],
["cfg", "CFG", "number", 0, 30, 0.1],
["denoise", "Denoise", "number", 0, 1, 0.05],
]) {
const f = input(label, type, settings[name]);
Object.assign(f.control, { min, max, step });
f.field.hidden = this.linked("sigmas") && name !== "cfg";
controls[name] = f.control;
grid.append(f.field);
}
for (const [name, label, items] of [
["sampler", "Sampler", this.data?.samplers ?? [settings.sampler]],
["scheduler", "Scheduler", this.data?.schedulers ?? [settings.scheduler]],
[
"enhancer",
"Enhancer",
profile?.family === "krea2" ? ["none", "krea2t"] : ["none"],
],
]) {
const f = select(label, items, settings[name]);
f.field.hidden =
(name === "sampler" && this.linked("sampler")) ||
(name === "scheduler" && this.linked("sigmas"));
controls[name] = f.control;
grid.append(f.field);
}
this.commitManual = () => {
try {
const next = validateSettings({
steps: Number(controls.steps.value || NaN),
cfg: controls.cfg.value === "" ? NaN : Number(controls.cfg.value),
denoise:
controls.denoise.value === ""
? NaN
: Number(controls.denoise.value),
sampler: controls.sampler.value,
scheduler: controls.scheduler.value,
enhancer: controls.enhancer.value,
});
this.value.settings = next;
this.manualDrafts()[this.value.profile_id] = clone(next);
this.persist();
return true;
} catch (error) {
this.error(error);
return false;
}
};
for (const c of Object.values(controls))
c.addEventListener("change", () => {
if (this.commitManual()) this.note();
});
this.body.append(grid);
}
async importFile(file) {
if (!this.commitControls()) return;
if (file.size > 128 * 1024)
throw new Error("Preset files must be 128 KiB or smaller.");
let payload;
try {
payload = JSON.parse(await file.text());
} catch {
throw new Error("Preset file must contain valid JSON.");
}
const result = await envelope(this.context.api, "/presets/validate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (this.disposed || !this.commitControls()) return;
const presets = mergePresets(
this.value.presets,
result.presets,
this.data.presets,
);
const first = result.presets[0];
this.value.presets = presets;
if (this.value.profile_id !== first.model.id)
this.changeProfile(first.model.id);
if (this.value.mode === "manual")
this.manualDrafts()[this.value.profile_id] = clone(this.value.settings);
this.value.mode = "enhanced";
this.value.preset_id = first.id;
this.value.settings = clone(first.settings);
this.persist();
this.build();
this.note(
`${result.presets.length} preset${result.presets.length === 1 ? "" : "s"} imported.`,
);
}
managePresets() {
if (!this.commitControls()) return;
const d = this.show("Imported presets");
const draw = () => {
d.body.replaceChildren();
for (const preset of this.value.presets) {
const row = el("div", "dmai-row");
row.append(
el("span", "", preset.name),
button(`Remove ${preset.name}`, "close", () => {
const effective = clone(this.effective());
this.value.presets = this.value.presets.filter(
(p) => p.id !== preset.id,
);
for (const draft of Object.values(this.drafts()))
if (draft.preset_id === preset.id) {
draft.mode = "manual";
draft.preset_id = "";
draft.settings = clone(preset.settings);
}
if (this.value.preset_id === preset.id) {
this.value.mode = "manual";
this.value.preset_id = "";
this.value.settings = effective;
}
this.persist();
this.build();
if (this.value.presets.length) draw();
else d.root.close();
}),
);
d.body.append(row);
}
};
draw();
}
exportPreset() {
if (!this.commitControls()) return;
const d = this.show("Export preset"),
name = input("Preset name", "text", "My settings"),
message = el("p", "dmai-status"),
save = button(
"Download JSON",
"download",
() =>
this.guard(async () => {
try {
const p = this.profile(),
payload = {
schema_version: 1,
id: `custom-${uid()}`,
name: name.control.value,
model: { id: p.id, label: p.label, family: p.family },
settings: clone(this.effective()),
};
const result = await envelope(
this.context.api,
"/presets/validate",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
},
);
saveBlob(
new Blob([JSON.stringify(result.presets[0], null, 2) + "\n"], {
type: "application/json",
}),
`${payload.id}.json`,
);
d.root.close();
} catch (error) {
statusMessage(message, error.message, true);
}
}),
true,
);
save.classList.add("dmai-primary");
d.body.append(name.field, message, save);
}
}