diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index b4b9cfe..0690e2f 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -760,6 +760,7 @@ return { generate, look, vetoed, ask, modelAsked }; } var EXACT_GENERATE_PARAM_KEYS = ["steps", "cfg", "sigma_shift"]; + var SESSION_PINNED_PARAM_KEYS = ["sampler", "scheduler", "aspect", "seed"]; function resolveExactProfileDefaults({ exact, profiles, profileName } = {}) { const gen = exact?.generation && typeof exact.generation === "object" ? exact.generation : {}; const profile = profileName || gen.profile || "turbo"; @@ -805,6 +806,14 @@ } } } + for (const key of SESSION_PINNED_PARAM_KEYS) { + if (out[key] != null) { + continue; + } + if (sessionExact?.[key] != null) { + out[key] = sessionExact[key]; + } + } return { patch: out, clearSessionKeys, profile: defaults.profile }; } function attachSession(SA2) { @@ -823,6 +832,7 @@ resolveExactProfileDefaults, mergeExactParamsForGenerate, EXACT_GENERATE_PARAM_KEYS, + SESSION_PINNED_PARAM_KEYS, GEN_KEYS }; } @@ -2221,7 +2231,7 @@ if (parseAspectFromUserText(t)) { return true; } - if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) { + if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw|sampler|scheduler|семплер|шедулер)\b/i.test(t)) { return true; } return cyrTokenRe2( @@ -2509,48 +2519,194 @@ ${patch.prompt}`; badge.title = `\u0420\u0435\u0436\u0438\u043C: ${pack}`; badge.classList.toggle("sa-mode-hot", pack === "critique_image" || pack === "inpaint_edit"); } - function formatLiveParamsLine() { + function valuesMatchParam(a, b, key) { + if (a == null && b == null) { + return true; + } + if (a == null || b == null) { + return false; + } + const numericKeys = /* @__PURE__ */ new Set([ + "steps", + "cfg", + "sigma_shift", + "seed", + "batch", + "images", + "width", + "height", + "init_creativity" + ]); + if (numericKeys.has(key)) { + const na = parseFloat(String(a).replace(",", ".")); + const nb = parseFloat(String(b).replace(",", ".")); + if (Number.isFinite(na) && Number.isFinite(nb)) { + if (key === "cfg" || key === "sigma_shift" || key === "init_creativity") { + return Math.abs(na - nb) < 0.011; + } + return na === nb; + } + } + return String(a).toLowerCase() === String(b).toLowerCase(); + } + function canonicalParamDefaults(profileName) { + const profile = profileName || detectKreaProfileName2(); + const profDefs = exactProfileDefaults(profile); + const gen = state.exact?.generation || {}; + const defaultAspect = gen.aspect || "1:1"; + const defaultSize = sizeFromAspect(defaultAspect) || ASPECT_TABLE["1:1"] || [1024, 1024]; + const batchDefault = gen.images ?? gen.batch ?? 1; + return { + profile, + aspect: defaultAspect, + width: defaultSize[0], + height: defaultSize[1], + steps: profDefs.steps, + cfg: profDefs.cfg, + sigma_shift: profDefs.sigma_shift, + seed: "-1", + batch: batchDefault, + images: batchDefault, + sampler: exactDefaultFor("sampler", profile), + scheduler: exactDefaultFor("scheduler", profile) + }; + } + function detectAppliedProfileName() { + const { profiles } = resolveExactBundle(); + const stepsLive = val("input_steps"); + const cfgLive = val("input_cfgscale") || val("input_cfg"); + for (const name of ["turbo", "raw"]) { + const d = profiles[name]; + if (!d) { + continue; + } + if (valuesMatchParam(stepsLive, d.steps, "steps") && valuesMatchParam(cfgLive, d.cfg, "cfg")) { + return name; + } + } + return null; + } + function paramDisplayTag(label, displayValue, isCustom, defaultHint) { + const cls = isCustom ? "sa-param-tag sa-param-custom" : "sa-param-tag"; + const hint = isCustom && defaultHint != null ? ` title="Exact: ${defaultHint}"` : ""; + return `${escapeHtml2(label)} ${escapeHtml2(String(displayValue))}`; + } + function buildLiveParamsHtml() { + const ckptProfile = detectKreaProfileName2(); + const defs = canonicalParamDefaults(ckptProfile); const w = parseInt(val("input_width") || "0", 10) || null; const h = parseInt(val("input_height") || "0", 10) || null; const aspect = guessAspectFromSize(w, h) || "\u2014"; - const steps = val("input_steps") || "\u2014"; - const cfg = val("input_cfgscale") || val("input_cfg") || "\u2014"; - const sigma = val("input_sigmashift") || ""; - const seed = val("input_seed") || "\u2014"; - const profile = detectKreaProfileName2(); - const batch = val("input_images") || val("input_batchsize") || ""; - const sampler = val("input_sampler") || ""; - const scheduler = val("input_scheduler") || ""; - const parts = [ - aspect, - `${w || "?"}\xD7${h || "?"}`, - `steps ${steps}`, - `cfg ${cfg}` + const stepsRaw = val("input_steps"); + const cfgRaw = val("input_cfgscale") || val("input_cfg"); + const sigmaRaw = val("input_sigmashift"); + const seedRaw = val("input_seed") || "-1"; + const batchRaw = val("input_images") || val("input_batchsize") || "1"; + const samplerRaw = val("input_sampler") || defs.sampler || "euler"; + const schedulerRaw = val("input_scheduler") || defs.scheduler || "normal"; + const appliedProfile = detectAppliedProfileName() || ckptProfile; + const tags = [ + paramDisplayTag( + "aspect", + aspect, + aspect !== "\u2014" && !valuesMatchParam(aspect, defs.aspect, "aspect"), + defs.aspect + ), + paramDisplayTag( + "size", + `${w || "?"}\xD7${h || "?"}`, + !!(w && h && (!valuesMatchParam(w, defs.width, "width") || !valuesMatchParam(h, defs.height, "height"))), + `${defs.width}\xD7${defs.height}` + ), + paramDisplayTag( + "profile", + appliedProfile, + appliedProfile !== ckptProfile, + ckptProfile + ), + paramDisplayTag( + "steps", + stepsRaw || "\u2014", + !!(stepsRaw && !valuesMatchParam(stepsRaw, defs.steps, "steps")), + defs.steps + ), + paramDisplayTag( + "cfg", + cfgRaw || "\u2014", + !!(cfgRaw && !valuesMatchParam(cfgRaw, defs.cfg, "cfg")), + defs.cfg + ), + paramDisplayTag( + "\u03C3", + sigmaRaw || (defs.sigma_shift != null ? defs.sigma_shift : "\u2014"), + !!(sigmaRaw && defs.sigma_shift != null && !valuesMatchParam(sigmaRaw, defs.sigma_shift, "sigma_shift")), + defs.sigma_shift + ), + paramDisplayTag( + "seed", + seedRaw === "-1" || seedRaw === "" ? "rand" : seedRaw, + !valuesMatchParam(seedRaw, defs.seed, "seed"), + "rand (\u22121)" + ), + paramDisplayTag( + "batch", + batchRaw, + !valuesMatchParam(batchRaw, defs.batch, "batch"), + defs.batch + ), + paramDisplayTag( + "sampler", + samplerRaw, + !valuesMatchParam(samplerRaw, defs.sampler, "sampler"), + defs.sampler + ), + paramDisplayTag( + "scheduler", + schedulerRaw, + !valuesMatchParam(schedulerRaw, defs.scheduler, "scheduler"), + defs.scheduler + ) ]; - if (sigma) { - parts.push(`\u03C3 ${sigma}`); + let initCtx = {}; + try { + initCtx = readInitContext(); + } catch (e) { } - parts.push(profile, `seed ${seed}`); - if (batch && batch !== "1") { - parts.push(`\xD7${batch}`); + if (initCtx.has_init_image) { + const cr = initCtx.init_creativity; + const crStr = cr != null ? `@${cr}` : ""; + tags.push(paramDisplayTag("init", `on${crStr}`, true, "off")); } - if (sampler) { - parts.push(sampler); + if (initCtx.has_mask_image) { + tags.push(paramDisplayTag("mask", "on", true, "off")); } - if (scheduler) { - parts.push(scheduler); + const negLive = liveNegativePrompt(); + const negDef = exactDefaultNegative(); + if (negLive && negDef && negLive !== negDef) { + tags.push(paramDisplayTag("neg", "\xB1", true, "Exact default")); + } else if (negLive && !negDef) { + tags.push(paramDisplayTag("neg", "on", true, "empty")); } - return parts.join(" \xB7 "); + const picCount = typeof countPromptImages === "function" ? countPromptImages() : 0; + if (picCount > 0) { + tags.push(paramDisplayTag("prompt img", picCount, true, "0")); + } + return tags.join(""); + } + function formatLiveParamsLine() { + const el = document.createElement("div"); + el.innerHTML = buildLiveParamsHtml(); + return el.textContent || "\u2014"; } function syncLiveParamsBar() { - const line = formatLiveParamsLine(); + const html = buildLiveParamsHtml(); const boardEl = $2("sa_live_params"); const composerEl = $2("sa_composer_params"); if (boardEl) { - boardEl.textContent = line; + boardEl.innerHTML = html; } if (composerEl) { - composerEl.textContent = line; + composerEl.innerHTML = html; } } function applyAspectTableFrom(obj) { @@ -2674,6 +2830,12 @@ ${patch.prompt}`; return { ...gen, ...fromProfile, profile, ...session }; } function exactDefaultFor(key, profileName) { + if (key === "sampler") { + return state.exact?.sampling?.defaults?.sampler ?? "euler"; + } + if (key === "scheduler") { + return state.exact?.sampling?.defaults?.scheduler ?? "normal"; + } const { exact, profiles } = resolveExactBundle(); const profile = profileName || exact.generation?.profile || detectKreaProfileName2(); const fromProfile = profiles[profile]?.[key]; @@ -2696,6 +2858,20 @@ ${patch.prompt}`; state.sessionExact.images = partial.batch; } } + function pinUserSessionParams(patch) { + if (!patch || typeof patch !== "object") { + return; + } + const partial = {}; + for (const k of QUICK_PARAM_KEYS) { + if (patch[k] != null) { + partial[k] = patch[k]; + } + } + if (Object.keys(partial).length) { + rememberSessionExact(partial); + } + } function shouldRememberSessionParam(key, value) { if (state.restoringChat || value == null) { return false; @@ -2736,6 +2912,11 @@ ${patch.prompt}`; state.chatSession.gen[key] = next[key]; } } + for (const key of S.SESSION_PINNED_PARAM_KEYS || ["sampler", "scheduler", "aspect", "seed"]) { + if (next[key] != null) { + state.chatSession.gen[key] = next[key]; + } + } } return next; } @@ -3621,6 +3802,7 @@ ${patch.prompt}`; return; } startBusyUi("silent_gen"); + pullLiveIntoSession(); const S = window.SA && window.SA.session; if (S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch); @@ -3648,6 +3830,9 @@ ${patch.prompt}`; const S = window.SA && window.SA.session; if (state.lastPatch && S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), state.lastPatch); + if (state.lastPatch.loras == null && state.chatSession?.gen) { + state.chatSession.gen.loras = readLiveGenFields().loras || []; + } } if (typeof startBusyUi === "function") startBusyUi(state.lastPatch ? "silent_gen" : "generating"); setStatus(state.lastPatch ? "\u0421\u0435\u0441\u0441\u0438\u044F \u2192 Generate\u2026" : "Generate \u0441 \u0442\u0435\u043A\u0443\u0449\u0435\u0439 \u0441\u0435\u0441\u0441\u0438\u0435\u0439\u2026"); @@ -5158,10 +5343,16 @@ ${patch.prompt}`; recommended_params: { steps: exactDefs.steps, cfg: exactDefs.cfg, - sigma_shift: exactDefs.sigma_shift + sigma_shift: exactDefs.sigma_shift, + sampler: val("input_sampler") || state.exact?.sampling?.defaults?.sampler || "euler", + scheduler: val("input_scheduler") || state.exact?.sampling?.defaults?.scheduler || "normal" }, + sampling_options: state.exact?.sampling || null, ...initCtx }; + if (state.sessionExact && Object.keys(state.sessionExact).length) { + extra.session_exact = { ...state.sessionExact }; + } if (S && typeof S.compactContext === "function") { const ctx = S.compactContext(state.chatSession, { architecture_ok: typeof isKreaSelected === "function" ? isKreaSelected() : true, @@ -5618,18 +5809,22 @@ ${patch.prompt}`; } else if (patch.sigma_shift == null && isEmptyParamField(val("input_sigmashift")) && defaults.sigma_shift != null) { setVal("input_sigmashift", String(defaults.sigma_shift)); } - if (patch.sampler != null) { - if (document.getElementById("input_sampler")) { - setVal("input_sampler", String(patch.sampler)); - } - if (shouldRememberSessionParam("sampler", patch.sampler)) { - rememberSessionExact({ sampler: patch.sampler }); + if (patch.scheduler != null && document.getElementById("input_scheduler")) { + if (!shouldSkipSessionRollback("scheduler", patch.scheduler)) { + setVal("input_scheduler", String(patch.scheduler)); + if (shouldRememberSessionParam("scheduler", patch.scheduler)) { + rememberSessionExact({ scheduler: patch.scheduler }); + } } } - if (patch.scheduler != null && document.getElementById("input_scheduler")) { - setVal("input_scheduler", String(patch.scheduler)); - if (shouldRememberSessionParam("scheduler", patch.scheduler)) { - rememberSessionExact({ scheduler: patch.scheduler }); + if (patch.sampler != null) { + if (!shouldSkipSessionRollback("sampler", patch.sampler)) { + if (document.getElementById("input_sampler")) { + setVal("input_sampler", String(patch.sampler)); + } + if (shouldRememberSessionParam("sampler", patch.sampler)) { + rememberSessionExact({ sampler: patch.sampler }); + } } } const batch = patch.images != null ? patch.images : patch.batch; @@ -6119,6 +6314,9 @@ ${patch.prompt}`; const S = window.SA && window.SA.session; const exactKeys = S && S.EXACT_GENERATE_PARAM_KEYS || ["steps", "cfg", "sigma_shift"]; if (typeof pullLiveIntoSession === "function") pullLiveIntoSession(); + if (state.chatSession?.gen) { + state.chatSession.gen.loras = readLiveGenFields().loras || []; + } if (state.chatSession && state.chatSession.gen) { for (const key of exactKeys) { const fromDelta = deltaBeforeLive[key] != null; @@ -6323,7 +6521,7 @@ ${patch.prompt}`; } setPackValue("critique_image", { flash: true }); if ($2("sa_input")) { - $2("sa_input").value = "Critique this result and improve the prompt for the next generation."; + $2("sa_input").value = "Strict QC of this Generate frame vs the live prompt: list concrete defects and artifacts first, then one fenced JSON patch with an improved prompt and any param/LoRA fixes. No empty praise."; } const gen = generateSlot(); if (gen) { @@ -6354,7 +6552,7 @@ ${patch.prompt}`; } setPackValue("critique_image", { flash: true }); if ($2("sa_input")) { - $2("sa_input").value = "Look at the Generate result and briefly say what worked and what to fix next."; + $2("sa_input").value = "Strict review of the Generate frame vs the prompt: what failed, what artifacts you see, what to change in prompt and params next. Skip hollow compliments."; } setStatus("Auto look_at\u2026"); await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true }); @@ -6386,7 +6584,7 @@ ${patch.prompt}`; const label = (state.genResults || []).find((r) => r.id === state.selectedGenResultId)?.label; setPackValue("critique_image", { flash: true }); if ($2("sa_input")) { - $2("sa_input").value = label ? `\u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442 \xAB${label}\xBB: \u0447\u0442\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C, \u0447\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C, \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u043F\u0442 \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430.` : "\u041F\u043E\u0441\u043C\u043E\u0442\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442: \u0447\u0442\u043E \u043F\u043E\u043B\u0443\u0447\u0438\u043B\u043E\u0441\u044C, \u0447\u0442\u043E \u0441\u043B\u043E\u043C\u0430\u043B\u043E\u0441\u044C, \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u043E\u043C\u043F\u0442 \u0438 \u043F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430."; + $2("sa_input").value = label ? `\u041A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438 \xAB${label}\xBB: \u0447\u0442\u043E \u043D\u0435 \u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441 \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C, \u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B, \u043A\u043E\u043C\u043F\u043E\u0437\u0438\u0446\u0438\u044F/\u0441\u0432\u0435\u0442 \u2014 \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C prompt \u0438 params \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430. \u0411\u0435\u0437 \u043E\u0431\u0449\u0438\u0445 \u043F\u043E\u0445\u0432\u0430\u043B.` : "\u041A\u0440\u0438\u0442\u0438\u0447\u0435\u0441\u043A\u0438 \u0440\u0430\u0437\u0431\u0435\u0440\u0438 \u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442: \u0447\u0442\u043E \u043D\u0435 \u0441\u0445\u043E\u0434\u0438\u0442\u0441\u044F \u0441 \u043F\u0440\u043E\u043C\u043F\u0442\u043E\u043C, \u0430\u0440\u0442\u0435\u0444\u0430\u043A\u0442\u044B, \u043A\u043E\u043C\u043F\u043E\u0437\u0438\u0446\u0438\u044F/\u0441\u0432\u0435\u0442 \u2014 \u0438 \u043A\u0430\u043A \u043F\u043E\u043F\u0440\u0430\u0432\u0438\u0442\u044C prompt \u0438 params \u0434\u043B\u044F \u0441\u043B\u0435\u0434\u0443\u044E\u0449\u0435\u0433\u043E \u043A\u0430\u0434\u0440\u0430. \u0411\u0435\u0437 \u043E\u0431\u0449\u0438\u0445 \u043F\u043E\u0445\u0432\u0430\u043B."; } await sendChat({ forceSlotIds: [GEN_ID], skipAutoPack: true }); } @@ -7041,6 +7239,9 @@ ${patch.prompt}`; const prevPersona = state.config?.persona || $2("sa_persona")?.value || ""; const prevControls = state.config?.control_values && typeof state.config.control_values === "object" ? { ...state.config.control_values } : null; state.config = data; + if (window.SA?.training?.onConfig) { + window.SA.training.onConfig(data); + } if (window.SA?.applyConfigPatchKeys) { window.SA.applyConfigPatchKeys(data); } @@ -7499,6 +7700,10 @@ ${data.ui.help_extra}`.trim(); btn.setAttribute("data-vary", value || "1"); } else if (action === "krea_profile") { btn.setAttribute("data-krea-profile", value); + } else if (action === "sampler") { + btn.setAttribute("data-sampler", value); + } else if (action === "scheduler") { + btn.setAttribute("data-scheduler", value); } box.appendChild(btn); } @@ -8486,43 +8691,64 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; const base = s.split(/[/\\]/).pop() || s; return base.replace(/\.safetensors$/i, "").slice(0, 28); } - function renderLoraChips() { - const root = $2("sa_lora_chips"); - if (!root) { - return; - } - root.innerHTML = ""; - let selected = []; + function readSelectedLoras() { try { - if (typeof loraHelper !== "undefined" && Array.isArray(loraHelper?.selected)) { - selected = loraHelper.selected.map((l) => ({ + if (typeof loraHelper !== "undefined" && loraHelper && Array.isArray(loraHelper.selected)) { + return loraHelper.selected.map((l) => ({ name: l.name || l, weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l] || 1 })); } } catch (e) { } + return []; + } + function syncLorasToSession(loras) { + const S = window.SA && window.SA.session; + if (!S) { + return; + } + const list = Array.isArray(loras) ? loras : readSelectedLoras(); + state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), { loras: list }); + } + async function applyLorasList(loras) { + await applyPatch({ loras }, "loras"); + syncLorasToSession(loras); + renderLoraChips(); + } + function renderLoraChips() { + const root = $2("sa_lora_chips"); + if (!root) { + return; + } + root.innerHTML = ""; + const selected = readSelectedLoras(); for (const l of selected) { - const btn = document.createElement("button"); - btn.type = "button"; - btn.className = "sa-lora-chip"; - btn.title = `${l.name} \xD7${l.weight} \u2014 \u043A\u043B\u0438\u043A \u0441\u043D\u044F\u0442\u044C`; - btn.textContent = `${shortLoraName(l.name)} ${Number(l.weight).toFixed(2)}`; - btn.addEventListener("click", () => { - try { - if (typeof loraHelper !== "undefined" && typeof loraHelper.removeLora === "function") { - loraHelper.removeLora(l.name); - } else if (loraHelper?.selected) { - loraHelper.selected = loraHelper.selected.filter((x) => (x.name || x) !== l.name); - if (typeof loraHelper.rebuildUI === "function") { - loraHelper.rebuildUI(); - } - } - } catch (e) { - } - renderLoraChips(); + const chip = document.createElement("div"); + chip.className = "sa-lora-chip"; + chip.title = l.name; + const main = document.createElement("button"); + main.type = "button"; + main.className = "sa-lora-chip-main"; + main.textContent = `${shortLoraName(l.name)} ${Number(l.weight).toFixed(2)}`; + main.title = `${l.name} \u2014 \u043A\u043B\u0438\u043A \u0438\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u0441\u0438\u043B\u0443`; + main.addEventListener("click", (e) => { + e.stopPropagation(); + openLoraWeightEditor(main, l.name, l.weight); }); - root.appendChild(btn); + const rm = document.createElement("button"); + rm.type = "button"; + rm.className = "sa-lora-chip-rm"; + rm.textContent = "\xD7"; + rm.title = "\u0421\u043D\u044F\u0442\u044C LoRA"; + rm.addEventListener("click", async (e) => { + e.stopPropagation(); + const next = selected.filter((x) => x.name !== l.name); + await applyLorasList(next); + }); + chip.appendChild(main); + chip.appendChild(rm); + root.appendChild(chip); } const add = document.createElement("button"); add.type = "button"; @@ -8561,23 +8787,17 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; btn.textContent = `${shortLoraName(name)}${l.krea_likely ? " \xB7 krea" : ""}`; btn.title = name; btn.addEventListener("click", async () => { - await applyPatch({ - loras: [ - ...(() => { - try { - return (loraHelper?.selected || []).map((x) => ({ - name: x.name || x, - weight: loraHelper.loraWeightPref && loraHelper.loraWeightPref[x.name || x] || 1 - })); - } catch (e) { - return []; - } - })(), - { name, weight: l.default_weight ? parseFloat(l.default_weight) : 0.8, triggers: l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []) } - ] - }, "loras"); + const cur = readSelectedLoras(); + const next = [ + ...cur.filter((x) => x.name !== name), + { + name, + weight: l.default_weight ? parseFloat(l.default_weight) : 0.8, + triggers: l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []) + } + ]; + await applyLorasList(next); picker.remove(); - renderLoraChips(); }); list.appendChild(btn); if (++n >= 40) { @@ -8602,6 +8822,68 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; setTimeout(() => document.addEventListener("mousedown", onDoc), 0); filter.focus(); } + function openLoraWeightEditor(anchor, name, weight) { + document.querySelectorAll(".sa-lora-weight-pop").forEach((n) => n.remove()); + const pop = document.createElement("div"); + pop.className = "sa-lora-weight-pop"; + const label = document.createElement("label"); + label.textContent = shortLoraName(name); + const input = document.createElement("input"); + input.type = "number"; + input.min = "0"; + input.max = "2"; + input.step = "0.05"; + input.value = String(Number(weight) || 1); + input.title = "\u0421\u0438\u043B\u0430 LoRA (0\u20132)"; + label.appendChild(input); + pop.appendChild(label); + const row = document.createElement("div"); + row.className = "sa-lora-weight-actions"; + const ok = document.createElement("button"); + ok.type = "button"; + ok.className = "basic-button sa-primary"; + ok.textContent = "OK"; + const cancel = document.createElement("button"); + cancel.type = "button"; + cancel.className = "basic-button"; + cancel.textContent = "\u041E\u0442\u043C\u0435\u043D\u0430"; + row.appendChild(ok); + row.appendChild(cancel); + pop.appendChild(row); + const apply = async () => { + const w = Math.min(2, Math.max(0, parseFloat(input.value) || 0)); + const next = readSelectedLoras().map((l) => l.name === name ? { ...l, weight: w } : l); + await applyLorasList(next); + pop.remove(); + }; + ok.addEventListener("click", apply); + cancel.addEventListener("click", () => pop.remove()); + input.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + apply(); + } + if (e.key === "Escape") { + pop.remove(); + } + }); + const composer = $2("sa_composer") || document.body; + composer.style.position = composer.style.position || "relative"; + composer.appendChild(pop); + const rect = anchor.getBoundingClientRect(); + const cRect = composer.getBoundingClientRect(); + pop.style.left = `${Math.max(4, rect.left - cRect.left)}px`; + pop.style.top = `${rect.bottom - cRect.top + 4}px`; + input.focus(); + input.select(); + const onDoc = (ev) => { + if (!pop.contains(ev.target) && ev.target !== anchor) { + pop.remove(); + document.removeEventListener("mousedown", onDoc, true); + } + }; + setTimeout(() => document.addEventListener("mousedown", onDoc, true), 0); + } function inventoryIsStale(maxAgeMs = 2e4) { if (!state.inventoryFetchedAt) { return true; @@ -8784,6 +9066,9 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; }); } if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); + if (effective?.loras == null && state.chatSession?.gen) { + state.chatSession.gen.loras = readLiveGenFields().loras || []; + } await pushSessionToSwarm(state.chatSession); if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); if (typeof appendSystemNote === "function") { @@ -8857,6 +9142,9 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } if (S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions); + if (withActions.loras == null && state.chatSession?.gen) { + state.chatSession.gen.loras = readLiveGenFields().loras || []; + } } state._quietParamApply = (state._quietParamApply || 0) + 1; try { @@ -8870,6 +9158,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } finally { state._quietParamApply = Math.max(0, (state._quietParamApply || 1) - 1); } + pinUserSessionParams(withActions); state.lastUserParamIntent = prevIntent; setStatus(note || (wantGenerate ? "Applied" : "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B (\u0431\u0435\u0437 Generate)")); if (wantGenerate) { @@ -8883,15 +9172,44 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (!bar) { return; } + const defs = canonicalParamDefaults(); + const ckptProfile = detectKreaProfileName2(); const cur = guessAspectFromSize(val("input_width"), val("input_height")); const seed = val("input_seed"); bar.querySelectorAll("[data-aspect]").forEach((btn) => { - btn.classList.toggle("sa-chip-active", btn.getAttribute("data-aspect") === cur); + const aspectVal = btn.getAttribute("data-aspect"); + const active = aspectVal === cur; + btn.classList.toggle("sa-chip-active", active); + btn.classList.toggle("sa-chip-custom", active && !valuesMatchParam(aspectVal, defs.aspect, "aspect")); }); bar.querySelectorAll("[data-seed]").forEach((btn) => { const mode = btn.getAttribute("data-seed"); const active = mode === "lock" && seed && seed !== "-1" || mode === "random" && (!seed || seed === "-1"); btn.classList.toggle("sa-chip-active", active); + btn.classList.toggle("sa-chip-custom", mode === "lock" && active); + }); + bar.querySelectorAll("[data-krea-profile]").forEach((btn) => { + const p = btn.getAttribute("data-krea-profile"); + const profDefs = exactProfileDefaults(p); + const stepsMatch = valuesMatchParam(val("input_steps"), profDefs.steps, "steps"); + const cfgMatch = valuesMatchParam(val("input_cfgscale") || val("input_cfg"), profDefs.cfg, "cfg"); + const active = stepsMatch && cfgMatch; + btn.classList.toggle("sa-chip-active", active); + btn.classList.toggle("sa-chip-custom", active && p !== ckptProfile); + }); + const curSampler = (val("input_sampler") || defs.sampler || "").toLowerCase(); + const curScheduler = (val("input_scheduler") || defs.scheduler || "").toLowerCase(); + bar.querySelectorAll("[data-sampler]").forEach((btn) => { + const v = btn.getAttribute("data-sampler").toLowerCase(); + const active = v === curSampler; + btn.classList.toggle("sa-chip-active", active); + btn.classList.toggle("sa-chip-custom", active && !valuesMatchParam(v, defs.sampler, "sampler")); + }); + bar.querySelectorAll("[data-scheduler]").forEach((btn) => { + const v = btn.getAttribute("data-scheduler").toLowerCase(); + const active = v === curScheduler; + btn.classList.toggle("sa-chip-active", active); + btn.classList.toggle("sa-chip-custom", active && !valuesMatchParam(v, defs.scheduler, "scheduler")); }); } function appendSystemNote(text) { @@ -9096,7 +9414,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; slot.attach = true; renderBoard(); if ($2("sa_input")) { - $2("sa_input").value = `Look at ${id} and describe what you see.`; + const critical = id === GEN_ID || slot.type === "generate"; + $2("sa_input").value = critical ? "Strict QC of this Generate frame vs the live prompt: concrete defects first, then JSON patch with prompt/param fixes. No empty praise." : `Critically review ${id} vs the intended scene: defects and how to fix prompt/params.`; } setPackValue("critique_image", { flash: true }); await sendChat({ forceSlotIds: [id], skipAutoPack: true }); @@ -9153,6 +9472,26 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; await applyQuickPatch({ vary: true, seed: -1 }, "Vary (new seed)", { generate: true }); return true; } + if (cmd === "sampler" || cmd === "\u0441\u0435\u043C\u043F\u043B\u0435\u0440") { + const name = (arg || "").trim().toLowerCase(); + if (!name) { + const opts = (state.exact?.sampling?.samplers || ["euler", "heun", "dpmpp_2m"]).join(", "); + setStatus(`Usage: /sampler ${opts}`); + return true; + } + await applyQuickPatch({ sampler: name }, `Sampler ${name}`); + return true; + } + if (cmd === "scheduler" || cmd === "\u0448\u0435\u0434\u0443\u043B\u0435\u0440") { + const name = (arg || "").trim().toLowerCase(); + if (!name) { + const opts = (state.exact?.sampling?.schedulers || ["normal", "simple", "karras"]).join(", "); + setStatus(`Usage: /scheduler ${opts}`); + return true; + } + await applyQuickPatch({ scheduler: name }, `Scheduler ${name}`); + return true; + } if (cmd === "inventory" || cmd === "inv") { setStatus("Rescanning models\u2026"); triggerSwarmModelRefresh(async () => { @@ -10164,6 +10503,8 @@ ${HELP_TEXT}`); const seed = btn.getAttribute("data-seed"); const vary = btn.getAttribute("data-vary"); const profile = btn.getAttribute("data-krea-profile"); + const sampler = btn.getAttribute("data-sampler"); + const scheduler = btn.getAttribute("data-scheduler"); if (aspect) { await applyQuickPatch({ aspect }, `Aspect ${aspect}`); } else if (seed === "lock") { @@ -10186,6 +10527,10 @@ ${HELP_TEXT}`); cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift }, "RAW"); + } else if (sampler) { + await applyQuickPatch({ sampler }, `Sampler ${sampler}`); + } else if (scheduler) { + await applyQuickPatch({ scheduler }, `Scheduler ${scheduler}`); } renderLoraChips(); }); @@ -10326,6 +10671,7 @@ ${HELP_TEXT}`); function escapeHtml(s) { return String(s ?? "").replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); } + var QLORA_HF_CUSTOM = "__custom__"; function attachTraining(SA2) { const state = { ttab: "dataset", @@ -10336,6 +10682,7 @@ ${HELP_TEXT}`); hfMapping: null, trainWs: null, polling: null, + qloraTraining: null, agentSettings: { enabled: true, auto_link_on_approve: true, heard_quota: 3 }, agentLinked: 0 }; @@ -10360,6 +10707,8 @@ ${HELP_TEXT}`); if ($("sa_agent_heard_quota")) $("sa_agent_heard_quota").value = String(state.agentSettings.heard_quota); setAgentHeardStats(state.agentLinked); } catch (e) { + const msg = String(e.message || e); + setTrainStatus(isSqliteError(msg) ? `${msg} \u2014 ${sqliteHint()}` : msg); console.warn("loadAgentHeardSettings", e); } } @@ -10394,6 +10743,51 @@ ${HELP_TEXT}`); const el = $("sa_train_status"); if (el) el.textContent = msg || ""; } + function isSqliteError(msg) { + const s = String(msg || "").toLowerCase(); + return s.includes("sqlite") || s.includes("sqlconnection"); + } + function sqliteHint() { + return "\u0411\u0430\u0437\u0430 Assistent (SQLite) \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0430 \u2014 gpu-rent seed-extensions + restart SwarmUI (\u22650.15.6)."; + } + function parseJobProgress(job) { + if (!job) return null; + const raw = job.progress_json; + if (!raw) return null; + try { + return typeof raw === "string" ? JSON.parse(raw) : raw; + } catch { + return null; + } + } + function renderLastTrainJob(job) { + const box = $("sa_train_last_job"); + if (!box) return; + if (!job) { + box.hidden = true; + box.innerHTML = ""; + return; + } + const prog = parseJobProgress(job); + const status = job.status || prog?.status || "\u2014"; + const out = job.output_name || prog?.ollama?.name || "\u2014"; + const base = job.base_model || "\u2014"; + const ollama = prog?.ollama; + let ollamaLine = ""; + if (ollama?.success) { + ollamaLine = `
${escapeHtml(base)} \u2192 \u0438\u043C\u044F: ${escapeHtml(out)}${escapeHtml(String(prog.log).slice(-4e3))}` : ""}`;
+ }
function setTrainingTab(id) {
state.ttab = id || "dataset";
document.querySelectorAll("#sa_training .sa-ttab").forEach((btn) => {
@@ -10420,9 +10814,23 @@ ${HELP_TEXT}`);
const status = $("sa_train_filter_status")?.value || "all";
const persona = $("sa_train_filter_persona")?.value || "all";
const data = await SA2.request("AssistentListTrainSamples", { status, persona, limit: 300 });
+ if (data?.error) {
+ const msg = data.error;
+ setTrainStatus(isSqliteError(msg) ? `${msg} \u2014 ${sqliteHint()}` : msg);
+ const stats2 = $("sa_train_stats");
+ if (stats2) stats2.textContent = "\u0414\u0430\u0442\u0430\u0441\u0435\u0442 \u043D\u0435\u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D (SQLite)";
+ state.samples = [];
+ renderSamples();
+ return;
+ }
state.samples = data?.samples || [];
const stats = $("sa_train_stats");
- if (stats) stats.textContent = `\u041E\u0434\u043E\u0431\u0440\u0435\u043D\u043E: ${data?.approved ?? "\u2014"} \xB7 \u0432\u0441\u0435\u0433\u043E: ${data?.total ?? "\u2014"}`;
+ if (stats) {
+ const appr = data?.approved ?? "\u2014";
+ const draft = data?.draft ?? "\u2014";
+ const total = data?.total ?? "\u2014";
+ stats.textContent = `\u041E\u0434\u043E\u0431\u0440\u0435\u043D\u043E: ${appr} \xB7 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438: ${draft} \xB7 \u0432\u0441\u0435\u0433\u043E: ${total}`;
+ }
const personaSel = $("sa_train_filter_persona");
if (personaSel && $("sa_persona")) {
const cur = personaSel.value || "all";
@@ -10443,8 +10851,10 @@ ${HELP_TEXT}`);
function renderSamples() {
const root = $("sa_train_samples");
if (!root) return;
+ const filter = $("sa_train_filter_status")?.value || "all";
if (!state.samples.length) {
- root.innerHTML = '/help.generate./help.generate; steps/cfg подставляет клиент из Exact.Base model — HF id (safetensors). Тренер скачает веса сам (нужен HF_TOKEN в User Settings).
- + + @@ -279,7 +284,8 @@Обученные и созданные модели (Ollama tags).
+После QLoRA/Modelfile — итог здесь и в списке Ollama. Выбери модель в шапке Assistent или ⚙ → Модели.
+${escapeHtml(base)} → имя: ${escapeHtml(out)}${escapeHtml(String(prog.log).slice(-4000))}` : ''}`;
+ }
+
function setTrainingTab(id) {
state.ttab = id || 'dataset';
document.querySelectorAll('#sa_training .sa-ttab').forEach((btn) => {
@@ -111,6 +165,15 @@ export function attachTraining(SA) {
const status = $('sa_train_filter_status')?.value || 'all';
const persona = $('sa_train_filter_persona')?.value || 'all';
const data = await SA.request('AssistentListTrainSamples', { status, persona, limit: 300 });
+ if (data?.error) {
+ const msg = data.error;
+ setTrainStatus(isSqliteError(msg) ? `${msg} — ${sqliteHint()}` : msg);
+ const stats = $('sa_train_stats');
+ if (stats) stats.textContent = 'Датасет недоступен (SQLite)';
+ state.samples = [];
+ renderSamples();
+ return;
+ }
state.samples = data?.samples || [];
const stats = $('sa_train_stats');
if (stats) {
@@ -233,8 +296,103 @@ export function attachTraining(SA) {
return state.hfMapping;
}
+ function getQloraHfBase() {
+ const sel = $('sa_qlora_base');
+ if (!sel) return '';
+ if (sel.value === QLORA_HF_CUSTOM) {
+ return ($('sa_qlora_base_custom')?.value || '').trim();
+ }
+ return (sel.value || '').trim();
+ }
+
+ function applyQloraPresetFromSelect({ fillName = true } = {}) {
+ const sel = $('sa_qlora_base');
+ const customRow = $('sa_qlora_base_custom_row');
+ if (!sel) return;
+ if (sel.value === QLORA_HF_CUSTOM) {
+ if (customRow) customRow.hidden = false;
+ return;
+ }
+ if (customRow) customRow.hidden = true;
+ const presetJson = sel.selectedOptions[0]?.dataset?.preset;
+ if (!presetJson) return;
+ let preset;
+ try {
+ preset = JSON.parse(presetJson);
+ } catch {
+ return;
+ }
+ const nameEl = $('sa_qlora_name');
+ if (fillName && nameEl && !nameEl.value.trim() && preset.default_output) {
+ nameEl.value = preset.default_output;
+ }
+ const ollamaSel = $('sa_qlora_ollama_base');
+ if (ollamaSel && preset.ollama_hint) {
+ const hint = preset.ollama_hint;
+ if ([...ollamaSel.options].some((o) => o.value === hint)) {
+ ollamaSel.value = hint;
+ }
+ }
+ if (preset.rank != null && $('sa_qlora_rank')) {
+ $('sa_qlora_rank').value = preset.rank;
+ }
+ if (preset.seq_len != null && $('sa_qlora_seq')) {
+ $('sa_qlora_seq').value = preset.seq_len;
+ }
+ }
+
+ function populateQloraHfPresets(training) {
+ const sel = $('sa_qlora_base');
+ if (!sel) return;
+ const prevBase = getQloraHfBase();
+ const models = Array.isArray(training?.hf_models) ? training.hf_models : [];
+ sel.innerHTML = '';
+ for (const m of models) {
+ const hfId = (m.hf_id || m.id || '').trim();
+ if (!hfId) continue;
+ const opt = document.createElement('option');
+ opt.value = hfId;
+ opt.textContent = m.title ? `${m.title} (${hfId})` : hfId;
+ opt.dataset.preset = JSON.stringify(m);
+ sel.appendChild(opt);
+ }
+ const customOpt = document.createElement('option');
+ customOpt.value = QLORA_HF_CUSTOM;
+ customOpt.textContent = 'Другая (ввести HF id…)';
+ sel.appendChild(customOpt);
+ if (prevBase && [...sel.options].some((o) => o.value === prevBase)) {
+ sel.value = prevBase;
+ } else if (prevBase) {
+ sel.value = QLORA_HF_CUSTOM;
+ const custom = $('sa_qlora_base_custom');
+ if (custom) custom.value = prevBase;
+ } else if (models.length) {
+ sel.value = (models[0].hf_id || models[0].id || '').trim();
+ }
+ applyQloraPresetFromSelect({ fillName: !prevBase });
+ }
+
+ async function ensureTrainingQloraConfig(force = false) {
+ if (!force && state.qloraTraining) {
+ return state.qloraTraining;
+ }
+ try {
+ const persona = $('sa_persona')?.value || '';
+ const data = await SA.request('AssistentGetConfig', { persona });
+ state.qloraTraining = data?.training && typeof data.training === 'object'
+ ? data.training
+ : { hf_models: [] };
+ return state.qloraTraining;
+ } catch {
+ state.qloraTraining = state.qloraTraining || { hf_models: [] };
+ return state.qloraTraining;
+ }
+ }
+
async function syncQloraModels() {
try {
+ const training = await ensureTrainingQloraConfig();
+ populateQloraHfPresets(training);
const baseUrl = $('sa_base_url')?.value || localStorage.getItem('swarm_assistent_base_url') || '';
const data = await SA.request('AssistentListModels', { baseUrl });
const models = data?.models || [];
@@ -250,6 +408,7 @@ export function attachTraining(SA) {
}
if (cur) sel.value = cur;
else if ($('sa_model')?.value) sel.value = $('sa_model').value;
+ applyQloraPresetFromSelect({ fillName: false });
} catch (e) { /* ignore */ }
}
@@ -363,8 +522,9 @@ export function attachTraining(SA) {
$('sa_train_samples')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (e) {
const err = String(e.message || e);
- setTrainStatus(err);
- if (hfSt) hfSt.textContent = err;
+ const show = isSqliteError(err) ? `${err} — ${sqliteHint()}` : err;
+ setTrainStatus(show);
+ if (hfSt) hfSt.textContent = show;
} finally {
if (hfSt) hfSt.classList.remove('sa-hf-busy');
if (btn) {
@@ -477,17 +637,19 @@ export function attachTraining(SA) {
if (status === 'completed' || status === 'completed_with_warnings') {
const ollama = prog?.ollama;
if (ollama?.success) {
- setTrainStatus(`Готово: модель ${ollama.name} в Ollama`);
+ setTrainStatus(`Готово: модель ${ollama.name} в Ollama — вкладка «Модели»`);
SA.app?.refreshModels?.();
} else if (ollama?.skipped) {
- setTrainStatus(ollama.note || ollama.error || 'Адаптер сохранён, Ollama — вручную');
+ setTrainStatus(ollama.note || ollama.error || 'Адаптер сохранён — см. вкладку «Модели»');
} else if (ollama?.error) {
setTrainStatus(`Обучение OK, Ollama: ${ollama.error}`);
} else if (status === 'completed_with_warnings') {
- setTrainStatus('Обучение завершено с предупреждениями — см. лог');
+ setTrainStatus('Завершено с предупреждениями — лог на «Модели»');
} else {
- setTrainStatus('QLoRA завершено');
+ setTrainStatus('QLoRA завершено — вкладка «Модели»');
}
+ setTrainingTab('models');
+ await refreshTrainModels();
} else if (status === 'failed') {
setTrainStatus(`Ошибка тренировки (exit ${prog?.exit_code ?? '?'})`);
}
@@ -509,6 +671,18 @@ export function attachTraining(SA) {
}
async function startQlora() {
+ const baseModel = getQloraHfBase();
+ const outputName = ($('sa_qlora_name')?.value || '').trim();
+ if (!baseModel) {
+ setTrainStatus('Выберите HF base model из списка или укажите свой HF id');
+ $('sa_qlora_base')?.focus();
+ return;
+ }
+ if (!outputName) {
+ setTrainStatus('Укажите имя модели в Ollama (например my-lora:v1)');
+ $('sa_qlora_name')?.focus();
+ return;
+ }
setTrainStatus('Запуск…');
try {
const hfDs = ($('sa_qlora_hf_dataset')?.value || '').trim();
@@ -516,9 +690,9 @@ export function attachTraining(SA) {
await SA.request('AssistentStartTrainJob', {
base_url: $('sa_base_url')?.value,
chat_model: $('sa_model')?.value,
- base_model: $('sa_qlora_base')?.value,
+ base_model: baseModel,
ollama_base: $('sa_qlora_ollama_base')?.value,
- output_name: $('sa_qlora_name')?.value,
+ output_name: outputName,
rank: Number($('sa_qlora_rank')?.value) || 16,
alpha: Number($('sa_qlora_alpha')?.value) || 32,
lr: Number($('sa_qlora_lr')?.value) || 0.0002,
@@ -556,13 +730,20 @@ export function attachTraining(SA) {
const root = $('sa_train_models_list');
if (!root) return;
try {
+ const jobData = await SA.request('AssistentGetTrainJob', {});
+ renderLastTrainJob(jobData?.last_job || jobData?.job);
const data = await SA.request('AssistentListModels', { baseUrl: $('sa_base_url')?.value });
const models = data?.models || [];
+ const lastOut = jobData?.last_job?.output_name;
root.innerHTML = models.length
- ? models.map((m) => `