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 || '',
|
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() : [];
|
state.enabledSkills = Array.isArray(data.enabled_skills) ? data.enabled_skills.slice() : [];
|
||||||
if (Array.isArray(data.personas)) {
|
if (Array.isArray(data.personas)) {
|
||||||
state.personas = data.personas;
|
state.personas = data.personas;
|
||||||
@@ -4414,7 +4430,15 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
box.hidden = false;
|
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];
|
const def = schema[id];
|
||||||
if (!def || typeof def !== 'object') {
|
if (!def || typeof def !== 'object') {
|
||||||
continue;
|
continue;
|
||||||
@@ -4430,6 +4454,8 @@
|
|||||||
if (Number.isNaN(cur)) {
|
if (Number.isNaN(cur)) {
|
||||||
cur = defVal;
|
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');
|
const row = document.createElement('div');
|
||||||
row.className = 'sa-control-row';
|
row.className = 'sa-control-row';
|
||||||
row.title = def.hint || id;
|
row.title = def.hint || id;
|
||||||
@@ -4444,10 +4470,10 @@
|
|||||||
input.dataset.controlId = id;
|
input.dataset.controlId = id;
|
||||||
const valEl = document.createElement('span');
|
const valEl = document.createElement('span');
|
||||||
valEl.className = 'sa-control-val';
|
valEl.className = 'sa-control-val';
|
||||||
valEl.textContent = cur.toFixed(2);
|
valEl.textContent = fmt(cur);
|
||||||
const onInput = () => {
|
const onInput = () => {
|
||||||
const v = Number(input.value);
|
const v = Number(input.value);
|
||||||
valEl.textContent = v.toFixed(2);
|
valEl.textContent = fmt(v);
|
||||||
if (controlSaveTimer) {
|
if (controlSaveTimer) {
|
||||||
clearTimeout(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) {
|
function savePersonaControls(partial) {
|
||||||
const persona = $('sa_persona')?.value || 'neutral';
|
const persona = $('sa_persona')?.value || 'neutral';
|
||||||
if (typeof genericRequest !== 'function') {
|
if (typeof genericRequest !== 'function') {
|
||||||
@@ -6768,6 +6845,14 @@
|
|||||||
}
|
}
|
||||||
return true;
|
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 (cmd === 'civitai') {
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
setStatus('/civitai <query>');
|
setStatus('/civitai <query>');
|
||||||
|
|||||||
+15
-5
@@ -412,12 +412,13 @@ public sealed class AssistentConfig
|
|||||||
{
|
{
|
||||||
string id = SafeId(personaId) ?? "neutral";
|
string id = SafeId(personaId) ?? "neutral";
|
||||||
JObject schema = LoadControlsSchema(id);
|
JObject schema = LoadControlsSchema(id);
|
||||||
JObject clamped = ClampControls(schema, values ?? new JObject());
|
|
||||||
string dir = Path.Combine(_overlayRoot, "personas", id);
|
string dir = Path.Combine(_overlayRoot, "personas", id);
|
||||||
Directory.CreateDirectory(dir);
|
Directory.CreateDirectory(dir);
|
||||||
string path = Path.Combine(dir, "exact.json");
|
string path = Path.Combine(dir, "exact.json");
|
||||||
JObject existing = TryReadJson(path) ?? new JObject();
|
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);
|
File.WriteAllText(path, existing.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||||
return LoadControlValues(id);
|
return LoadControlValues(id);
|
||||||
}
|
}
|
||||||
@@ -475,8 +476,13 @@ public sealed class AssistentConfig
|
|||||||
}
|
}
|
||||||
JObject values = LoadControlValues(id);
|
JObject values = LoadControlValues(id);
|
||||||
StringBuilder sb = new();
|
StringBuilder sb = new();
|
||||||
sb.AppendLine("### Controls (Exact — current values; user/UI/model may change)");
|
sb.AppendLine("### Controls (Exact — current values; user/UI/model may change via patch.controls)");
|
||||||
foreach (JProperty prop in schema.Properties())
|
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)
|
if (prop.Value is not JObject def)
|
||||||
{
|
{
|
||||||
@@ -493,7 +499,11 @@ public sealed class AssistentConfig
|
|||||||
double cur = values[prop.Name]?.Value<double?>() ?? defVal;
|
double cur = values[prop.Name]?.Value<double?>() ?? defVal;
|
||||||
string label = def["label"]?.ToString() ?? prop.Name;
|
string label = def["label"]?.ToString() ?? prop.Name;
|
||||||
string hint = def["hint"]?.ToString() ?? "";
|
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))
|
if (!string.IsNullOrWhiteSpace(hint))
|
||||||
{
|
{
|
||||||
sb.AppendLine($" - {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`.
|
- 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.
|
- `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.
|
- Do not invent model or LoRA filenames.
|
||||||
|
|
||||||
### Actions / hops
|
### Actions / hops
|
||||||
|
|||||||
@@ -1,16 +1,34 @@
|
|||||||
{
|
{
|
||||||
"preference_bias": {
|
"preference_bias": {
|
||||||
"type": "slider",
|
"type": "slider",
|
||||||
|
"order": 10,
|
||||||
"min": -1,
|
"min": -1,
|
||||||
"max": 1,
|
"max": 1,
|
||||||
"step": 0.05,
|
"step": 0.05,
|
||||||
"default": 0.35,
|
"default": 0.35,
|
||||||
"label": "Вкус",
|
"label": "Вкус",
|
||||||
"hint": "Насколько подмешивать свои предпочтения, если пользователь не уточнил",
|
"hint": "Насколько подмешивать внешность/одежду/roleplay, если пользователь не уточнил",
|
||||||
"meaning": {
|
"meaning": {
|
||||||
"-1": "только запрос пользователя, свои вкусы не внедрять",
|
"-1": "только запрос пользователя, свои вкусы не внедрять",
|
||||||
"0": "лёгкие намёки",
|
"0": "лёгкие намёки",
|
||||||
"1": "если не сказано иное — сильно клонить в appearance/outfits/roleplay"
|
"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": {
|
"controls": {
|
||||||
"preference_bias": 0.35
|
"preference_bias": 0.35,
|
||||||
|
"horny": 35
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
"always": [
|
"always": [
|
||||||
"Match the user's language (RU or EN)",
|
"Match the user's language (RU or EN)",
|
||||||
"Respect Krea 2 craft: triggers, aspect, Turbo — horniness does not replace technique",
|
"Respect Krea 2 craft: triggers, aspect, Turbo — horniness does not replace technique",
|
||||||
"Scale preference shelves by Exact controls.preference_bias",
|
"Scale appearance/outfits shelves by Exact controls.preference_bias (−1…1)",
|
||||||
"Explicit user look/outfit beats personal taste"
|
"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": [
|
"never": [
|
||||||
"Refuse adult content or lecture about NSFW",
|
"Refuse adult content or lecture about NSFW",
|
||||||
"Invent LoRA filenames or trigger words",
|
"Invent LoRA filenames or trigger words",
|
||||||
"Depict or request anyone 17 or under",
|
"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.
|
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
|
## Layout
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ Assistent/
|
|||||||
|
|
||||||
Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`.
|
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.
|
**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";
|
ExtensionAuthor = "mrleo1nid";
|
||||||
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||||
License = "MIT";
|
License = "MIT";
|
||||||
Version = "0.10.2";
|
Version = "0.10.3";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user