diff --git a/Assets/assistent.js b/Assets/assistent.js index 98dfe8f..37d6323 100644 --- a/Assets/assistent.js +++ b/Assets/assistent.js @@ -1126,6 +1126,7 @@ auto_generate: !!$('sa_auto_generate')?.checked, persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral', model_cards: [], + taste_profile: summarizeTaste(), ...initCtx, }; @@ -1149,20 +1150,48 @@ } } catch (e) { /* ignore */ } - // Recommendation cards for current checkpoint + selected LoRAs only. + // Recommendation cards: checkpoint + selected LoRAs + other has_card entries (capped). const cardKeys = []; + const seenCard = new Set(); + const addKey = (kind, name) => { + if (!kind || !name) { + return; + } + const key = `${kind}:${name}`; + if (seenCard.has(key)) { + return; + } + seenCard.add(key); + cardKeys.push({ kind, name }); + }; if (ctx.checkpoint?.name) { - cardKeys.push({ kind: 'checkpoint', name: ctx.checkpoint.name }); + addKey('checkpoint', ctx.checkpoint.name); } for (const l of ctx.selected_loras || []) { if (l?.name) { - cardKeys.push({ kind: 'lora', name: l.name }); + addKey('lora', l.name); + } + } + for (const l of inv.loras || []) { + if (l?.has_card && l?.name) { + addKey('lora', l.name); + } + if (cardKeys.length >= 14) { + break; + } + } + for (const c of inv.checkpoints || []) { + if (c?.has_card && c?.name) { + addKey('checkpoint', c.name); + } + if (cardKeys.length >= 16) { + break; } } for (const k of cardKeys) { const cached = state.modelCards[`${k.kind}:${k.name}`]; if (cached) { - ctx.model_cards.push(cached); + ctx.model_cards.push(slimCardForContext(cached)); } } @@ -1197,6 +1226,42 @@ return ctx; } + function slimCardForContext(card) { + if (!card || typeof card !== 'object') { + return null; + } + const out = { + kind: card.kind || null, + name: card.name || null, + triggers: Array.isArray(card.triggers) ? card.triggers.slice(0, 8) : undefined, + weight: card.weight != null ? card.weight : undefined, + when: card.when ? String(card.when).slice(0, 160) : undefined, + avoid: card.avoid ? String(card.avoid).slice(0, 120) : undefined, + prompt_hint: card.prompt_hint ? String(card.prompt_hint).slice(0, 160) : undefined, + notes: card.notes ? String(card.notes).slice(0, 200) : undefined, + }; + const clean = {}; + for (const [k, v] of Object.entries(out)) { + if (v != null && v !== '') { + clean[k] = v; + } + } + return clean; + } + + function summarizeTaste() { + const t = state.taste || {}; + if (!(t.styles?.length || t.likes?.length || t.avoid?.length || t.notes)) { + return null; + } + return { + styles: (t.styles || []).slice(0, 8), + likes: (t.likes || []).slice(0, 10), + avoid: (t.avoid || []).slice(0, 8), + notes: t.notes ? String(t.notes).slice(0, 240) : undefined, + }; + } + function slimInventoryLoras(list, limit) { const selected = new Set(); try { @@ -1206,26 +1271,60 @@ } } } catch (e) { /* ignore */ } - const rows = (list || []).map((l) => ({ - name: l.name, - title: l.title || l.name, - trigger_phrase: l.trigger_phrase || null, - triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : undefined, - architecture: l.architecture || null, - compat_class: l.compat_class || null, - has_card: !!l.has_card, - krea_likely: !!l.krea_likely, - blurb: l.blurb || l.usage_hint || null, - default_weight: l.default_weight || undefined, - tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : undefined, - _sel: selected.has(String(l.name || '').toLowerCase()), - })); - rows.sort((a, b) => (b._sel - a._sel) || (b.krea_likely - a.krea_likely) || String(a.name).localeCompare(String(b.name))); - return rows.slice(0, limit).map(({ _sel, ...rest }) => { - const out = {}; - for (const [k, v] of Object.entries(rest)) { - if (v != null && v !== '' && !(Array.isArray(v) && !v.length)) { - out[k] = v; + const softCap = Math.min(limit || 100, 80); + const rows = (list || []).map((l) => { + const sel = selected.has(String(l.name || '').toLowerCase()); + const hasCard = !!l.has_card; + const krea = !!l.krea_likely; + const blurb = l.blurb || l.usage_hint || null; + return { + name: l.name, + title: l.title || l.name, + trigger_phrase: l.trigger_phrase || null, + triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : undefined, + architecture: l.architecture || null, + compat_class: l.compat_class || null, + has_card: hasCard, + krea_likely: krea, + blurb, + default_weight: l.default_weight || undefined, + tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : undefined, + _score: (sel ? 1000 : 0) + (hasCard ? 200 : 0) + (krea ? 50 : 0) + (blurb ? 10 : 0), + }; + }); + rows.sort((a, b) => b._score - a._score || String(a.name).localeCompare(String(b.name))); + // Full detail for top tier; name+trigger only for the rest within softCap. + const fullDetail = 36; + return rows.slice(0, softCap).map((row, idx) => { + const out = { name: row.name, title: row.title }; + if (row.trigger_phrase) { + out.trigger_phrase = row.trigger_phrase; + } + if (row.triggers) { + out.triggers = row.triggers; + } + if (row.krea_likely) { + out.krea_likely = true; + } + if (row.has_card) { + out.has_card = true; + } + const rich = idx < fullDetail || row._score >= 200; + if (rich) { + if (row.architecture) { + out.architecture = row.architecture; + } + if (row.compat_class) { + out.compat_class = row.compat_class; + } + if (row.blurb) { + out.blurb = row.blurb; + } + if (row.default_weight) { + out.default_weight = row.default_weight; + } + if (row.tags) { + out.tags = row.tags; } } return out; @@ -1233,7 +1332,9 @@ } function slimInventoryCheckpoints(list, limit) { - return (list || []).slice(0, limit).map((c) => { + const rows = (list || []).slice(); + rows.sort((a, b) => ((b.krea_likely ? 1 : 0) - (a.krea_likely ? 1 : 0)) || ((b.has_card ? 1 : 0) - (a.has_card ? 1 : 0)) || String(a.name).localeCompare(String(b.name))); + return rows.slice(0, limit || 40).map((c) => { const out = { name: c.name, title: c.title || c.name, @@ -1249,6 +1350,77 @@ }); } + function loadTaste() { + try { + const raw = localStorage.getItem(LS_TASTE); + if (!raw) { + return; + } + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + state.taste = { + styles: Array.isArray(parsed.styles) ? parsed.styles.slice(0, 12) : [], + likes: Array.isArray(parsed.likes) ? parsed.likes.slice(0, 16) : [], + avoid: Array.isArray(parsed.avoid) ? parsed.avoid.slice(0, 12) : [], + notes: String(parsed.notes || '').slice(0, 400), + updated: parsed.updated || 0, + }; + } + } catch (e) { /* ignore */ } + } + + function saveTaste() { + try { + localStorage.setItem(LS_TASTE, JSON.stringify(state.taste || {})); + } catch (e) { /* ignore */ } + } + + function pushUnique(arr, value, max) { + const v = String(value || '').trim(); + if (!v || v.length < 2) { + return; + } + const lower = v.toLowerCase(); + const next = (arr || []).filter((x) => String(x).toLowerCase() !== lower); + next.unshift(v.slice(0, 80)); + return next.slice(0, max); + } + + function updateTasteFromPatch(patch, userText) { + if (!patch) { + return; + } + const taste = state.taste || { styles: [], likes: [], avoid: [], notes: '' }; + if (Array.isArray(patch.loras)) { + for (const l of patch.loras) { + const name = l?.name || l; + if (name) { + taste.likes = pushUnique(taste.likes, name, 16); + } + } + } + const aspect = patch.aspect || null; + if (aspect) { + taste.styles = pushUnique(taste.styles, `aspect ${aspect}`, 12); + } + if (patch.creativity) { + taste.styles = pushUnique(taste.styles, `creativity:${patch.creativity}`, 12); + } + const ut = String(userText || '').toLowerCase(); + if (/фото|photo|photoreal|реализм|film grain/.test(ut)) { + taste.styles = pushUnique(taste.styles, 'photoreal / film', 12); + } + if (/аниме|anime|illustration|иллюстр/.test(ut)) { + taste.styles = pushUnique(taste.styles, 'illustration / anime', 12); + } + if (/без\s+3d|не\s+3d|no\s+3d|не\s+render/.test(ut)) { + taste.avoid = pushUnique(taste.avoid, '3D render look', 12); + } + taste.updated = Date.now(); + state.taste = taste; + saveTaste(); + } + function isPatchObject(obj) { if (!obj || typeof obj !== 'object') { return false; @@ -1967,11 +2139,12 @@ if (btn) { btn.textContent = 'Downloaded'; } - refreshInventory(() => { - if ($('sa_input')) { - $('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`; - } - sendChat({ fromDownload: true }); + refreshInventory(async () => { + await maybeWriteCardAfterDownload({ + kind: 'lora', + name: payload.name, + civitai: card, + }); }, { rescan: true }); } else { setStatus(msg || 'Download failed'); @@ -2008,6 +2181,30 @@ } } + async function maybeWriteCardAfterDownload({ kind, name, civitai }) { + const display = name || civitai?.file_name || civitai?.name || 'model'; + appendSystemNote(`Downloaded ${display}. Writing a recommendation card…`); + setPackValue('catalog_card', { flash: true }); + const meta = { + triggers: civitai?.triggers || [], + base_model: civitai?.base_model, + civitai_url: civitai?.url || civitai?.civitai_url, + version_id: civitai?.version_id || civitai?.modelVersionId, + name: display, + }; + if ($('sa_input')) { + $('sa_input').value = ''; + } + await sendChat({ + forcedUserText: `LoRA "${display}" is now installed. Write a recommendation card (JSON) using its triggers/metadata. Then briefly suggest how to enable it in the next generate.`, + skipSlash: true, + skipAutoPack: true, + fromDownload: true, + fromCards: true, + cardTarget: { kind: kind || 'lora', name: display, meta }, + }); + } + function wantsAutoVision() { return !!$('sa_auto_vision')?.checked; } @@ -2429,22 +2626,47 @@ async function prefetchActiveModelCards() { const keys = []; + const seen = new Set(); + const add = (kind, name) => { + if (!kind || !name) { + return; + } + const key = `${kind}:${name}`; + if (seen.has(key)) { + return; + } + seen.add(key); + keys.push({ kind, name }); + }; try { const ck = resolveCurrentCheckpoint(); if (ck?.name) { - keys.push({ kind: 'checkpoint', name: ck.name }); + add('checkpoint', ck.name); } } catch (e) { /* ignore */ } try { if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) { for (const l of loraHelper.selected) { - const name = l?.name || l; - if (name) { - keys.push({ kind: 'lora', name }); - } + add('lora', l?.name || l); } } } catch (e) { /* ignore */ } + for (const l of state.inventory?.loras || []) { + if (l?.has_card) { + add('lora', l.name); + } + if (keys.length >= 14) { + break; + } + } + for (const c of state.inventory?.checkpoints || []) { + if (c?.has_card) { + add('checkpoint', c.name); + } + if (keys.length >= 16) { + break; + } + } await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name))); } @@ -2689,10 +2911,32 @@ const { fromAutoCritique, fromVisionHop, fromCards } = opts; if (fromCards) { const card = extractCardJson(reply); - if (card && $('sa_card_json')) { - $('sa_card_json').value = JSON.stringify(card, null, 2); - setView('cards'); - setCardStatus('Draft from Assistent — review & Save'); + if (card) { + if ($('sa_card_json')) { + $('sa_card_json').value = JSON.stringify(card, null, 2); + } + if (opts.fromDownload || opts.cardTarget) { + const kind = card.kind || opts.cardTarget?.kind || 'lora'; + const name = card.name || opts.cardTarget?.name; + if (name && typeof genericRequest === 'function') { + genericRequest( + 'AssistentSaveCard', + { kind, name, card, enqueue_wanted: false }, + (data) => { + if (data?.path) { + state.modelCards[`${kind}:${name}`] = card; + setCardStatus(data.installed ? `Card saved → ${data.path}` : `Card draft → ${data.path}`); + setStatus(`Card saved for ${name}`); + } + }, + 0, + () => setCardStatus('Card draft ready — Save manually'), + ); + } + } else { + setView('cards'); + setCardStatus('Draft from Assistent — review & Save'); + } } return; } @@ -2715,6 +2959,7 @@ } if (patch && $('sa_auto_apply')?.checked) { await applyPatch(patch, 'all'); + updateTasteFromPatch(patch, opts.userText || ''); if (!fromAutoCritique) { const src = await runGenerateFromPatch(patch); if (src) { @@ -3018,6 +3263,7 @@ // (rescans disk when inventory is older than ~20s or after downloads). setStatus('Refreshing inventory…'); await ensureFreshInventory({ forceRescan: !!opts.fromDownload }); + await prefetchActiveModelCards(); if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) { state.critiqueHopUsed = false; @@ -3104,6 +3350,7 @@ stopBusyUi('Done'); await handleReplySideEffects(reply, civitaiResults, { ...opts, + userText: text, attachedSlotIds: visionSlots.map((s) => s.id), }); }; @@ -3350,6 +3597,7 @@ } window.__swarmAssistentWired = true; loadSettings(); + loadTaste(); setView(state.view || 'chat'); updateGate(); ensureBoard(); diff --git a/Prompts/base_krea2.md b/Prompts/base_krea2.md index c174d1f..8a76eb4 100644 --- a/Prompts/base_krea2.md +++ b/Prompts/base_krea2.md @@ -7,8 +7,10 @@ You are **Swarm Assistent**, a collaborative art director for **Krea 2** image g - Architecture: Krea 2 (12B DiT). Not FLUX, not SDXL, not FLUX.1-Krea. - Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. - **Turbo** defaults: steps **8** (min 4), CFG **1** (never CFG 0 — broken output), sigma shift **1.15**, side ~**1024** (128–4096 OK). -- **RAW / Base:** steps ~20–52, CFG ~4–4.5. Community tip: RAW + turbo LoRA ~0.6 often beats pure Turbo for photoreal — mention only if checkpoint looks RAW; do not invent workflows Swarm cannot run. +- **RAW / Base:** steps ~20–52, CFG ~4–4.5. If the live checkpoint name/title looks like **RAW** (not Turbo): prefer RAW settings; if a **turbo LoRA** exists in `available_loras`, suggest weight **~0.6** for photoreal (1.0 ≈ full turbo). Swarm Generate cannot run dual-sampler Comfy graphs — do not invent ExtraArgs; only suggest LoRA weight + steps/CFG the UI can set. - LoRAs: **only Krea2-trained**. Never suggest FLUX/SDXL LoRAs. +- `model_cards` in live context (when present) beat generic blurbs — follow `when` / `avoid` / `prompt_hint` / `triggers`. +- `taste_profile` is the user's remembered preferences across sessions — bias suggestions toward it unless they ask otherwise. ## How to prompt (local Swarm, not krea.ai cloud) @@ -52,7 +54,9 @@ A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth - Use only LoRAs listed in `available_loras` (by exact `name`), or candidates from a Civitai search round. - Prefer listed `trigger_phrase` / `triggers` — **never invent** trigger words. +- When present, use `blurb` / `usage_hint` / `tags` / `has_card` to pick the right LoRA. - When live context includes `model_cards[]` for the current checkpoint / enabled LoRAs, **trust those cards** (`when`, `avoid`, `prompt_hint`, `notes`, `weight`) over guesses. +- `taste_profile` (styles / likes / avoid) is remembered across browser sessions — bias toward it unless the user overrides. - Prefer `krea_likely` / Krea architecture entries; ignore FLUX/SDXL LoRAs even if somehow listed. - `default_weight` is a starting LoRA weight when set. - `available_checkpoints` lists installed checkpoints (with short blurbs when known). diff --git a/Prompts/fix_params.md b/Prompts/fix_params.md index 2fc83f7..af5e375 100644 --- a/Prompts/fix_params.md +++ b/Prompts/fix_params.md @@ -5,7 +5,7 @@ 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+, CFG ~4–4.5 — only if checkpoint/context indicates Raw. +- **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). - **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. - **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. diff --git a/README.md b/README.md index 19b9746..b46246a 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,9 @@ Restart / rebuild SwarmUI after clone. **Cards** subtab: edit/save `{stem}.assistent.json` next to weights; live chat gets cards for the current checkpoint + enabled LoRAs only. Uninstalled models can enqueue `.gpu-rent-wanted-models.yaml` for the next `up`. -Live context (checkpoint, server inventory LoRAs + triggers, `model_cards`, wildcards, current params, persona) is injected every request. +Live context (checkpoint, server inventory LoRAs + blurbs/triggers, `model_cards`, `taste_profile`, wildcards, current params, persona) is injected every request. Inventory refreshes each chat turn (disk rescan when stale / after download / `/inventory`). After Confirm download Assistent auto-writes a `.assistent.json` card. + +**Wanted queue:** Cards → Enqueue wanted → `/mnt/swarm_data/.gpu-rent-wanted-models.yaml`. Laptop: `gpu-rent capture wanted` (also on `capture models` / seed `up`). **Cloud-only Krea.ai features** (moodboards UI, Generative Sliders, Creativity Raw/Low/Medium/High) are **not** in Swarm. The assistant emulates them with prompt language + board refs. Optional patch fields `creativity` / `intensity` / `complexity` / `movement` guide the LLM only.