Ship Assistent 0.14.1: Exact generate params and cheap park/warm.

Client always merges Exact turbo|raw steps/cfg/sigma before Generate so sparse LLM omissions and leftover SD 20/7 cannot stick; Ollama park/warm skip no-op /api/ps round-trips when the chat model is already unloaded or resident.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 06:50:08 +03:00
co-authored by Cursor
parent 9d2cbca8f2
commit 5f33130381
17 changed files with 1005 additions and 50 deletions
+339 -18
View File
@@ -640,6 +640,48 @@
const ask = patchAskList(delta); const ask = patchAskList(delta);
return { generate, look, vetoed, ask }; return { generate, look, vetoed, ask };
} }
var EXACT_GENERATE_PARAM_KEYS = ["steps", "cfg", "sigma_shift"];
function resolveExactProfileDefaults({ exact, profiles, profileName } = {}) {
const gen = exact?.generation && typeof exact.generation === "object" ? exact.generation : {};
const profile = profileName || gen.profile || "turbo";
const fromProfile = profiles?.[profile] && typeof profiles[profile] === "object" ? profiles[profile] : {};
return {
profile,
steps: fromProfile.steps ?? gen.steps ?? null,
cfg: fromProfile.cfg ?? gen.cfg ?? null,
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null
};
}
function mergeExactParamsForGenerate(patch, {
exact,
profiles,
profileName,
sessionExact,
userParamIntent
} = {}) {
if (!patchWantsGenerate(patch)) {
return { patch, clearSessionKeys: [], profile: null };
}
const defaults = resolveExactProfileDefaults({ exact, profiles, profileName });
const out = { ...patch };
const clearSessionKeys = [];
for (const key of EXACT_GENERATE_PARAM_KEYS) {
if (out[key] != null) {
continue;
}
if (userParamIntent && sessionExact?.[key] != null) {
out[key] = sessionExact[key];
continue;
}
if (defaults[key] != null) {
out[key] = defaults[key];
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
clearSessionKeys.push(key);
}
}
}
return { patch: out, clearSessionKeys, profile: defaults.profile };
}
function attachSession(SA2) { function attachSession(SA2) {
SA2.session = { SA2.session = {
emptySession, emptySession,
@@ -653,6 +695,9 @@
compactContext, compactContext,
fullSettingsDump, fullSettingsDump,
resolveTurnIntent, resolveTurnIntent,
resolveExactProfileDefaults,
mergeExactParamsForGenerate,
EXACT_GENERATE_PARAM_KEYS,
GEN_KEYS GEN_KEYS
}; };
} }
@@ -1038,6 +1083,86 @@
SA2.createActivityController = createActivityController; SA2.createActivityController = createActivityController;
} }
// src/kreaProfile.js
function numClose(a, b, eps = 0.051) {
const x = Number(a);
const y = Number(b);
if (!Number.isFinite(x) || !Number.isFinite(y)) {
return false;
}
return Math.abs(x - y) <= eps;
}
function detectKreaProfileName(modelBlob, exactProfile) {
const blob = String(modelBlob || "").toLowerCase();
const hasTurbo = /turbo/.test(blob);
const hasRaw = /\braw\b|_raw\b|-raw\b|\/raw\b/.test(blob);
if (hasTurbo) {
return "turbo";
}
if (hasRaw) {
return "raw";
}
void exactProfile;
return "raw";
}
function profileParamDefaults(profiles, profileName, generation = {}) {
const profile = String(profileName || "raw");
const fromProfile = profiles && typeof profiles[profile] === "object" ? profiles[profile] : {};
const gen = generation && typeof generation === "object" ? generation : {};
return {
profile,
steps: fromProfile.steps ?? gen.steps ?? null,
cfg: fromProfile.cfg ?? gen.cfg ?? null,
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null
};
}
function liveMatchesExactProfile(live, defaults) {
if (!defaults || defaults.steps == null || defaults.cfg == null) {
return true;
}
const stepsOk = live?.steps == null || numClose(live.steps, defaults.steps, 0.5);
const cfgOk = live?.cfg == null || numClose(live.cfg, defaults.cfg);
const sigmaOk = defaults.sigma_shift == null || live?.sigma_shift == null || numClose(live.sigma_shift, defaults.sigma_shift);
return stepsOk && cfgOk && sigmaOk;
}
function exactKeysToForce(live, defaults, {
patch = null,
sessionExact = null,
userParamIntent = false
} = {}) {
if (!defaults) {
return [];
}
const out = [];
for (const key of ["steps", "cfg", "sigma_shift"]) {
const want = defaults[key];
if (want == null) {
continue;
}
const eps = key === "steps" ? 0.5 : 0.051;
if (patch && patch[key] != null && !numClose(patch[key], want, eps)) {
continue;
}
if (userParamIntent && sessionExact && sessionExact[key] != null) {
continue;
}
const have = live?.[key];
if (have == null || !numClose(have, want, eps)) {
out.push(key);
}
}
return out;
}
function attachKreaProfile(SA2) {
SA2.kreaProfile = {
numClose,
detectKreaProfileName,
profileParamDefaults,
liveMatchesExactProfile,
exactKeysToForce
};
}
// src/app.js // src/app.js
(function() { (function() {
const LS_BASE = "swarm_assistent_base_url"; const LS_BASE = "swarm_assistent_base_url";
@@ -1559,6 +1684,7 @@
return false; return false;
} }
} }
let lastExactForceCkpt = null;
function updateGate() { function updateGate() {
const ok = isKreaSelected(); const ok = isKreaSelected();
const gate = $2("sa_gate"); const gate = $2("sa_gate");
@@ -1579,6 +1705,19 @@
if (layout) { if (layout) {
layout.classList.toggle("sa-disabled", !ok); layout.classList.toggle("sa-disabled", !ok);
} }
try {
const ckptName = resolveCurrentCheckpoint()?.name || null;
if (ok && ckptName && ckptName !== lastExactForceCkpt) {
const defs = typeof exactProfileDefaults === "function" ? exactProfileDefaults(detectKreaProfileName2()) : null;
if (defs && defs.steps != null && defs.cfg != null) {
lastExactForceCkpt = ckptName;
forceExactParamsForGenerate({});
}
} else if (!ok) {
lastExactForceCkpt = null;
}
} catch (e) {
}
return ok; return ok;
} }
function escapeHtml2(s) { function escapeHtml2(s) {
@@ -1863,7 +2002,9 @@
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)\b/i.test(t)) {
return true; return true;
} }
return cyrTokenRe("\u0440\u0430\u0437\u043C\u0435\u0440|\u0448\u0438\u0440\u0438\u043D[\u0430-\u044F\u0451]*|\u0432\u044B\u0441\u043E\u0442[\u0430-\u044F\u0451]*|\u0441\u043E\u043E\u0442\u043D\u043E\u0448\u0435\u043D[\u0430-\u044F\u0451]*|\u0442\u0443\u0440\u0431\u043E|\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*").test(t); return cyrTokenRe(
"\u0440\u0430\u0437\u043C\u0435\u0440|\u0448\u0438\u0440\u0438\u043D[\u0430-\u044F\u0451]*|\u0432\u044B\u0441\u043E\u0442[\u0430-\u044F\u0451]*|\u0441\u043E\u043E\u0442\u043D\u043E\u0448\u0435\u043D[\u0430-\u044F\u0451]*|\u0442\u0443\u0440\u0431\u043E|\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*|\u0448\u0430\u0433|\u0448\u0430\u0433\u043E\u043C|\u0448\u0430\u0433\u0430\u043C\u0438|\u0448\u0430\u0433\u043E\u0432|\u0448\u0430\u0433\u0430"
).test(t);
} }
function replyMissingJsonPatch(reply) { function replyMissingJsonPatch(reply) {
const t = String(reply || ""); const t = String(reply || "");
@@ -2127,7 +2268,7 @@ ${patch.prompt}`;
const steps = val("input_steps") || "\u2014"; const steps = val("input_steps") || "\u2014";
const cfg = val("input_cfgscale") || val("input_cfg") || "\u2014"; const cfg = val("input_cfgscale") || val("input_cfg") || "\u2014";
const seed = val("input_seed") || "\u2014"; const seed = val("input_seed") || "\u2014";
const profile = detectKreaProfileName(); const profile = detectKreaProfileName2();
el.textContent = `${aspect} \xB7 ${w || "?"}\xD7${h || "?"} \xB7 steps ${steps} \xB7 cfg ${cfg} \xB7 ${profile} \xB7 seed ${seed}`; el.textContent = `${aspect} \xB7 ${w || "?"}\xD7${h || "?"} \xB7 steps ${steps} \xB7 cfg ${cfg} \xB7 ${profile} \xB7 seed ${seed}`;
} }
function applyAspectTableFrom(obj) { function applyAspectTableFrom(obj) {
@@ -2151,28 +2292,116 @@ ${patch.prompt}`;
const profiles = exact.profiles || state.kreaProfiles || {}; const profiles = exact.profiles || state.kreaProfiles || {};
return { exact, profiles }; return { exact, profiles };
} }
function detectKreaProfileName() { function detectKreaProfileName2() {
const KP = window.SA?.kreaProfile;
try { try {
const model = resolveCurrentCheckpoint(); const model = resolveCurrentCheckpoint();
const blob = `${model?.name || ""} ${model?.title || ""}`.toLowerCase(); const blob = `${model?.name || ""} ${model?.title || ""}`;
const hasRaw = /\braw\b/.test(blob); const exactProfile = state.exact?.generation?.profile;
const hasTurbo = /\bturbo\b/.test(blob); if (KP?.detectKreaProfileName) {
return hasRaw && !hasTurbo ? "raw" : "turbo"; return KP.detectKreaProfileName(blob, exactProfile);
} catch (e) {
return state.exact?.generation?.profile || "turbo";
} }
const lower = blob.toLowerCase();
if (/turbo/.test(lower)) {
return "turbo";
}
if (/\braw\b|_raw\b|-raw\b/.test(lower)) {
return "raw";
}
return "raw";
} catch (e) {
return state.exact?.generation?.profile || "raw";
}
}
function exactProfileDefaults(profileName) {
const { exact, profiles } = resolveExactBundle();
const profile = profileName || detectKreaProfileName2();
const KP = window.SA?.kreaProfile;
if (KP?.profileParamDefaults) {
return KP.profileParamDefaults(profiles, profile, exact.generation);
}
const gen = exact.generation && typeof exact.generation === "object" ? exact.generation : {};
const fromProfile = profiles[profile] && typeof profiles[profile] === "object" ? profiles[profile] : {};
return {
profile,
steps: fromProfile.steps ?? gen.steps ?? null,
cfg: fromProfile.cfg ?? gen.cfg ?? null,
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null
};
}
function readLiveStepsCfgSigma() {
return {
steps: parseInt(val("input_steps") || "", 10) || null,
cfg: parseFloat(val("input_cfgscale") || val("input_cfg") || "") || null,
sigma_shift: parseFloat(val("input_sigmashift") || "") || null
};
}
function setCfgVal(n) {
if (document.getElementById("input_cfgscale")) {
setVal("input_cfgscale", String(n));
} else if (document.getElementById("input_cfg")) {
setVal("input_cfg", String(n));
}
}
function forceExactParamsForGenerate({ patch = null, profileName = null } = {}) {
if (typeof isKreaSelected === "function" && !isKreaSelected()) {
return false;
}
const profile = profileName || detectKreaProfileName2();
const defaults = exactProfileDefaults(profile);
const live = readLiveStepsCfgSigma();
const KP = window.SA?.kreaProfile;
const userIntent = !!state.lastUserParamIntent;
const keys = KP?.exactKeysToForce ? KP.exactKeysToForce(live, defaults, {
patch,
sessionExact: state.sessionExact,
userParamIntent: userIntent
}) : ["steps", "cfg", "sigma_shift"].filter((k) => {
const want = defaults[k];
if (want == null) {
return false;
}
if (patch?.[k] != null && String(patch[k]) !== String(want)) {
return false;
}
if (userIntent && state.sessionExact?.[k] != null) {
return false;
}
return live[k] == null || String(live[k]) !== String(want);
});
if (!keys.length) {
return false;
}
for (const key of keys) {
const want = defaults[key];
if (key === "steps") {
setVal("input_steps", String(want));
} else if (key === "cfg") {
setCfgVal(want);
} else if (key === "sigma_shift") {
setVal("input_sigmashift", String(want));
}
if (state.sessionExact && state.sessionExact[key] != null && String(state.sessionExact[key]) !== String(want)) {
delete state.sessionExact[key];
}
if (state.chatSession?.gen) {
state.chatSession.gen[key] = want;
}
}
syncLiveParamsBar();
return true;
} }
function mergedGenerationDefaults(profileName) { function mergedGenerationDefaults(profileName) {
const { exact, profiles } = resolveExactBundle(); const { exact, profiles } = resolveExactBundle();
const gen = exact.generation && typeof exact.generation === "object" ? { ...exact.generation } : {}; const gen = exact.generation && typeof exact.generation === "object" ? { ...exact.generation } : {};
const profile = profileName || gen.profile || detectKreaProfileName(); const profile = profileName || detectKreaProfileName2();
const fromProfile = profiles[profile] && typeof profiles[profile] === "object" ? { ...profiles[profile] } : {}; const fromProfile = profiles[profile] && typeof profiles[profile] === "object" ? { ...profiles[profile] } : {};
const session = state.sessionExact && typeof state.sessionExact === "object" ? { ...state.sessionExact } : {}; const session = state.sessionExact && typeof state.sessionExact === "object" ? { ...state.sessionExact } : {};
return { ...gen, ...fromProfile, profile, ...session }; return { ...gen, ...fromProfile, profile, ...session };
} }
function exactDefaultFor(key, profileName) { function exactDefaultFor(key, profileName) {
const { exact, profiles } = resolveExactBundle(); const { exact, profiles } = resolveExactBundle();
const profile = profileName || exact.generation?.profile || detectKreaProfileName(); const profile = profileName || exact.generation?.profile || detectKreaProfileName2();
const fromProfile = profiles[profile]?.[key]; const fromProfile = profiles[profile]?.[key];
if (fromProfile != null) { if (fromProfile != null) {
return fromProfile; return fromProfile;
@@ -2206,6 +2435,36 @@ ${patch.prompt}`;
} }
return String(value) !== String(exactVal); return String(value) !== String(exactVal);
} }
function ensureExactParamsForGenerate(patch) {
const S = window.SA && window.SA.session;
if (!patch || !S || typeof S.mergeExactParamsForGenerate !== "function") {
return patch;
}
if (!S.patchWantsGenerate(patch)) {
return patch;
}
const { exact, profiles } = resolveExactBundle();
const { patch: next, clearSessionKeys } = S.mergeExactParamsForGenerate(patch, {
exact,
profiles,
profileName: detectKreaProfileName2(),
sessionExact: state.sessionExact,
userParamIntent: !!state.lastUserParamIntent
});
for (const key of clearSessionKeys || []) {
if (state.sessionExact && state.sessionExact[key] != null) {
delete state.sessionExact[key];
}
}
if (state.chatSession && state.chatSession.gen) {
for (const key of S.EXACT_GENERATE_PARAM_KEYS || ["steps", "cfg", "sigma_shift"]) {
if (next[key] != null) {
state.chatSession.gen[key] = next[key];
}
}
}
return next;
}
function fillEmptyParamsFromExact() { function fillEmptyParamsFromExact() {
const defaults = mergedGenerationDefaults(); const defaults = mergedGenerationDefaults();
if (isEmptyParamField(val("input_steps"), { treatZeroEmpty: true }) && defaults.steps != null) { if (isEmptyParamField(val("input_steps"), { treatZeroEmpty: true }) && defaults.steps != null) {
@@ -4554,6 +4813,8 @@ ${patch.prompt}`;
if (typeof readInitContext === "function") initCtx = readInitContext(); if (typeof readInitContext === "function") initCtx = readInitContext();
} catch (e) { } catch (e) {
} }
const kreaProfile = detectKreaProfileName2();
const exactDefs = exactProfileDefaults(kreaProfile);
const extra = { const extra = {
prompt_image_count: typeof countPromptImages === "function" ? countPromptImages() : 0, prompt_image_count: typeof countPromptImages === "function" ? countPromptImages() : 0,
has_vision_image: typeof visionReadySlots === "function" ? visionReadySlots().length > 0 : false, has_vision_image: typeof visionReadySlots === "function" ? visionReadySlots().length > 0 : false,
@@ -4561,6 +4822,12 @@ ${patch.prompt}`;
attached_slot_ids: typeof attachableSlots === "function" ? attachableSlots().map((s) => s.id) : [], attached_slot_ids: typeof attachableSlots === "function" ? attachableSlots().map((s) => s.id) : [],
auto_apply: !!$2("sa_auto_apply")?.checked, auto_apply: !!$2("sa_auto_apply")?.checked,
auto_generate: true, auto_generate: true,
krea_profile: kreaProfile,
recommended_params: {
steps: exactDefs.steps,
cfg: exactDefs.cfg,
sigma_shift: exactDefs.sigma_shift
},
...initCtx ...initCtx
}; };
if (S && typeof S.compactContext === "function") { if (S && typeof S.compactContext === "function") {
@@ -4576,7 +4843,7 @@ ${patch.prompt}`;
return ctx; return ctx;
} }
const g = state.chatSession && state.chatSession.gen || {}; const g = state.chatSession && state.chatSession.gen || {};
return { session: true, prompt: g.prompt || "", negative: g.negative || "", ...extra }; return { session: true, prompt: g.prompt || "", negative: g.negative || "", krea_profile: kreaProfile, ...extra };
} }
function slimInventoryLoras(list, limit) { function slimInventoryLoras(list, limit) {
const selected = /* @__PURE__ */ new Set(); const selected = /* @__PURE__ */ new Set();
@@ -4858,6 +5125,11 @@ ${patch.prompt}`;
if (!patch) { if (!patch) {
return; return;
} }
const S = window.SA && window.SA.session;
const wantsGen = S && typeof S.patchWantsGenerate === "function" && S.patchWantsGenerate(patch);
if (wantsGen) {
patch = ensureExactParamsForGenerate(patch);
}
const doPrompt = !which || which === "all" || which === "prompt"; const doPrompt = !which || which === "all" || which === "prompt";
const doLoras = !which || which === "all" || which === "loras"; const doLoras = !which || which === "all" || which === "loras";
const doParams = !which || which === "all" || which === "size" || which === "params"; const doParams = !which || which === "all" || which === "size" || which === "params";
@@ -5038,6 +5310,10 @@ ${patch.prompt}`;
setVal(batchId, String(defBatch)); setVal(batchId, String(defBatch));
} }
} }
const Sgen = window.SA && window.SA.session;
if (Sgen?.patchWantsGenerate?.(patch) || patch.generate === true || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate")) {
forceExactParamsForGenerate({ patch });
}
} }
if (doInit) { if (doInit) {
const creativity = patch.init_creativity != null ? patch.init_creativity : patch.denoise; const creativity = patch.init_creativity != null ? patch.init_creativity : patch.denoise;
@@ -5442,16 +5718,41 @@ ${patch.prompt}`;
return true; return true;
} }
async function runGenerateFromPatch(patch, opts = {}) { async function runGenerateFromPatch(patch, opts = {}) {
const deltaBeforeLive = patch && typeof patch === "object" ? { ...patch } : {};
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 (typeof pullLiveIntoSession === "function") pullLiveIntoSession();
if (state.chatSession && state.chatSession.gen) {
for (const key of exactKeys) {
const fromDelta = deltaBeforeLive[key] != null;
const fromUser = !!state.lastUserParamIntent && state.sessionExact?.[key] != null;
if (!fromDelta && !fromUser) {
state.chatSession.gen[key] = null;
}
}
}
let working = {
...deltaBeforeLive,
generate: true,
actions: Array.isArray(deltaBeforeLive.actions) && deltaBeforeLive.actions.map(String).includes("generate") ? deltaBeforeLive.actions : [...Array.isArray(deltaBeforeLive.actions) ? deltaBeforeLive.actions : [], "generate"]
};
working = ensureExactParamsForGenerate(working);
if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) { if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) {
const fromSess = { ...state.chatSession.gen, generate: true, actions: ["generate"] }; const fromSess = { ...state.chatSession.gen, generate: true, actions: ["generate"] };
if (patch && typeof patch === "object") { for (const k of Object.keys(working)) {
for (const k of Object.keys(patch)) { if (working[k] != null) fromSess[k] = working[k];
if (patch[k] != null) fromSess[k] = patch[k]; }
for (const key of exactKeys) {
const fromDelta = deltaBeforeLive[key] != null;
const fromUser = !!state.lastUserParamIntent && state.sessionExact?.[key] != null;
if (!fromDelta && !fromUser && working[key] != null) {
fromSess[key] = working[key];
} }
} }
patch = fromSess; patch = ensureExactParamsForGenerate(fromSess);
if (typeof pushSessionToSwarm === "function") await pushSessionToSwarm(state.chatSession); if (typeof pushSessionToSwarm === "function") await pushSessionToSwarm(state.chatSession);
} else {
patch = working;
} }
const force = !!opts.force; const force = !!opts.force;
if (!force && false || !patchHasGenTrigger(patch)) { if (!force && false || !patchHasGenTrigger(patch)) {
@@ -5506,6 +5807,10 @@ ${patch.prompt}`;
syncLiveParamsBar(); syncLiveParamsBar();
} }
ensureNegativeForGenerate(job.patch); ensureNegativeForGenerate(job.patch);
forceExactParamsForGenerate({ patch: job.patch });
if (typeof pullLiveIntoSession === "function") {
pullLiveIntoSession();
}
if (epoch !== state.chatEpoch) { if (epoch !== state.chatEpoch) {
break; break;
} }
@@ -6406,7 +6711,12 @@ ${data.ui.help_extra}`.trim();
fillKnobsFromConfig(data); fillKnobsFromConfig(data);
updateCtxChip(); updateCtxChip();
if (applyDefaults || data.exact) { if (applyDefaults || data.exact) {
lastExactForceCkpt = null;
fillEmptyParamsFromExact(); fillEmptyParamsFromExact();
if (typeof isKreaSelected === "function" && isKreaSelected()) {
forceExactParamsForGenerate({});
lastExactForceCkpt = resolveCurrentCheckpoint()?.name || null;
}
} }
const nextPersona = data.persona || $2("sa_persona")?.value || ""; const nextPersona = data.persona || $2("sa_persona")?.value || "";
let controlValues = data.control_values || data.exact?.controls || {}; let controlValues = data.control_values || data.exact?.controls || {};
@@ -8035,6 +8345,13 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
status: intent.vetoed ? "skip" : "running" status: intent.vetoed ? "skip" : "running"
}); });
if (typeof startBusyUi === "function") startBusyUi("silent_gen"); if (typeof startBusyUi === "function") startBusyUi("silent_gen");
if (effective) {
effective = ensureExactParamsForGenerate({
...effective,
generate: true,
actions: Array.isArray(effective.actions) && effective.actions.map(String).includes("generate") ? effective.actions : [...Array.isArray(effective.actions) ? effective.actions : [], "generate"]
});
}
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
await pushSessionToSwarm(state.chatSession); await pushSessionToSwarm(state.chatSession);
if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); if (typeof syncLiveParamsBar === "function") syncLiveParamsBar();
@@ -8058,13 +8375,16 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
state.pendingSilentGen = false; state.pendingSilentGen = false;
} }
async function applyQuickPatch(patch, note) { async function applyQuickPatch(patch, note) {
const withActions = { ...patch }; let withActions = { ...patch };
if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) { if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) {
withActions.actions = ["generate"]; withActions.actions = ["generate"];
} }
const prevIntent = state.lastUserParamIntent; const prevIntent = state.lastUserParamIntent;
state.lastUserParamIntent = true; state.lastUserParamIntent = true;
const S = window.SA && window.SA.session; const S = window.SA && window.SA.session;
if (S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) {
withActions = ensureExactParamsForGenerate(withActions);
}
if (S) { if (S) {
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions); state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
} }
@@ -8126,7 +8446,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
const pack = $2("sa_pack")?.value || defaultPackId(); const pack = $2("sa_pack")?.value || defaultPackId();
const chatModel = $2("sa_model")?.value || "\u2014"; const chatModel = $2("sa_model")?.value || "\u2014";
const embed = $2("sa_embed_model")?.value || state.preferredEmbed || "\u2014"; const embed = $2("sa_embed_model")?.value || state.preferredEmbed || "\u2014";
const profile = detectKreaProfileName(); const profile = detectKreaProfileName2();
const defaults = mergedGenerationDefaults(profile); const defaults = mergedGenerationDefaults(profile);
const session = state.sessionExact || {}; const session = state.sessionExact || {};
const exactGen = state.exact?.generation || state.config?.exact?.generation || {}; const exactGen = state.exact?.generation || state.config?.exact?.generation || {};
@@ -10095,6 +10415,7 @@ ${HELP_TEXT}`);
attachSession(window.SA); attachSession(window.SA);
attachContext(window.SA); attachContext(window.SA);
attachActivity(window.SA); attachActivity(window.SA);
attachKreaProfile(window.SA);
window.SA.applyConfigPatchKeys = function(config) { window.SA.applyConfigPatchKeys = function(config) {
const keys = config?.patch_keys; const keys = config?.patch_keys;
if (Array.isArray(keys) && keys.length) { if (Array.isArray(keys) && keys.length) {
+73 -3
View File
@@ -10,7 +10,9 @@ namespace Mrleo1nid.SwarmAssistent;
/// <summary>VRAM handover between Ollama and the image backend: park the chat model before /// <summary>VRAM handover between Ollama and the image backend: park the chat model before
/// Generate, warm it again once the user is back in the chat. Embed / memory models are never /// Generate, warm it again once the user is back in the chat. Embed / memory models are never
/// parked — they are tiny and reloading them stalls every retrieve.</summary> /// parked — they are tiny and reloading them stalls every retrieve.
/// Warm is a no-op when Ollama already has the chat model resident (/api/ps) so post-Generate
/// reload is cheap when Krea did not evict VL.</summary>
public partial class SwarmAssistentExtension public partial class SwarmAssistentExtension
{ {
const string WarmKeepAlive = "15m"; const string WarmKeepAlive = "15m";
@@ -28,6 +30,18 @@ public partial class SwarmAssistentExtension
{ {
return new JObject { ["success"] = true, ["parked"] = false, ["skipped"] = "memory model — never parked" }; return new JObject { ["success"] = true, ["parked"] = false, ["skipped"] = "memory model — never parked" };
} }
// Already unloaded — skip the keep_alive:0 round-trip.
if (!await IsOllamaModelResident(root, name))
{
return new JObject
{
["success"] = true,
["parked"] = false,
["skipped"] = "not_resident",
["model"] = name,
["base_url"] = root,
};
}
JObject generate = new() JObject generate = new()
{ {
["model"] = name, ["model"] = name,
@@ -56,7 +70,8 @@ public partial class SwarmAssistentExtension
return new JObject { ["success"] = true, ["parked"] = true, ["model"] = name, ["base_url"] = root }; return new JObject { ["success"] = true, ["parked"] = true, ["model"] = name, ["base_url"] = root };
} }
/// <summary>Single-token chat so the model is resident again by the time the user types.</summary> /// <summary>Single-token chat so the model is resident again by the time the user types.
/// Skips the load when /api/ps already lists the model (avoids ~2030s no-op warm).</summary>
public async Task<JObject> AssistentWarmLlm(Session session, string baseUrl, string model) public async Task<JObject> AssistentWarmLlm(Session session, string baseUrl, string model)
{ {
string root = NormalizeBaseUrl(baseUrl); string root = NormalizeBaseUrl(baseUrl);
@@ -69,6 +84,17 @@ public partial class SwarmAssistentExtension
{ {
return new JObject { ["success"] = true, ["warmed"] = false, ["skipped"] = "memory model" }; return new JObject { ["success"] = true, ["warmed"] = false, ["skipped"] = "memory model" };
} }
if (await IsOllamaModelResident(root, name))
{
return new JObject
{
["success"] = true,
["warmed"] = true,
["skipped"] = "already_resident",
["model"] = name,
["keep_alive"] = WarmKeepAlive,
};
}
int numCtx = CfgInt("num_ctx", DefaultNumCtxFallback); int numCtx = CfgInt("num_ctx", DefaultNumCtxFallback);
JObject payload = new() JObject payload = new()
{ {
@@ -101,6 +127,50 @@ public partial class SwarmAssistentExtension
}; };
} }
/// <summary>True when Ollama /api/ps lists <paramref name="model"/> (or a matching tag).</summary>
async Task<bool> IsOllamaModelResident(string root, string model)
{
if (string.IsNullOrWhiteSpace(root) || string.IsNullOrWhiteSpace(model))
{
return false;
}
try
{
using HttpResponseMessage resp = await HttpClient.GetAsync($"{root}/api/ps");
if (!resp.IsSuccessStatusCode)
{
return false;
}
string body = await resp.Content.ReadAsStringAsync();
JObject parsed = JObject.Parse(body);
JArray models = parsed["models"] as JArray ?? [];
foreach (JToken m in models)
{
string name = m["name"]?.ToString() ?? m["model"]?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
if (string.Equals(name, model, StringComparison.OrdinalIgnoreCase))
{
return true;
}
// Tags may differ by :latest vs bare name.
if (name.StartsWith(model + ":", StringComparison.OrdinalIgnoreCase)
|| model.StartsWith(name + ":", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
catch (Exception ex)
{
Logs.Debug($"IsOllamaModelResident: {ex.Message}");
return false;
}
}
static async Task<(bool ok, string body)> PostOllamaJson(string root, string route, JObject payload) static async Task<(bool ok, string body)> PostOllamaJson(string root, string route, JObject payload)
{ {
try try
@@ -108,7 +178,7 @@ public partial class SwarmAssistentExtension
using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json"); using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
using HttpResponseMessage resp = await HttpClient.PostAsync($"{root}{route}", content); using HttpResponseMessage resp = await HttpClient.PostAsync($"{root}{route}", content);
string body = await resp.Content.ReadAsStringAsync(); string body = await resp.Content.ReadAsStringAsync();
return (resp.IsSuccessStatusCode, resp.IsSuccessStatusCode ? body : $"HTTP {(int)resp.StatusCode}: {body}"); return (resp.IsSuccessStatusCode, body);
} }
catch (Exception ex) catch (Exception ex)
{ {
+4 -1
View File
@@ -16,7 +16,9 @@ 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 users param request. 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 users param request.
**Sparse deltas only.** Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` / full `prompt` when they already match the session and the user did not ask to change them. **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.
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.
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` (24). Prompt structure lives in skill `prompting`. 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` (24). Prompt structure lives in skill `prompting`.
@@ -57,6 +59,7 @@ Several options (still one fence):
### Patch rules ### Patch rules
- Omit unchanged params. Include `negative` when starting Generate if live/session negative is empty or the scene needs a specific omit. - 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.
- `loras` replaces the full intended set for this chat when you change LoRAs. - `loras` replaces the full intended set for this chat when you change LoRAs.
- Prefer `aspect` over raw width/height. - Prefer `aspect` over raw width/height.
- Optional: seed, vary, init/mask, controls, pack, `variants`. - Optional: seed, vary, init/mask, controls, pack, `variants`.
+2 -2
View File
@@ -34,7 +34,7 @@
"prompt_language": "Chat model always prep's Generate prompt for Krea: English natural prose for Qwen3-VL, structured (subject→pose→setting→camera→light). Chat may be RU; never leave Russian or thin drafts in patch.prompt.", "prompt_language": "Chat model always prep's Generate prompt for Krea: English natural prose for Qwen3-VL, structured (subject→pose→setting→camera→light). Chat may be RU; never leave Russian or thin drafts in patch.prompt.",
"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.", "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.", "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 (1284096 OK).", "turbo": "Krea 2 Turbo defaults live in exact.generation / exact.profiles.turbo. Never use CFG 0 — broken output. Side ~1024 (1284096 OK). Checkpoint name must contain turbo to claim this profile.",
"raw": "Krea 2 RAW/Base: prefer exact.profiles.raw when checkpoint name/title looks like RAW (not Turbo). 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."
} }
} }
+2 -1
View File
@@ -15,4 +15,5 @@ Goal: co-create a scene / moodboard direction for **Krea 2** (local Swarm).
## Deliverable ## Deliverable
- Scene brief + JSON patch (`prompt`, optional `loras`, optional `aspect`). - Scene brief + JSON patch (`prompt`, optional `loras`, optional `aspect`).
- `actions: ["generate"]` when ready to try the scene. - `actions: ["generate"]` / `"generate": true` when ready to try the scene.
- Omitting `steps` / `cfg` / `sigma_shift` is safe — the client fills Exact turbo|raw for the live checkpoint. Prefer including them when live session disagrees with `krea_profile` (keeps the session honest).
+1 -1
View File
@@ -5,7 +5,7 @@ Goal: adjust **generation parameters** for Krea 2 Turbo (or RAW if context says
## Guidelines ## Guidelines
- Prefer **Exact memory** (`profiles.turbo` / `profiles.raw`), live `recommended_params`, and `session_exact` over invented numbers. Never CFG 0. - Prefer **Exact memory** (`profiles.turbo` / `profiles.raw`), live `recommended_params`, and `session_exact` over invented numbers. Never CFG 0.
- Prefer live context field `krea_profile` (`turbo` | `raw`) when present. - Prefer live context field `krea_profile` (`turbo` | `raw`) when present. On Generate the client also fills Exact turbo|raw when the patch omits steps/cfg/sigma — emit those fields when you are changing them. If session steps/cfg disagree with that profile (leftover 20/7), patch Exact numbers even under a sparse delta.
- **Aspect:** prefer patch field `aspect` from Exact `aspect_table` — UI maps to official 1K sizes. Else set width/height near 1024. - **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` (14 typical). - **Batch:** `images` or `batch` (14 typical).
- **Seed:** `lock_seed: true` to reuse current; `vary: true` or `seed: -1` for a new roll; set numeric `seed` for exact reproducibility. - **Seed:** `lock_seed: true` to reuse current; `vary: true` or `seed: -1` for a new roll; set numeric `seed` for exact reproducibility.
+1 -1
View File
@@ -6,7 +6,7 @@ Default all-rounder. Handle this turn from the user message + **chat session** c
- **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. - **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. - **Light critique** → only when they ask to look / critique. Then `look_at: ["generate"]` if pixels are not already in the request.
- **Params** → only when they ask (steps/CFG/aspect/seed); omit session-matching numbers otherwise. - **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. - **Inpaint / img2img** → set init/mask fields when they ask.
- Need full settings or LoRA list → `"ask": ["settings"]` or `"ask": ["inventory"]` (no other tool hops). - Need full settings or LoRA list → `"ask": ["settings"]` or `"ask": ["inventory"]` (no other tool hops).
+8 -3
View File
@@ -4,6 +4,10 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both. **Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both.
**Version 0.14.1** — On Generate, force Exact turbo/raw params when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7). `detectKreaProfileName` no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects `krea_profile` + `recommended_params`. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile.
**Version 0.14.1** — Generate always merges Exact turbo|raw `steps`/`cfg`/`sigma_shift` (client-authoritative; sparse LLM omit is safe). Park/warm skip no-op Ollama round-trips when the chat model is already (un)loaded via `/api/ps`. Builds on 0.14.0.
**Version 0.14.0****Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`. **Version 0.14.0****Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`.
**Version 0.13.1** — Сборка 0.13.0: `using` для `WebSocket`/`HttpClient`, instance-методы с `Config`/`FilePath`, Sqlite dll рядом с extension (иначе вкладка не грузится / API пустые). **Version 0.13.1** — Сборка 0.13.0: `using` для `WebSocket`/`HttpClient`, instance-методы с `Config`/`FilePath`, Sqlite dll рядом с extension (иначе вкладка не грузится / API пустые).
@@ -92,7 +96,7 @@ Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/person
- `Config/_base/exact.json` — canonical generation defaults, profiles (turbo/raw), aspect table, short facts - `Config/_base/exact.json` — canonical generation defaults, profiles (turbo/raw), aspect table, short facts
- Persona / disk overlays merge via DeepMerge (matching keys overwrite) - Persona / disk overlays merge via DeepMerge (matching keys overwrite)
- Always injected into the system prompt; UI fills **empty** SwarmUI fields from Exact (no LLM call) - Always injected into the system prompt; UI fills **empty** SwarmUI fields from Exact (no LLM call). On **Generate**, the client also **overwrites** non-empty leftovers for `steps` / `cfg` / `sigma_shift` with Exact turbo|raw for the live checkpoint (unless the user asked for different numbers this turn).
- Chat-session overrides (`session_exact`) last until persona change or clear chat — not written to disk - Chat-session overrides (`session_exact`) last until persona change or clear chat — not written to disk
- Priority: core → user message → **About the user** → session_exact → exact (+ persona) → live fields → craft `memory_hits` - Priority: core → user message → **About the user** → session_exact → exact (+ persona) → live fields → craft `memory_hits`
@@ -124,8 +128,9 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
- `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights. - `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights.
## VRAM handover ## VRAM handover
- Before Generate the chat model is unloaded only if **Park LLM** is enabled (`keep_alive: 0`) — default off - Before Generate the chat model is unloaded only if **Park LLM** is enabled (`keep_alive: 0`) — default **off** (VL reload is often 30120s; enable only when Generate OOMs)
- After Generate the chat model is **always** force-warmed (`AssistentWarmLlm`) — Krea still often evicts VL from VRAM even without park - Park is a no-op when Ollama `/api/ps` already shows the chat model unloaded
- After Generate the chat model is force-warmed (`AssistentWarmLlm`, `keep_alive: 15m`) so the next chat turn is not a surprise cold load — **but** warm is a no-op when `/api/ps` already lists the model (skips the ~2030s reload when Krea did not evict VL)
- Embed / memory models are never parked — reloading them would stall every retrieve - Embed / memory models are never parked — reloading them would stall every retrieve
## UX ## UX
+2 -2
View File
@@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid"; ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
License = "MIT"; License = "MIT";
Version = "0.14.0"; Version = "0.14.1";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
} }
@@ -98,7 +98,7 @@ public partial class SwarmAssistentExtension : Extension
API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse); API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse);
API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse); API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse);
API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse); API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse);
Logs.Init("Swarm Assistent extension loaded (0.14.0 chat session)"); Logs.Init("Swarm Assistent extension loaded (0.14.1 Exact force / krea_profile)");
} }
int CfgInt(string key, int fallback) int CfgInt(string key, int fallback)
+1 -1
View File
@@ -299,7 +299,7 @@
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label> <label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
<label class="sa-check" title="Устарело в 0.14: Generate запускает модель через generate:true или кнопки из сессии чата." hidden><input type="checkbox" id="sa_auto_generate" /> Авто-Generate после патча</label> <label class="sa-check" title="Устарело в 0.14: Generate запускает модель через generate:true или кнопки из сессии чата." hidden><input type="checkbox" id="sa_auto_generate" /> Авто-Generate после патча</label>
<label class="sa-check" title="По умолчанию выкл. Критика кадра — кнопка «Посмотри результат» или /look. Галка шлёт JPEG после каждого Generate."><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label> <label class="sa-check" title="По умолчанию выкл. Критика кадра — кнопка «Посмотри результат» или /look. Галка шлёт JPEG после каждого Generate."><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
<label class="sa-check" title="Выгружает чат-модель перед Generate (keep_alive:0). Для VL 7B обратная загрузка часто 1–2 мин — включай только если Generate падает по VRAM."><input type="checkbox" id="sa_park_llm" /> Park LLM перед Generate (VRAM)</label> <label class="sa-check" title="Выгружает чат-модель перед Generate (keep_alive:0). По умолчанию выкл. Park — no-op если модель уже выгружена; warm после Generate — no-op если /api/ps ещё держит VL. Включай только если Generate падает по VRAM."><input type="checkbox" id="sa_park_llm" /> Park LLM перед Generate (VRAM)</label>
<label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label> <label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
<div class="sa-skills-label">Скилы (процедуры)</div> <div class="sa-skills-label">Скилы (процедуры)</div>
<div class="sa-skills-box" id="sa_skills_box"></div> <div class="sa-skills-box" id="sa_skills_box"></div>
+1 -1
View File
@@ -5,7 +5,7 @@
"scripts": { "scripts": {
"build": "node scripts/build.mjs", "build": "node scripts/build.mjs",
"watch": "node scripts/build.mjs --watch", "watch": "node scripts/build.mjs --watch",
"test": "node --test test/intent.test.js test/patch.test.js test/context.test.js" "test": "node --test test/intent.test.js test/patch.test.js test/context.test.js test/kreaProfile.test.js"
}, },
"devDependencies": { "devDependencies": {
"esbuild": "^0.25.0" "esbuild": "^0.25.0"
+249 -14
View File
@@ -571,6 +571,8 @@
} }
} }
let lastExactForceCkpt = null;
function updateGate() { function updateGate() {
const ok = isKreaSelected(); const ok = isKreaSelected();
const gate = $('sa_gate'); const gate = $('sa_gate');
@@ -595,6 +597,21 @@
if (layout) { if (layout) {
layout.classList.toggle('sa-disabled', !ok); layout.classList.toggle('sa-disabled', !ok);
} }
// On Krea checkpoint change: align Swarm leftovers to Exact profile once.
try {
const ckptName = resolveCurrentCheckpoint()?.name || null;
if (ok && ckptName && ckptName !== lastExactForceCkpt) {
const defs = typeof exactProfileDefaults === 'function'
? exactProfileDefaults(detectKreaProfileName())
: null;
if (defs && defs.steps != null && defs.cfg != null) {
lastExactForceCkpt = ckptName;
forceExactParamsForGenerate({});
}
} else if (!ok) {
lastExactForceCkpt = null;
}
} catch (e) { /* ignore */ }
return ok; return ok;
} }
@@ -923,7 +940,10 @@
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)\b/i.test(t)) {
return true; return true;
} }
return cyrTokenRe('размер|ширин[а-яё]*|высот[а-яё]*|соотношен[а-яё]*|турбо|портрет|вертикал[а-яё]*').test(t); return cyrTokenRe(
'размер|ширин[а-яё]*|высот[а-яё]*|соотношен[а-яё]*|турбо|портрет|вертикал[а-яё]*|'
+ 'шаг|шагом|шагами|шагов|шага',
).test(t);
} }
function replyMissingJsonPatch(reply) { function replyMissingJsonPatch(reply) {
@@ -1252,21 +1272,123 @@
} }
function detectKreaProfileName() { function detectKreaProfileName() {
const KP = window.SA?.kreaProfile;
try { try {
const model = resolveCurrentCheckpoint(); const model = resolveCurrentCheckpoint();
const blob = `${model?.name || ''} ${model?.title || ''}`.toLowerCase(); const blob = `${model?.name || ''} ${model?.title || ''}`;
const hasRaw = /\braw\b/.test(blob); const exactProfile = state.exact?.generation?.profile;
const hasTurbo = /\bturbo\b/.test(blob); if (KP?.detectKreaProfileName) {
return hasRaw && !hasTurbo ? 'raw' : 'turbo'; return KP.detectKreaProfileName(blob, exactProfile);
} catch (e) {
return (state.exact?.generation?.profile) || 'turbo';
} }
const lower = blob.toLowerCase();
if (/turbo/.test(lower)) {
return 'turbo';
}
if (/\braw\b|_raw\b|-raw\b/.test(lower)) {
return 'raw';
}
// Unlabeled ckpt (e.g. realismByStableYogi) — never invent turbo.
return 'raw';
} catch (e) {
return (state.exact?.generation?.profile) || 'raw';
}
}
/** Exact turbo/raw numbers only (no session overlay) — used to force stale SD leftovers. */
function exactProfileDefaults(profileName) {
const { exact, profiles } = resolveExactBundle();
const profile = profileName || detectKreaProfileName();
const KP = window.SA?.kreaProfile;
if (KP?.profileParamDefaults) {
return KP.profileParamDefaults(profiles, profile, exact.generation);
}
const gen = exact.generation && typeof exact.generation === 'object' ? exact.generation : {};
const fromProfile = profiles[profile] && typeof profiles[profile] === 'object' ? profiles[profile] : {};
return {
profile,
steps: fromProfile.steps ?? gen.steps ?? null,
cfg: fromProfile.cfg ?? gen.cfg ?? null,
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null,
};
}
function readLiveStepsCfgSigma() {
return {
steps: parseInt(val('input_steps') || '', 10) || null,
cfg: parseFloat(val('input_cfgscale') || val('input_cfg') || '') || null,
sigma_shift: parseFloat(val('input_sigmashift') || '') || null,
};
}
function setCfgVal(n) {
if (document.getElementById('input_cfgscale')) {
setVal('input_cfgscale', String(n));
} else if (document.getElementById('input_cfg')) {
setVal('input_cfg', String(n));
}
}
/**
* P0: before Generate, overwrite foreign SD-like leftovers (e.g. 20/7) with Exact
* turbo 8/1/σ1.15 or raw 28/4.5. Always honors explicit patch keys; sessionExact
* only when the user asked for params this turn.
*/
function forceExactParamsForGenerate({ patch = null, profileName = null } = {}) {
if (typeof isKreaSelected === 'function' && !isKreaSelected()) {
return false;
}
const profile = profileName || detectKreaProfileName();
const defaults = exactProfileDefaults(profile);
const live = readLiveStepsCfgSigma();
const KP = window.SA?.kreaProfile;
const userIntent = !!state.lastUserParamIntent;
const keys = KP?.exactKeysToForce
? KP.exactKeysToForce(live, defaults, {
patch,
sessionExact: state.sessionExact,
userParamIntent: userIntent,
})
: ['steps', 'cfg', 'sigma_shift'].filter((k) => {
const want = defaults[k];
if (want == null) {
return false;
}
if (patch?.[k] != null && String(patch[k]) !== String(want)) {
return false;
}
if (userIntent && state.sessionExact?.[k] != null) {
return false;
}
return live[k] == null || String(live[k]) !== String(want);
});
if (!keys.length) {
return false;
}
for (const key of keys) {
const want = defaults[key];
if (key === 'steps') {
setVal('input_steps', String(want));
} else if (key === 'cfg') {
setCfgVal(want);
} else if (key === 'sigma_shift') {
setVal('input_sigmashift', String(want));
}
if (state.sessionExact && state.sessionExact[key] != null
&& String(state.sessionExact[key]) !== String(want)) {
delete state.sessionExact[key];
}
if (state.chatSession?.gen) {
state.chatSession.gen[key] = want;
}
}
syncLiveParamsBar();
return true;
} }
function mergedGenerationDefaults(profileName) { function mergedGenerationDefaults(profileName) {
const { exact, profiles } = resolveExactBundle(); const { exact, profiles } = resolveExactBundle();
const gen = exact.generation && typeof exact.generation === 'object' ? { ...exact.generation } : {}; const gen = exact.generation && typeof exact.generation === 'object' ? { ...exact.generation } : {};
const profile = profileName || gen.profile || detectKreaProfileName(); const profile = profileName || detectKreaProfileName();
const fromProfile = profiles[profile] && typeof profiles[profile] === 'object' ? { ...profiles[profile] } : {}; const fromProfile = profiles[profile] && typeof profiles[profile] === 'object' ? { ...profiles[profile] } : {};
const session = state.sessionExact && typeof state.sessionExact === 'object' ? { ...state.sessionExact } : {}; const session = state.sessionExact && typeof state.sessionExact === 'object' ? { ...state.sessionExact } : {};
// Profile (turbo/raw) overrides generation defaults; session overrides both. // Profile (turbo/raw) overrides generation defaults; session overrides both.
@@ -1313,6 +1435,42 @@
return String(value) !== String(exactVal); return String(value) !== String(exactVal);
} }
/**
* Client-authoritative Exact fill for Generate: inject turbo/raw steps/cfg/sigma
* when the sparse patch omitted them, and drop stale sessionExact that would
* block applyPatch (e.g. leftover SD-like 20/7 after a turbo checkpoint).
*/
function ensureExactParamsForGenerate(patch) {
const S = window.SA && window.SA.session;
if (!patch || !S || typeof S.mergeExactParamsForGenerate !== 'function') {
return patch;
}
if (!S.patchWantsGenerate(patch)) {
return patch;
}
const { exact, profiles } = resolveExactBundle();
const { patch: next, clearSessionKeys } = S.mergeExactParamsForGenerate(patch, {
exact,
profiles,
profileName: detectKreaProfileName(),
sessionExact: state.sessionExact,
userParamIntent: !!state.lastUserParamIntent,
});
for (const key of clearSessionKeys || []) {
if (state.sessionExact && state.sessionExact[key] != null) {
delete state.sessionExact[key];
}
}
if (state.chatSession && state.chatSession.gen) {
for (const key of S.EXACT_GENERATE_PARAM_KEYS || ['steps', 'cfg', 'sigma_shift']) {
if (next[key] != null) {
state.chatSession.gen[key] = next[key];
}
}
}
return next;
}
function fillEmptyParamsFromExact() { function fillEmptyParamsFromExact() {
const defaults = mergedGenerationDefaults(); const defaults = mergedGenerationDefaults();
if (isEmptyParamField(val('input_steps'), { treatZeroEmpty: true }) && defaults.steps != null) { if (isEmptyParamField(val('input_steps'), { treatZeroEmpty: true }) && defaults.steps != null) {
@@ -3831,6 +3989,8 @@
const S = window.SA && window.SA.session; const S = window.SA && window.SA.session;
let initCtx = {}; let initCtx = {};
try { if (typeof readInitContext === 'function') initCtx = readInitContext(); } catch (e) { /* ignore */ } try { if (typeof readInitContext === 'function') initCtx = readInitContext(); } catch (e) { /* ignore */ }
const kreaProfile = detectKreaProfileName();
const exactDefs = exactProfileDefaults(kreaProfile);
const extra = { const extra = {
prompt_image_count: typeof countPromptImages === 'function' ? countPromptImages() : 0, prompt_image_count: typeof countPromptImages === 'function' ? countPromptImages() : 0,
has_vision_image: typeof visionReadySlots === 'function' ? visionReadySlots().length > 0 : false, has_vision_image: typeof visionReadySlots === 'function' ? visionReadySlots().length > 0 : false,
@@ -3838,6 +3998,12 @@
attached_slot_ids: typeof attachableSlots === 'function' ? attachableSlots().map((s) => s.id) : [], attached_slot_ids: typeof attachableSlots === 'function' ? attachableSlots().map((s) => s.id) : [],
auto_apply: !!$('sa_auto_apply')?.checked, auto_apply: !!$('sa_auto_apply')?.checked,
auto_generate: !false /* auto_generate ignored 0.14 */, auto_generate: !false /* auto_generate ignored 0.14 */,
krea_profile: kreaProfile,
recommended_params: {
steps: exactDefs.steps,
cfg: exactDefs.cfg,
sigma_shift: exactDefs.sigma_shift,
},
...initCtx, ...initCtx,
}; };
if (S && typeof S.compactContext === 'function') { if (S && typeof S.compactContext === 'function') {
@@ -3853,7 +4019,7 @@
return ctx; return ctx;
} }
const g = (state.chatSession && state.chatSession.gen) || {}; const g = (state.chatSession && state.chatSession.gen) || {};
return { session: true, prompt: g.prompt || '', negative: g.negative || '', ...extra }; return { session: true, prompt: g.prompt || '', negative: g.negative || '', krea_profile: kreaProfile, ...extra };
} }
function slimInventoryLoras(list, limit) { function slimInventoryLoras(list, limit) {
const selected = new Set(); const selected = new Set();
@@ -4188,6 +4354,11 @@
if (!patch) { if (!patch) {
return; return;
} }
const S = window.SA && window.SA.session;
const wantsGen = S && typeof S.patchWantsGenerate === 'function' && S.patchWantsGenerate(patch);
if (wantsGen) {
patch = ensureExactParamsForGenerate(patch);
}
const doPrompt = !which || which === 'all' || which === 'prompt'; const doPrompt = !which || which === 'all' || which === 'prompt';
const doLoras = !which || which === 'all' || which === 'loras'; const doLoras = !which || which === 'all' || which === 'loras';
const doParams = !which || which === 'all' || which === 'size' || which === 'params'; const doParams = !which || which === 'all' || which === 'size' || which === 'params';
@@ -4372,6 +4543,12 @@
setVal(batchId, String(defBatch)); setVal(batchId, String(defBatch));
} }
} }
// Generate: force Exact profile numbers when live still has foreign leftovers (20/7).
const Sgen = window.SA && window.SA.session;
if (Sgen?.patchWantsGenerate?.(patch) || patch.generate === true
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'))) {
forceExactParamsForGenerate({ patch });
}
} }
if (doInit) { if (doInit) {
@@ -4795,17 +4972,53 @@
} }
async function runGenerateFromPatch(patch, opts = {}) { async function runGenerateFromPatch(patch, opts = {}) {
// Capture the model/user delta before live UI pollutes Exact keys.
const deltaBeforeLive = patch && typeof patch === 'object' ? { ...patch } : {};
const S = window.SA && window.SA.session;
const exactKeys = (S && S.EXACT_GENERATE_PARAM_KEYS) || ['steps', 'cfg', 'sigma_shift'];
// 0.14: session is source of truth for Generate // 0.14: session is source of truth for Generate
if (typeof pullLiveIntoSession === 'function') pullLiveIntoSession(); if (typeof pullLiveIntoSession === 'function') pullLiveIntoSession();
// Live UI often still holds SD-like leftovers (steps 20 / cfg 7). Drop those
// Exact keys from the session unless this delta or the user explicitly set them.
if (state.chatSession && state.chatSession.gen) {
for (const key of exactKeys) {
const fromDelta = deltaBeforeLive[key] != null;
const fromUser = !!state.lastUserParamIntent && state.sessionExact?.[key] != null;
if (!fromDelta && !fromUser) {
state.chatSession.gen[key] = null;
}
}
}
let working = {
...deltaBeforeLive,
generate: true,
actions: Array.isArray(deltaBeforeLive.actions)
&& deltaBeforeLive.actions.map(String).includes('generate')
? deltaBeforeLive.actions
: [...(Array.isArray(deltaBeforeLive.actions) ? deltaBeforeLive.actions : []), 'generate'],
};
working = ensureExactParamsForGenerate(working);
if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) { if (state.chatSession && state.chatSession.gen && (opts.fromSession || opts.force)) {
const fromSess = { ...state.chatSession.gen, generate: true, actions: ['generate'] }; const fromSess = { ...state.chatSession.gen, generate: true, actions: ['generate'] };
if (patch && typeof patch === 'object') { for (const k of Object.keys(working)) {
for (const k of Object.keys(patch)) { if (working[k] != null) fromSess[k] = working[k];
if (patch[k] != null) fromSess[k] = patch[k]; }
// Re-clear Exact keys that still came only from stale live snapshot.
for (const key of exactKeys) {
const fromDelta = deltaBeforeLive[key] != null;
const fromUser = !!state.lastUserParamIntent && state.sessionExact?.[key] != null;
if (!fromDelta && !fromUser && working[key] != null) {
fromSess[key] = working[key];
} }
} }
patch = fromSess; patch = ensureExactParamsForGenerate(fromSess);
if (typeof pushSessionToSwarm === 'function') await pushSessionToSwarm(state.chatSession); if (typeof pushSessionToSwarm === 'function') await pushSessionToSwarm(state.chatSession);
} else {
patch = working;
} }
const force = !!opts.force; const force = !!opts.force;
@@ -4871,6 +5084,11 @@
syncLiveParamsBar(); syncLiveParamsBar();
} }
ensureNegativeForGenerate(job.patch); ensureNegativeForGenerate(job.patch);
// P0: never Generate with leftover 20/7 under a turbo/raw Exact profile.
forceExactParamsForGenerate({ patch: job.patch });
if (typeof pullLiveIntoSession === 'function') {
pullLiveIntoSession();
}
if (epoch !== state.chatEpoch) { if (epoch !== state.chatEpoch) {
break; break;
@@ -5817,7 +6035,12 @@ if (!(meta && meta.historical)) {
fillKnobsFromConfig(data); fillKnobsFromConfig(data);
updateCtxChip(); updateCtxChip();
if (applyDefaults || data.exact) { if (applyDefaults || data.exact) {
lastExactForceCkpt = null;
fillEmptyParamsFromExact(); fillEmptyParamsFromExact();
if (typeof isKreaSelected === 'function' && isKreaSelected()) {
forceExactParamsForGenerate({});
lastExactForceCkpt = resolveCurrentCheckpoint()?.name || null;
}
} }
const nextPersona = data.persona || $('sa_persona')?.value || ''; const nextPersona = data.persona || $('sa_persona')?.value || '';
let controlValues = data.control_values || data.exact?.controls || {}; let controlValues = data.control_values || data.exact?.controls || {};
@@ -7531,6 +7754,15 @@ if (!(meta && meta.historical)) {
status: intent.vetoed ? 'skip' : 'running', status: intent.vetoed ? 'skip' : 'running',
}); });
if (typeof startBusyUi === 'function') startBusyUi('silent_gen'); if (typeof startBusyUi === 'function') startBusyUi('silent_gen');
if (effective) {
effective = ensureExactParamsForGenerate({
...effective,
generate: true,
actions: Array.isArray(effective.actions) && effective.actions.map(String).includes('generate')
? effective.actions
: [...(Array.isArray(effective.actions) ? effective.actions : []), 'generate'],
});
}
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
await pushSessionToSwarm(state.chatSession); await pushSessionToSwarm(state.chatSession);
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar(); if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
@@ -7554,13 +7786,16 @@ if (!(meta && meta.historical)) {
state.pendingSilentGen = false; state.pendingSilentGen = false;
} }
async function applyQuickPatch(patch, note) { async function applyQuickPatch(patch, note) {
const withActions = { ...patch }; let withActions = { ...patch };
if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) { if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) {
withActions.actions = ['generate']; withActions.actions = ['generate'];
} }
const prevIntent = state.lastUserParamIntent; const prevIntent = state.lastUserParamIntent;
state.lastUserParamIntent = true; state.lastUserParamIntent = true;
const S = window.SA && window.SA.session; const S = window.SA && window.SA.session;
if (S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) {
withActions = ensureExactParamsForGenerate(withActions);
}
if (S) { if (S) {
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions); state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
} }
+108
View File
@@ -0,0 +1,108 @@
/**
* Krea turbo/raw profile detection + Exact param alignment helpers (pure).
* Prevents labeling ambiguous checkpoints as turbo while Swarm still has SD-like 20/7.
*/
export function numClose(a, b, eps = 0.051) {
const x = Number(a);
const y = Number(b);
if (!Number.isFinite(x) || !Number.isFinite(y)) {
return false;
}
return Math.abs(x - y) <= eps;
}
/**
* Detect turbo|raw from checkpoint name/title.
* Names without turbo/raw (e.g. realismByStableYogi) must NOT invent "turbo".
* Note: INT8Turbo has no word-boundary before "turbo" — use substring match.
*/
export function detectKreaProfileName(modelBlob, exactProfile) {
const blob = String(modelBlob || '').toLowerCase();
const hasTurbo = /turbo/.test(blob);
const hasRaw = /\braw\b|_raw\b|-raw\b|\/raw\b/.test(blob);
if (hasTurbo) {
return 'turbo';
}
if (hasRaw) {
return 'raw';
}
// Unlabeled ckpt — Exact generation.profile alone must not invent turbo.
void exactProfile;
return 'raw';
}
/** Profile-only defaults (no session overlay). */
export function profileParamDefaults(profiles, profileName, generation = {}) {
const profile = String(profileName || 'raw');
const fromProfile = profiles && typeof profiles[profile] === 'object' ? profiles[profile] : {};
const gen = generation && typeof generation === 'object' ? generation : {};
return {
profile,
steps: fromProfile.steps ?? gen.steps ?? null,
cfg: fromProfile.cfg ?? gen.cfg ?? null,
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null,
};
}
/**
* Whether live Swarm numbers already match the Exact profile (steps/cfg/σ).
*/
export function liveMatchesExactProfile(live, defaults) {
if (!defaults || defaults.steps == null || defaults.cfg == null) {
return true;
}
const stepsOk = live?.steps == null || numClose(live.steps, defaults.steps, 0.5);
const cfgOk = live?.cfg == null || numClose(live.cfg, defaults.cfg);
const sigmaOk = defaults.sigma_shift == null
|| live?.sigma_shift == null
|| numClose(live.sigma_shift, defaults.sigma_shift);
return stepsOk && cfgOk && sigmaOk;
}
/**
* Keys that should be forced from Exact onto live before Generate.
* - Always rewrite live when it disagrees with Exact profile defaults.
* - Skip only when the patch sets a *different* intentional value, or
* sessionExact holds an override and userParamIntent is true.
* - Stale sessionExact leftovers (SD 20/7) must NOT block Exact.
*/
export function exactKeysToForce(live, defaults, {
patch = null,
sessionExact = null,
userParamIntent = false,
} = {}) {
if (!defaults) {
return [];
}
const out = [];
for (const key of ['steps', 'cfg', 'sigma_shift']) {
const want = defaults[key];
if (want == null) {
continue;
}
const eps = key === 'steps' ? 0.5 : 0.051;
// Patch asked for a non-Exact number — honor it.
if (patch && patch[key] != null && !numClose(patch[key], want, eps)) {
continue;
}
if (userParamIntent && sessionExact && sessionExact[key] != null) {
continue;
}
const have = live?.[key];
if (have == null || !numClose(have, want, eps)) {
out.push(key);
}
}
return out;
}
export function attachKreaProfile(SA) {
SA.kreaProfile = {
numClose,
detectKreaProfileName,
profileParamDefaults,
liveMatchesExactProfile,
exactKeysToForce,
};
}
+2
View File
@@ -4,6 +4,7 @@ import { attachPersist } from './persist.js';
import { attachSession } from './session.js'; import { attachSession } from './session.js';
import { attachContext } from './context.js'; import { attachContext } from './context.js';
import { attachActivity } from './activity.js'; import { attachActivity } from './activity.js';
import { attachKreaProfile } from './kreaProfile.js';
window.SA = window.SA || {}; window.SA = window.SA || {};
attachApi(window.SA); attachApi(window.SA);
@@ -12,6 +13,7 @@ attachPersist(window.SA);
attachSession(window.SA); attachSession(window.SA);
attachContext(window.SA); attachContext(window.SA);
attachActivity(window.SA); attachActivity(window.SA);
attachKreaProfile(window.SA);
/** Called from app after AssistentGetConfig — single source: Config/_base/patch-keys.json */ /** Called from app after AssistentGetConfig — single source: Config/_base/patch-keys.json */
window.SA.applyConfigPatchKeys = function (config) { window.SA.applyConfigPatchKeys = function (config) {
+61
View File
@@ -350,6 +350,64 @@ export function resolveTurnIntent(patch, userText, { vetoFn } = {}) {
return { generate, look, vetoed, ask }; return { generate, look, vetoed, ask };
} }
/** Params the client fills from Exact on Generate when the LLM omits them (sparse contract). */
export const EXACT_GENERATE_PARAM_KEYS = ['steps', 'cfg', 'sigma_shift'];
/**
* Turbo/RAW profile numbers from Exact — no sessionExact overlay.
* profileName should already reflect the live checkpoint (turbo vs raw).
*/
export function resolveExactProfileDefaults({ exact, profiles, profileName } = {}) {
const gen = exact?.generation && typeof exact.generation === 'object' ? exact.generation : {};
const profile = profileName || gen.profile || 'turbo';
const fromProfile = profiles?.[profile] && typeof profiles[profile] === 'object'
? profiles[profile]
: {};
return {
profile,
steps: fromProfile.steps ?? gen.steps ?? null,
cfg: fromProfile.cfg ?? gen.cfg ?? null,
sigma_shift: fromProfile.sigma_shift ?? gen.sigma_shift ?? null,
};
}
/**
* On generate, inject Exact profile steps/cfg/sigma when the patch omitted them.
* Client is authoritative so sparse LLM deltas are safe. Honors explicit patch values
* and sessionExact when the user asked for params this turn.
* Returns clearSessionKeys so the UI apply path is not blocked by stale sessionExact.
*/
export function mergeExactParamsForGenerate(patch, {
exact,
profiles,
profileName,
sessionExact,
userParamIntent,
} = {}) {
if (!patchWantsGenerate(patch)) {
return { patch, clearSessionKeys: [], profile: null };
}
const defaults = resolveExactProfileDefaults({ exact, profiles, profileName });
const out = { ...patch };
const clearSessionKeys = [];
for (const key of EXACT_GENERATE_PARAM_KEYS) {
if (out[key] != null) {
continue;
}
if (userParamIntent && sessionExact?.[key] != null) {
out[key] = sessionExact[key];
continue;
}
if (defaults[key] != null) {
out[key] = defaults[key];
if (sessionExact?.[key] != null && String(sessionExact[key]) !== String(defaults[key])) {
clearSessionKeys.push(key);
}
}
}
return { patch: out, clearSessionKeys, profile: defaults.profile };
}
export function attachSession(SA) { export function attachSession(SA) {
SA.session = { SA.session = {
emptySession, emptySession,
@@ -363,6 +421,9 @@ export function attachSession(SA) {
compactContext, compactContext,
fullSettingsDump, fullSettingsDump,
resolveTurnIntent, resolveTurnIntent,
resolveExactProfileDefaults,
mergeExactParamsForGenerate,
EXACT_GENERATE_PARAM_KEYS,
GEN_KEYS, GEN_KEYS,
}; };
} }
+84
View File
@@ -0,0 +1,84 @@
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import {
detectKreaProfileName,
profileParamDefaults,
liveMatchesExactProfile,
exactKeysToForce,
numClose,
} from '../src/kreaProfile.js';
const PROFILES = {
turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 },
raw: { steps: 28, cfg: 4.5, sigma_shift: 1.15 },
};
describe('kreaProfile', () => {
it('detectKreaProfileName: turbo token wins', () => {
assert.equal(detectKreaProfileName('realismByStableYogi_v25INT8Turbo', 'raw'), 'turbo');
assert.equal(detectKreaProfileName('Krea2 Turbo fp8', 'raw'), 'turbo');
});
it('detectKreaProfileName: raw token without turbo', () => {
assert.equal(detectKreaProfileName('Krea2 RAW Base', 'turbo'), 'raw');
});
it('detectKreaProfileName: unlabeled Yogi is not fake turbo', () => {
assert.equal(detectKreaProfileName('realismByStableYogi_v25', 'turbo'), 'raw');
assert.equal(detectKreaProfileName('realismByStableYogi', null), 'raw');
});
it('liveMatchesExactProfile rejects SD leftovers under turbo', () => {
const turbo = profileParamDefaults(PROFILES, 'turbo');
assert.equal(liveMatchesExactProfile({ steps: 20, cfg: 7, sigma_shift: 1.15 }, turbo), false);
assert.equal(liveMatchesExactProfile({ steps: 8, cfg: 1, sigma_shift: 1.15 }, turbo), true);
});
it('exactKeysToForce overwrites 20/7 unless intentional non-Exact patch or user session', () => {
const turbo = profileParamDefaults(PROFILES, 'turbo');
assert.deepEqual(
exactKeysToForce({ steps: 20, cfg: 7, sigma_shift: 1 }, turbo, {}),
['steps', 'cfg', 'sigma_shift'],
);
// Injected Exact values in the patch still force live leftovers.
assert.deepEqual(
exactKeysToForce(
{ steps: 20, cfg: 7, sigma_shift: 1 },
turbo,
{ patch: { steps: 8, cfg: 1, sigma_shift: 1.15 } },
),
['steps', 'cfg', 'sigma_shift'],
);
// Stale sessionExact without user intent must not block Exact.
assert.deepEqual(
exactKeysToForce(
{ steps: 20, cfg: 7, sigma_shift: 1.15 },
turbo,
{ sessionExact: { steps: 20 }, userParamIntent: false },
),
['steps', 'cfg'],
);
assert.deepEqual(
exactKeysToForce(
{ steps: 20, cfg: 7, sigma_shift: 1.15 },
turbo,
{ sessionExact: { steps: 20 }, userParamIntent: true },
),
['cfg'],
);
// Intentional non-Exact patch value is kept.
assert.deepEqual(
exactKeysToForce({ steps: 20, cfg: 7, sigma_shift: 1.15 }, turbo, { patch: { cfg: 7 } }),
['steps'],
);
assert.deepEqual(
exactKeysToForce({ steps: 8, cfg: 1, sigma_shift: 1.15 }, turbo, {}),
[],
);
});
it('numClose', () => {
assert.equal(numClose(1.15, 1.14), true);
assert.equal(numClose(1, 7), false);
});
});
+65
View File
@@ -13,6 +13,8 @@ import {
sessionFromLegacyParams, sessionFromLegacyParams,
toPersistParams, toPersistParams,
resolveTurnIntent, resolveTurnIntent,
mergeExactParamsForGenerate,
resolveExactProfileDefaults,
} from '../src/session.js'; } from '../src/session.js';
describe('patch.js', () => { describe('patch.js', () => {
@@ -74,4 +76,67 @@ describe('session.js', () => {
assert.equal(intent.generate, false); assert.equal(intent.generate, false);
assert.equal(intent.vetoed, true); assert.equal(intent.vetoed, true);
}); });
it('mergeExactParamsForGenerate fills turbo profile when LLM omits steps/cfg', () => {
const exact = {
generation: { profile: 'turbo', steps: 8, cfg: 1, sigma_shift: 1.15 },
profiles: {
turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 },
raw: { steps: 28, cfg: 4.5, sigma_shift: 1.15 },
},
};
const { patch, clearSessionKeys, profile } = mergeExactParamsForGenerate(
{ prompt: 'a fox', generate: true },
{
exact,
profiles: exact.profiles,
profileName: 'turbo',
sessionExact: { steps: 20, cfg: 7 },
userParamIntent: false,
},
);
assert.equal(profile, 'turbo');
assert.equal(patch.steps, 8);
assert.equal(patch.cfg, 1);
assert.equal(patch.sigma_shift, 1.15);
assert.deepEqual(clearSessionKeys.sort(), ['cfg', 'steps']);
});
it('mergeExactParamsForGenerate keeps user-intent sessionExact and explicit patch', () => {
const exact = {
generation: { steps: 8, cfg: 1 },
profiles: { raw: { steps: 28, cfg: 4.5, sigma_shift: 1.15 } },
};
const kept = mergeExactParamsForGenerate(
{ generate: true, actions: ['generate'] },
{
exact,
profiles: exact.profiles,
profileName: 'raw',
sessionExact: { steps: 20 },
userParamIntent: true,
},
);
assert.equal(kept.patch.steps, 20);
assert.equal(kept.patch.cfg, 4.5);
assert.deepEqual(kept.clearSessionKeys, []);
const explicit = mergeExactParamsForGenerate(
{ generate: true, steps: 12, cfg: 2 },
{ exact, profiles: exact.profiles, profileName: 'turbo' },
);
assert.equal(explicit.patch.steps, 12);
assert.equal(explicit.patch.cfg, 2);
});
it('resolveExactProfileDefaults picks raw over generation defaults', () => {
const d = resolveExactProfileDefaults({
exact: { generation: { steps: 8, cfg: 1, sigma_shift: 1.15 } },
profiles: { raw: { steps: 28, cfg: 4.5 } },
profileName: 'raw',
});
assert.equal(d.steps, 28);
assert.equal(d.cfg, 4.5);
assert.equal(d.sigma_shift, 1.15);
});
}); });