Files
olivv-cs-ComfyUI-FunPack/app/legacy/sampler_panel.js
T
digital-garbageandClaude Sonnet 5 29ac44ffcd feat: v4 UI port Phase A start -- module categories, legacy frontend served live
Two pieces of the "port v4's exact UI onto v5's backend" plan:

- core/contract.py: new optional ModuleSpec.category field (closed set:
  continuity, guidance, conditioning, sampling, post, system), validated in
  core/schema.py, threaded through core/registry.py's FIELDS and
  to_manifest(). Presentation grouping, independent of STAGE (execution
  order) -- a module can be stage="sampling" and category="guidance" at
  once. Only the 4 existing modules that actually render a settings panel
  (alg, sharpen, appearance, full_control) got a category assigned; the
  other 12 have none to categorize (no SETTINGS block) and stay
  uncategorised, which is valid.

- v4's frontend (movie_editor/frontend/*, ~29k lines, 61 files) copied
  verbatim into app/legacy/. core/routes.py's "/" route now serves
  legacy/index.html instead of the composer entry point; a <base> tag in
  that file resolves its own relative script/link tags against
  /funpack/app/legacy/ regardless of what URL it's served at, so every
  file moved as a unit with zero per-file path edits. Verified live: all
  61 assets load 200 OK, the real v4 onboarding screen renders pixel-for-
  pixel (it's the actual DOM/CSS, not a reimplementation), and several v4
  API calls (/api/media, /api/projects, /api/git/status) already succeed
  against v5's real backend by coincidence of shared route naming -- the
  rest 404/405 as expected, since api.js's ~90 functions haven't been
  rewired to v5's actual routes yet (next piece of this plan).

The old composer entry point (app/index.html, app/composer/**) stays in
the tree, unreferenced, until a later cleanup pass once nothing imports it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 23:58:51 +03:00

479 lines
24 KiB
JavaScript

// Studio sampler settings panel — ported from web/funpack_studio.js renderSampler().
// Renders two pass sections (High / Low) into a given container element.
// Exposes window.SamplerPanel.render(container, samplers, save, saveNow).
// save(samplers) — debounced/quiet, for numeric inputs
// saveNow(samplers) — immediate notify + re-render, for selects / checkboxes
(function () {
const { el } = window.dom;
// Simple mode has no rating UI — velocity bias / rescue are no-ops without a rated
// velocity bank, so they're hidden there (see engine_settings.js for the same gate).
const EASY = () => !!window.FunPackMode?.isSimple();
const SAMPLER_TYPES = ["Hybrid Euler 2S", "Distilled Flow", "KSampler"];
const VELOCITY_BIAS_MODES = ["off", "capture", "apply", "capture_and_apply"];
const MOTION_PULSE_MODES = ["off", "balanced", "aggressive", "custom"];
// Fallback only. The real list is whatever THIS ComfyUI has — read from the live
// KSampler node spec, because a hardcoded set silently hides samplers the backend can
// already run (it passes the name straight to comfy.samplers.sampler_object). That is
// how res_multistep — the one ComfyUI's own MiniMax H3 templates use — went missing.
const KSAMPLER_NAMES_FALLBACK = ["euler", "euler_ancestral", "dpm_2", "dpm_2_ancestral", "dpmpp_2m", "dpmpp_sde", "ddim", "uni_pc"];
// Same story for the schedulers: the backend hands the name to comfy.samplers.calculate_sigmas.
const KSAMPLER_SCHEDULERS_FALLBACK = ["normal", "karras", "exponential", "sgm_uniform", "simple", "ddim_uniform", "beta", "linear_quadratic", "kl_optimal"];
// Not a ComfyUI scheduler — the entry that means "run the Sigmas field as typed". First in the
// list AND the default, so a hand-written schedule can sit in the field untouched while the
// dropdown switches between it and a computed one. Mirrors conditioning.KSAMPLER_USER_SIGMAS.
const KSAMPLER_USER_SIGMAS = "use_user_sigmas";
let _ksamplerNames = null;
let _ksamplerSchedulers = null;
let _ksamplerPending = null;
// One fetch feeds both lists; whichever renders first kicks it off.
function loadKsamplerSpec(onLoaded) {
if (_ksamplerPending || !window.MovieEditorAPI?.nodeSpec) return;
_ksamplerPending = window.MovieEditorAPI.nodeSpec("KSampler")
.then((spec) => {
const choices = (name) => ((spec?.inputs || []).find((i) => i.name === name)?.choices) || [];
const names = choices("sampler_name");
const scheds = choices("scheduler");
if (names.length) _ksamplerNames = names;
if (scheds.length) _ksamplerSchedulers = scheds;
if ((names.length || scheds.length) && onLoaded) onLoaded();
})
.catch(() => {});
}
function ksamplerNames(onLoaded) {
if (_ksamplerNames) return _ksamplerNames;
loadKsamplerSpec(onLoaded);
return KSAMPLER_NAMES_FALLBACK;
}
function ksamplerSchedulers(onLoaded) {
if (_ksamplerSchedulers) return [KSAMPLER_USER_SIGMAS].concat(_ksamplerSchedulers);
loadKsamplerSpec(onLoaded);
return [KSAMPLER_USER_SIGMAS].concat(KSAMPLER_SCHEDULERS_FALLBACK);
}
function defaultHybrid() {
return {
eta: 1.0, eta_final: 1.0, normalize_strength: 0.0, normalize_start_sigma: 0.9,
s_noise: 1.0, high_quality_pct: 0.35, correction_blend: 1.0, quality_sharpness: 0.0,
motion_pulse_mode: "off", motion_pulse_start_pct: 0.3, motion_pulse_count: 2,
motion_pulse_spacing_pct: 0.22, motion_pulse_strength: 0.85,
velocity_bias_mode: "off", velocity_bias_strength: 0.0,
velocity_bias_source: "mean", velocity_refinement_key: "default",
rescue_mode: false, rescue_threshold: 0.15, rescue_strength: 0.2,
};
}
function defaultDistilled() {
return {
order: 2, final_correction_steps: 1, ab2_ramp: false, quality_sharpness: 0.0,
normalize_strength: 0.0, normalize_start_sigma: 0.9, s_noise: 0.0,
velocity_bias_mode: "off", velocity_bias_strength: 0.0,
velocity_bias_source: "mean", velocity_refinement_key: "default",
rescue_mode: false, rescue_threshold: 0.15, rescue_strength: 0.2,
alg_enabled: false, alg_strength: 2.0, alg_sigma_threshold: 0.975,
mg_enabled: false, mg_strength: 0.5, mg_decay: 0.5, mg_sigma_threshold: 0.975,
};
}
function defaultPass(type) {
return {
type: type || "Hybrid Euler 2S", sigmas: "",
hybrid: defaultHybrid(), distilled: defaultDistilled(),
ksampler_name: "euler", ksampler_steps: 8, ksampler_scheduler: KSAMPLER_USER_SIGMAS,
ksampler_sharpness: 0.0, ksampler_sharpen_start_pct: 0.35,
};
}
function defaultSamplers() {
return { high: defaultPass("Hybrid Euler 2S"), low: defaultPass("Distilled Flow") };
}
function deepMerge(def, over) {
if (!over || typeof over !== "object") return def;
const out = Object.assign({}, def);
for (const k of Object.keys(over)) {
if (over[k] !== null && typeof over[k] === "object" && !Array.isArray(over[k]) && typeof def[k] === "object")
out[k] = deepMerge(def[k], over[k]);
else if (over[k] !== undefined)
out[k] = over[k];
}
return out;
}
// ── micro DOM helpers (match inspector style) ────────────────────────────────
function row(container, labelText, ctrl) {
const f = el("label", "field");
f.append(el("span", null, labelText));
f.append(ctrl);
container.append(f);
return ctrl;
}
function hint(container, text) {
container.append(el("div", "insp-hint", text));
}
function sectionTag(container, text) {
const t = el("div", "insp-tag sp-sub"); t.textContent = text; container.append(t);
}
function numCtrl(val, min, max, step, dk, onChange) {
const i = el("input"); i.type = "number";
i.value = Number.isFinite(+val) ? +val : 0;
if (min != null) i.min = String(min);
if (max != null) i.max = String(max);
if (step != null) i.step = String(step);
if (dk) i.dataset.k = dk;
i.oninput = () => onChange(parseFloat(i.value) || 0);
return i;
}
function intCtrl(val, min, max, dk, onChange) {
const i = el("input"); i.type = "number"; i.step = "1";
i.value = Math.round(+val) || 0;
if (min != null) i.min = String(min);
if (max != null) i.max = String(max);
if (dk) i.dataset.k = dk;
i.oninput = () => onChange(parseInt(i.value) || 0);
return i;
}
function selCtrl(values, selected, dk, onChange) {
const s = el("select");
for (const v of values) {
const o = el("option", null, v); o.value = v; if (v === selected) o.selected = true; s.append(o);
}
if (dk) s.dataset.k = dk;
s.onchange = () => onChange(s.value);
return s;
}
function checkCtrl(checked, dk, onChange) {
const i = el("input"); i.type = "checkbox"; i.checked = !!checked; i.style.width = "auto";
if (dk) i.dataset.k = dk;
i.onchange = () => onChange(i.checked);
return i;
}
function textCtrl(value, placeholder, dk, onChange) {
const i = el("input"); i.type = "text"; i.value = value || ""; i.placeholder = placeholder || "";
if (dk) i.dataset.k = dk;
i.oninput = () => onChange(i.value);
return i;
}
// ── velocity-bias / rescue block (shared by Hybrid and Distilled) ──
function renderVelocityBlock(c, dk, sub, save, saveNow) {
if (EASY()) {
hint(c, "Velocity bias / rescue mode hidden — both are no-ops without a rated velocity bank, "
+ "which Easy Gen has no UI for. Use the Cutting Room or ComfyUI graph instead.");
return;
}
const vbm = selCtrl(VELOCITY_BIAS_MODES, sub.velocity_bias_mode || "off", dk + "-vbm",
(v) => { sub.velocity_bias_mode = v; saveNow(); });
row(c, "velocity bias mode", vbm);
if (sub.velocity_bias_mode && sub.velocity_bias_mode !== "off") {
row(c, "velocity bias strength",
numCtrl(sub.velocity_bias_strength, 0, 3, 0.05, dk + "-vbs",
(v) => { sub.velocity_bias_strength = v; save(); }));
row(c, "bias source",
selCtrl(["mean", "nearest"], sub.velocity_bias_source || "mean", dk + "-vbsrc",
(v) => { sub.velocity_bias_source = v; save(); }));
row(c, "velocity key",
textCtrl(sub.velocity_refinement_key, "default", dk + "-vrk",
(v) => { sub.velocity_refinement_key = v; save(); }));
}
const rsm = selCtrl(["off", "on"], sub.rescue_mode ? "on" : "off", dk + "-rsm",
(v) => { sub.rescue_mode = (v === "on"); saveNow(); });
row(c, "rescue mode", rsm);
if (sub.rescue_mode) {
row(c, "rescue threshold",
numCtrl(sub.rescue_threshold, 0, 1, 0.01, dk + "-rst",
(v) => { sub.rescue_threshold = v; save(); }));
row(c, "rescue strength",
numCtrl(sub.rescue_strength, 0, 0.5, 0.01, dk + "-rss",
(v) => { sub.rescue_strength = v; save(); }));
if (!sub.velocity_bias_mode || sub.velocity_bias_mode === "off") {
row(c, "bias source",
selCtrl(["mean", "nearest"], sub.velocity_bias_source || "mean", dk + "-rsrc",
(v) => { sub.velocity_bias_source = v; save(); }));
}
}
}
// ── per-pass rendering ────────────────────────────────────────────────────────
function renderPass(container, passKey, label, cfg, save, saveNow) {
sectionTag(container, label);
const typeCtrl = selCtrl(SAMPLER_TYPES, cfg.type || "Hybrid Euler 2S", "sp-" + passKey + "-type",
(v) => { cfg.type = v; saveNow(); });
row(container, "Sampler", typeCtrl);
const sigCtrl = textCtrl(cfg.sigmas, "e.g. 20.0, 14.0, 8.0, 4.0, 1.0, 0.0", "sp-" + passKey + "-sig",
(v) => { cfg.sigmas = v; save(); });
row(container, "Sigmas", sigCtrl);
hint(container, "Comma-separated floats. Leave empty to let the sampler decide.");
const dk = "sp-" + passKey;
// Steps + schedule belong to the PASS, not to the KSampler branch they first grew in:
// every sampler here walks a sigma list handed in from outside, so Distilled Flow and
// Hybrid Euler 2S can be driven by a computed schedule exactly like a stock KSampler.
// ksampler_* are the pre-split key names — read once so an older project keeps its
// schedule, then written under the shared ones.
if (cfg.scheduler === undefined) cfg.scheduler = cfg.ksampler_scheduler || KSAMPLER_USER_SIGMAS;
if (cfg.steps === undefined) cfg.steps = cfg.ksampler_steps === undefined ? 8 : cfg.ksampler_steps;
const scheds = ksamplerSchedulers(saveNow); // saveNow re-renders once the live list lands
const schedCur = cfg.scheduler || KSAMPLER_USER_SIGMAS;
row(container, "Schedule",
selCtrl(scheds.includes(schedCur) ? scheds : scheds.concat([schedCur]), schedCur, dk + "-sch",
(v) => { cfg.scheduler = v; saveNow(); }));
hint(container, "'" + KSAMPLER_USER_SIGMAS + "' runs the Sigmas field above exactly as typed; "
+ "any other entry COMPUTES the schedule from the step count, the way ComfyUI's "
+ "own scheduler does, and the Sigmas field is ignored until you switch back — so "
+ "you can leave a hand-written schedule parked there. Works for every sampler "
+ "below, not just KSampler.");
if (schedCur !== KSAMPLER_USER_SIGMAS) {
row(container, "Steps",
intCtrl(cfg.steps, 1, 200, dk + "-steps", (v) => { cfg.steps = v; save(); }));
hint(container, "8 suits the distilled LTX model; a non-distilled one wants 20-30.");
}
renderAlgorithm(container, dk, cfg, save, saveNow);
}
// The algorithm-specific block of a pass, split out so the second pass can show it
// too — it has its own sigmas and schedule controls already, and only needed this.
function renderAlgorithm(container, dk, cfg, save, saveNow) {
if (cfg.type === "Hybrid Euler 2S") {
const hc = cfg.hybrid;
sectionTag(container, "Hybrid Euler 2S");
row(container, "normalize strength",
numCtrl(hc.normalize_strength, 0, 1, 0.05, dk + "-hn",
(v) => { hc.normalize_strength = v; save(); }));
hint(container, "Video-only latent normalization (anti-overbake). 0 = off. 0.5 = gentle. Audio untouched.");
row(container, "normalize start sigma",
numCtrl(hc.normalize_start_sigma, 0, 1, 0.025, dk + "-hns",
(v) => { hc.normalize_start_sigma = v; save(); }));
// Variability macro
const macroWrap = el("div", "sp-macro");
const macroLbl = el("div", "sp-macro-title", "Variability macro");
macroWrap.append(macroLbl);
const presetRow = el("div", "sp-macro-presets");
[["Chill", 0.1], ["Spicy", 0.5], ["Chaos", 0.9]].forEach(([name, val]) => {
const b = el("button", "btn ghost tiny", name); b.type = "button";
b.onclick = () => { applyVariabilityMacro(hc, val); saveNow(); };
presetRow.append(b);
});
macroWrap.append(presetRow);
const valDisplay = el("span", "sp-macro-val", (+hc.variability || 0).toFixed(2));
const slider = el("input"); slider.type = "range"; slider.min = "0"; slider.max = "1"; slider.step = "0.05";
slider.value = String(Number.isFinite(+hc.variability) ? +hc.variability : 0.5);
slider.oninput = () => { valDisplay.textContent = (+slider.value).toFixed(2); };
slider.onchange = () => { applyVariabilityMacro(hc, parseFloat(slider.value)); saveNow(); };
const sliderWrap = el("div", "sp-macro-slider"); sliderWrap.append(slider, valDisplay);
macroWrap.append(sliderWrap);
hint(macroWrap, "Chill = clean + reproducible · Spicy = lively · Chaos = wild. Sets eta / velocity bias / motion pulses below.");
container.append(macroWrap);
[["eta", 0, 1, 0.01, dk + "-eta"],
["eta_final", 0, 1, 0.01, dk + "-etaf"],
["s_noise", 0, 10, 0.01, dk + "-sn"],
["high_quality_pct", 0, 1, 0.01, dk + "-hqp"],
["correction_blend", 0, 1, 0.01, dk + "-cb"],
["quality_sharpness", 0, 1, 0.01, dk + "-qs"]].forEach(([k, mn, mx, st, dkk]) => {
row(container, k.replace(/_/g, " "),
numCtrl(hc[k], mn, mx, st, dkk, (v) => { hc[k] = v; save(); }));
});
const mpm = selCtrl(MOTION_PULSE_MODES, hc.motion_pulse_mode || "off", dk + "-mpm",
(v) => { hc.motion_pulse_mode = v; saveNow(); });
row(container, "motion pulse mode", mpm);
if (hc.motion_pulse_mode !== "off") {
[["motion_pulse_start_pct", 0, 0.95, 0.01, dk + "-mpsp"],
["motion_pulse_spacing_pct", 0.04, 0.45, 0.01, dk + "-mpspg"],
["motion_pulse_strength", 0, 1, 0.01, dk + "-mpstr"]].forEach(([k, mn, mx, st, dkk]) => {
row(container, k.replace(/_/g, " "),
numCtrl(hc[k], mn, mx, st, dkk, (v) => { hc[k] = v; save(); }));
});
row(container, "motion pulse count",
intCtrl(hc.motion_pulse_count, 1, 6, dk + "-mpc",
(v) => { hc.motion_pulse_count = v; save(); }));
}
renderVelocityBlock(container, dk + "-h", hc, save, saveNow);
} else if (cfg.type === "Distilled Flow") {
const dc = cfg.distilled;
sectionTag(container, "Distilled Flow");
row(container, "order",
intCtrl(dc.order, 1, 2, dk + "-dord", (v) => { dc.order = v; save(); }));
row(container, "AB2 ramp",
checkCtrl(!!dc.ab2_ramp, dk + "-dab2", (v) => { dc.ab2_ramp = v; save(); }));
hint(container, "Graduated 2nd order: ramp AB2 0->1 across schedule. Helps low-step distilled runs.");
row(container, "normalize strength",
numCtrl(dc.normalize_strength, 0, 1, 0.05, dk + "-dn",
(v) => { dc.normalize_strength = v; save(); }));
hint(container, "Video-only latent normalization. 0 = plain euler. Audio untouched.");
row(container, "normalize start sigma",
numCtrl(dc.normalize_start_sigma, 0, 1, 0.025, dk + "-dns",
(v) => { dc.normalize_start_sigma = v; save(); }));
row(container, "final correction steps",
intCtrl(dc.final_correction_steps, 0, 3, dk + "-dfcs",
(v) => { dc.final_correction_steps = v; save(); }));
row(container, "quality sharpness",
numCtrl(dc.quality_sharpness, 0, 1, 0.01, dk + "-dqs",
(v) => { dc.quality_sharpness = v; save(); }));
hint(container, "Free unsharp mask on the final-correction steps only (needs final correction steps > 0). 0 = off, 0.2-0.4 typical.");
row(container, "s_noise",
numCtrl(dc.s_noise, 0, 0.5, 0.01, dk + "-dsn",
(v) => { dc.s_noise = v; save(); }));
renderVelocityBlock(container, dk + "-d", dc, save, saveNow);
// No ALG block here on purpose. This sampler has its own alg_enabled input, but the
// chain sampler's "Anchor blur (ALG)" is the same guidance, works on every sampler,
// and already drives this one when it is on — so showing both put two switches on
// screen for one behaviour, with precedence that depended on which you had set.
// The single control lives in the "Anchor blur (ALG)" group just below this panel;
// engine_settings.js migrates a project that still carries the old switch.
sectionTag(container, "Momentum Guidance (experimental)");
row(container, "enabled",
checkCtrl(!!dc.mg_enabled, dk + "-dmge", (v) => { dc.mg_enabled = v; saveNow(); }));
hint(container, "Momentum Guidance (arXiv:2602.20360). Smooths the step direction toward a running average once sigma drops below the threshold below — the complementary window to ALG's blur. May damp motion as a side effect; experimental.");
if (dc.mg_enabled) {
row(container, "strength",
numCtrl(dc.mg_strength, 0, 1, 0.05, dk + "-dmgs",
(v) => { dc.mg_strength = v; save(); }));
row(container, "decay",
numCtrl(dc.mg_decay, 0, 0.99, 0.05, dk + "-dmgd",
(v) => { dc.mg_decay = v; save(); }));
row(container, "sigma threshold",
numCtrl(dc.mg_sigma_threshold, 0.5, 0.999, 0.005, dk + "-dmgt",
(v) => { dc.mg_sigma_threshold = v; save(); }));
}
} else if (cfg.type === "KSampler") {
sectionTag(container, "KSampler");
// saveNow re-renders, which is how the list refreshes once the live names land.
const names = ksamplerNames(saveNow);
const cur = cfg.ksampler_name || "euler";
row(container, "sampler name",
selCtrl(names.includes(cur) ? names : names.concat([cur]), cur, dk + "-ks",
(v) => { cfg.ksampler_name = v; save(); }));
hint(container, "A KSampler is only the step function — it carries no schedule of its own, "
+ "so the Schedule / Steps above are the only thing that can give it one.");
row(container, "quality sharpness",
numCtrl(cfg.ksampler_sharpness, 0, 1, 0.01, dk + "-kqs",
(v) => { cfg.ksampler_sharpness = v; saveNow(); }));
hint(container, "The same free unsharp mask FunPack's own samplers have, driven from "
+ "outside this sampler's loop. Recovers the fine detail that goes missing "
+ "at low resolution. 0 = off, 0.2-0.4 typical.");
if (+cfg.ksampler_sharpness > 0) {
row(container, "sharpen last %",
numCtrl(cfg.ksampler_sharpen_start_pct, 0, 1, 0.05, dk + "-kqsp",
(v) => { cfg.ksampler_sharpen_start_pct = v; save(); }));
hint(container, "Fraction of the schedule it applies to, counted from the end — the same "
+ "meaning as Hybrid Euler 2S's high quality pct. Sharpening the noisy "
+ "early steps amplifies noise, not detail.");
}
}
}
// ── variability macro (matches Studio exactly) ────────────────────────────────
function applyVariabilityMacro(hc, v) {
v = Math.max(0, Math.min(1, v));
hc.variability = +v.toFixed(2);
hc.eta = +v.toFixed(2);
hc.eta_final = +(Math.max(0, (v - 0.6) / 0.4) * 0.4).toFixed(2);
hc.s_noise = 1.0;
hc.high_quality_pct = 0.35;
hc.quality_sharpness = +(0.15 + v * 0.25).toFixed(2);
if (v < 0.3) {
hc.velocity_bias_mode = "off";
} else {
hc.velocity_bias_mode = "apply";
hc.velocity_bias_source = "nearest";
hc.velocity_bias_strength = +(((v - 0.3) / 0.7) * 1.2).toFixed(2);
}
hc.motion_pulse_mode = v < 0.7 ? "off" : (v < 0.9 ? "balanced" : "aggressive");
}
// ── public API ────────────────────────────────────────────────────────────────
function render(container, samplers, save, saveNow) {
const s = deepMerge(defaultSamplers(), samplers || {});
function mkSave(quiet) {
return () => quiet ? save(s) : saveNow(s);
}
// The high-pass sampler is the one the Movie Editor graph runs (studio outputs 4+5).
renderPass(container, "high", "Sampler", s.high, mkSave(true), mkSave(false));
// Second pass. Studio's low-pass SIGMAS (output 7) and SAMPLER (output 6) are both wired
// to the chain sampler now, so this section can offer the whole pass: its schedule
// always, and its algorithm once "Own sampler" is on. Off, Studio mirrors the high
// pass's algorithm into that output, so a project that already used a second pass keeps
// doing exactly what it did.
sectionTag(container, "Second pass");
const spOn = !!(window.Store?.get()?.project?.sampler_inputs || {}).second_pass;
row(container, "Enable", checkCtrl(spOn, "sp-second-on", (v) => {
window.Store?.setSamplerInputNow?.("second_pass", v);
}));
hint(container, "Samples each scene twice. Pass 1 runs the main schedule in full, then "
+ "pass 2 runs the one below in full — total steps are the two added up.");
const spSig = textCtrl(s.low.sigmas, "e.g. 0.812, 0.6, 0.35, 0.15, 0.0", "sp-second-sig",
(v) => { s.low.sigmas = v; mkSave(true)(); });
row(container, "Schedule", spSig);
hint(container, "Comma-separated, high to low, ending at 0. The FIRST sigma is the strength "
+ "dial: 0.8 reworks the shot, 0.4 polishes, 0.2 is detail work. The rest sets "
+ "how many steps pass 2 gets.");
// Pass 2 can be computed instead of typed, same switch as pass 1.
if (s.low.scheduler === undefined) s.low.scheduler = s.low.ksampler_scheduler || KSAMPLER_USER_SIGMAS;
if (s.low.steps === undefined) s.low.steps = s.low.ksampler_steps === undefined ? 4 : s.low.ksampler_steps;
const spScheds = ksamplerSchedulers(mkSave(false));
const spSchedCur = s.low.scheduler || KSAMPLER_USER_SIGMAS;
row(container, "Schedule (computed)",
selCtrl(spScheds.includes(spSchedCur) ? spScheds : spScheds.concat([spSchedCur]), spSchedCur,
"sp-second-sch", (v) => { s.low.scheduler = v; mkSave(false)(); }));
hint(container, "Leave on '" + KSAMPLER_USER_SIGMAS + "' to use the field above. A computed "
+ "schedule starts at the model's top sigma, so pass 2 reworks rather than polishes.");
if (spSchedCur !== KSAMPLER_USER_SIGMAS) {
row(container, "Steps",
intCtrl(s.low.steps, 1, 200, "sp-second-steps", (v) => { s.low.steps = v; mkSave(true)(); }));
}
if (spOn && spSchedCur === KSAMPLER_USER_SIGMAS && !String(s.low.sigmas || "").trim()) {
hint(container, "Pass 2 is on but has no schedule — give it one above, or pick a computed one.");
}
// A different ALGORITHM for pass 2. Opt-in, because Studio otherwise mirrors pass 1's
// sampler into the socket and every existing second-pass project keeps its behaviour.
row(container, "Own sampler", checkCtrl(!!s.low.own_sampler, "sp-second-own",
(v) => { s.low.own_sampler = v; mkSave(false)(); }));
hint(container, "Off, pass 2 uses the sampler configured above. On, it gets its own — what "
+ "builds a shot well is often not what finishes it: few-step distilled to "
+ "build, an ordinary KSampler with more steps to polish, or the reverse.");
if (s.low.own_sampler) {
const spType = selCtrl(SAMPLER_TYPES, s.low.type || "Distilled Flow", "sp-second-type",
(v) => { s.low.type = v; mkSave(false)(); });
row(container, "Algorithm", spType);
renderAlgorithm(container, "sp-low", s.low, mkSave(true), mkSave(false));
}
}
window.SamplerPanel = { render, defaultSamplers, defaultPass };
})();