Add Leonid Horny control, /ostyn cool-down, and /horny-game.

Second persona slider (0-100%) auto-rendered from controls schema; cool-down drops 30 points; horny-game lets the model patch horny from taste match. Fix partial control saves to DeepMerge.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-22 02:33:22 +03:00
co-authored by Cursor
parent 26e4ef76fb
commit fee3eb8965
9 changed files with 143 additions and 17 deletions
+88 -3
View File
@@ -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)} (0100).\n` +
`Оцени, насколько вкусы пользователя в этом чате / последнем сообщении совпадают с твоими (roleplay, outfits, realism, fetishes).\n` +
`Поставь новый controls.horny: умножь/сдвинь текущее значение пропорционально «насколько тебе это зашло» ` +
`(слабое совпадение → чуть вниз или почти без изменений; сильное → заметный рост, clamp 0100).\n` +
`В прозе скажи кратко: совпало ли, какой множитель/сдвиг и новый %. ` +
`Обязателен JSON patch с "controls": { "horny": <number> }. Без 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 <query>');
+15 -5
View File
@@ -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<double?>() ?? 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<double?>() ?? 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}");
+2 -1
View File
@@ -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
+19 -1
View File
@@ -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 и т.п., пока пользователь не скажет иначе"
}
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
{
"controls": {
"preference_bias": 0.35
"preference_bias": 0.35,
"horny": 35
}
}
+6 -3
View File
@@ -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 0100 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"
]
}
+8
View File
@@ -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 — мини-игра: модель оценивает совпадение вкусов и крутит Хорни"
}
+2 -2
View File
@@ -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 **Хорни** (0100, 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.
+1 -1
View File
@@ -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"];
}