Files
olivv-cs-ComfyUI-FunPack/app/ui/features/sampler/index.js
T

92 lines
6.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// "⏱ Sampler": the pipeline's steps, sampler and scheduler (the inputs its preset marks as sampling) and Engine ▸ Sampling's
// settings, one click away in a thin window over the timeline. Same pipeline and values tree as those pages, so they cannot
// disagree. Absent when the pipeline marks no sampling input and no module has a sampling setting.
import { offer } from "../../shell/actions.js";
import { composer as c } from "../../composer/composer.js";
import { settingRow, whenSatisfied } from "../settings/field.js";
import { widgetControl } from "../settings/models.js";
export default {
id: "sampler",
mount: "timeline.status",
needs: ["pipeline"],
setup({ host, app }) {
const ps = app.pipeline;
let pop = null, specs = {}, unreadable = false;
const latest = {}; // per input, the value whose save is still on its way: newer than the slot
const sampling = () => { const ok = ps.usefulness(); return ps.activeModules().filter((m) => (m.category || "") === "sampling" && Object.keys(m.settings || {}).length && ok(m)); };
const marked = () => (ps.slots() || []).flatMap((slot) => (slot.roles || []).filter((r) => r.at === "generation.sampling" && r.input && !Array.isArray((slot.inputs || {})[r.input])).map((role) => ({ slot, role })));
const seeds = () => (ps.slots() || []).flatMap((slot) => (slot.roles || []).filter((r) => r.at === "generation.seed" && r.input).map((role) => ({ slot, role })));
const describe = async () => {
const missing = [...new Set([...marked(), ...seeds()].map(({ slot }) => slot.node))].filter((n) => !(n in specs));
if (missing.length) try { Object.assign(specs, (await app.api.describeNodes(missing)).nodes || {}); unreadable = false; } catch { unreadable = true; } // asked again on the next change
};
const setter = ({ slot, role }) => async (v) => { // compared with the newest value: the one drawn may be several saves old
const key = `${slot.id}.${role.input}`;
const now = (ps.slots() || []).find((s) => s.id === slot.id) || slot;
if (JSON.stringify(key in latest ? latest[key] : (now.inputs || {})[role.input]) === JSON.stringify(v)) return;
latest[key] = v;
await ps.save({ inputs: { [slot.id]: { [role.input]: v } } });
if (ps.settled) await ps.settled(); // a save queued behind another returns before it lands
if (latest[key] !== v) return; // a newer edit of this input is the one that counts
delete latest[key]; // landed or not, the slot is the truth again
const after = (ps.slots() || []).find((s) => s.id === slot.id) || {};
if (JSON.stringify((after.inputs || {})[role.input]) !== JSON.stringify(v)) { // not saved: show what will really run, and why
c.toast.warn({ text: [...(ps.saveNotes ? ps.saveNotes() : [])].find((n) => /could not save/i.test(n)) || `${role.label || role.input} was not saved.` });
draw();
}
};
const inputRow = ({ slot, role }) => {
const w = ((specs[slot.node] || {}).widgets || []).find((x) => x.name === role.input);
if (!w) return null;
const set = setter({ slot, role });
return c.settingsRow.default({ label: role.label || w.name, hint: w.tooltip || "", control: widgetControl({ ...w, name: role.label || w.name }, (slot.inputs || {})[role.input] ?? w.default, set) });
};
// Seed: fresh every Generate (the default), or the seed node's own number, so the same settings give the same clip.
const seedRows = () => {
const p = app.project, list = seeds();
if (!list.length || !p || !p.project) return [];
const fresh = p.pref("random_seed", true) !== false, last = app.lastRun && app.lastRun.seed;
const keep = () => { p.setPref("random_seed", false); Promise.all(list.map((x) => setter(x)(last))).then(draw); };
return [c.toggle.default({ label: "New seed every Generate", hint: fresh ? "Each Generate picks a random seed; the number on the seed node is not used." : "Every Generate uses the Seed below: same settings, same clip. Takes ×N still use fresh seeds.",
checked: fresh, onChange: (v) => { p.setPref("random_seed", v); draw(); } }),
...(fresh ? [] : list.map(inputRow)),
...(Number.isInteger(last) ? [c.settingsRow.default({ label: "Last seed used", hint: "The seed of the last clip generated.", control: c.button.sm({ label: `Keep ${last}`, tone: "ghost", title: "Use this seed from now on", onClick: keep }) })] : [])];
};
const button = c.button.sm({ label: "⏱ Sampler", tone: "neutral", title: "Steps, sampler, scheduler and sampling settings: one click, no modal.", onClick: () => (pop ? pop.close() : open()) });
button.node.hidden = true;
host.append(button.node);
const body = c.region.stack({ gap: "md" });
const draw = () => {
if (ps.loading()) return body.set([c.hint.default({ text: "Loading…" })]);
if (ps.loadError()) return body.set([c.banner.warn({ text: `Could not load: ${ps.loadError()}` })]);
const mods = sampling(), values = ps.currentValues();
const change = async (id, name, value) => {
await ps.setModuleValue(id, name, value);
if (ps.settled) await ps.settled();
const why = (ps.saveNotes ? ps.saveNotes() : []).find((n) => /could not save/i.test(n));
if (why) c.toast.warn({ text: why }); // refused, or not saved: said here, not only on the Engine page
draw();
};
const rows = [...marked().map(inputRow), ...seedRows()];
const unread = marked().length && !rows.some(Boolean) && (unreadable || marked().every(({ slot }) => slot.node in specs)) ? [c.hint.default({ text: "Steps, sampler and scheduler could not be read from the sampler node: change them in Settings ▸ Models & Pipeline." })] : [];
body.set([...unread, ...rows, ...mods.flatMap((m) => [mods.length > 1 ? c.label.section({ text: m.title || m.id }) : null,
...Object.entries(m.settings).filter(([, spec]) => whenSatisfied(values, m.id, spec.when)).map(([name, spec]) => settingRow(m.id, name, spec, values, change))])].filter(Boolean));
};
const typing = () => body.node.contains(document.activeElement) && /^(input|textarea)$/i.test(document.activeElement.tagName);
const off = ps.subscribe(() => { if (pop && !typing()) describe().then(draw); });
const show = () => { button.node.hidden = !sampling().length && !marked().length && !seeds().length; if (button.node.hidden && pop) pop.close(); };
ps.ensureLoaded().then(show);
const offShow = ps.subscribe(show);
function open() {
draw();
pop = c.popover.anchored({ anchor: button, body, side: "top", align: "end", onClose: () => { pop = null; } });
describe().then(() => { if (pop) draw(); });
}
const offered = offer(app, { id: "sampler", label: "Sampler", icon: "⏱", run: () => { if (!button.node.hidden) (pop ? pop.close() : open()); } });
return () => { off(); offShow(); offered(); if (pop) pop.close(); button.node.remove(); };
},
};