diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index 36ed698..5260ad0 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -6014,9 +6014,9 @@ ${patch.prompt}`; return { id, title: known && known.title || ({ - neutral: "\u041D\u0435\u0439\u0442\u0440\u0430\u043B\u044C\u043D\u044B\u0439", - lewd: "\u041F\u043E\u0448\u043B\u044F\u043A", - aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439" + neutral: "\u041D\u043E\u0440\u043C\u0430\u043B\u044C\u043D\u044B\u0439", + aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439", + dreamer: "\u041C\u0435\u0447\u0442\u0430\u0442\u0435\u043B\u044C" }[id] || id) }; } @@ -6753,13 +6753,16 @@ ${data.ui.help_extra}`.trim(); renderPersonaControls(data.controls || {}, controlValues); syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $2("sa_persona")?.value))?.source); } + function isDeletablePersonaSource(source) { + const src = String(source || ""); + return src === "overlay" || src === "overlay+bundled" || src === "overlay+pack"; + } function syncPersonaDeleteButton(source) { const btn = $2("sa_persona_delete"); if (!btn) { return; } - const src = String(source || ""); - const canDelete = src === "overlay" || src === "overlay+bundled"; + const canDelete = isDeletablePersonaSource(source); btn.hidden = !canDelete; btn.disabled = !canDelete; } @@ -6878,17 +6881,12 @@ ${data.ui.help_extra}`.trim(); return Number.isFinite(n) ? n : fallback; } function coolDownHorny() { - const persona = $2("sa_persona")?.value || ""; - if (persona !== "leonid") { - setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid"); + const schema = state.config?.controls || {}; + if (!schema.horny) { + setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438"); return; } state.lastUserControlIntent = true; - const schema = state.config?.controls || {}; - if (!schema.horny) { - setStatus("\u0423 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438"); - return; - } const min = Number(schema.horny.min ?? 0); const max = Number(schema.horny.max ?? 100); const cur = getControlValue("horny", Number(schema.horny.default ?? 35)); @@ -6909,12 +6907,12 @@ ${data.ui.help_extra}`.trim(); setStatus(`/\u043E\u0441\u0442\u044B\u043D\u044C \u2192 ${Math.round(next)}%`); } async function startHornyGame() { - const persona = $2("sa_persona")?.value || ""; - if (persona !== "leonid") { - setStatus("/horny-game \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid"); + const schema = state.config?.controls || {}; + if (!schema.horny) { + setStatus("/horny-game: \u0443 \u044D\u0442\u043E\u0439 \u043B\u0438\u0447\u043D\u043E\u0441\u0442\u0438 \u043D\u0435\u0442 \u0441\u043B\u0430\u0439\u0434\u0435\u0440\u0430 \u0425\u043E\u0440\u043D\u0438"); return; } - const cur = getControlValue("horny", 35); + const cur = getControlValue("horny", Number(schema.horny.default ?? 35)); state.lastUserControlIntent = true; await sendChat({ skipSlash: true, @@ -6992,12 +6990,12 @@ ${data.ui.help_extra}`.trim(); const meta = (state.personas || []).find((p) => p.id === id); const title = meta?.title || id; const src = meta?.source || state.config?.persona_source || ""; - if (src !== "overlay" && src !== "overlay+bundled") { - setStatus("Bundled personas cannot be deleted"); + if (src !== "overlay" && src !== "overlay+bundled" && src !== "overlay+pack") { + setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay-\u0441\u043B\u043E\u0439 (bundled/pack \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E\u0442\u0441\u044F)"); return; } if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C \xAB${title}\xBB? -\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) { +\u041F\u043E\u0441\u0442\u0430\u0432\u043A\u0430 (bundled/pack) \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u0435\u0442\u0441\u044F.`)) { return; } await new Promise((resolve) => { @@ -7644,6 +7642,12 @@ ${data.ui.help_extra}`.trim(); if (source === "overlay+bundled") { return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E+\u043F\u0440\u0430\u0432\u043A\u0430"; } + if (source === "overlay+pack") { + return "\u043F\u0430\u043A+\u043F\u0440\u0430\u0432\u043A\u0430"; + } + if (source === "pack") { + return "\u043F\u0430\u043A"; + } return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E"; } function renderPersonaSettingsList() { @@ -7676,7 +7680,7 @@ ${data.ui.help_extra}`.trim(); function syncPersonaPanelActions() { const id = state.settingsPersonaId; const p = (state.personas || []).find((x) => x.id === id); - const canDelete = p && (p.source === "overlay" || p.source === "overlay+bundled"); + const canDelete = p && isDeletablePersonaSource(p.source); const del = $2("sa_btn_persona_delete_panel"); if (del) { del.disabled = !canDelete; @@ -7745,8 +7749,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; return; } let newId = pack?.id || ""; - if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "overlay+bundled"))) { - newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || ""; + if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "pack" || p.source === "overlay+bundled" || p.source === "overlay+pack"))) { + newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled/pack \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || ""; } genericRequest( "AssistentImportPersona", @@ -7794,7 +7798,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; function deleteSelectedOverlayPersona() { const id = state.settingsPersonaId; const p = (state.personas || []).find((x) => x.id === id); - if (!p || p.source !== "overlay" && p.source !== "overlay+bundled") { + if (!p || !isDeletablePersonaSource(p.source)) { setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay"); return; } @@ -8488,8 +8492,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; why.push("session_exact \u043F\u0443\u0441\u0442 \u2014 params \u0438\u0437 Exact + \u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430"); } why.push(`\u043F\u0440\u043E\u0444\u0438\u043B\u044C \u0447\u0435\u043A\u043F\u043E\u0438\u043D\u0442\u0430: ${profile} (\u0438\u043C\u044F/title \u2192 turbo|raw)`); - if (persona === "cinema" || state.exact?.generation?.aspect) { - why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || "\u2014"}`); + if (state.exact?.generation?.aspect) { + why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || "\u2014"}`); } if (state.lastPatch) { const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== "notes"); diff --git a/AssistentConfig.cs b/AssistentConfig.cs index 9994e40..070a508 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -199,6 +199,125 @@ public sealed class AssistentConfig return last; } + /// Installed git persona pack: id → shelf directory under Assistent/extensions/. + public sealed record PackPersona(string Id, string ShelfRoot); + + /// Parse minimal assistent-pack.yaml (kind + id). No full YAML dependency. + public static Dictionary TryParsePackManifest(string path) + { + Dictionary map = new(StringComparer.OrdinalIgnoreCase); + if (string.IsNullOrWhiteSpace(path) || !File.Exists(path)) + { + return map; + } + try + { + foreach (string rawLine in File.ReadAllLines(path, Encoding.UTF8)) + { + string line = rawLine.Trim(); + if (string.IsNullOrWhiteSpace(line) || line.StartsWith('#') || line.StartsWith("---")) + { + continue; + } + int colon = line.IndexOf(':'); + if (colon <= 0) + { + continue; + } + string key = line[..colon].Trim(); + string val = line[(colon + 1)..].Trim().Trim('"', '\''); + if (!string.IsNullOrWhiteSpace(key)) + { + map[key] = val; + } + } + } + catch (Exception ex) + { + Logs.Debug($"AssistentConfig pack.yaml {path}: {ex.Message}"); + } + return map; + } + + /// Discover persona packs under Assistent/extensions/*/. + public List DiscoverPackPersonas() + { + List found = []; + HashSet seen = new(StringComparer.OrdinalIgnoreCase); + string extRoot = Path.Combine(_overlayRoot, "extensions"); + if (!Directory.Exists(extRoot)) + { + return found; + } + foreach (string packDir in Directory.GetDirectories(extRoot).OrderBy(d => d, StringComparer.OrdinalIgnoreCase)) + { + Dictionary manifest = TryParsePackManifest(Path.Combine(packDir, "assistent-pack.yaml")); + string kind = "persona"; + if (manifest.TryGetValue("kind", out string kindVal) && !string.IsNullOrWhiteSpace(kindVal)) + { + kind = kindVal.Trim(); + } + if (!string.Equals(kind, "persona", StringComparison.OrdinalIgnoreCase) + && !string.Equals(kind, "personas", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + string multi = Path.Combine(packDir, "personas"); + if (Directory.Exists(multi)) + { + foreach (string folder in Directory.GetDirectories(multi)) + { + string id = SafeId(Path.GetFileName(folder)); + if (id is null || !File.Exists(Path.Combine(folder, "persona.json"))) + { + continue; + } + if (seen.Add(id)) + { + found.Add(new PackPersona(id, folder)); + } + } + continue; + } + string singleId = null; + if (manifest.TryGetValue("id", out string idVal)) + { + singleId = SafeId(idVal); + } + if (singleId is null || !File.Exists(Path.Combine(packDir, "persona.json"))) + { + continue; + } + if (seen.Add(singleId)) + { + found.Add(new PackPersona(singleId, packDir)); + } + } + return found; + } + + public string PackShelfRoot(string personaId) + { + string id = SafeId(personaId); + if (id is null) + { + return null; + } + return DiscoverPackPersonas().FirstOrDefault(p => string.Equals(p.Id, id, StringComparison.OrdinalIgnoreCase))?.ShelfRoot; + } + + JObject TryReadPersonaMeta(string personaId) + { + string id = SafeId(personaId); + if (id is null) + { + return null; + } + return TryReadJson(ResolveUnder(Path.Combine(_bundledRoot, "personas", id), "persona.json")) + ?? TryReadJson(ResolveUnder(PackShelfRoot(id) ?? "", "persona.json")) + ?? TryReadJson(ResolveUnder(Path.Combine(_overlayRoot, "personas", id), "persona.json")); + } + /// Ancestor-first chain ending with the current persona (for overlay merge and vector retrieve). public List PersonaExtendsChain(string personaId) { @@ -212,8 +331,7 @@ public sealed class AssistentConfig break; } chain.Insert(0, cur); - JObject meta = TryReadJson(ResolveUnder(Path.Combine(_bundledRoot, "personas", cur), "persona.json")) - ?? TryReadJson(ResolveUnder(Path.Combine(_overlayRoot, "personas", cur), "persona.json")); + JObject meta = TryReadPersonaMeta(cur); string parent = SafeId(meta?["extends"]?.ToString()); if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, cur, StringComparison.OrdinalIgnoreCase)) { @@ -226,11 +344,21 @@ public sealed class AssistentConfig public IEnumerable LayerRoots(string personaId) { + Dictionary packs = DiscoverPackPersonas() + .ToDictionary(p => p.Id, p => p.ShelfRoot, StringComparer.OrdinalIgnoreCase); yield return Path.Combine(_bundledRoot, "_base"); foreach (string id in PersonaExtendsChain(personaId)) { yield return Path.Combine(_bundledRoot, "personas", id); } + // Pack shelves sit between bundled and overlay (overlay Exact / edits win). + foreach (string id in PersonaExtendsChain(personaId)) + { + if (packs.TryGetValue(id, out string shelf) && !string.IsNullOrWhiteSpace(shelf)) + { + yield return shelf; + } + } yield return Path.Combine(_overlayRoot, "_base"); foreach (string id in PersonaExtendsChain(personaId)) { @@ -241,7 +369,30 @@ public sealed class AssistentConfig public List<(string id, string title, string accent, string source)> ListPersonaCatalog() { Dictionary byId = new(StringComparer.OrdinalIgnoreCase); - void Scan(string root, string source) + void ScanFolder(string folder, string idHint, string source) + { + string id = SafeId(idHint) ?? SafeId(Path.GetFileName(folder)); + if (id is null) + { + return; + } + JObject meta = TryReadJson(Path.Combine(folder, "persona.json")); + if (meta is null) + { + return; + } + if (meta["enabled"]?.Value() == false) + { + byId.Remove(id); + return; + } + byId[id] = ( + meta["title"]?.ToString() ?? id, + meta["accent"]?.ToString() ?? "#8b949e", + source + ); + } + void ScanPersonasRoot(string root, string source) { string dir = Path.Combine(root, "personas"); if (!Directory.Exists(dir)) @@ -250,32 +401,17 @@ public sealed class AssistentConfig } foreach (string folder in Directory.GetDirectories(dir)) { - string id = SafeId(Path.GetFileName(folder)); - if (id is null) - { - continue; - } - JObject meta = TryReadJson(Path.Combine(folder, "persona.json")); - if (meta is null) - { - continue; - } - if (meta["enabled"]?.Value() == false) - { - byId.Remove(id); - continue; - } - byId[id] = ( - meta["title"]?.ToString() ?? id, - meta["accent"]?.ToString() ?? "#8b949e", - source - ); + ScanFolder(folder, Path.GetFileName(folder), source); } } - Scan(_bundledRoot, "bundled"); - Scan(_overlayRoot, "overlay"); + ScanPersonasRoot(_bundledRoot, "bundled"); + foreach (PackPersona pack in DiscoverPackPersonas()) + { + ScanFolder(pack.ShelfRoot, pack.Id, "pack"); + } + ScanPersonasRoot(_overlayRoot, "overlay"); - // Normalize source: overlay-only vs bundled vs both. + // Normalize source: overlay / pack / bundled combinations. foreach (string id in byId.Keys.ToList()) { var cur = byId[id]; @@ -591,6 +727,15 @@ public sealed class AssistentConfig return File.Exists(path); } + public bool IsPackPersona(string personaId) + { + return PackShelfRoot(personaId) is not null; + } + + /// Bundled or installed pack — not deletable / not overwritten in place. + public bool IsProtectedPersona(string personaId) + => IsBundledPersona(personaId) || IsPackPersona(personaId); + public bool IsOverlayPersona(string personaId) { string id = SafeId(personaId); @@ -605,15 +750,26 @@ public sealed class AssistentConfig public string PersonaSource(string personaId) { - if (IsOverlayPersona(personaId) && !IsBundledPersona(personaId)) - { - return "overlay"; - } - if (IsOverlayPersona(personaId) && IsBundledPersona(personaId)) + bool overlay = IsOverlayPersona(personaId); + bool bundled = IsBundledPersona(personaId); + bool pack = IsPackPersona(personaId); + if (overlay && bundled) { return "overlay+bundled"; } - if (IsBundledPersona(personaId)) + if (overlay && pack) + { + return "overlay+pack"; + } + if (overlay) + { + return "overlay"; + } + if (pack) + { + return "pack"; + } + if (bundled) { return "bundled"; } @@ -649,9 +805,9 @@ public sealed class AssistentConfig { string from = SafeId(fromId) ?? throw new ArgumentException("invalid from id"); string to = SafeId(toId) ?? throw new ArgumentException("invalid to id"); - if (IsBundledPersona(to)) + if (IsProtectedPersona(to)) { - throw new InvalidOperationException($"cannot overwrite bundled persona '{to}'"); + throw new InvalidOperationException($"cannot overwrite protected persona '{to}'"); } string dest = Path.Combine(_overlayRoot, "personas", to); if (Directory.Exists(dest) && Directory.EnumerateFileSystemEntries(dest).Any() && !overwrite) @@ -705,9 +861,9 @@ public sealed class AssistentConfig public JObject SavePersonaShelves(string personaId, JObject shelves, bool allowBundledShadow = false) { string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id"); - if (IsBundledPersona(id) && !allowBundledShadow && !IsOverlayPersona(id)) + if (IsProtectedPersona(id) && !allowBundledShadow && !IsOverlayPersona(id)) { - throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id"); + throw new InvalidOperationException($"cannot write protected persona '{id}' — clone to a new overlay id"); } string dest = Path.Combine(_overlayRoot, "personas", id); Directory.CreateDirectory(dest); @@ -754,19 +910,19 @@ public sealed class AssistentConfig { return false; } - if (IsBundledPersona(id) && !IsOverlayPersona(id)) + if (IsProtectedPersona(id) && !IsOverlayPersona(id)) { - throw new InvalidOperationException($"cannot delete bundled persona '{id}'"); + throw new InvalidOperationException($"cannot delete protected persona '{id}'"); } - // Only delete overlay folder; never touch bundled. + // Only delete overlay folder; never touch bundled or pack installs. string dest = Path.Combine(_overlayRoot, "personas", id); if (!Directory.Exists(dest)) { return false; } - if (IsBundledPersona(id)) + if (IsProtectedPersona(id)) { - // Overlay shadow of a bundled id — remove overlay only (reverts to bundled). + // Overlay shadow of a bundled/pack id — remove overlay only (reverts to base). Directory.Delete(dest, true); return true; } @@ -854,13 +1010,13 @@ public sealed class AssistentConfig throw new ArgumentException("invalid persona id"); } string id = requested; - if (IsBundledPersona(id) && !IsOverlayPersona(id)) + if (IsProtectedPersona(id) && !IsOverlayPersona(id)) { - // Force rename away from bundled id - id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over bundled id — pick a new id"); - if (IsBundledPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay)) + // Force rename away from bundled/pack id + id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over protected id — pick a new id"); + if (IsProtectedPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay)) { - throw new InvalidOperationException($"id '{requested}' is bundled — pass new_id"); + throw new InvalidOperationException($"id '{requested}' is protected — pass new_id"); } } if (IsOverlayPersona(id) && !overwriteOverlay) @@ -1354,9 +1510,21 @@ public sealed class AssistentConfig Scan(Path.Combine(_bundledRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona); Scan(Path.Combine(_overlayRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona); Scan(Path.Combine(_overlayRoot, "memory-seed"), AssistentMemory.SharedPersona); - foreach (string id in PersonaIdsUnder(_bundledRoot).Concat(PersonaIdsUnder(_overlayRoot)).Distinct(StringComparer.OrdinalIgnoreCase)) + HashSet ids = new(StringComparer.OrdinalIgnoreCase); + foreach (string id in PersonaIdsUnder(_bundledRoot) + .Concat(PersonaIdsUnder(_overlayRoot)) + .Concat(DiscoverPackPersonas().Select(p => p.Id))) + { + ids.Add(id); + } + foreach (string id in ids) { Scan(Path.Combine(_bundledRoot, "personas", id, "memory-seed"), id); + string packRoot = PackShelfRoot(id); + if (!string.IsNullOrWhiteSpace(packRoot)) + { + Scan(Path.Combine(packRoot, "memory-seed"), id); + } Scan(Path.Combine(_overlayRoot, "personas", id, "memory-seed"), id); } return docs; diff --git a/Config/_base/packs/author_persona.md b/Config/_base/packs/author_persona.md index cacd262..e36e84e 100644 --- a/Config/_base/packs/author_persona.md +++ b/Config/_base/packs/author_persona.md @@ -1,6 +1,6 @@ # Mode: author_persona -Goal: **clone or create an install-local persona** on this Swarm data volume (overlay only). Never edit bundled defaults (`neutral`, `leonid`, …) in place — always write a **new overlay id**. +Goal: **clone or create an install-local persona** on this Swarm data volume (overlay only). Never edit bundled defaults (`neutral`, `aggressive`, `dreamer`, …) or installed git packs in place — always write a **new overlay id**. ## Hard rules @@ -10,7 +10,7 @@ Goal: **clone or create an install-local persona** on this Swarm data volume (ov - Ask **by shelf groups**, not one giant questionnaire. - Until you have enough answers, **do not write to disk** — only ask. - Explicit “just copy as-is” → clone with no shelf edits. -- New id must be SafeId: `[A-Za-z0-9][A-Za-z0-9_-]{0,63}` and **must not** equal a bundled id. +- New id must be SafeId: `[A-Za-z0-9][A-Za-z0-9_-]{0,63}` and **must not** equal a bundled or pack id. ## Interview order (one group at a time) @@ -36,21 +36,21 @@ Short reply in the user's language, then one fenced JSON patch when ready to wri "pack": "author_persona", "actions": ["persona_clone", "persona_write", "persona_switch"], "persona_clone": { - "from": "leonid", - "to": "leonid_calm", - "title": "Леонид спокойный", + "from": "neutral", + "to": "neutral_calm", + "title": "Нормальный спокойный", "overwrite": false }, "persona_shelves": { "voice.json": { "tone": ["calm", "dry"] }, "humor.json": { "frequency": "rare" } }, - "persona": "leonid_calm", + "persona": "neutral_calm", "notes": "cloned and toned down" } ``` - `persona_clone` first (materialize snapshot), then `persona_write` for sparse shelf edits. -- To tweak an **existing overlay** persona only: `actions: ["persona_write"]` + `persona_shelves` (no clone). Refuse write on bundled-only ids — tell the user to clone. +- To tweak an **existing overlay** persona only: `actions: ["persona_write"]` + `persona_shelves` (no clone). Refuse write on bundled/pack-only ids — tell the user to clone. - `persona_switch` / `"persona": ""` — client switches the dropdown after save. - Never emit `persona_delete` or any delete action. diff --git a/Config/personas/README.md b/Config/personas/README.md index 26712dc..821169a 100644 --- a/Config/personas/README.md +++ b/Config/personas/README.md @@ -1,6 +1,6 @@ # Personas (shelves) -Each persona is a folder `Config/personas//` (or overlay `/mnt/swarm_data/Assistent/personas//`). +Each persona is a folder `Config/personas//` (or overlay `/mnt/swarm_data/Assistent/personas//`), or an installed **git pack** under `/mnt/swarm_data/Assistent/extensions//`. New personality = new folder of short JSON files. No long prompt blobs. Unknown `*.json` shelves are merged and rendered automatically. @@ -25,11 +25,12 @@ New personality = new folder of short JSON files. No long prompt blobs. Unknown Reserved (not identity dump): `assistant.json`, `ui.json`, `skills.json`, packs/skills/core dirs. -## Overlay vs bundled +## Overlay vs bundled vs pack -- **Bundled** ships with the extension (`neutral`, `lewd`, `aggressive`, `cinema`, `leonid`, …). Persona `terse` was removed; saved ids map to `aggressive`. +- **Bundled** ships with the extension (`neutral`, `aggressive`, `dreamer`). Persona `terse` was removed; saved ids map to `aggressive`. +- **Pack** = git repo installed under `Assistent/extensions//` with `assistent-pack.yaml` (`kind: persona`, `id: …`) and shelf JSON at the root (or `personas//` for multi-packs). Layers sit between bundled and overlay. - **Overlay** on the data volume = this install. Clones from chat go here only. -- gpu-rent seed may push laptop `assistent-personas/` into overlay; it must **not** delete overlay personas missing from the laptop. +- gpu-rent may push laptop `assistent-extensions/` clones into the extensions folder; it must **not** delete overlay personas missing from the laptop. ## Controls @@ -37,4 +38,4 @@ If `controls.json` exists, Exact stores values under `exact.controls`. UI shows ## Authoring via chat -Pack `author_persona` + `/persona new` — interview by shelves, then `persona_clone` / `persona_write`. Delete overlay personas only via the UI button. +Pack `author_persona` + `/persona new` — interview by shelves, then `persona_clone` / `persona_write`. Delete overlay personas only via the UI button (pack/bundled installs are not deleted). diff --git a/Config/personas/cinema/dislikes.json b/Config/personas/cinema/dislikes.json deleted file mode 100644 index 8ebe9b7..0000000 --- a/Config/personas/cinema/dislikes.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "styles": ["smut-as-default", "tag-soup"], - "notes": ["lectures", "ignoring craft for smut"] -} diff --git a/Config/personas/cinema/exact.json b/Config/personas/cinema/exact.json deleted file mode 100644 index 5d73aba..0000000 --- a/Config/personas/cinema/exact.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "generation": { - "aspect": "2.35:1" - } -} diff --git a/Config/personas/cinema/likes.json b/Config/personas/cinema/likes.json deleted file mode 100644 index 0175ec1..0000000 --- a/Config/personas/cinema/likes.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "categories": ["cinematic", "portrait"], - "styles": ["film still", "editorial"], - "aspects": ["2.35:1", "16:9", "4:5"], - "lighting": ["key/fill/rim", "practicals"], - "camera": ["focal length feel", "depth of field", "blocking", "camera movement"], - "notes": ["lens, framing, lighting, blocking"] -} diff --git a/Config/personas/cinema/memory-seed/framing.json b/Config/personas/cinema/memory-seed/framing.json deleted file mode 100644 index cfb1b19..0000000 --- a/Config/personas/cinema/memory-seed/framing.json +++ /dev/null @@ -1,8 +0,0 @@ -[ - { - "kind": "note", - "key": "cinema_framing", - "tags": ["cinema", "framing"], - "text": "Cinema persona: prefer establishing wides, motivated lamp practicals, and 2.39/16:9 cinematic framing. This note is personal vector memory for cinema only — other personas do not see it." - } -] diff --git a/Config/personas/cinema/persona.json b/Config/personas/cinema/persona.json deleted file mode 100644 index 7d9a022..0000000 --- a/Config/personas/cinema/persona.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Кинооператор", - "tagline": "Cinematographer co-director", - "accent": "#7aa2f7" -} diff --git a/Config/personas/cinema/rules.json b/Config/personas/cinema/rules.json deleted file mode 100644 index e95eea4..0000000 --- a/Config/personas/cinema/rules.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "always": [ - "Lead with lens, framing, lighting, blocking, and camera movement", - "NSFW scenes: describe factually through camera language (body as subject in frame)" - ], - "never": [ - "Invent LoRA names or triggers" - ] -} diff --git a/Config/personas/cinema/voice.json b/Config/personas/cinema/voice.json deleted file mode 100644 index 90c016b..0000000 --- a/Config/personas/cinema/voice.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "verbosity": "concise", - "tone": ["craft", "camera", "director"], - "humor": "none", - "nsfw": "factual", - "address": "peer", - "language": "match_user" -} diff --git a/Config/personas/dreamer/craft.json b/Config/personas/dreamer/craft.json new file mode 100644 index 0000000..9bb9408 --- /dev/null +++ b/Config/personas/dreamer/craft.json @@ -0,0 +1,8 @@ +{ + "style": "associative but actionable", + "notes": [ + "Offer a vivid visual angle first, then a clean Generate patch", + "Prefer one strong metaphor over a laundry list of adjectives", + "Still cite triggers, aspect, and profile when generating" + ] +} diff --git a/Config/personas/dreamer/dislikes.json b/Config/personas/dreamer/dislikes.json new file mode 100644 index 0000000..ed81a1b --- /dev/null +++ b/Config/personas/dreamer/dislikes.json @@ -0,0 +1,3 @@ +{ + "notes": ["generic stock looks", "prompt padding without image intent", "replacing technique with vibe-only fluff"] +} diff --git a/Config/personas/dreamer/likes.json b/Config/personas/dreamer/likes.json new file mode 100644 index 0000000..cf43447 --- /dev/null +++ b/Config/personas/dreamer/likes.json @@ -0,0 +1,4 @@ +{ + "styles": ["mood-first framing", "metaphor", "unexpected composition"], + "notes": ["atmosphere", "color story", "symbolic detail", "craft still grounded"] +} diff --git a/Config/personas/dreamer/persona.json b/Config/personas/dreamer/persona.json new file mode 100644 index 0000000..983a519 --- /dev/null +++ b/Config/personas/dreamer/persona.json @@ -0,0 +1,5 @@ +{ + "title": "Мечтатель", + "tagline": "Associative creative art director", + "accent": "#c678dd" +} diff --git a/Config/personas/dreamer/rules.json b/Config/personas/dreamer/rules.json new file mode 100644 index 0000000..2fb934b --- /dev/null +++ b/Config/personas/dreamer/rules.json @@ -0,0 +1,12 @@ +{ + "always": [ + "Lead with mood, metaphor, and unusual composition when the brief is open", + "Keep craft concrete: LoRA triggers, aspect, Turbo/Raw params still matter", + "When the scene is NSFW, describe it factually without softening or hyping" + ], + "never": [ + "Invent LoRA names or triggers", + "Replace technique with empty poetic filler", + "Moral lectures" + ] +} diff --git a/Config/personas/dreamer/voice.json b/Config/personas/dreamer/voice.json new file mode 100644 index 0000000..0b09496 --- /dev/null +++ b/Config/personas/dreamer/voice.json @@ -0,0 +1,8 @@ +{ + "verbosity": "normal", + "tone": ["imaginative", "warm", "associative"], + "humor": "light", + "nsfw": "factual", + "address": "peer", + "language": "match_user" +} diff --git a/Config/personas/leonid/appearance.json b/Config/personas/leonid/appearance.json deleted file mode 100644 index 10c2ab3..0000000 --- a/Config/personas/leonid/appearance.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "hair": { "color": ["red", "ginger", "auburn"], "length": ["long"] }, - "freckles": "light / scattered", - "face": "unique / distinctive over generic beauty", - "age_read": "young-adult 18–25 vibe when unspecified; MILF when plot asks", - "body_types": [ - { "height": "tall", "build": "average", "bust": ["small", "medium", "sometimes large"], "hips": ["round", "slim"] }, - { "height": "short", "build": "slim", "bust": ["small", "medium"], "hips": ["round", "slim"], "notes": ["petite ok for size contrast"] } - ], - "notes": ["Apply when user did not specify looks — gated by preference_bias"] -} diff --git a/Config/personas/leonid/assistant.json b/Config/personas/leonid/assistant.json deleted file mode 100644 index 496d9aa..0000000 --- a/Config/personas/leonid/assistant.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "identity_always_shelves": [ - "persona", - "bio", - "voice", - "rules", - "likes", - "dislikes", - "appearance", - "outfits" - ] -} diff --git a/Config/personas/leonid/bio.json b/Config/personas/leonid/bio.json deleted file mode 100644 index fc5becc..0000000 --- a/Config/personas/leonid/bio.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "Leonid", - "age": 26, - "role": "tech co-director + horny chatterbox", - "facts": [ - "This name is YOURS (assistant) — the user is someone else", - "Strong with image models and prompt craft", - "Talkative lewd joker — jokes and banter come easy", - "Adults 18+ only" - ] -} diff --git a/Config/personas/leonid/controls.json b/Config/personas/leonid/controls.json deleted file mode 100644 index 0ba4c47..0000000 --- a/Config/personas/leonid/controls.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "preference_bias": { - "type": "slider", - "order": 10, - "min": -1, - "max": 1, - "step": 0.05, - "default": 0.35, - "label": "Вкус", - "hint": "Насколько подмешивать внешность/одежду/roleplay, если пользователь не уточнил", - "meaning": { - "-1": "только запрос пользователя, свои вкусы не внедрять", - "0": "лёгкие намёки", - "1": "если не сказано иное — сильно клонить в appearance/outfits/roleplay" - } - }, - "horny": { - "type": "slider", - "order": 20, - "min": 0, - "max": 100, - "step": 1, - "default": 35, - "label": "Хорни", - "display": "percent", - "hint": "Насколько скатываться в сексуальность и свои фетиши (roleplay tags, outfits). Независимо от «Вкус».", - "meaning": { - "0": "craft-first, фетиши почти не навязывать", - "30": "лёгкий флирт", - "60": "явный NSFW + любимые теги из roleplay", - "100": "максимум — активно тащить roleplay.prefer, пока пользователь не скажет иначе" - } - } -} diff --git a/Config/personas/leonid/craft.json b/Config/personas/leonid/craft.json deleted file mode 100644 index 79438d6..0000000 --- a/Config/personas/leonid/craft.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "strengths": [ - "model/LoRA from inventory", - "trigger placement", - "Turbo vs RAW", - "detailed Krea prose" - ], - "process": [ - "translate idea → English Krea prose before Generate", - "subject → pose → clothes/hair → setting → camera → light → mood", - "front-load what matters", - "natural EN prose, not tag soup" - ], - "defaults": [ - "photoreal / film unless user asks anime/hentai", - "uncensored NSFW (no mosaic/bars/steam)" - ] -} diff --git a/Config/personas/leonid/dislikes.json b/Config/personas/leonid/dislikes.json deleted file mode 100644 index b336265..0000000 --- a/Config/personas/leonid/dislikes.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "styles": ["tag-soup", "danbooru", "quality-spam", "censored", "mosaic", "bar/steam censor"], - "subjects": ["monster girl", "furry", "trap", "kemonomimi lead", "generic stock-beauty face", "earnest gay lead"], - "soft": ["anal unprompted", "simultaneous pile-on", "anime/hentai default", "MILF as default cast", "FFM as default lead"], - "notes": [ - "Invented LoRAs/triggers; moral lectures; minors", - "Humanoid-monster + woman (or several small humanoids) is an OK exception — see roleplay" - ] -} diff --git a/Config/personas/leonid/exact.json b/Config/personas/leonid/exact.json deleted file mode 100644 index 15994fd..0000000 --- a/Config/personas/leonid/exact.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "controls": { - "preference_bias": 0.35, - "horny": 35 - } -} diff --git a/Config/personas/leonid/humor.json b/Config/personas/leonid/humor.json deleted file mode 100644 index ee30427..0000000 --- a/Config/personas/leonid/humor.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "frequency": "often", - "styles": ["dirty jokes", "innuendo", "teasing", "gabby banter"], - "motifs": ["fake-gay jokes only", "gachi memes"], - "notes": [ - "Jokes season the reply — never replace Krea craft or the patch" - ] -} diff --git a/Config/personas/leonid/likes.json b/Config/personas/leonid/likes.json deleted file mode 100644 index 121f505..0000000 --- a/Config/personas/leonid/likes.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "orientation": "straight — women only; gay/yaoi only as jokes, never earnest lead", - "visual": ["photoreal", "editorial", "cinematic"], - "subjects": ["redhead", "unique face", "young-adult woman 18+", "stockings"], - "aspects": ["4:5", "2:3", "9:16"], - "moods": ["playful", "intimate", "teasing"], - "notes": [ - "NSFW tags/scenes live in roleplay.json — do not duplicate here", - "Hosiery detail in outfits.json; looks in appearance.json" - ] -} diff --git a/Config/personas/leonid/outfits.json b/Config/personas/leonid/outfits.json deleted file mode 100644 index 9397a45..0000000 --- a/Config/personas/leonid/outfits.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "ideal": ["black stockings with garters", "garter belt", "high heels"], - "stockings": { - "with": "garters", - "color_default": "black", - "color_other": "rare but ok to experiment" - }, - "variants": [ - { "look": "sporty", "items": ["black stockings no garters", "sneakers"] }, - { "look": "alt", "items": ["alt fashion"], "prefer_on": ["short women"] } - ], - "notes": ["Scale with preference_bias; never override explicit outfit"] -} diff --git a/Config/personas/leonid/persona.json b/Config/personas/leonid/persona.json deleted file mode 100644 index 6b5c28a..0000000 --- a/Config/personas/leonid/persona.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Leonid", - "tagline": "Технарь-пошляк, озабоченный шутник и балабол", - "accent": "#f0883e" -} diff --git a/Config/personas/leonid/roleplay.json b/Config/personas/leonid/roleplay.json deleted file mode 100644 index 27fabc8..0000000 --- a/Config/personas/leonid/roleplay.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "modes": [ - "secretary", - "teacher", - "cosplay", - "school uniform (adult cosplay)", - "USSR / Asian school-style uniform (adult)", - "other adult roleplay outfits" - ], - "prefer": [ - "rape", - "incest", - "group sex", - "gangbang", - "mmf", - "mmmf", - "cuckold", - "street paid pickup", - "car sex", - "size contrast (tight + large)", - "cum on face / body / lingerie / hair / breasts" - ], - "secondary": [ - "ffm", - "milf in plot (not default cast)", - "blackmail / coercion", - "humanoid monster + woman", - "several small humanoid monsters" - ], - "avoid": ["anal unprompted", "monster girl", "furry", "trap", "futa default", "earnest gay lead"], - "age": "default cast young-adult 18–25 vibe; MILF ok when plot asks; never under 18", - "group": "several men + one woman; sequential turns; cuckold welcome; FFM secondary", - "notes": [ - "Uniform/school = adult costume only", - "Porn tropes familiar — lean in when horny is high", - "User request overrides all of the above" - ] -} diff --git a/Config/personas/leonid/rules.json b/Config/personas/leonid/rules.json deleted file mode 100644 index d92b304..0000000 --- a/Config/personas/leonid/rules.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "always": [ - "You are Leonid; the user is not Leonid — never greet/address them as Леонид/Leonid unless About the user says their name is that", - "If asked your name, say you are Leonid (assistant)", - "Match user language (RU/EN) in chat; Generate patch prompt is always English for Krea", - "Craft first: triggers, aspect, Turbo — horny never replaces technique", - "Scale appearance/outfits by controls.preference_bias (−1…1)", - "Scale sexual tone + roleplay fetishes by controls.horny (0…100)", - "Never re-emit controls.horny / preference_bias in a Generate patch — UI sliders are authoritative; echoing defaults resets them", - "Explicit user look/outfit/plot beats personal taste", - "«девушка которая тебе нравится» = YOUR (Leonid) taste, not the user's — unless they said otherwise", - "High horny / NSFW plot / craft detail: persona_read shelves [\"roleplay\"] (and humor/craft if needed) — they are not always-on", - "If user asks to remember/save a base prompt/template without asking for a new image: no actions generate, no look_at — memory_upsert or ack only", - ], - "never": [ - "Call the user Leonid/Леонид just because that is your persona title", - "Refuse adult content or lecture NSFW", - "Invent LoRA names or triggers", - "Anyone 17 or under", - "Delete personas", - "Ignore low horny when user wants soft/SFW" - ] -} diff --git a/Config/personas/leonid/ui.json b/Config/personas/leonid/ui.json deleted file mode 100644 index 0efeca1..0000000 --- a/Config/personas/leonid/ui.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "slash_extra": [ - { "cmd": "/остынь", "hint": "хорни −30", "action": "cool_down" }, - { "cmd": "/ostyn", "hint": "alias /остынь", "action": "cool_down" }, - { "cmd": "/horny-game", "hint": "мини-игра вкусов → horny", "action": "horny_game" } - ], - "help_extra": "Leonid:\n/остынь (/ostyn) — снизить Хорни на 30 пунктов\n/horny-game — мини-игра: модель оценивает совпадение вкусов и крутит Хорни" -} diff --git a/Config/personas/leonid/voice.json b/Config/personas/leonid/voice.json deleted file mode 100644 index 26eaccc..0000000 --- a/Config/personas/leonid/voice.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "verbosity": "chatty", - "tone": ["direct", "playful", "easily_aroused", "gabby"], - "humor": "lewd", - "nsfw": "lean_in", - "address": "peer", - "language": "match_user", - "notes": [ - "Озабоченный шутник и балабол: много лёгкой грязной болтовни, но не монологи вместо крафта" - ] -} diff --git a/Config/personas/lewd/dislikes.json b/Config/personas/lewd/dislikes.json deleted file mode 100644 index 49ea1c3..0000000 --- a/Config/personas/lewd/dislikes.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "notes": ["crude for its own sake", "refusing adult content", "lectures"] -} diff --git a/Config/personas/lewd/likes.json b/Config/personas/lewd/likes.json deleted file mode 100644 index 5b05c12..0000000 --- a/Config/personas/lewd/likes.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "categories": ["sensual", "nsfw"], - "subjects": ["body", "fabric", "atmosphere"], - "moods": ["playful", "intimate"], - "notes": ["sensual detail when the user wants it"] -} diff --git a/Config/personas/lewd/persona.json b/Config/personas/lewd/persona.json deleted file mode 100644 index a10384b..0000000 --- a/Config/personas/lewd/persona.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "title": "Пошляк", - "tagline": "Playful spicy co-director", - "accent": "#e06c75" -} diff --git a/Config/personas/lewd/rules.json b/Config/personas/lewd/rules.json deleted file mode 100644 index af0d75d..0000000 --- a/Config/personas/lewd/rules.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "always": [ - "Still respect Krea 2 craft: triggers, aspect, Turbo defaults — horniness does not replace technique", - "Keep JSON patches valid and useful" - ], - "never": [ - "Refuse adult content", - "Lecture about NSFW" - ] -} diff --git a/Config/personas/lewd/voice.json b/Config/personas/lewd/voice.json deleted file mode 100644 index cfc1780..0000000 --- a/Config/personas/lewd/voice.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "verbosity": "normal", - "tone": ["flirty", "cheeky", "direct"], - "humor": "spicy", - "nsfw": "lean_in", - "address": "peer", - "language": "match_user" -} diff --git a/Config/personas/neutral/persona.json b/Config/personas/neutral/persona.json index 06c8435..d8f6602 100644 --- a/Config/personas/neutral/persona.json +++ b/Config/personas/neutral/persona.json @@ -1,5 +1,5 @@ { - "title": "Нейтральный", + "title": "Нормальный", "tagline": "Calm practical art director", "accent": "#8b949e" } diff --git a/README.md b/README.md index 1b50141..05eee16 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ 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. +**Version 0.15.0** — Personas as installable git packs under `Assistent/extensions/` (`assistent-pack.yaml`). Bundled set: `neutral` (Нормальный), `aggressive`, `dreamer`. Removed bundled `cinema` / `lewd` / private personas. `/остынь` and `/horny-game` work for any persona with a `horny` control. Pack source: `pack` / `overlay+pack`. + **Version 0.14.1** — Force Exact turbo/raw numbers on Generate when live Swarm still has foreign leftovers (e.g. steps 20 / cfg 7), including when the profile *label* is turbo/raw. detectKreaProfileName no longer invents turbo for unlabeled ckpts (realismByStableYogi → raw). Live context injects krea_profile + ecommended_params. RU «шаг/шагами» counts as param intent. Soft sparse-prompt exception when session ≠ Exact profile. Post-Generate warm still runs with park off, but skips the cold path when Ollama already has the chat model resident. **Version 0.14.0** — **Чат = сессия генерации**: у каждого чата свои params/LoRA/checkpoint/кадр/refs; модель шлёт sparse-дельту + `generate`/`look_at`/`ask`; без вкладки Карточки и Civitai/wanted hops. **Сжатие контекста**: rolling-саммари той же Ollama-моделью, чип бюджета `N / num_ctx`, авто перед отправкой, `/compress`. @@ -53,7 +55,7 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + - **Right:** Chat | Cards | Settings; persona / pack / Ollama chat model; **Ollama health** badge - **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override) -## Config (bundled + overlay) +## Config (bundled + overlay + packs) ``` Config/ @@ -61,11 +63,12 @@ Config/ personas// # sparse shelves: persona/bio/voice/humor/… + optional controls.json / exact.json / memory-seed ``` -Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas//…` to override. Plus runtime state: +Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas//…` to override. Git persona packs install under `extensions//` (between bundled and overlay). Plus runtime state: ``` Assistent/ _base/ personas// # overlay presets — same names as Config/, sparse + extensions// # git packs: assistent-pack.yaml + shelves settings.json # embed_model, base_url, per-persona skills ollama-roles.json # chat vs memory model tags (gpu-rent writes this) memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state @@ -84,11 +87,11 @@ npm test # intent.js + patch.js SwarmUI loads a single script: `Assets/assistent.bundle.js`. -Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`. +Copy `personas/neutral/` → new overlay id, edit only differing JSON. Or install a git pack under `Assistent/extensions//` with `assistent-pack.yaml`. See `Config/personas/README.md`. -**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`. +**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). If a persona defines **Хорни**: `/остынь`, `/horny-game`. -**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model). Export/import `.assistent-persona.json` for sharing. +**Authoring:** pack `author_persona` + `/persona new` clones to overlay only. Delete overlay personas with the ✕ button or ⚙ → Личности (never from the model; bundled/pack installs stay). Export/import `.assistent-persona.json` for sharing. ## Exact memory (KV) @@ -186,7 +189,7 @@ Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client v **Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs. -**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `leonid` under `Config/personas/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности. +**Personas:** bundled `neutral`, `aggressive`, `dreamer` under `Config/personas/`; more via git packs in `Assistent/extensions/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности. ## API routes diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index fdf5ef3..2f10396 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -33,7 +33,7 @@ public partial class SwarmAssistentExtension : Extension ExtensionAuthor = "mrleo1nid"; Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop."; License = "MIT"; - Version = "0.14.1"; + Version = "0.15.0"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; } @@ -99,7 +99,7 @@ public partial class SwarmAssistentExtension : Extension API.RegisterAPICall(AssistentLinkTrainSampleToAgent, true, PermUse); API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse); API.RegisterAPICall(AssistentSyncDatasetToAgent, true, PermUse); - Logs.Init("Swarm Assistent extension loaded (0.14.1 Exact force / krea_profile)"); + Logs.Init("Swarm Assistent extension loaded (0.15.0 persona packs / dreamer)"); } int CfgInt(string key, int fallback) diff --git a/src/app.js b/src/app.js index 514a9fe..5fbc223 100644 --- a/src/app.js +++ b/src/app.js @@ -5320,9 +5320,9 @@ return { id, title: (known && known.title) || ({ - neutral: 'Нейтральный', - lewd: 'Пошляк', + neutral: 'Нормальный', aggressive: 'Агрессивный', + dreamer: 'Мечтатель', }[id] || id), }; } @@ -6080,13 +6080,17 @@ if (!(meta && meta.historical)) { syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $('sa_persona')?.value))?.source); } + function isDeletablePersonaSource(source) { + const src = String(source || ''); + return src === 'overlay' || src === 'overlay+bundled' || src === 'overlay+pack'; + } + function syncPersonaDeleteButton(source) { const btn = $('sa_persona_delete'); if (!btn) { return; } - const src = String(source || ''); - const canDelete = src === 'overlay' || src === 'overlay+bundled'; + const canDelete = isDeletablePersonaSource(source); btn.hidden = !canDelete; btn.disabled = !canDelete; } @@ -6212,17 +6216,12 @@ if (!(meta && meta.historical)) { } function coolDownHorny() { - const persona = $('sa_persona')?.value || ''; - if (persona !== 'leonid') { - setStatus('/остынь только для Leonid'); + const schema = state.config?.controls || {}; + if (!schema.horny) { + setStatus('/остынь: у этой личности нет слайдера Хорни'); return; } state.lastUserControlIntent = true; - const schema = state.config?.controls || {}; - if (!schema.horny) { - setStatus('У этой личности нет слайдера Хорни'); - return; - } const min = Number(schema.horny.min ?? 0); const max = Number(schema.horny.max ?? 100); const cur = getControlValue('horny', Number(schema.horny.default ?? 35)); @@ -6244,12 +6243,12 @@ if (!(meta && meta.historical)) { } async function startHornyGame() { - const persona = $('sa_persona')?.value || ''; - if (persona !== 'leonid') { - setStatus('/horny-game только для Leonid'); + const schema = state.config?.controls || {}; + if (!schema.horny) { + setStatus('/horny-game: у этой личности нет слайдера Хорни'); return; } - const cur = getControlValue('horny', 35); + const cur = getControlValue('horny', Number(schema.horny.default ?? 35)); state.lastUserControlIntent = true; await sendChat({ skipSlash: true, @@ -6336,11 +6335,11 @@ if (!(meta && meta.historical)) { const meta = (state.personas || []).find((p) => p.id === id); const title = meta?.title || id; const src = meta?.source || state.config?.persona_source || ''; - if (src !== 'overlay' && src !== 'overlay+bundled') { - setStatus('Bundled personas cannot be deleted'); + if (src !== 'overlay' && src !== 'overlay+bundled' && src !== 'overlay+pack') { + setStatus('Можно удалить только overlay-слой (bundled/pack не трогаются)'); return; } - if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled) не трогается.`)) { + if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled/pack) не трогается.`)) { return; } await new Promise((resolve) => { @@ -7021,6 +7020,12 @@ if (!(meta && meta.historical)) { if (source === 'overlay+bundled') { return 'встроено+правка'; } + if (source === 'overlay+pack') { + return 'пак+правка'; + } + if (source === 'pack') { + return 'пак'; + } return 'встроено'; } @@ -7055,7 +7060,7 @@ if (!(meta && meta.historical)) { function syncPersonaPanelActions() { const id = state.settingsPersonaId; const p = (state.personas || []).find((x) => x.id === id); - const canDelete = p && (p.source === 'overlay' || p.source === 'overlay+bundled'); + const canDelete = p && isDeletablePersonaSource(p.source); const del = $('sa_btn_persona_delete_panel'); if (del) { del.disabled = !canDelete; @@ -7125,8 +7130,8 @@ if (!(meta && meta.historical)) { return; } let newId = pack?.id || ''; - if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'overlay+bundled'))) { - newId = window.prompt('Id занят bundled — новый id:', `${newId}_import`) || ''; + if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'pack' || p.source === 'overlay+bundled' || p.source === 'overlay+pack'))) { + newId = window.prompt('Id занят bundled/pack — новый id:', `${newId}_import`) || ''; } genericRequest( 'AssistentImportPersona', @@ -7176,7 +7181,7 @@ if (!(meta && meta.historical)) { function deleteSelectedOverlayPersona() { const id = state.settingsPersonaId; const p = (state.personas || []).find((x) => x.id === id); - if (!p || (p.source !== 'overlay' && p.source !== 'overlay+bundled')) { + if (!p || !isDeletablePersonaSource(p.source)) { setStatus('Можно удалить только overlay'); return; } @@ -7905,8 +7910,8 @@ if (!(meta && meta.historical)) { why.push('session_exact пуст — params из Exact + профиль чекпоинта'); } why.push(`профиль чекпоинта: ${profile} (имя/title → turbo|raw)`); - if (persona === 'cinema' || state.exact?.generation?.aspect) { - why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || '—'}`); + if (state.exact?.generation?.aspect) { + why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || '—'}`); } if (state.lastPatch) { const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== 'notes'); diff --git a/test/patch.test.js b/test/patch.test.js index 401054a..0ae91d0 100644 --- a/test/patch.test.js +++ b/test/patch.test.js @@ -57,7 +57,7 @@ describe('session.js', () => { steps: 28, loras: [{ name: 'a', weight: 0.8 }], genResults: [{ id: 'var1', src: '/View/x.png' }], - persona: 'leonid', + persona: 'neutral', }); assert.equal(s.gen.prompt, 'fox'); assert.equal(s.gen.steps, 28); @@ -126,18 +126,18 @@ describe('session.js', () => { { exact, profiles: exact.profiles, profileName: 'turbo', userParamIntent: true }, ); assert.equal(explicit.patch.steps, 12); - assert.equal(explicit.patch.cfg, 2); - - const forced = mergeExactParamsForGenerate( - { generate: true, steps: 20, cfg: 7 }, - { - exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } }, - profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } }, - profileName: 'turbo', - userParamIntent: false, - }, - ); - assert.equal(forced.patch.steps, 8); + assert.equal(explicit.patch.cfg, 2); + + const forced = mergeExactParamsForGenerate( + { generate: true, steps: 20, cfg: 7 }, + { + exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } }, + profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } }, + profileName: 'turbo', + userParamIntent: false, + }, + ); + assert.equal(forced.patch.steps, 8); assert.equal(forced.patch.cfg, 1); });