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:
+88
-3
@@ -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": <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
@@ -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}");
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 и т.п., пока пользователь не скажет иначе"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"controls": {
|
||||
"preference_bias": 0.35
|
||||
"preference_bias": 0.35,
|
||||
"horny": 35
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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,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.
|
||||
|
||||
|
||||
@@ -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"];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user