Bump Assistent to 0.5.3: live inventory, cards, taste, wanted YAML.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+287
-39
@@ -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();
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user