From 1b9cc1ad788b49cb834fd8ccdf8a2c67ba8ecbbc Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Fri, 21 Aug 2026 23:04:34 +0300 Subject: [PATCH] Add exact KV memory with persona overlays and param priority. Canonical defaults live in exact.json (UI fills empties without an LLM call); chat session overrides beat file defaults until clear/persona change. Co-authored-by: Cursor --- Assets/assistent.js | 311 ++++++++++++++++++++--- AssistentConfig.cs | 5 + Config/_base/assistant.json | 2 +- Config/_base/core/core.md | 18 +- Config/_base/exact.json | 38 +++ Config/_base/memory-seed/aspect.json | 46 +--- Config/_base/memory-seed/krea_facts.json | 10 +- Config/_base/models/krea2.json | 24 +- Config/_base/packs/fix_params.md | 8 +- Config/_base/packs/write_prompt.md | 2 +- Config/_base/skills/memory.json | 2 +- Config/_base/skills/memory.md | 12 +- Config/personas/cinema/exact.json | 5 + README.md | 19 +- SwarmAssistentExtension.cs | 37 ++- 15 files changed, 418 insertions(+), 121 deletions(-) create mode 100644 Config/_base/exact.json create mode 100644 Config/personas/cinema/exact.json diff --git a/Assets/assistent.js b/Assets/assistent.js index 5090a8c..0925fc2 100644 --- a/Assets/assistent.js +++ b/Assets/assistent.js @@ -1,6 +1,6 @@ /** * Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). - * v0.7.0: Config presets, persona folders, vector memory, chat|memory model roles. + * v0.7.0: Config presets, persona folders, exact KV + vector memory, chat|memory model roles. */ (function () { const LS_BASE = 'swarm_assistent_base_url'; @@ -99,6 +99,9 @@ history: [], packsLoaded: false, config: null, + exact: null, + sessionExact: {}, + lastUserParamIntent: false, enabledSkills: [], kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } }, preferredEmbed: null, @@ -424,6 +427,158 @@ el.dispatchEvent(new Event('change', { bubbles: true })); } + function isEmptyParamField(raw, { treatZeroEmpty = false } = {}) { + if (raw == null) { + return true; + } + const s = String(raw).trim(); + if (s === '') { + return true; + } + if (treatZeroEmpty && (s === '0' || Number(s) === 0)) { + return true; + } + return false; + } + + function userTextMentionsParams(text) { + return /\b(steps?|cfg|seed|sigma|размер|aspect|ширин|высот|resolution|batch|турбо|turbo|raw)\b/i.test(String(text || '')); + } + + function applyAspectTableFrom(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + const next = {}; + for (const [k, v] of Object.entries(obj)) { + if (Array.isArray(v) && v.length >= 2) { + next[k] = [Number(v[0]), Number(v[1])]; + } + } + if (!Object.keys(next).length) { + return false; + } + ASPECT_TABLE = next; + return true; + } + + function resolveExactBundle() { + const exact = state.exact || state.config?.exact || {}; + const profiles = exact.profiles || state.kreaProfiles || {}; + return { exact, profiles }; + } + + function detectKreaProfileName() { + try { + const model = resolveCurrentCheckpoint(); + const blob = `${model?.name || ''} ${model?.title || ''}`.toLowerCase(); + const hasRaw = /\braw\b/.test(blob); + const hasTurbo = /\bturbo\b/.test(blob); + return hasRaw && !hasTurbo ? 'raw' : 'turbo'; + } catch (e) { + return (state.exact?.generation?.profile) || 'turbo'; + } + } + + function mergedGenerationDefaults(profileName) { + const { exact, profiles } = resolveExactBundle(); + const gen = exact.generation && typeof exact.generation === 'object' ? { ...exact.generation } : {}; + const profile = profileName || gen.profile || detectKreaProfileName(); + const fromProfile = profiles[profile] && typeof profiles[profile] === 'object' ? { ...profiles[profile] } : {}; + const session = state.sessionExact && typeof state.sessionExact === 'object' ? { ...state.sessionExact } : {}; + // Profile (turbo/raw) overrides generation defaults; session overrides both. + return { ...gen, ...fromProfile, profile, ...session }; + } + + function exactDefaultFor(key, profileName) { + const { exact, profiles } = resolveExactBundle(); + const profile = profileName || exact.generation?.profile || detectKreaProfileName(); + const fromProfile = profiles[profile]?.[key]; + if (fromProfile != null) { + return fromProfile; + } + return exact.generation?.[key]; + } + + function rememberSessionExact(partial) { + if (!partial || typeof partial !== 'object') { + return; + } + const keys = ['steps', 'cfg', 'sigma_shift', 'aspect', 'width', 'height', 'images', 'batch', 'seed', 'sampler', 'scheduler']; + for (const k of keys) { + if (partial[k] != null) { + state.sessionExact[k] = partial[k]; + } + } + if (partial.images == null && partial.batch != null) { + state.sessionExact.images = partial.batch; + } + } + + function fillEmptyParamsFromExact() { + const defaults = mergedGenerationDefaults(); + if (isEmptyParamField(val('input_steps'), { treatZeroEmpty: true }) && defaults.steps != null) { + setVal('input_steps', String(defaults.steps)); + } + const cfgRaw = val('input_cfgscale') || val('input_cfg'); + if (isEmptyParamField(cfgRaw, { treatZeroEmpty: true }) && defaults.cfg != null) { + if (document.getElementById('input_cfgscale')) { + setVal('input_cfgscale', String(defaults.cfg)); + } else if (document.getElementById('input_cfg')) { + setVal('input_cfg', String(defaults.cfg)); + } + } + if (isEmptyParamField(val('input_sigmashift')) && defaults.sigma_shift != null) { + setVal('input_sigmashift', String(defaults.sigma_shift)); + } + const wEmpty = isEmptyParamField(val('input_width'), { treatZeroEmpty: true }); + const hEmpty = isEmptyParamField(val('input_height'), { treatZeroEmpty: true }); + if ((wEmpty || hEmpty) && defaults.aspect) { + const size = sizeFromAspect(defaults.aspect); + if (size) { + if (wEmpty) { + setVal('input_width', String(size[0])); + } + if (hEmpty) { + setVal('input_height', String(size[1])); + } + } + } else { + if (wEmpty && defaults.width != null) { + setVal('input_width', String(defaults.width)); + } + if (hEmpty && defaults.height != null) { + setVal('input_height', String(defaults.height)); + } + } + const batchId = document.getElementById('input_images') ? 'input_images' : (document.getElementById('input_batchsize') ? 'input_batchsize' : null); + if (batchId && isEmptyParamField(val(batchId), { treatZeroEmpty: true })) { + const batch = defaults.images != null ? defaults.images : defaults.batch; + if (batch != null) { + setVal(batchId, String(batch)); + } + } + } + + function shouldSkipSessionRollback(key, patchValue) { + if (state.lastUserParamIntent) { + return false; + } + if (state.sessionExact[key] == null) { + return false; + } + const sessionVal = state.sessionExact[key]; + if (String(sessionVal) === String(patchValue)) { + return false; + } + const exactVal = exactDefaultFor(key); + if (exactVal == null) { + return false; + } + // Model trying to restore file exact while session override differs — keep session. + return String(patchValue) === String(exactVal); + } + function openAssistentTab() { const tab = document.getElementById(TAB_BUTTON_ID); if (tab) { @@ -1143,6 +1298,8 @@ function onPersonaChanged() { const id = $('sa_persona')?.value || 'neutral'; + state.sessionExact = {}; + state.lastUserParamIntent = false; saveSettings(); loadConfig(id, (data) => { const title = data?.personas?.find((p) => p.id === id)?.title @@ -1159,6 +1316,7 @@ $('sa_pack').value = packId; } } + fillEmptyParamsFromExact(); }); } @@ -1480,15 +1638,32 @@ const blob = `${model.name || ''} ${model.title || ''}`.toLowerCase(); const hasRaw = /\braw\b/.test(blob); const hasTurbo = /\bturbo\b/.test(blob); - ctx.krea_profile = hasRaw && !hasTurbo ? 'raw' : 'turbo'; - ctx.recommended_params = ctx.krea_profile === 'raw' - ? { steps: 28, cfg: 4.5 } - : { steps: 8, cfg: 1, sigma_shift: 1.15 }; + const profile = hasRaw && !hasTurbo ? 'raw' : 'turbo'; + ctx.krea_profile = profile; + const defaults = mergedGenerationDefaults(profile); + ctx.recommended_params = { + steps: defaults.steps ?? 8, + cfg: defaults.cfg ?? 1, + sigma_shift: defaults.sigma_shift ?? 1.15, + }; + if (defaults.aspect) { + ctx.recommended_params.aspect = defaults.aspect; + } } catch (e) { ctx.krea_profile = 'turbo'; - ctx.recommended_params = { steps: 8, cfg: 1, sigma_shift: 1.15 }; + const defaults = mergedGenerationDefaults('turbo'); + ctx.recommended_params = { + steps: defaults.steps ?? 8, + cfg: defaults.cfg ?? 1, + sigma_shift: defaults.sigma_shift ?? 1.15, + }; } + ctx.exact = state.exact || state.config?.exact || null; + ctx.session_exact = state.sessionExact && Object.keys(state.sessionExact).length + ? { ...state.sessionExact } + : {}; + return ctx; } @@ -1901,7 +2076,7 @@ if (/\b(inpaint|замажь|закрась|руки|лицо|маск|mask|img2img|init\s*image)\b/i.test(t)) { return 'inpaint_edit'; } - if (/\b(steps?|cfg|seed|sigma|размер|aspect|ширин|высот|resolution|batch)\b/i.test(t)) { + if (userTextMentionsParams(t)) { return 'fix_params'; } if (/\b(сцен|moodboard|атмосфер|compose|scene|мизансцен)\b/i.test(t)) { @@ -2010,27 +2185,68 @@ } if (doParams) { + const defaults = mergedGenerationDefaults(); + const capture = state.lastUserParamIntent; const aspectSize = sizeFromAspect(patch.aspect); - if (aspectSize) { - setVal('input_width', String(aspectSize[0])); - setVal('input_height', String(aspectSize[1])); + if (patch.aspect != null && !shouldSkipSessionRollback('aspect', patch.aspect)) { + if (aspectSize) { + setVal('input_width', String(aspectSize[0])); + setVal('input_height', String(aspectSize[1])); + } + if (capture) { + rememberSessionExact({ aspect: patch.aspect }); + } + } else if (patch.aspect == null && isEmptyParamField(val('input_width'), { treatZeroEmpty: true }) + && isEmptyParamField(val('input_height'), { treatZeroEmpty: true }) && defaults.aspect) { + const fill = sizeFromAspect(defaults.aspect); + if (fill) { + setVal('input_width', String(fill[0])); + setVal('input_height', String(fill[1])); + } } else { - if (patch.width != null) { + if (patch.width != null && !shouldSkipSessionRollback('width', patch.width)) { setVal('input_width', String(patch.width)); + if (capture) { + rememberSessionExact({ width: patch.width }); + } + } else if (patch.width == null && isEmptyParamField(val('input_width'), { treatZeroEmpty: true }) && defaults.width != null) { + setVal('input_width', String(defaults.width)); } - if (patch.height != null) { + if (patch.height != null && !shouldSkipSessionRollback('height', patch.height)) { setVal('input_height', String(patch.height)); + if (capture) { + rememberSessionExact({ height: patch.height }); + } + } else if (patch.height == null && isEmptyParamField(val('input_height'), { treatZeroEmpty: true }) && defaults.height != null) { + setVal('input_height', String(defaults.height)); } } - if (patch.steps != null) { + if (patch.steps != null && !shouldSkipSessionRollback('steps', patch.steps)) { setVal('input_steps', String(patch.steps)); + if (capture) { + rememberSessionExact({ steps: patch.steps }); + } + } else if (patch.steps == null && isEmptyParamField(val('input_steps'), { treatZeroEmpty: true }) && defaults.steps != null) { + setVal('input_steps', String(defaults.steps)); } - if (patch.cfg != null) { + if (patch.cfg != null && !shouldSkipSessionRollback('cfg', patch.cfg)) { if (document.getElementById('input_cfgscale')) { setVal('input_cfgscale', String(patch.cfg)); } else { setVal('input_cfg', String(patch.cfg)); } + if (capture) { + rememberSessionExact({ cfg: patch.cfg }); + } + } else if (patch.cfg == null) { + const cfgRaw = val('input_cfgscale') || val('input_cfg'); + if (isEmptyParamField(cfgRaw, { treatZeroEmpty: true }) && defaults.cfg != null) { + if (document.getElementById('input_cfgscale')) { + setVal('input_cfgscale', String(defaults.cfg)); + } else if (document.getElementById('input_cfg')) { + setVal('input_cfg', String(defaults.cfg)); + } + } } if (patch.vary === true) { setVal('input_seed', '-1'); @@ -2039,27 +2255,50 @@ if (cur && String(cur) !== '-1') { setVal('input_seed', cur); } - } else if (patch.seed != null) { + } else if (patch.seed != null && !shouldSkipSessionRollback('seed', patch.seed)) { setVal('input_seed', String(patch.seed)); + if (capture) { + rememberSessionExact({ seed: patch.seed }); + } } - if (patch.sigma_shift != null) { + if (patch.sigma_shift != null && !shouldSkipSessionRollback('sigma_shift', patch.sigma_shift)) { setVal('input_sigmashift', String(patch.sigma_shift)); + if (capture) { + rememberSessionExact({ sigma_shift: patch.sigma_shift }); + } + } 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 (capture) { + rememberSessionExact({ sampler: patch.sampler }); + } } if (patch.scheduler != null && document.getElementById('input_scheduler')) { setVal('input_scheduler', String(patch.scheduler)); + if (capture) { + rememberSessionExact({ scheduler: patch.scheduler }); + } } const batch = patch.images != null ? patch.images : patch.batch; - if (batch != null) { + if (batch != null && !shouldSkipSessionRollback('images', batch)) { if (document.getElementById('input_images')) { setVal('input_images', String(batch)); } else if (document.getElementById('input_batchsize')) { setVal('input_batchsize', String(batch)); } + if (capture) { + rememberSessionExact({ images: batch }); + } + } else if (batch == null) { + const batchId = document.getElementById('input_images') ? 'input_images' : (document.getElementById('input_batchsize') ? 'input_batchsize' : null); + const defBatch = defaults.images != null ? defaults.images : defaults.batch; + if (batchId && isEmptyParamField(val(batchId), { treatZeroEmpty: true }) && defBatch != null) { + setVal(batchId, String(defBatch)); + } } } @@ -2835,19 +3074,16 @@ return; } state.config = data; - if (data.model?.aspect_table && typeof data.model.aspect_table === 'object') { - const next = {}; - for (const [k, v] of Object.entries(data.model.aspect_table)) { - if (Array.isArray(v) && v.length >= 2) { - next[k] = [Number(v[0]), Number(v[1])]; - } - } - if (Object.keys(next).length) { - ASPECT_TABLE = next; - } + if (data.exact && typeof data.exact === 'object') { + state.exact = data.exact; } - if (data.model?.profiles) { - state.kreaProfiles = data.model.profiles; + const aspectSource = data.exact?.aspect_table || data.model?.aspect_table; + if (aspectSource && typeof aspectSource === 'object') { + applyAspectTableFrom(aspectSource); + } + const profileSource = data.exact?.profiles || data.model?.profiles; + if (profileSource && typeof profileSource === 'object') { + state.kreaProfiles = profileSource; } if (data.ui?.pack_aliases) { PACK_ALIASES = { ...PACK_ALIASES, ...data.ui.pack_aliases }; @@ -2879,6 +3115,9 @@ if (data.assistant?.embed_model && !state.preferredEmbed) { state.preferredEmbed = data.assistant.embed_model; } + if (applyDefaults || data.exact) { + fillEmptyParamsFromExact(); + } } function renderPersonaOptions(personas, selected) { @@ -3958,7 +4197,10 @@ if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) { withActions.actions = ['generate']; } + const prevIntent = state.lastUserParamIntent; + state.lastUserParamIntent = true; await applyPatch(withActions, 'all'); + state.lastUserParamIntent = prevIntent; setStatus(note || 'Applied'); if ($('sa_auto_generate')?.checked) { await runGenerateFromPatch(withActions); @@ -4209,6 +4451,9 @@ if (!text) { return; } + if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) { + state.lastUserParamIntent = userTextMentionsParams(text); + } if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.skipSlash) { if (rawInput.startsWith('/')) { @@ -4745,6 +4990,8 @@ state.visionHopUsed = false; state.packUserTouched = false; state.pendingPersonaNote = null; + state.sessionExact = {}; + state.lastUserParamIntent = false; clearPersistedHistory(); const box = $('sa_messages'); if (box) { @@ -4786,11 +5033,11 @@ } else if (vary) { await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary'); } else if (profile === 'turbo') { - const p = state.kreaProfiles?.turbo || { steps: 8, cfg: 1, sigma_shift: 1.15 }; + const p = state.kreaProfiles?.turbo || mergedGenerationDefaults('turbo'); await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ['generate'] }, 'Turbo'); } else if (profile === 'raw') { - const p = state.kreaProfiles?.raw || { steps: 28, cfg: 4.5 }; - await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, actions: ['generate'] }, 'RAW'); + const p = state.kreaProfiles?.raw || mergedGenerationDefaults('raw'); + await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ['generate'] }, 'RAW'); } renderLoraChips(); }); diff --git a/AssistentConfig.cs b/AssistentConfig.cs index ea862ce..138e903 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -319,6 +319,9 @@ public sealed class AssistentConfig public JObject LoadUi(string personaId) => MergeJsonLayers("ui.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)); + public JObject LoadModelProfile(string personaId) { JObject assistant = LoadAssistant(personaId); @@ -712,6 +715,7 @@ public sealed class AssistentConfig JObject assistant = LoadAssistant(id); JObject ui = LoadUi(id); JObject model = LoadModelProfile(id); + JObject exact = LoadExact(id); var packs = ListPacks(id); var skills = ListSkills(id); var personas = ListPersonaCatalog(); @@ -724,6 +728,7 @@ public sealed class AssistentConfig ["assistant"] = assistant, ["ui"] = ui, ["model"] = model, + ["exact"] = exact, ["packs"] = new JArray(packs.Select(p => new JObject { ["id"] = p.id, diff --git a/Config/_base/assistant.json b/Config/_base/assistant.json index 07f5820..690eb02 100644 --- a/Config/_base/assistant.json +++ b/Config/_base/assistant.json @@ -10,7 +10,7 @@ "default_persona": "neutral", "embed_model": "nomic-embed-text", "memory_top_k": 10, - "seed_version": 1, + "seed_version": 2, "gate": { "architecture": "krea2", "keywords": ["krea"] diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md index 1bd5757..9e4c685 100644 --- a/Config/_base/core/core.md +++ b/Config/_base/core/core.md @@ -2,13 +2,28 @@ You are **Swarm Assistent**, a collaborative art director for image generation inside SwarmUI. +## Priority (mandatory) + +When instructions conflict, apply this order (highest wins): + +1. **This core contract** — output format, never invent LoRA/checkpoint names, never use CFG 0. +2. **Current user message** — explicit “use steps 20 / aspect 16:9 now” wins for that turn. +3. **Live `session_exact`** — prior user overrides this chat (until persona change / clear chat). +4. **Exact memory** (`## Exact memory` JSON) — canonical defaults (steps/CFG/aspect/facts). Persona overlays are already merged into it. +5. **Filled live SwarmUI fields** — respect what is already set unless the user or pack asks to change. +6. **`memory_hits` (vector RAG)** — notes, pitfalls, LoRA blurbs. Never override exact numbers or the user’s param request. +7. Guesses — last resort only. + +Exact = encyclopedia of defaults. RAG = soft notes. Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` when they already match exact (or session_exact) and the user did not ask to change them. + ## Live context A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth — refreshed every chat turn: - Use only LoRAs listed in `available_loras` / `enabled_loras` (exact `name`), or Civitai search candidates. - Prefer listed `trigger_phrase` / `triggers` — **never invent** trigger words. -- `memory_hits` are retrieved facts (model knowledge, LoRA notes, pitfalls). Trust them over guesses. +- `exact` / `session_exact` / `recommended_params` — generation defaults; see Priority. +- `memory_hits` are retrieved notes (LoRA tips, pitfalls). Trust them over guesses, but **not** over exact or the user. - `model_cards` for **enabled** models beat generic blurbs — follow `when` / `avoid` / `prompt_hint` / `triggers`. - `taste_profile` is the user's remembered preferences — bias toward it unless they override. - Prefer `krea_likely` / Krea architecture entries; ignore FLUX/SDXL LoRAs. @@ -67,6 +82,7 @@ A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth ### Patch rules - Omit keys you are not changing. +- Prefer omitting `steps` / `cfg` / `sigma_shift` / `aspect` when they already match Exact memory (or `session_exact`) and the user did not request a change — the UI fills empties from exact. - `loras` replaces the intended LoRA set for Apply (list all that should be on). - Prefer `aspect` over raw width/height when framing changes. - `vary: true` — new random seed. `lock_seed: true` — reuse current seed. diff --git a/Config/_base/exact.json b/Config/_base/exact.json new file mode 100644 index 0000000..9e55a90 --- /dev/null +++ b/Config/_base/exact.json @@ -0,0 +1,38 @@ +{ + "generation": { + "profile": "turbo", + "steps": 8, + "cfg": 1, + "sigma_shift": 1.15, + "images": 1 + }, + "profiles": { + "turbo": { + "steps": 8, + "cfg": 1, + "sigma_shift": 1.15 + }, + "raw": { + "steps": 28, + "cfg": 4.5, + "sigma_shift": 1.15 + } + }, + "aspect_table": { + "1:1": [1024, 1024], + "4:3": [1184, 896], + "3:2": [1248, 832], + "16:9": [1376, 768], + "2.35:1": [1568, 672], + "4:5": [928, 1152], + "2:3": [832, 1248], + "9:16": [768, 1376] + }, + "facts": { + "architecture": "Krea 2 is a 12B DiT architecture. Not FLUX, not SDXL, not FLUX.1-Krea. Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. Use only Krea2-trained LoRAs — never suggest FLUX/SDXL LoRAs.", + "negatives": "Negative prompts are nearly useless with Qwen3-VL. Prefer positives (sharp focus, empty street) over no blur / no people. 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).", + "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." + } +} diff --git a/Config/_base/memory-seed/aspect.json b/Config/_base/memory-seed/aspect.json index 8ff9792..fb404a8 100644 --- a/Config/_base/memory-seed/aspect.json +++ b/Config/_base/memory-seed/aspect.json @@ -1,50 +1,8 @@ [ { "kind": "aspect", - "key": "1:1", + "key": "table", "tags": ["aspect", "1k"], - "text": "Aspect 1:1 maps to 1024×1024 on the official Krea 1K table. Prefer patch field aspect over raw width/height." - }, - { - "kind": "aspect", - "key": "4:3", - "tags": ["aspect", "1k"], - "text": "Aspect 4:3 maps to 1184×896." - }, - { - "kind": "aspect", - "key": "3:2", - "tags": ["aspect", "1k"], - "text": "Aspect 3:2 maps to 1248×832." - }, - { - "kind": "aspect", - "key": "16:9", - "tags": ["aspect", "1k", "widescreen"], - "text": "Aspect 16:9 maps to 1376×768." - }, - { - "kind": "aspect", - "key": "2.35:1", - "tags": ["aspect", "1k", "cinematic"], - "text": "Aspect 2.35:1 (cinematic ultrawide) maps to 1568×672." - }, - { - "kind": "aspect", - "key": "4:5", - "tags": ["aspect", "1k", "portrait"], - "text": "Aspect 4:5 maps to 928×1152 — good for portrait." - }, - { - "kind": "aspect", - "key": "2:3", - "tags": ["aspect", "1k", "portrait"], - "text": "Aspect 2:3 maps to 832×1248." - }, - { - "kind": "aspect", - "key": "9:16", - "tags": ["aspect", "1k", "stories"], - "text": "Aspect 9:16 maps to 768×1376 — vertical / stories." + "text": "Official Krea 1K aspect → width/height table lives in Exact memory aspect_table. Prefer patch field aspect over raw width/height. UI fills empties from Exact." } ] diff --git a/Config/_base/memory-seed/krea_facts.json b/Config/_base/memory-seed/krea_facts.json index 4529f7d..977029b 100644 --- a/Config/_base/memory-seed/krea_facts.json +++ b/Config/_base/memory-seed/krea_facts.json @@ -3,30 +3,30 @@ "kind": "model", "key": "krea2_architecture", "tags": ["krea", "architecture"], - "text": "Krea 2 is a 12B DiT architecture. Not FLUX, not SDXL, not FLUX.1-Krea. Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. Use only Krea2-trained LoRAs — never suggest FLUX/SDXL LoRAs." + "text": "Krea 2 architecture facts live in Exact memory (exact.facts.architecture). Prefer Exact KV over this note. Never suggest FLUX/SDXL LoRAs." }, { "kind": "model", "key": "krea2_turbo", "tags": ["krea", "turbo", "params"], - "text": "Krea 2 Turbo defaults: steps 8 (min 4), CFG 1 (never CFG 0 — broken output), sigma shift 1.15, side ~1024 (128–4096 OK)." + "text": "Turbo numeric defaults (steps/CFG/sigma) live in Exact memory profiles.turbo / generation — do not invent numbers; never use CFG 0." }, { "kind": "model", "key": "krea2_raw", "tags": ["krea", "raw", "params"], - "text": "Krea 2 RAW/Base: steps ~20–52, CFG ~4–4.5. If checkpoint name/title looks like RAW (not Turbo), prefer RAW settings. 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." + "text": "RAW numeric defaults live in Exact memory profiles.raw. If checkpoint looks like RAW (not Turbo), use that profile. Turbo LoRA weight ~0.6 for photoreal when needed." }, { "kind": "model", "key": "krea2_negatives", "tags": ["krea", "prompting"], - "text": "Negative prompts are nearly useless with Qwen3-VL. Prefer positives (sharp focus, empty street) over no blur / no people. Built-in NSFW text-refiner may strip risque words; LoRAs/finetunes may restore — stay practical." + "text": "See Exact memory facts.negatives: negatives are nearly useless with Qwen3-VL — prefer positive phrasing." }, { "kind": "model", "key": "krea2_prompt_images", "tags": ["krea", "board"], - "text": "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." + "text": "See Exact memory facts.prompt_images: Prompt Images overpower text; Init ≠ Mask ≠ Prompt Images." } ] diff --git a/Config/_base/models/krea2.json b/Config/_base/models/krea2.json index 36405cb..dc9749c 100644 --- a/Config/_base/models/krea2.json +++ b/Config/_base/models/krea2.json @@ -1,26 +1,4 @@ { "id": "krea2", - "gate_keywords": ["krea"], - "profiles": { - "turbo": { - "steps": 8, - "cfg": 1, - "sigma_shift": 1.15 - }, - "raw": { - "steps": 28, - "cfg": 4.5, - "sigma_shift": 1.15 - } - }, - "aspect_table": { - "1:1": [1024, 1024], - "4:3": [1184, 896], - "3:2": [1248, 832], - "16:9": [1376, 768], - "2.35:1": [1568, 672], - "4:5": [928, 1152], - "2:3": [832, 1248], - "9:16": [768, 1376] - } + "gate_keywords": ["krea"] } diff --git a/Config/_base/packs/fix_params.md b/Config/_base/packs/fix_params.md index fdb442c..e27f3ca 100644 --- a/Config/_base/packs/fix_params.md +++ b/Config/_base/packs/fix_params.md @@ -4,16 +4,16 @@ Goal: adjust **generation parameters** for Krea 2 Turbo (or RAW if context says ## Guidelines -- **Turbo:** steps 4–12 (default **8**), CFG **1** (never 0), sigma shift ~**1.15**. -- **RAW/base:** steps 20–52, CFG ~4–4.5 — only if checkpoint/context indicates Raw. If a turbo-distill LoRA is available, weight **0.6** is the usual photoreal compromise (UI LoRA only — no dual-sampler). -- Prefer live context field `krea_profile` (`turbo` | `raw`) and `recommended_params` when present. -- **Aspect:** prefer patch field `aspect` (`1:1`, `4:5`, `2:3`, `16:9`, `9:16`, `4:3`, `3:2`, `2.35:1`) — UI maps to official 1K sizes. Else set width/height near 1024. +- 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. +- **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). - **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. +- User’s requested params for this turn beat Exact; do not silently roll them back to Exact defaults. ## Deliverable diff --git a/Config/_base/packs/write_prompt.md b/Config/_base/packs/write_prompt.md index 4053ef5..6f5361d 100644 --- a/Config/_base/packs/write_prompt.md +++ b/Config/_base/packs/write_prompt.md @@ -15,7 +15,7 @@ Structure as flowing prose (not tag soup): - Put **LoRA trigger phrases** near the subject they affect. - Short user ideas → expand. User already wrote a polished paragraph → keep it; only fix tags/weights/negatives-as-positives. -- Keep Turbo defaults unless asked (steps 8, cfg 1). Prefer `aspect` for framing. +- Keep Exact Turbo defaults unless asked (see Exact memory / `recommended_params`). Prefer `aspect` for framing. Omit steps/cfg/sigma/aspect from the patch when they already match Exact and the user did not ask to change them. - Missing style LoRA → `actions: ["search_civitai"]` + clear `search_query` (Krea-compatible). - Optional `creativity` / intensity/complexity/movement: expand or restrain wording accordingly; bake slider intent into the prose. diff --git a/Config/_base/skills/memory.json b/Config/_base/skills/memory.json index e6950d0..f890c9a 100644 --- a/Config/_base/skills/memory.json +++ b/Config/_base/skills/memory.json @@ -1,6 +1,6 @@ { "id": "memory", - "title": "Vector memory", + "title": "Exact + vector memory", "default": true, "prompt_file": "memory.md" } diff --git a/Config/_base/skills/memory.md b/Config/_base/skills/memory.md index d2e5aed..08ddeeb 100644 --- a/Config/_base/skills/memory.md +++ b/Config/_base/skills/memory.md @@ -1,8 +1,15 @@ # Skill: memory -You have a persistent vector memory (`memory_hits` in live context). +You have two memory layers: -## When to write +1. **Exact memory** (`## Exact memory` + live `exact` / `session_exact`) — canonical KV defaults (generation params, aspect table, architecture facts). Always prefer Exact over RAG for numbers and defaults. +2. **Vector memory** (`memory_hits`) — soft notes from retrieve (LoRA tips, pitfalls, paths). + +## Priority + +User (this turn) > `session_exact` > Exact KV > filled live fields > `memory_hits` > guesses. Never let a vector hit override Exact steps/CFG/aspect or an explicit user param request. + +## When to write (vector only) - Durable facts about a LoRA/checkpoint (when it works, what it breaks, good weight). - Bad paths / pitfalls you discovered this session. @@ -10,6 +17,7 @@ You have a persistent vector memory (`memory_hits` in live context). ## When not to write +- Do not dump Exact defaults into vector memory — they already live in `exact.json`. - Do not dump the full inventory — retrieve already surfaces relevant blurbs. - Do not store the user's taste profile (that is `taste_profile` / taste.json). - Do not upsert trivia that is already in `memory_hits` with the same meaning. diff --git a/Config/personas/cinema/exact.json b/Config/personas/cinema/exact.json new file mode 100644 index 0000000..5d73aba --- /dev/null +++ b/Config/personas/cinema/exact.json @@ -0,0 +1,5 @@ +{ + "generation": { + "aspect": "2.35:1" + } +} diff --git a/README.md b/README.md index 0a814b2..3073b44 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **vector memory**, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate. -**Version 0.7.0** — Config/_base + persona folders, skills, memory-seed → SQLite, Ollama `use: chat|memory`, slim inventory via retrieve. +**Version 0.7.0** — Config/_base + persona folders, skills, **exact KV memory** + vector memory-seed → SQLite, Ollama `use: chat|memory`, slim inventory via retrieve. ## Layout @@ -15,20 +15,29 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + ``` Config/ - _base/ # defaults (assistant, ui, models/krea2, core, packs, skills, memory-seed, identity) - personas// # sparse preset: persona/voice/likes/dislikes/rules + optional overrides + _base/ # defaults (assistant, ui, models/krea2, exact.json, core, packs, skills, memory-seed, identity) + personas// # sparse preset: persona/voice/likes/dislikes/rules + optional exact.json / overrides ``` Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same layout, plus `settings.json`, `taste.json`, `personas.json` (legacy prompt overlay), `ollama-roles.json`, `memory/assistent.sqlite`. Copy `personas/cinema/` → `noir/`, edit only differing JSON files. +## Exact memory (KV) + +- `Config/_base/exact.json` — canonical generation defaults, profiles (turbo/raw), aspect table, short facts +- 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) +- Chat-session overrides (`session_exact`) last until persona change or clear chat — not written to disk +- Priority: core contract → current user → session_exact → exact (+ persona) → live fields → vector `memory_hits` + ## Vector memory - SQLite + Ollama `/api/embed` (default `nomic-embed-text`, pick in ⚙) -- First chat seeds `Config/_base/memory-seed/` (Krea facts, aspect, pitfalls) +- First chat seeds `Config/_base/memory-seed/` (pointers + pitfalls; numbers live in Exact) - Agents upsert via patch `memory_upsert` / `memory_forget` - Cards ingest on save; retrieve → `memory_hits` in live context (inventory slimmed) +- Soft notes only — Exact and the user beat RAG for params ## UX @@ -76,7 +85,7 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart. **Packs** (one active): `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`. -**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures, not model encyclopedia (facts live in memory-seed). +**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG. **Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse` under `Config/personas/`. diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index 51bd9cf..c8c7ec5 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -1347,6 +1347,16 @@ public class SwarmAssistentExtension : Extension } } + JObject exact = Config.LoadExact(pid); + if (exact is not null && exact.Count > 0) + { + system.AppendLine(); + system.AppendLine("## Exact memory (canonical KV — always trust over RAG guesses)"); + system.AppendLine("```json"); + system.AppendLine(exact.ToString(Newtonsoft.Json.Formatting.None)); + system.AppendLine("```"); + } + foreach (string skillId in skillIds ?? Config.ResolveEnabledSkills(pid, null)) { string skillText = Config.LoadSkillPrompt(pid, skillId); @@ -1564,7 +1574,7 @@ public class SwarmAssistentExtension : Extension Logs.Debug($"Assistent memory retrieve: {ex.Message}"); } - string enrichedContext = InjectMemoryHits(contextJson, hits); + string enrichedContext = InjectMemoryHits(contextJson, hits, Config.LoadExact(pid)); List messages = BuildOllamaMessages(packName, includeBase, enrichedContext, userMessages, personaId: pid, skillIds: skills); JArray civitaiResults = []; string reply = ""; @@ -1659,7 +1669,7 @@ public class SwarmAssistentExtension : Extension return string.IsNullOrWhiteSpace(q) ? "krea2 prompting" : q; } - static string InjectMemoryHits(string contextJson, JArray hits) + static string InjectMemoryHits(string contextJson, JArray hits, JObject exact = null) { JObject ctx; try @@ -1671,6 +1681,29 @@ public class SwarmAssistentExtension : Extension ctx = new JObject { ["_raw_context"] = contextJson }; } ctx["memory_hits"] = hits ?? new JArray(); + if (exact is not null && exact.Count > 0 && ctx["exact"] is null) + { + ctx["exact"] = exact; + } + if (ctx["session_exact"] is null) + { + ctx["session_exact"] = new JObject(); + } + if (ctx["recommended_params"] is null && exact?["generation"] is JObject gen) + { + JObject rec = new(); + foreach (string key in new[] { "steps", "cfg", "sigma_shift", "aspect", "images" }) + { + if (gen[key] is not null) + { + rec[key] = gen[key].DeepClone(); + } + } + if (rec.Count > 0) + { + ctx["recommended_params"] = rec; + } + } // Slim inventory for LLM: keep enabled + current, drop full dump if present if (ctx["available_loras"] is JArray allLoras && allLoras.Count > 24) {