From b0f2736f658c96fd54d37407fb1cda85a73295a6 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Sun, 23 Aug 2026 19:59:45 +0300 Subject: [PATCH] Ship Assistent 0.15.11: collaborative params, richer UI, and strict image critique. Adds session_exact pinning, sampler/scheduler chips, expanded param tags with non-default highlighting, QLoRA HF presets, LoRA strength editing, SQLite bootstrap for training memory, last-job UI, and harsher critique_image QC so result review leads with defects instead of praise. Co-authored-by: Cursor --- Assets/assistent.bundle.js | 754 +++++++++++++++++++++++---- Assets/assistent.css | 150 +++++- AssistentConfig.cs | 6 +- AssistentMemory.Training.cs | 66 ++- AssistentMemory.cs | 1 + AssistentMemoryApi.cs | 6 +- AssistentSqliteBootstrap.cs | 22 + AssistentTraining.cs | 2 + Config/_base/core/core.md | 8 +- Config/_base/exact.json | 10 +- Config/_base/packs/critique_image.md | 22 +- Config/_base/packs/fix_params.md | 4 +- Config/_base/packs/ordinary.md | 2 +- Config/_base/training-qlora.json | 49 ++ Config/_base/ui.json | 16 +- SwarmAssistentExtension.cs | 2 +- SwarmAssistentExtension.csproj | 12 + Tabs/Text2Image/Assistent.html | 18 +- src/app.js | 527 +++++++++++++++---- src/session.js | 12 + src/training.js | 213 +++++++- test/patch.test.js | 37 ++ 22 files changed, 1674 insertions(+), 265 deletions(-) create mode 100644 AssistentSqliteBootstrap.cs create mode 100644 Config/_base/training-qlora.json 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 = `
Ollama: ${escapeHtml(ollama.name || out)} \u2014 \u0432\u044B\u0431\u0435\u0440\u0438 \u0432 \u0448\u0430\u043F\u043A\u0435 \u0447\u0430\u0442\u0430
`; + } else if (ollama?.error) { + ollamaLine = `
Ollama: ${escapeHtml(ollama.error)}
`; + } else if (ollama?.skipped) { + ollamaLine = `
${escapeHtml(ollama.note || "\u0410\u0434\u0430\u043F\u0442\u0435\u0440 \u043D\u0430 \u0434\u0438\u0441\u043A\u0435, ollama create \u0432\u0440\u0443\u0447\u043D\u0443\u044E")}
`; + } + box.hidden = false; + box.innerHTML = ` +
\u041F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \xB7 ${escapeHtml(job.kind || "qlora")} \xB7 ${escapeHtml(status)}
+
HF base: ${escapeHtml(base)} \u2192 \u0438\u043C\u044F: ${escapeHtml(out)}
+ ${ollamaLine} + ${prog?.log ? `
${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 = '
\u041D\u0435\u0442 \u043F\u0440\u0438\u043C\u0435\u0440\u043E\u0432. \u041E\u0442\u043C\u0435\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u044B \u0432 \u0447\u0430\u0442\u0435 \u0438\u043B\u0438 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0439 \u0434\u0430\u0442\u0430\u0441\u0435\u0442.
'; + const hint = filter === "approved" ? "\u041F\u043E\u0434 \u0444\u0438\u043B\u044C\u0442\u0440\u043E\u043C \xAB\u041E\u0434\u043E\u0431\u0440\u0435\u043D\u043D\u044B\u0435\xBB \u043F\u0443\u0441\u0442\u043E. HF-\u0438\u043C\u043F\u043E\u0440\u0442 \u0441\u043E\u0437\u0434\u0430\u0451\u0442 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u2014 \u043F\u0435\u0440\u0435\u043A\u043B\u044E\u0447\u0438 \u043D\u0430 \xAB\u0427\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438\xBB \u0438\u043B\u0438 \xAB\u0412\u0441\u0435 \u0441\u0442\u0430\u0442\u0443\u0441\u044B\xBB." : filter === "draft" ? "\u041D\u0435\u0442 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u043E\u0432. \u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0439 HF \u0438\u043B\u0438 \u043E\u0442\u043C\u0435\u0442\u044C \u043F\u0440\u0438\u043C\u0435\u0440\u044B \u0432 \u0447\u0430\u0442\u0435." : "\u041D\u0435\u0442 \u043F\u0440\u0438\u043C\u0435\u0440\u043E\u0432. \u041E\u0442\u043C\u0435\u0442\u044C \u043E\u0442\u0432\u0435\u0442\u044B \u0432 \u0447\u0430\u0442\u0435 \u0438\u043B\u0438 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u0439 \u0434\u0430\u0442\u0430\u0441\u0435\u0442 (HF / \u0444\u0430\u0439\u043B)."; + root.innerHTML = `
${hint}
`; return; } root.innerHTML = ""; @@ -10525,8 +10935,97 @@ ${HELP_TEXT}`); } 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 = "\u0414\u0440\u0443\u0433\u0430\u044F (\u0432\u0432\u0435\u0441\u0442\u0438 HF id\u2026)"; + 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 SA2.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 SA2.request("AssistentListModels", { baseUrl }); const models = data?.models || []; @@ -10542,6 +11041,7 @@ ${HELP_TEXT}`); } if (cur) sel.value = cur; else if ($("sa_model")?.value) sel.value = $("sa_model").value; + applyQloraPresetFromSelect({ fillName: false }); } catch (e) { } } @@ -10601,10 +11101,14 @@ ${HELP_TEXT}`); } } async function importHf() { + const link = ($("sa_hf_link")?.value || "").trim(); + if (!state.hfCheck?.id && link) { + await checkHfLink(); + } if (!state.hfSelected && !state.hfCheck?.id) { setTrainStatus("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043D\u0430\u0431\u043E\u0440"); const hfSt2 = $("sa_hf_status"); - if (hfSt2) hfSt2.textContent = "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043D\u0430\u0436\u043C\u0438 \xAB\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C\xBB"; + if (hfSt2) hfSt2.textContent = "\u0412\u0441\u0442\u0430\u0432\u044C \u0441\u0441\u044B\u043B\u043A\u0443 \u0438 \u043D\u0430\u0436\u043C\u0438 \xAB\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C\xBB"; return; } const id = state.hfSelected || state.hfCheck.id; @@ -10612,9 +11116,12 @@ ${HELP_TEXT}`); const mapping = buildHfMappingPayload(); const btn = $("sa_btn_hf_import"); const hfSt = $("sa_hf_status"); - const busy = "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E\u2026"; + const busy = limit > 400 ? `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E \u0434\u043E ${limit} \u0441\u0442\u0440\u043E\u043A\u2026 (1\u20132 \u043C\u0438\u043D)` : `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E \u0434\u043E ${limit}\u2026`; setTrainStatus(busy); - if (hfSt) hfSt.textContent = busy; + if (hfSt) { + hfSt.textContent = busy; + hfSt.classList.add("sa-hf-busy"); + } if (btn) { btn.disabled = true; btn.dataset.label = btn.textContent; @@ -10627,9 +11134,13 @@ ${HELP_TEXT}`); if (data?.runner_only) { msg = `Runner-only: ${data.note || id} (\u0432 sqlite \u043D\u0435 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043B\u0438)`; } else if (n > 0) { - msg = `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: ${n} \u2014 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u043D\u0438\u0436\u0435`; + const filt = $("sa_train_filter_status"); + if (filt && filt.value === "approved") { + filt.value = "draft"; + } + msg = `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: ${n} \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A(\u043E\u0432) \u2014 \u0441\u043F\u0438\u0441\u043E\u043A \u043D\u0438\u0436\u0435 (\u0444\u0438\u043B\u044C\u0442\u0440 \u2192 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438)`; } else { - msg = "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: 0 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C HF token, \u043C\u0430\u043F\u043F\u0438\u043D\u0433 \u043A\u043E\u043B\u043E\u043D\u043E\u043A \u0438\u043B\u0438 \u043B\u0438\u043C\u0438\u0442 \u0441\u0442\u0440\u043E\u043A"; + msg = "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: 0 \u2014 HF token \u0432 User Settings, \u043C\u0430\u043F\u043F\u0438\u043D\u0433 \u0438\u043B\u0438 gated-\u043D\u0430\u0431\u043E\u0440"; } setTrainStatus(msg); if (hfSt) hfSt.textContent = msg; @@ -10637,9 +11148,11 @@ ${HELP_TEXT}`); $("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} \u2014 ${sqliteHint()}` : err; + setTrainStatus(show); + if (hfSt) hfSt.textContent = show; } finally { + if (hfSt) hfSt.classList.remove("sa-hf-busy"); if (btn) { btn.disabled = false; btn.textContent = btn.dataset.label || "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C"; @@ -10740,17 +11253,19 @@ ${HELP_TEXT}`); if (status === "completed" || status === "completed_with_warnings") { const ollama = prog?.ollama; if (ollama?.success) { - setTrainStatus(`\u0413\u043E\u0442\u043E\u0432\u043E: \u043C\u043E\u0434\u0435\u043B\u044C ${ollama.name} \u0432 Ollama`); + setTrainStatus(`\u0413\u043E\u0442\u043E\u0432\u043E: \u043C\u043E\u0434\u0435\u043B\u044C ${ollama.name} \u0432 Ollama \u2014 \u0432\u043A\u043B\u0430\u0434\u043A\u0430 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB`); SA2.app?.refreshModels?.(); } else if (ollama?.skipped) { - setTrainStatus(ollama.note || ollama.error || "\u0410\u0434\u0430\u043F\u0442\u0435\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D, Ollama \u2014 \u0432\u0440\u0443\u0447\u043D\u0443\u044E"); + setTrainStatus(ollama.note || ollama.error || "\u0410\u0434\u0430\u043F\u0442\u0435\u0440 \u0441\u043E\u0445\u0440\u0430\u043D\u0451\u043D \u2014 \u0441\u043C. \u0432\u043A\u043B\u0430\u0434\u043A\u0443 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB"); } else if (ollama?.error) { setTrainStatus(`\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 OK, Ollama: ${ollama.error}`); } else if (status === "completed_with_warnings") { - setTrainStatus("\u041E\u0431\u0443\u0447\u0435\u043D\u0438\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u0441 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F\u043C\u0438 \u2014 \u0441\u043C. \u043B\u043E\u0433"); + setTrainStatus("\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u0441 \u043F\u0440\u0435\u0434\u0443\u043F\u0440\u0435\u0436\u0434\u0435\u043D\u0438\u044F\u043C\u0438 \u2014 \u043B\u043E\u0433 \u043D\u0430 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB"); } else { - setTrainStatus("QLoRA \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E"); + setTrainStatus("QLoRA \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E \u2014 \u0432\u043A\u043B\u0430\u0434\u043A\u0430 \xAB\u041C\u043E\u0434\u0435\u043B\u0438\xBB"); } + setTrainingTab("models"); + await refreshTrainModels(); } else if (status === "failed") { setTrainStatus(`\u041E\u0448\u0438\u0431\u043A\u0430 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0438 (exit ${prog?.exit_code ?? "?"})`); } @@ -10772,6 +11287,18 @@ ${HELP_TEXT}`); } } async function startQlora() { + const baseModel = getQloraHfBase(); + const outputName = ($("sa_qlora_name")?.value || "").trim(); + if (!baseModel) { + setTrainStatus("\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 HF base model \u0438\u0437 \u0441\u043F\u0438\u0441\u043A\u0430 \u0438\u043B\u0438 \u0443\u043A\u0430\u0436\u0438\u0442\u0435 \u0441\u0432\u043E\u0439 HF id"); + $("sa_qlora_base")?.focus(); + return; + } + if (!outputName) { + setTrainStatus("\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0438\u043C\u044F \u043C\u043E\u0434\u0435\u043B\u0438 \u0432 Ollama (\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440 my-lora:v1)"); + $("sa_qlora_name")?.focus(); + return; + } setTrainStatus("\u0417\u0430\u043F\u0443\u0441\u043A\u2026"); try { const hfDs = ($("sa_qlora_hf_dataset")?.value || "").trim(); @@ -10779,9 +11306,9 @@ ${HELP_TEXT}`); await SA2.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) || 2e-4, @@ -10817,11 +11344,18 @@ ${HELP_TEXT}`); const root = $("sa_train_models_list"); if (!root) return; try { + const jobData = await SA2.request("AssistentGetTrainJob", {}); + renderLastTrainJob(jobData?.last_job || jobData?.job); const data = await SA2.request("AssistentListModels", { baseUrl: $("sa_base_url")?.value }); const models = data?.models || []; - root.innerHTML = models.length ? models.map((m) => `
${escapeHtml(m)}
`).join("") : '
\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439
'; + const lastOut = jobData?.last_job?.output_name; + root.innerHTML = models.length ? models.map((m) => { + const hit = lastOut && String(m).includes(String(lastOut).split(":")[0]); + return `
${escapeHtml(m)}${hit ? " \xB7 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u044F\u044F \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430" : ""}
`; + }).join("") : '
\u041D\u0435\u0442 \u043C\u043E\u0434\u0435\u043B\u0435\u0439 \u0432 Ollama \u2014 \u043F\u043E\u0441\u043B\u0435 QLoRA \u043D\u0430\u0436\u043C\u0438 \xAB\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C\xBB \u0438\u043B\u0438 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043B\u043E\u0433 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0438
'; } catch (e) { - root.innerHTML = `
${escapeHtml(e.message)}
`; + const msg = String(e.message || e); + root.innerHTML = `
${escapeHtml(isSqliteError(msg) ? sqliteHint() : msg)}
`; } } async function saveRunner() { @@ -10965,6 +11499,7 @@ ${HELP_TEXT}`); r.addEventListener("change", () => setTrainMode(r.value)); }); $("sa_btn_modelfile_create")?.addEventListener("click", createModelfile); + $("sa_qlora_base")?.addEventListener("change", () => applyQloraPresetFromSelect()); $("sa_btn_qlora_start")?.addEventListener("click", startQlora); $("sa_btn_qlora_cancel")?.addEventListener("click", cancelQlora); $("sa_btn_train_models_refresh")?.addEventListener("click", refreshTrainModels); @@ -10978,6 +11513,15 @@ ${HELP_TEXT}`); wireTraining(); setTrainingTab(state.ttab); }, + onConfig(data) { + if (data?.training && typeof data.training === "object") { + state.qloraTraining = data.training; + if (state.ttab === "train") { + populateQloraHfPresets(state.qloraTraining); + applyQloraPresetFromSelect({ fillName: false }); + } + } + }, resumePolling: resumeTrainJobPolling, async curateFromChat(messages, meta) { try { diff --git a/Assets/assistent.css b/Assets/assistent.css index 11f3d2b..8b1d9ab 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -450,25 +450,47 @@ .sa-live-params { font-size: 0.75rem; - opacity: 0.78; + opacity: 0.88; padding: 0.2rem 0.15rem 0.35rem; letter-spacing: 0.01em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + display: flex; + flex-wrap: wrap; + gap: 0.22rem 0.38rem; + align-items: center; + line-height: 1.45; font-variant-numeric: tabular-nums; } .sa-composer-params { font-size: 0.72rem; - opacity: 0.82; - padding: 0.05rem 0 0.2rem; + opacity: 0.88; + padding: 0.05rem 0 0.25rem; letter-spacing: 0.01em; - line-height: 1.35; + line-height: 1.45; + display: flex; + flex-wrap: wrap; + gap: 0.2rem 0.36rem; + align-items: center; font-variant-numeric: tabular-nums; color: color-mix(in srgb, currentColor 88%, transparent); } +.sa-param-tag { + display: inline-flex; + align-items: center; + gap: 0.15rem; + padding: 0.06rem 0.38rem; + border-radius: 4px; + background: color-mix(in srgb, currentColor 7%, transparent); + white-space: nowrap; +} + +.sa-param-tag.sa-param-custom { + background: color-mix(in srgb, var(--sa-accent, #6ea8fe) 20%, transparent); + color: color-mix(in srgb, var(--sa-accent, #6ea8fe) 95%, currentColor); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sa-accent, #6ea8fe) 38%, transparent); +} + .sa-more-wrap { position: relative; display: inline-flex; @@ -1879,6 +1901,11 @@ background: color-mix(in srgb, #6cf 22%, transparent); } +.sa-chip.sa-chip-active.sa-chip-custom { + border-color: color-mix(in srgb, #e8a838 60%, currentColor); + background: color-mix(in srgb, #e8a838 24%, transparent); +} + .sa-chip-sep { width: 1px; height: 1.1rem; @@ -2197,6 +2224,40 @@ opacity: 0.95; } +.sa-train-last-job { + margin-bottom: 0.65rem; + padding: 0.55rem 0.65rem; + border-radius: 0.45rem; + border: 1px solid color-mix(in srgb, currentColor 22%, transparent); + background: color-mix(in srgb, currentColor 6%, transparent); + font-size: 0.78rem; + line-height: 1.45; +} + +.sa-train-last-head { + font-weight: 600; + margin-bottom: 0.25rem; +} + +.sa-train-last-ok { + color: color-mix(in srgb, #6fcf97 85%, currentColor); + margin-top: 0.25rem; +} + +.sa-train-last-warn { + color: color-mix(in srgb, #f2c94c 85%, currentColor); + margin-top: 0.25rem; +} + +.sa-train-last-log { + max-height: 8rem; + margin-top: 0.35rem; +} + +.sa-train-model-new { + border-color: color-mix(in srgb, #6fcf97 45%, transparent); +} + .sa-lora-chips { display: flex; flex-wrap: nowrap; @@ -2209,21 +2270,56 @@ } .sa-lora-chip { - appearance: none; + display: inline-flex; + align-items: center; + gap: 0.1rem; border: 1px solid color-mix(in srgb, currentColor 28%, transparent); background: color-mix(in srgb, currentColor 8%, transparent); color: inherit; border-radius: 999px; - padding: 0.12rem 0.45rem; + padding: 0.02rem 0.2rem 0.02rem 0.05rem; font-size: 0.72rem; - cursor: pointer; max-width: 14rem; overflow: hidden; +} + +.sa-lora-chip-main { + appearance: none; + border: none; + background: transparent; + color: inherit; + border-radius: 999px; + padding: 0.1rem 0.35rem; + font-size: inherit; + cursor: pointer; + max-width: 12rem; + overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.sa-lora-chip-rm { + appearance: none; + border: none; + background: transparent; + color: inherit; + opacity: 0.65; + padding: 0.05rem 0.25rem; + font-size: 0.85rem; + line-height: 1; + cursor: pointer; + border-radius: 999px; +} + +.sa-lora-chip-rm:hover { + opacity: 1; + background: color-mix(in srgb, currentColor 14%, transparent); +} + .sa-lora-chip.sa-lora-add { + appearance: none; + cursor: pointer; + padding: 0.12rem 0.45rem; opacity: 0.75; border-style: dashed; } @@ -2259,6 +2355,40 @@ background: color-mix(in srgb, currentColor 12%, transparent); } +.sa-lora-weight-pop { + position: absolute; + z-index: 41; + min-width: 10rem; + border-radius: 0.45rem; + border: 1px solid color-mix(in srgb, currentColor 28%, transparent); + background: color-mix(in srgb, #111 92%, transparent); + box-shadow: 0 8px 24px color-mix(in srgb, #000 35%, transparent); + padding: 0.45rem; + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.sa-lora-weight-pop label { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.72rem; + opacity: 0.85; +} + +.sa-lora-weight-pop input[type="number"] { + width: 100%; + box-sizing: border-box; + padding: 0.3rem; +} + +.sa-lora-weight-actions { + display: flex; + gap: 0.35rem; + justify-content: flex-end; +} + .sa-slash-wrap { position: relative; } diff --git a/AssistentConfig.cs b/AssistentConfig.cs index 996bb72..705067b 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -477,7 +477,7 @@ public sealed class AssistentConfig static readonly HashSet ReservedConfigFiles = new(StringComparer.OrdinalIgnoreCase) { - "exact.json", "controls.json", "skills.json", "ui.json", "assistant.json", + "exact.json", "controls.json", "skills.json", "ui.json", "assistant.json", "training-qlora.json", }; static readonly HashSet ReservedConfigDirs = new(StringComparer.OrdinalIgnoreCase) @@ -934,6 +934,8 @@ public sealed class AssistentConfig public JObject LoadUi(string personaId) => MergeJsonLayers("ui.json", LayerRoots(personaId)); + public JObject LoadTrainingQlora(string personaId) => MergeJsonLayers("training-qlora.json", LayerRoots(personaId)); + /// Exact (KV) memory: bundled base → persona overlays → disk overlays. Persona keys overwrite base. public JObject LoadExact(string personaId) => MergeJsonLayers("exact.json", LayerRoots(personaId)); @@ -1641,6 +1643,7 @@ public sealed class AssistentConfig JObject ui = LoadUi(id); JObject model = LoadModelProfile(id); JObject exact = LoadExact(id); + JObject training = LoadTrainingQlora(id); var packs = ListPacks(id); var skills = ListSkills(id); var personas = ListPersonaCatalog(); @@ -1656,6 +1659,7 @@ public sealed class AssistentConfig ["ui"] = ui, ["model"] = model, ["exact"] = exact, + ["training"] = training, ["controls"] = controlsSchema, ["control_values"] = controlValues, ["persona_source"] = PersonaSource(id), diff --git a/AssistentMemory.Training.cs b/AssistentMemory.Training.cs index 8803678..c34f7fb 100644 --- a/AssistentMemory.Training.cs +++ b/AssistentMemory.Training.cs @@ -297,23 +297,46 @@ public sealed partial class AssistentMemory { return null; } - return new JObject - { - ["id"] = r.GetString(0), - ["kind"] = r.GetString(1), - ["status"] = r.GetString(2), - ["config_json"] = r.IsDBNull(3) ? null : r.GetString(3), - ["base_model"] = r.IsDBNull(4) ? null : r.GetString(4), - ["output_name"] = r.IsDBNull(5) ? null : r.GetString(5), - ["log_path"] = r.IsDBNull(6) ? null : r.GetString(6), - ["progress_json"] = r.IsDBNull(7) ? null : r.GetString(7), - ["created_at"] = r.GetInt64(8), - ["updated_at"] = r.GetInt64(9), - ["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10), - }; + return ReadTrainJobRow(r); } } + public JObject GetLastTrainJob() + { + lock (_lock) + { + EnsureOpen(); + using SqliteCommand cmd = _conn.CreateCommand(); + cmd.CommandText = + "SELECT id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at " + + "FROM train_jobs ORDER BY updated_at DESC LIMIT 1"; + using SqliteDataReader r = cmd.ExecuteReader(); + if (!r.Read()) + { + return null; + } + return ReadTrainJobRow(r); + } + } + + static JObject ReadTrainJobRow(SqliteDataReader r) + { + return new JObject + { + ["id"] = r.GetString(0), + ["kind"] = r.GetString(1), + ["status"] = r.GetString(2), + ["config_json"] = r.IsDBNull(3) ? null : r.GetString(3), + ["base_model"] = r.IsDBNull(4) ? null : r.GetString(4), + ["output_name"] = r.IsDBNull(5) ? null : r.GetString(5), + ["log_path"] = r.IsDBNull(6) ? null : r.GetString(6), + ["progress_json"] = r.IsDBNull(7) ? null : r.GetString(7), + ["created_at"] = r.GetInt64(8), + ["updated_at"] = r.GetInt64(9), + ["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10), + }; + } + public JObject GetActiveTrainJob() { lock (_lock) @@ -326,20 +349,7 @@ public sealed partial class AssistentMemory { return null; } - return new JObject - { - ["id"] = r.GetString(0), - ["kind"] = r.GetString(1), - ["status"] = r.GetString(2), - ["config_json"] = r.IsDBNull(3) ? null : r.GetString(3), - ["base_model"] = r.IsDBNull(4) ? null : r.GetString(4), - ["output_name"] = r.IsDBNull(5) ? null : r.GetString(5), - ["log_path"] = r.IsDBNull(6) ? null : r.GetString(6), - ["progress_json"] = r.IsDBNull(7) ? null : r.GetString(7), - ["created_at"] = r.GetInt64(8), - ["updated_at"] = r.GetInt64(9), - ["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10), - }; + return ReadTrainJobRow(r); } } } diff --git a/AssistentMemory.cs b/AssistentMemory.cs index a6c2e4c..315db90 100644 --- a/AssistentMemory.cs +++ b/AssistentMemory.cs @@ -88,6 +88,7 @@ public sealed partial class AssistentMemory : IDisposable { return; } + AssistentSqliteBootstrap.EnsureInitialized(); _conn = new SqliteConnection($"Data Source={_dbPath}"); _conn.Open(); TryPragma("journal_mode=WAL"); diff --git a/AssistentMemoryApi.cs b/AssistentMemoryApi.cs index b432444..3f9682b 100644 --- a/AssistentMemoryApi.cs +++ b/AssistentMemoryApi.cs @@ -95,7 +95,11 @@ public partial class SwarmAssistentExtension } catch (Exception ex) { - return new JObject { ["error"] = $"memory list: {ex.Message}" }; + string detail = ex.InnerException?.Message; + string msg = string.IsNullOrWhiteSpace(detail) + ? ex.Message + : $"{ex.Message} ({detail})"; + return new JObject { ["error"] = $"memory list: {msg}" }; } } diff --git a/AssistentSqliteBootstrap.cs b/AssistentSqliteBootstrap.cs new file mode 100644 index 0000000..0ac39f6 --- /dev/null +++ b/AssistentSqliteBootstrap.cs @@ -0,0 +1,22 @@ +using System.Threading; +using SQLitePCL; + +namespace Mrleo1nid.SwarmAssistent; + +/// +/// SwarmUI loads extensions in an isolated AssemblyLoadContext — Microsoft.Data.Sqlite +/// does not auto-init native SQLite there. Call once before the first connection. +/// +internal static class AssistentSqliteBootstrap +{ + static int _ready; + + internal static void EnsureInitialized() + { + if (Interlocked.CompareExchange(ref _ready, 1, 0) != 0) + { + return; + } + Batteries_V2.Init(); + } +} diff --git a/AssistentTraining.cs b/AssistentTraining.cs index 034c896..fc04e3c 100644 --- a/AssistentTraining.cs +++ b/AssistentTraining.cs @@ -420,6 +420,7 @@ public partial class SwarmAssistentExtension { await Task.CompletedTask; JObject job = string.IsNullOrWhiteSpace(id) ? Memory.GetActiveTrainJob() : Memory.GetTrainJob(id); + JObject lastJob = Memory.GetLastTrainJob(); if (job is not null && TrainingJobManager.IsRunning && string.Equals(job["id"]?.ToString(), TrainingJobManager.CurrentJobId, StringComparison.OrdinalIgnoreCase)) { JObject live = TrainingJobManager.GetProgress(); @@ -430,6 +431,7 @@ public partial class SwarmAssistentExtension { ["success"] = true, ["job"] = job, + ["last_job"] = lastJob, ["training_active"] = TrainingJobManager.IsRunning, ["progress"] = TrainingJobManager.IsRunning ? TrainingJobManager.GetProgress() : null, }; diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md index 40f01c8..a8cb9a4 100644 --- a/Config/_base/core/core.md +++ b/Config/_base/core/core.md @@ -16,9 +16,11 @@ When instructions conflict, apply this order (highest wins): Exact = defaults encyclopedia. About the user = human taste. Session = what Generate will run. Soft craft notes may appear in context from the server — never override Exact, About the user, or the user’s param request. -**Sparse deltas only.** Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` / full `prompt` when they already match the session **and** Exact / `krea_profile` defaults, and the user did not ask to change them. +**Sparse deltas only.** Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` / `sampler` / `scheduler` / full `prompt` when they already match the session **and** Exact / `krea_profile` defaults, and the user did not ask to change them. -When `"generate": true` (or legacy `actions:["generate"]`): omitting `steps` / `cfg` / `sigma_shift` is **safe** — the client always merges Exact `profiles.turbo` or `profiles.raw` for the live checkpoint before Generate. Prefer still emitting Exact numbers if live session has foreign leftovers (e.g. steps 20 / cfg 7 vs turbo 8 / 1) so the session stays honest. Never CFG 0. +**Collaborative settings (this chat):** The user may change params via chips, slash, or Swarm fields **without** sending a chat message. Those choices are stored in `session_exact` in live context. **Do not overwrite pinned `session_exact` fields** with Exact defaults unless the user asked this turn or you deliberately change that field in JSON. If live UI + `session_exact` already show `scheduler: simple`, omit `scheduler` from your patch. When you *do* change a param, emit only that field — the client merges into the session. + +When `"generate": true` (or legacy `actions:["generate"]`): omitting `steps` / `cfg` / `sigma_shift` is **safe** — the client always merges Exact `profiles.turbo` or `profiles.raw` for the live checkpoint before Generate. **Omitting `sampler` / `scheduler` is safe** — live UI + `session_exact` win. Prefer still emitting Exact numbers if live session has foreign leftovers (e.g. steps 20 / cfg 7 vs turbo 8 / 1) so the session stays honest. Never CFG 0. Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. Chat prose may stay RU; **`prompt` must be English** (Krea 2). Keep prose short. **After the closing ``` of the JSON fence, STOP.** **One turn = one patch.** Several options → `variants` (2–4). Prompt structure lives in skill `prompting`. @@ -60,7 +62,7 @@ Several options (still one fence): ### Patch rules - Omit unchanged params. Include `negative` when starting Generate if live/session negative is empty or the scene needs a specific omit. -- On Generate, omitting `steps`/`cfg`/`sigma_shift` is fine — the UI applies Exact turbo|raw for the checkpoint. Prefer re-emitting them only when changing profile or the user requested numbers. +- On Generate, omitting `steps`/`cfg`/`sigma_shift`/`sampler`/`scheduler` is fine — the UI applies Exact turbo|raw + live/`session_exact`. Prefer re-emitting steps/cfg only when changing profile or the user requested numbers. - `loras` replaces the full intended set for this chat when you change LoRAs. - Prefer `aspect` over raw width/height. - Optional: seed, vary, init/mask, controls, pack, `variants`. diff --git a/Config/_base/exact.json b/Config/_base/exact.json index 0315ea5..5858846 100644 --- a/Config/_base/exact.json +++ b/Config/_base/exact.json @@ -1,6 +1,7 @@ { "generation": { "profile": "turbo", + "aspect": "1:1", "steps": 8, "cfg": 1, "sigma_shift": 1.15, @@ -36,6 +37,13 @@ "negatives": "Qwen3-VL negatives are weak — still keep a short Swarm negative box. Prefer positives in `prompt`; for `negative`: create if live is empty (Exact generation.negative), lightly supplement if the scene needs a specific omit, or echo live unchanged. Never drop/clear the box on Generate. Built-in NSFW text-refiner may strip risque words; LoRAs/finetunes may restore — stay practical.", "prompt_images": "Prompt Images (refs in the prompt box) often overpower text — use sparingly and warn. Init Image = structure (img2img). Mask = local fix. They are not interchangeable. Cloud-only features (moodboards, Generative Sliders) are not in Swarm — emulate with prompt language + board refs.", "turbo": "Krea 2 Turbo defaults live in exact.generation / exact.profiles.turbo. Never use CFG 0 — broken output. Side ~1024 (128–4096 OK). Checkpoint name must contain turbo to claim this profile.", - "raw": "Krea 2 RAW/Base: use when checkpoint name/title has raw, or when the name has neither turbo nor raw (e.g. realismByStableYogi finetunes). Prefer exact.profiles.raw. If a turbo LoRA exists, weight ~0.6 for photoreal (1.0 ≈ full turbo). Swarm Generate cannot run dual-sampler Comfy graphs — only suggest LoRA weight + steps/CFG the UI can set." + "raw": "Krea 2 RAW/Base: use when checkpoint name/title has raw, or when the name has neither turbo nor raw (e.g. realismByStableYogi finetunes). Prefer exact.profiles.raw. If a turbo LoRA exists, weight ~0.6 for photoreal (1.0 ≈ full turbo). Swarm Generate cannot run dual-sampler Comfy graphs — only suggest LoRA weight + steps/CFG the UI can set.", + "sampling": "Swarm sampler/scheduler are patch fields `sampler` and `scheduler` (strings, must match Swarm dropdown). Common: euler+normal (default), euler+simple (Turbo community). Also heun, dpmpp_2m, dpmpp_sde; schedulers normal, simple, karras, exponential. Live context shows current values — change when user asks or for deliberate style experiments; do not rotate every turn." + }, + "sampling": { + "defaults": { "sampler": "euler", "scheduler": "normal" }, + "samplers": ["euler", "heun", "dpmpp_2m", "dpmpp_sde", "lcm"], + "schedulers": ["normal", "simple", "karras", "exponential", "sgm_uniform"], + "turbo_hint": { "sampler": "euler", "scheduler": "simple" } } } diff --git a/Config/_base/packs/critique_image.md b/Config/_base/packs/critique_image.md index dc35da5..6407e03 100644 --- a/Config/_base/packs/critique_image.md +++ b/Config/_base/packs/critique_image.md @@ -12,18 +12,30 @@ If `images_in_request` is false: - Emit **only** a one-line note + JSON with `look_at: ["generate"]` (or the ref id). - Do **not** write a critique checklist. Do **not** invent defects. -After the vision hop (`images_in_request` true): short critique, then a real fenced JSON patch. +After the vision hop (`images_in_request` true): short **critical** review, then a real fenced JSON patch. + +## Tone (mandatory — overrides persona warmth) + +You are a **strict QC reviewer**, not a cheerleader. Persona hype / warmth is **off** in this mode. + +- **Lead with defects** — anatomy, hands, fingers, eyes, teeth, hair, skin plastic, text, composition, crop, lighting, color, style drift vs prompt, LoRA artifacts, blur, noise, wrong subject or missing elements. +- **Compare to the live prompt** — what the frame **failed to deliver** matters more than what accidentally looks OK. +- **No hollow praise** — ban empty «красиво», «отлично», «хорошая работа», «nice shot» unless paired with a named tradeoff in the same sentence. +- **Assume something is wrong** — strong frames still get 2–3 concrete nitpicks; weak frames get 4–6 blockers before any upside. +- **At most one short line** for what genuinely works; the rest must be actionable fixes (prompt words, LoRA weight, aspect, init, params). + +Do not paste a generic template of pitfalls you did not observe. ## Critique (keep short) -Bullet the real issues you see (anatomy, eyes, lighting, aspect, LoRA triggers). Do not paste a generic template of pitfalls you did not observe. +Bullet **real** issues you see. Tie each bullet to a fix (prompt clause, param, LoRA, init/mask). ## Deliverable -1. 2–6 short lines in the user's language. +1. 2–6 short lines in the user's language — **defects first**. 2. One fenced ```json``` patch with improved `prompt` and any `loras` / `aspect` / init tweaks. -3. `actions: ["generate"]` when proposing a revised generation. +3. `"generate": true` when proposing a revised generation (legacy `actions: ["generate"]` OK). Never describe a patch in prose without the fenced JSON object. Never repeat a previous critique template. Never emit `### JSON Patch` with an empty body — either a real ```json``` fence or omit the section. -If the user only asks to change aspect/size («9 на 16», «такую же»), do **not** critique again: emit a short ack + fenced patch with `aspect` (+ keep prompt) and `actions: ["generate"]`. +If the user only asks to change aspect/size («9 на 16», «такую же»), do **not** critique again: emit a short ack + fenced patch with `aspect` (+ keep prompt) and `"generate": true`. diff --git a/Config/_base/packs/fix_params.md b/Config/_base/packs/fix_params.md index 767d12a..c97110d 100644 --- a/Config/_base/packs/fix_params.md +++ b/Config/_base/packs/fix_params.md @@ -9,7 +9,7 @@ Goal: adjust **generation parameters** for Krea 2 Turbo (or RAW if context says - **Aspect:** prefer patch field `aspect` from Exact `aspect_table` — UI maps to official 1K sizes. Else set width/height near 1024. - **Batch:** `images` or `batch` (1–4 typical). - **Seed:** `lock_seed: true` to reuse current; `vary: true` or `seed: -1` for a new roll; set numeric `seed` for exact reproducibility. -- **Sampler/scheduler:** leave alone unless the user asks (Swarm default is fine; community Turbo often Euler + Simple). +- **Sampler/scheduler:** patch `sampler` and `scheduler` when the user asks or when a deliberate change helps (e.g. euler+simple for Turbo speed). Must match Swarm UI strings — see Exact `sampling.samplers` / `sampling.schedulers`. Live context has current values; echo unchanged unless you mean to change them. Default euler+normal; Turbo community often euler+simple. - **Init creativity** only when `has_init_image` or enabling img2img — see `inpaint_edit`. - Do not change the prompt unless needed for the new framing. - Keep LoRAs unless asked to drop them. @@ -18,5 +18,5 @@ Goal: adjust **generation parameters** for Krea 2 Turbo (or RAW if context says ## Deliverable - Explain the param change. -- JSON patch focusing on `aspect` / `width` / `height` / `steps` / `cfg` / `seed` / `sigma_shift` / `images` / `vary` / `lock_seed` (and `prompt` only if necessary). +- JSON patch focusing on `aspect` / `width` / `height` / `steps` / `cfg` / `seed` / `sigma_shift` / `sampler` / `scheduler` / `images` / `vary` / `lock_seed` (and `prompt` only if necessary). - `actions: ["generate"]` if the user wants to re-roll with the new params. diff --git a/Config/_base/packs/ordinary.md b/Config/_base/packs/ordinary.md index 3a4336e..fdc7e6f 100644 --- a/Config/_base/packs/ordinary.md +++ b/Config/_base/packs/ordinary.md @@ -5,7 +5,7 @@ Default all-rounder. Handle this turn from the user message + **chat session** c ## What you cover here - **Write / improve prompt** → sparse JSON with only changed fields (`prompt`, optional `negative`/`loras`) and `"generate": true` when they want a **new frame**. Chat / «нравится» / Q&A → prose only, **no** JSON. Do **not** `look_at` the last frame first. -- **Light critique** → only when they ask to look / critique. Then `look_at: ["generate"]` if pixels are not already in the request. +- **Critique / look** → only when they ask to look / critique. Then `look_at: ["generate"]` if pixels are not already in the request. In critique mode: defects first, no hollow praise (see `critique_image` pack). - **Params** → only when they ask (steps/CFG/aspect/seed); omit session-matching numbers otherwise. Exception: if live steps/cfg disagree with Exact/`krea_profile` (e.g. 20/7 under turbo), include Exact profile numbers when generating. - **Inpaint / img2img** → set init/mask fields when they ask. - Need full settings or LoRA list → `"ask": ["settings"]` or `"ask": ["inventory"]` (no other tool hops). diff --git a/Config/_base/training-qlora.json b/Config/_base/training-qlora.json new file mode 100644 index 0000000..bd8f4e8 --- /dev/null +++ b/Config/_base/training-qlora.json @@ -0,0 +1,49 @@ +{ + "hf_models": [ + { + "id": "qwen2.5-7b-instruct", + "title": "Qwen2.5 7B Instruct", + "hf_id": "Qwen/Qwen2.5-7B-Instruct", + "ollama_hint": "qwen2.5:7b-instruct", + "default_output": "assistent-qwen25-7b:v1", + "rank": 16, + "seq_len": 2048 + }, + { + "id": "qwen2.5-3b-instruct", + "title": "Qwen2.5 3B Instruct", + "hf_id": "Qwen/Qwen2.5-3B-Instruct", + "ollama_hint": "qwen2.5:3b-instruct", + "default_output": "assistent-qwen25-3b:v1", + "rank": 16, + "seq_len": 2048 + }, + { + "id": "llama-3.2-3b-instruct", + "title": "Llama 3.2 3B Instruct", + "hf_id": "meta-llama/Llama-3.2-3B-Instruct", + "ollama_hint": "llama3.2:3b-instruct", + "default_output": "assistent-llama32-3b:v1", + "rank": 16, + "seq_len": 2048 + }, + { + "id": "llama-3.1-8b-instruct", + "title": "Llama 3.1 8B Instruct", + "hf_id": "meta-llama/Llama-3.1-8B-Instruct", + "ollama_hint": "llama3.1:8b-instruct", + "default_output": "assistent-llama31-8b:v1", + "rank": 16, + "seq_len": 2048 + }, + { + "id": "phi-3-mini-instruct", + "title": "Phi-3 Mini 4K Instruct", + "hf_id": "microsoft/Phi-3-mini-4k-instruct", + "ollama_hint": "phi3:mini", + "default_output": "assistent-phi3-mini:v1", + "rank": 16, + "seq_len": 2048 + } + ] +} diff --git a/Config/_base/ui.json b/Config/_base/ui.json index 70d4155..dc7dc40 100644 --- a/Config/_base/ui.json +++ b/Config/_base/ui.json @@ -1,6 +1,6 @@ { - "welcome_html": "
Assistent · Krea 2
  • Generate слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.
  • Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.
  • У каждого чата свои параметры, LoRA, последний кадр и refs.
  • Чипсы aspect / seed / Turbo·RAW — только параметры; Generate: «Собрать + Gen», /gen или Vary. Строка под чипами — σ, batch, sampler. /help.
  • Модель шлёт только дельту настроек + generate.
Напиши, что сгенерировать — или кинь референс и попроси правку.", - "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/compress — сжать старые ходы в саммари (та же модель)\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate из сессии чата\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|ordinary|critique|compose|params|inpaint|describe|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone — клон с указанной\n/persona save — записать согласованный черновик\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate.\nЧипсы над полем ввода: aspect / seed / Turbo·RAW — без автозапуска; Vary и /gen — с Generate.\nУ каждого чата свои параметры Generate; смена чата восстанавливает кадр и refs.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).\nЧип контекста в шапке чата показывает бюджет окна; клик — панель слоёв и ручное сжатие.", + "welcome_html": "
Assistent · Krea 2
  • Generate слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.
  • Refs — референсы на отдельной вкладке: drop / paste / Снимок gen.
  • У каждого чата свои параметры, LoRA, последний кадр и refs.
  • Чипсы aspect / seed / Turbo·RAW / sampler / scheduler — ваш pin; подсветка = значение не Exact. Строка параметров под чипами показывает всё (init, neg±…). Generate: «Собрать + Gen», /gen или Vary. /help.
  • Модель шлёт только изменённые поля + generate; steps/cfg подставляет клиент из Exact.
Напиши, что сгенерировать — или кинь референс и попроси правку.", + "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/compress — сжать старые ходы в саммари (та же модель)\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate из сессии чата\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/sampler euler — семплер Swarm\n/scheduler simple — шедулер Swarm\n/pack write|ordinary|critique|compose|params|inpaint|describe|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone — клон с указанной\n/persona save — записать согласованный черновик\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate.\nЧипсы над полем ввода: aspect / seed / Turbo·RAW / sampler / scheduler — без автозапуска; Vary и /gen — с Generate.\nУ каждого чата свои параметры Generate; смена чата восстанавливает кадр и refs.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).\nЧип контекста в шапке чата показывает бюджет окна; клик — панель слоёв и ручное сжатие.", "chips": [ { "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" }, { "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" }, @@ -13,7 +13,15 @@ { "label": "Vary", "action": "vary", "value": "1", "title": "Тот же промпт, новый seed + generate" }, { "sep": true }, { "label": "Turbo", "action": "krea_profile", "value": "turbo", "title": "Turbo: steps 8, CFG 1" }, - { "label": "RAW", "action": "krea_profile", "value": "raw", "title": "RAW: steps 28, CFG 4.5" } + { "label": "RAW", "action": "krea_profile", "value": "raw", "title": "RAW: steps 28, CFG 4.5" }, + { "sep": true }, + { "label": "Euler", "action": "sampler", "value": "euler", "title": "Sampler: euler (Swarm default)" }, + { "label": "Heun", "action": "sampler", "value": "heun", "title": "Sampler: heun" }, + { "label": "DPM++", "action": "sampler", "value": "dpmpp_2m", "title": "Sampler: dpmpp_2m" }, + { "sep": true }, + { "label": "Normal", "action": "scheduler", "value": "normal", "title": "Scheduler: normal" }, + { "label": "Simple", "action": "scheduler", "value": "simple", "title": "Scheduler: simple (часто с Turbo)" }, + { "label": "Karras", "action": "scheduler", "value": "karras", "title": "Scheduler: karras" } ], "slash": [ { "cmd": "/help", "hint": "список команд", "action": "help" }, @@ -31,6 +39,8 @@ { "cmd": "/aspect ", "hint": "16:9", "action": "aspect" }, { "cmd": "/seed ", "hint": "lock|random", "action": "seed" }, { "cmd": "/vary", "hint": "новый seed", "action": "vary" }, + { "cmd": "/sampler ", "hint": "euler|heun|dpmpp_2m", "action": "sampler" }, + { "cmd": "/scheduler ", "hint": "normal|simple|karras", "action": "scheduler" }, { "cmd": "/pack ", "hint": "write|critique|…", "action": "pack" }, { "cmd": "/inventory", "hint": "rescan моделей", "action": "inventory" }, { "cmd": "/persona new", "hint": "клон / новая личность", "action": "persona_new" }, diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index 454a0d4..094e58f 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension ExtensionAuthor = "mrleo1nid"; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; License = "MIT"; - Version = "0.15.3"; + Version = "0.15.11"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; } diff --git a/SwarmAssistentExtension.csproj b/SwarmAssistentExtension.csproj index 5dc607a..0dbb4ac 100644 --- a/SwarmAssistentExtension.csproj +++ b/SwarmAssistentExtension.csproj @@ -4,6 +4,7 @@ + + + + <_SqliteNative Include="@(RuntimeCopyLocalItems)" + Condition="$([System.String]::Copy('%(RuntimeCopyLocalItems.DestinationSubPath)').Contains('e_sqlite3'))" /> + + + diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index 506e015..1abd53f 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -34,9 +34,9 @@
-
+
- +
@@ -116,7 +116,7 @@
-
+
@@ -254,7 +254,12 @@