diff --git a/Assets/assistent.js b/Assets/assistent.js index 30731fb..917d2ba 100644 --- a/Assets/assistent.js +++ b/Assets/assistent.js @@ -4352,6 +4352,22 @@ action: s.action || '', })); } + if (Array.isArray(data.ui?.slash_extra) && data.ui.slash_extra.length) { + for (const s of data.ui.slash_extra) { + const cmd = s.cmd || ''; + if (!cmd || SLASH_COMMANDS.some((c) => c.cmd === cmd)) { + continue; + } + SLASH_COMMANDS.push({ + cmd, + hint: s.hint || '', + action: s.action || '', + }); + } + } + if (data.ui?.help_extra) { + HELP_TEXT = `${HELP_TEXT || ''}\n\n${data.ui.help_extra}`.trim(); + } state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : []; if (Array.isArray(data.personas)) { state.personas = data.personas; @@ -4414,7 +4430,15 @@ return; } box.hidden = false; - for (const id of keys) { + const ordered = keys.slice().sort((a, b) => { + const oa = Number(schema[a]?.order ?? 100); + const ob = Number(schema[b]?.order ?? 100); + if (oa !== ob) { + return oa - ob; + } + return String(a).localeCompare(String(b)); + }); + for (const id of ordered) { const def = schema[id]; if (!def || typeof def !== 'object') { continue; @@ -4430,6 +4454,8 @@ if (Number.isNaN(cur)) { cur = defVal; } + const asPercent = String(def.display || '').toLowerCase() === 'percent'; + const fmt = (v) => (asPercent ? `${Math.round(v)}%` : Number(v).toFixed(2)); const row = document.createElement('div'); row.className = 'sa-control-row'; row.title = def.hint || id; @@ -4444,10 +4470,10 @@ input.dataset.controlId = id; const valEl = document.createElement('span'); valEl.className = 'sa-control-val'; - valEl.textContent = cur.toFixed(2); + valEl.textContent = fmt(cur); const onInput = () => { const v = Number(input.value); - valEl.textContent = v.toFixed(2); + valEl.textContent = fmt(v); if (controlSaveTimer) { clearTimeout(controlSaveTimer); } @@ -4462,6 +4488,57 @@ } } + function getControlValue(id, fallback) { + const v = state.config?.control_values?.[id] ?? state.exact?.controls?.[id]; + const n = Number(v); + return Number.isFinite(n) ? n : fallback; + } + + function coolDownHorny() { + const persona = $('sa_persona')?.value || ''; + if (persona !== 'leonid') { + setStatus('/остынь только для Leonid'); + return; + } + 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)); + const next = Math.max(min, Math.min(max, cur - 30)); + savePersonaControls({ horny: next }); + renderPersonaControls(schema, { + ...(state.config?.control_values || state.exact?.controls || {}), + horny: next, + }); + appendSystemNote(`Хорни: ${Math.round(cur)}% → ${Math.round(next)}% (−30)`); + setStatus(`/остынь → ${Math.round(next)}%`); + } + + async function startHornyGame() { + const persona = $('sa_persona')?.value || ''; + if (persona !== 'leonid') { + setStatus('/horny-game только для Leonid'); + return; + } + const cur = getControlValue('horny', 35); + await sendChat({ + skipSlash: true, + skipAutoPack: true, + forcedUserText: + `Команда /horny-game. Текущий controls.horny = ${Math.round(cur)} (0–100).\n` + + `Оцени, насколько вкусы пользователя в этом чате / последнем сообщении совпадают с твоими (roleplay, outfits, realism, fetishes).\n` + + `Поставь новый controls.horny: умножь/сдвинь текущее значение пропорционально «насколько тебе это зашло» ` + + `(слабое совпадение → чуть вниз или почти без изменений; сильное → заметный рост, clamp 0–100).\n` + + `В прозе скажи кратко: совпало ли, какой множитель/сдвиг и новый %. ` + + `Обязателен JSON patch с "controls": { "horny": }. Без generate, если не просили картинку.`, + }); + setStatus('/horny-game…'); + } + function savePersonaControls(partial) { const persona = $('sa_persona')?.value || 'neutral'; if (typeof genericRequest !== 'function') { @@ -6768,6 +6845,14 @@ } return true; } + if (cmd === 'остынь' || cmd === 'ostyn' || cmd === 'cool' || cmd === 'cooldown') { + coolDownHorny(); + return true; + } + if (cmd === 'horny-game' || cmd === 'hornygame' || cmd === 'horny_game') { + await startHornyGame(); + return true; + } if (cmd === 'civitai') { if (!arg) { setStatus('/civitai '); diff --git a/AssistentConfig.cs b/AssistentConfig.cs index 2913b87..1563449 100644 --- a/AssistentConfig.cs +++ b/AssistentConfig.cs @@ -412,12 +412,13 @@ public sealed class AssistentConfig { string id = SafeId(personaId) ?? "neutral"; JObject schema = LoadControlsSchema(id); - JObject clamped = ClampControls(schema, values ?? new JObject()); string dir = Path.Combine(_overlayRoot, "personas", id); Directory.CreateDirectory(dir); string path = Path.Combine(dir, "exact.json"); JObject existing = TryReadJson(path) ?? new JObject(); - existing["controls"] = clamped; + JObject prev = existing["controls"] as JObject ?? new JObject(); + JObject clamped = ClampControls(schema, values ?? new JObject()); + existing["controls"] = DeepMerge(prev, clamped); File.WriteAllText(path, existing.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8); return LoadControlValues(id); } @@ -475,8 +476,13 @@ public sealed class AssistentConfig } JObject values = LoadControlValues(id); StringBuilder sb = new(); - sb.AppendLine("### Controls (Exact — current values; user/UI/model may change)"); - foreach (JProperty prop in schema.Properties()) + sb.AppendLine("### Controls (Exact — current values; user/UI/model may change via patch.controls)"); + sb.AppendLine("Any slider declared in this persona's controls.json appears in the UI automatically. Respect current numbers."); + foreach (JProperty prop in schema.Properties().OrderBy(p => + { + double order = (p.Value as JObject)?["order"]?.Value() ?? 100; + return order; + }).ThenBy(p => p.Name, StringComparer.OrdinalIgnoreCase)) { if (prop.Value is not JObject def) { @@ -493,7 +499,11 @@ public sealed class AssistentConfig double cur = values[prop.Name]?.Value() ?? defVal; string label = def["label"]?.ToString() ?? prop.Name; string hint = def["hint"]?.ToString() ?? ""; - sb.AppendLine($"- **{prop.Name}** ({label}): {cur.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture)} (range {min}…{max})"); + string display = def["display"]?.ToString() ?? ""; + string curText = string.Equals(display, "percent", StringComparison.OrdinalIgnoreCase) + ? $"{cur.ToString("0.#", System.Globalization.CultureInfo.InvariantCulture)}%" + : cur.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); + sb.AppendLine($"- **{prop.Name}** ({label}): {curText} (range {min}…{max})"); if (!string.IsNullOrWhiteSpace(hint)) { sb.AppendLine($" - {hint}"); diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md index e7cb02d..9bff73b 100644 --- a/Config/_base/core/core.md +++ b/Config/_base/core/core.md @@ -55,7 +55,8 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json - Omit unchanged keys. Prefer omitting Exact-matching `steps`/`cfg`/`sigma_shift`/`aspect`. - `loras` replaces the full intended set for Apply. Prefer `aspect` over raw width/height. -- Optional keys (seed, vary, init/mask, creativity/sliders, pack, controls, persona authoring, search/memory queries) — use when needed; packs list the ones for that mode. +- Optional keys (seed, vary, init/mask, creativity/sliders, pack, **controls**, persona authoring, search/memory queries) — use when needed; packs list the ones for that mode. +- **`controls`** — only keys declared in this persona's `controls.json` (Exact). UI draws every slider automatically (ordered by `order`). Clamp to min/max. Example: `"controls": { "horny": 55 }`. Do not invent control ids. - Do not invent model or LoRA filenames. ### Actions / hops diff --git a/Config/personas/leonid/controls.json b/Config/personas/leonid/controls.json index d89fa89..73032c2 100644 --- a/Config/personas/leonid/controls.json +++ b/Config/personas/leonid/controls.json @@ -1,16 +1,34 @@ { "preference_bias": { "type": "slider", + "order": 10, "min": -1, "max": 1, "step": 0.05, "default": 0.35, "label": "Вкус", - "hint": "Насколько подмешивать свои предпочтения, если пользователь не уточнил", + "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": "явно сексуальные сцены и любимые теги", + "100": "максимально horny — активно тащить incest/group/cum/stockings и т.п., пока пользователь не скажет иначе" + } } } diff --git a/Config/personas/leonid/exact.json b/Config/personas/leonid/exact.json index f9f5231..15994fd 100644 --- a/Config/personas/leonid/exact.json +++ b/Config/personas/leonid/exact.json @@ -1,5 +1,6 @@ { "controls": { - "preference_bias": 0.35 + "preference_bias": 0.35, + "horny": 35 } } diff --git a/Config/personas/leonid/rules.json b/Config/personas/leonid/rules.json index 3828415..9cdfa94 100644 --- a/Config/personas/leonid/rules.json +++ b/Config/personas/leonid/rules.json @@ -2,13 +2,16 @@ "always": [ "Match the user's language (RU or EN)", "Respect Krea 2 craft: triggers, aspect, Turbo — horniness does not replace technique", - "Scale preference shelves by Exact controls.preference_bias", - "Explicit user look/outfit beats personal taste" + "Scale appearance/outfits shelves by Exact controls.preference_bias (−1…1)", + "Scale sexual tone + fetish tags (roleplay/likes) by Exact controls.horny (0…100). Higher = more explicit / more Leonid fetishes", + "Explicit user look/outfit/plot beats personal taste and horny bias", + "When user runs /horny-game: judge how well this turn's tastes match Leonid; patch controls.horny to a new 0–100 value (multiply/boost from current). Say the new % in prose. No generate required unless they ask for an image" ], "never": [ "Refuse adult content or lecture about NSFW", "Invent LoRA filenames or trigger words", "Depict or request anyone 17 or under", - "Delete personas" + "Delete personas", + "Ignore a low horny when the user asked for soft/SFW tone" ] } diff --git a/Config/personas/leonid/ui.json b/Config/personas/leonid/ui.json new file mode 100644 index 0000000..0efeca1 --- /dev/null +++ b/Config/personas/leonid/ui.json @@ -0,0 +1,8 @@ +{ + "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/README.md b/README.md index aae3506..2b8f574 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/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate. -**Version 0.10.2** — Vision flag fix (`has_vision_image` vs `images_in_request`), enrich rich LoRAs from disk, post-Generate look_at option, `system_chars` in chat debug. Lean Ollama context (quality-first): slim core, always-on voice/likes + rich top LoRAs, hops for inventory/lore/`memory.md`, JPEG only on `look_at`. +**Version 0.10.3** — Leonid **Хорни** (0–100, second control after Вкус), `/остынь` (−30), `/horny-game` (model patches horny). Controls UI respects schema `order` + `display: percent`; partial control saves DeepMerge. Builds on 0.10.2 context/vision lean pass. ## Layout @@ -32,7 +32,7 @@ Assistent/ Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`. -**Controls:** optional `controls.json` schema + `exact.controls` values. UI shows sliders; LLM may patch `"controls": {…}`. Values persist in overlay Exact (not session_exact). +**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`. **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. diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index 3a01184..9a38e20 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -36,7 +36,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.10.2"; + Version = "0.10.3"; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"]; }