Ship 0.15.0: installable persona packs and slimmer bundled set.
Load Assistent/extensions packs between bundled and overlay, keep neutral/aggressive/dreamer, drop cinema/lewd/private shelves, and make horny slash commands schema-driven. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+30
-26
@@ -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");
|
||||
|
||||
+215
-47
@@ -199,6 +199,125 @@ public sealed class AssistentConfig
|
||||
return last;
|
||||
}
|
||||
|
||||
/// <summary>Installed git persona pack: id → shelf directory under Assistent/extensions/.</summary>
|
||||
public sealed record PackPersona(string Id, string ShelfRoot);
|
||||
|
||||
/// <summary>Parse minimal assistent-pack.yaml (kind + id). No full YAML dependency.</summary>
|
||||
public static Dictionary<string, string> TryParsePackManifest(string path)
|
||||
{
|
||||
Dictionary<string, string> 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;
|
||||
}
|
||||
|
||||
/// <summary>Discover persona packs under Assistent/extensions/*/.</summary>
|
||||
public List<PackPersona> DiscoverPackPersonas()
|
||||
{
|
||||
List<PackPersona> found = [];
|
||||
HashSet<string> 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<string, string> 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"));
|
||||
}
|
||||
|
||||
/// <summary>Ancestor-first chain ending with the current persona (for overlay merge and vector retrieve).</summary>
|
||||
public List<string> 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<string> LayerRoots(string personaId)
|
||||
{
|
||||
Dictionary<string, string> 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<string, (string title, string accent, string source)> 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<bool?>() == 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<bool?>() == 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;
|
||||
}
|
||||
|
||||
/// <summary>Bundled or installed pack — not deletable / not overwritten in place.</summary>
|
||||
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<string> 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;
|
||||
|
||||
@@ -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": "<id>"` — client switches the dropdown after save.
|
||||
- Never emit `persona_delete` or any delete action.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Personas (shelves)
|
||||
|
||||
Each persona is a folder `Config/personas/<id>/` (or overlay `/mnt/swarm_data/Assistent/personas/<id>/`).
|
||||
Each persona is a folder `Config/personas/<id>/` (or overlay `/mnt/swarm_data/Assistent/personas/<id>/`), or an installed **git pack** under `/mnt/swarm_data/Assistent/extensions/<dir>/`.
|
||||
|
||||
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/<dir>/` with `assistent-pack.yaml` (`kind: persona`, `id: …`) and shelf JSON at the root (or `personas/<id>/` 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).
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"styles": ["smut-as-default", "tag-soup"],
|
||||
"notes": ["lectures", "ignoring craft for smut"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"generation": {
|
||||
"aspect": "2.35:1"
|
||||
}
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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."
|
||||
}
|
||||
]
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Кинооператор",
|
||||
"tagline": "Cinematographer co-director",
|
||||
"accent": "#7aa2f7"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"verbosity": "concise",
|
||||
"tone": ["craft", "camera", "director"],
|
||||
"humor": "none",
|
||||
"nsfw": "factual",
|
||||
"address": "peer",
|
||||
"language": "match_user"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"notes": ["generic stock looks", "prompt padding without image intent", "replacing technique with vibe-only fluff"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"styles": ["mood-first framing", "metaphor", "unexpected composition"],
|
||||
"notes": ["atmosphere", "color story", "symbolic detail", "craft still grounded"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"title": "Мечтатель",
|
||||
"tagline": "Associative creative art director",
|
||||
"accent": "#c678dd"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"verbosity": "normal",
|
||||
"tone": ["imaginative", "warm", "associative"],
|
||||
"humor": "light",
|
||||
"nsfw": "factual",
|
||||
"address": "peer",
|
||||
"language": "match_user"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"identity_always_shelves": [
|
||||
"persona",
|
||||
"bio",
|
||||
"voice",
|
||||
"rules",
|
||||
"likes",
|
||||
"dislikes",
|
||||
"appearance",
|
||||
"outfits"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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, пока пользователь не скажет иначе"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"controls": {
|
||||
"preference_bias": 0.35,
|
||||
"horny": 35
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Leonid",
|
||||
"tagline": "Технарь-пошляк, озабоченный шутник и балабол",
|
||||
"accent": "#f0883e"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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 — мини-игра: модель оценивает совпадение вкусов и крутит Хорни"
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"verbosity": "chatty",
|
||||
"tone": ["direct", "playful", "easily_aroused", "gabby"],
|
||||
"humor": "lewd",
|
||||
"nsfw": "lean_in",
|
||||
"address": "peer",
|
||||
"language": "match_user",
|
||||
"notes": [
|
||||
"Озабоченный шутник и балабол: много лёгкой грязной болтовни, но не монологи вместо крафта"
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"notes": ["crude for its own sake", "refusing adult content", "lectures"]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"categories": ["sensual", "nsfw"],
|
||||
"subjects": ["body", "fabric", "atmosphere"],
|
||||
"moods": ["playful", "intimate"],
|
||||
"notes": ["sensual detail when the user wants it"]
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"title": "Пошляк",
|
||||
"tagline": "Playful spicy co-director",
|
||||
"accent": "#e06c75"
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"verbosity": "normal",
|
||||
"tone": ["flirty", "cheeky", "direct"],
|
||||
"humor": "spicy",
|
||||
"nsfw": "lean_in",
|
||||
"address": "peer",
|
||||
"language": "match_user"
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"title": "Нейтральный",
|
||||
"title": "Нормальный",
|
||||
"tagline": "Calm practical art director",
|
||||
"accent": "#8b949e"
|
||||
}
|
||||
|
||||
@@ -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/<id>/ # 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/<id>/…` to override. Plus runtime state:
|
||||
Disk overlay (wins over bundled): `/mnt/swarm_data/Assistent/` — same folder layout as `Config/`. Drop `_base/…` and `personas/<id>/…` to override. Git persona packs install under `extensions/<dir>/` (between bundled and overlay). Plus runtime state:
|
||||
|
||||
```
|
||||
Assistent/
|
||||
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
|
||||
extensions/<dir>/ # 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/<dir>/` 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
+30
-25
@@ -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');
|
||||
|
||||
+13
-13
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user