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 {
|
return {
|
||||||
id,
|
id,
|
||||||
title: known && known.title || ({
|
title: known && known.title || ({
|
||||||
neutral: "\u041D\u0435\u0439\u0442\u0440\u0430\u043B\u044C\u043D\u044B\u0439",
|
neutral: "\u041D\u043E\u0440\u043C\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",
|
||||||
aggressive: "\u0410\u0433\u0440\u0435\u0441\u0441\u0438\u0432\u043D\u044B\u0439"
|
dreamer: "\u041C\u0435\u0447\u0442\u0430\u0442\u0435\u043B\u044C"
|
||||||
}[id] || id)
|
}[id] || id)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -6753,13 +6753,16 @@ ${data.ui.help_extra}`.trim();
|
|||||||
renderPersonaControls(data.controls || {}, controlValues);
|
renderPersonaControls(data.controls || {}, controlValues);
|
||||||
syncPersonaDeleteButton(data.persona_source || data.personas?.find((p) => p.id === (data.persona || $2("sa_persona")?.value))?.source);
|
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) {
|
function syncPersonaDeleteButton(source) {
|
||||||
const btn = $2("sa_persona_delete");
|
const btn = $2("sa_persona_delete");
|
||||||
if (!btn) {
|
if (!btn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const src = String(source || "");
|
const canDelete = isDeletablePersonaSource(source);
|
||||||
const canDelete = src === "overlay" || src === "overlay+bundled";
|
|
||||||
btn.hidden = !canDelete;
|
btn.hidden = !canDelete;
|
||||||
btn.disabled = !canDelete;
|
btn.disabled = !canDelete;
|
||||||
}
|
}
|
||||||
@@ -6878,17 +6881,12 @@ ${data.ui.help_extra}`.trim();
|
|||||||
return Number.isFinite(n) ? n : fallback;
|
return Number.isFinite(n) ? n : fallback;
|
||||||
}
|
}
|
||||||
function coolDownHorny() {
|
function coolDownHorny() {
|
||||||
const persona = $2("sa_persona")?.value || "";
|
const schema = state.config?.controls || {};
|
||||||
if (persona !== "leonid") {
|
if (!schema.horny) {
|
||||||
setStatus("/\u043E\u0441\u0442\u044B\u043D\u044C \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
state.lastUserControlIntent = true;
|
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 min = Number(schema.horny.min ?? 0);
|
||||||
const max = Number(schema.horny.max ?? 100);
|
const max = Number(schema.horny.max ?? 100);
|
||||||
const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
|
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)}%`);
|
setStatus(`/\u043E\u0441\u0442\u044B\u043D\u044C \u2192 ${Math.round(next)}%`);
|
||||||
}
|
}
|
||||||
async function startHornyGame() {
|
async function startHornyGame() {
|
||||||
const persona = $2("sa_persona")?.value || "";
|
const schema = state.config?.controls || {};
|
||||||
if (persona !== "leonid") {
|
if (!schema.horny) {
|
||||||
setStatus("/horny-game \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F Leonid");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const cur = getControlValue("horny", 35);
|
const cur = getControlValue("horny", Number(schema.horny.default ?? 35));
|
||||||
state.lastUserControlIntent = true;
|
state.lastUserControlIntent = true;
|
||||||
await sendChat({
|
await sendChat({
|
||||||
skipSlash: true,
|
skipSlash: true,
|
||||||
@@ -6992,12 +6990,12 @@ ${data.ui.help_extra}`.trim();
|
|||||||
const meta = (state.personas || []).find((p) => p.id === id);
|
const meta = (state.personas || []).find((p) => p.id === id);
|
||||||
const title = meta?.title || id;
|
const title = meta?.title || id;
|
||||||
const src = meta?.source || state.config?.persona_source || "";
|
const src = meta?.source || state.config?.persona_source || "";
|
||||||
if (src !== "overlay" && src !== "overlay+bundled") {
|
if (src !== "overlay" && src !== "overlay+bundled" && src !== "overlay+pack") {
|
||||||
setStatus("Bundled personas cannot be deleted");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if (!window.confirm(`\u0423\u0434\u0430\u043B\u0438\u0442\u044C \xAB${title}\xBB?
|
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;
|
return;
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -7644,6 +7642,12 @@ ${data.ui.help_extra}`.trim();
|
|||||||
if (source === "overlay+bundled") {
|
if (source === "overlay+bundled") {
|
||||||
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E+\u043F\u0440\u0430\u0432\u043A\u0430";
|
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";
|
return "\u0432\u0441\u0442\u0440\u043E\u0435\u043D\u043E";
|
||||||
}
|
}
|
||||||
function renderPersonaSettingsList() {
|
function renderPersonaSettingsList() {
|
||||||
@@ -7676,7 +7680,7 @@ ${data.ui.help_extra}`.trim();
|
|||||||
function syncPersonaPanelActions() {
|
function syncPersonaPanelActions() {
|
||||||
const id = state.settingsPersonaId;
|
const id = state.settingsPersonaId;
|
||||||
const p = (state.personas || []).find((x) => x.id === id);
|
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");
|
const del = $2("sa_btn_persona_delete_panel");
|
||||||
if (del) {
|
if (del) {
|
||||||
del.disabled = !canDelete;
|
del.disabled = !canDelete;
|
||||||
@@ -7745,8 +7749,8 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let newId = pack?.id || "";
|
let newId = pack?.id || "";
|
||||||
if ((state.personas || []).some((p) => p.id === newId && (p.source === "bundled" || p.source === "overlay+bundled"))) {
|
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 \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || "";
|
newId = window.prompt("Id \u0437\u0430\u043D\u044F\u0442 bundled/pack \u2014 \u043D\u043E\u0432\u044B\u0439 id:", `${newId}_import`) || "";
|
||||||
}
|
}
|
||||||
genericRequest(
|
genericRequest(
|
||||||
"AssistentImportPersona",
|
"AssistentImportPersona",
|
||||||
@@ -7794,7 +7798,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`;
|
|||||||
function deleteSelectedOverlayPersona() {
|
function deleteSelectedOverlayPersona() {
|
||||||
const id = state.settingsPersonaId;
|
const id = state.settingsPersonaId;
|
||||||
const p = (state.personas || []).find((x) => x.id === id);
|
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");
|
setStatus("\u041C\u043E\u0436\u043D\u043E \u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u043E\u043B\u044C\u043A\u043E overlay");
|
||||||
return;
|
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("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)`);
|
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) {
|
if (state.exact?.generation?.aspect) {
|
||||||
why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || "\u2014"}`);
|
why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || "\u2014"}`);
|
||||||
}
|
}
|
||||||
if (state.lastPatch) {
|
if (state.lastPatch) {
|
||||||
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== "notes");
|
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;
|
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>
|
/// <summary>Ancestor-first chain ending with the current persona (for overlay merge and vector retrieve).</summary>
|
||||||
public List<string> PersonaExtendsChain(string personaId)
|
public List<string> PersonaExtendsChain(string personaId)
|
||||||
{
|
{
|
||||||
@@ -212,8 +331,7 @@ public sealed class AssistentConfig
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
chain.Insert(0, cur);
|
chain.Insert(0, cur);
|
||||||
JObject meta = TryReadJson(ResolveUnder(Path.Combine(_bundledRoot, "personas", cur), "persona.json"))
|
JObject meta = TryReadPersonaMeta(cur);
|
||||||
?? TryReadJson(ResolveUnder(Path.Combine(_overlayRoot, "personas", cur), "persona.json"));
|
|
||||||
string parent = SafeId(meta?["extends"]?.ToString());
|
string parent = SafeId(meta?["extends"]?.ToString());
|
||||||
if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, cur, StringComparison.OrdinalIgnoreCase))
|
if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, cur, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -226,11 +344,21 @@ public sealed class AssistentConfig
|
|||||||
|
|
||||||
public IEnumerable<string> LayerRoots(string personaId)
|
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");
|
yield return Path.Combine(_bundledRoot, "_base");
|
||||||
foreach (string id in PersonaExtendsChain(personaId))
|
foreach (string id in PersonaExtendsChain(personaId))
|
||||||
{
|
{
|
||||||
yield return Path.Combine(_bundledRoot, "personas", id);
|
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");
|
yield return Path.Combine(_overlayRoot, "_base");
|
||||||
foreach (string id in PersonaExtendsChain(personaId))
|
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()
|
public List<(string id, string title, string accent, string source)> ListPersonaCatalog()
|
||||||
{
|
{
|
||||||
Dictionary<string, (string title, string accent, string source)> byId = new(StringComparer.OrdinalIgnoreCase);
|
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");
|
string dir = Path.Combine(root, "personas");
|
||||||
if (!Directory.Exists(dir))
|
if (!Directory.Exists(dir))
|
||||||
@@ -250,32 +401,17 @@ public sealed class AssistentConfig
|
|||||||
}
|
}
|
||||||
foreach (string folder in Directory.GetDirectories(dir))
|
foreach (string folder in Directory.GetDirectories(dir))
|
||||||
{
|
{
|
||||||
string id = SafeId(Path.GetFileName(folder));
|
ScanFolder(folder, Path.GetFileName(folder), source);
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Scan(_bundledRoot, "bundled");
|
ScanPersonasRoot(_bundledRoot, "bundled");
|
||||||
Scan(_overlayRoot, "overlay");
|
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())
|
foreach (string id in byId.Keys.ToList())
|
||||||
{
|
{
|
||||||
var cur = byId[id];
|
var cur = byId[id];
|
||||||
@@ -591,6 +727,15 @@ public sealed class AssistentConfig
|
|||||||
return File.Exists(path);
|
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)
|
public bool IsOverlayPersona(string personaId)
|
||||||
{
|
{
|
||||||
string id = SafeId(personaId);
|
string id = SafeId(personaId);
|
||||||
@@ -605,15 +750,26 @@ public sealed class AssistentConfig
|
|||||||
|
|
||||||
public string PersonaSource(string personaId)
|
public string PersonaSource(string personaId)
|
||||||
{
|
{
|
||||||
if (IsOverlayPersona(personaId) && !IsBundledPersona(personaId))
|
bool overlay = IsOverlayPersona(personaId);
|
||||||
{
|
bool bundled = IsBundledPersona(personaId);
|
||||||
return "overlay";
|
bool pack = IsPackPersona(personaId);
|
||||||
}
|
if (overlay && bundled)
|
||||||
if (IsOverlayPersona(personaId) && IsBundledPersona(personaId))
|
|
||||||
{
|
{
|
||||||
return "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";
|
return "bundled";
|
||||||
}
|
}
|
||||||
@@ -649,9 +805,9 @@ public sealed class AssistentConfig
|
|||||||
{
|
{
|
||||||
string from = SafeId(fromId) ?? throw new ArgumentException("invalid from id");
|
string from = SafeId(fromId) ?? throw new ArgumentException("invalid from id");
|
||||||
string to = SafeId(toId) ?? throw new ArgumentException("invalid to 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);
|
string dest = Path.Combine(_overlayRoot, "personas", to);
|
||||||
if (Directory.Exists(dest) && Directory.EnumerateFileSystemEntries(dest).Any() && !overwrite)
|
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)
|
public JObject SavePersonaShelves(string personaId, JObject shelves, bool allowBundledShadow = false)
|
||||||
{
|
{
|
||||||
string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id");
|
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);
|
string dest = Path.Combine(_overlayRoot, "personas", id);
|
||||||
Directory.CreateDirectory(dest);
|
Directory.CreateDirectory(dest);
|
||||||
@@ -754,19 +910,19 @@ public sealed class AssistentConfig
|
|||||||
{
|
{
|
||||||
return false;
|
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);
|
string dest = Path.Combine(_overlayRoot, "personas", id);
|
||||||
if (!Directory.Exists(dest))
|
if (!Directory.Exists(dest))
|
||||||
{
|
{
|
||||||
return false;
|
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);
|
Directory.Delete(dest, true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -854,13 +1010,13 @@ public sealed class AssistentConfig
|
|||||||
throw new ArgumentException("invalid persona id");
|
throw new ArgumentException("invalid persona id");
|
||||||
}
|
}
|
||||||
string id = requested;
|
string id = requested;
|
||||||
if (IsBundledPersona(id) && !IsOverlayPersona(id))
|
if (IsProtectedPersona(id) && !IsOverlayPersona(id))
|
||||||
{
|
{
|
||||||
// Force rename away from bundled id
|
// Force rename away from bundled/pack id
|
||||||
id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over bundled id — pick a new id");
|
id = SafeId(id + "_import") ?? throw new InvalidOperationException("cannot import over protected id — pick a new id");
|
||||||
if (IsBundledPersona(id) || (IsOverlayPersona(id) && !overwriteOverlay))
|
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)
|
if (IsOverlayPersona(id) && !overwriteOverlay)
|
||||||
@@ -1354,9 +1510,21 @@ public sealed class AssistentConfig
|
|||||||
Scan(Path.Combine(_bundledRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
|
Scan(Path.Combine(_bundledRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
|
||||||
Scan(Path.Combine(_overlayRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
|
Scan(Path.Combine(_overlayRoot, "_base", "memory-seed"), AssistentMemory.SharedPersona);
|
||||||
Scan(Path.Combine(_overlayRoot, "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);
|
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);
|
Scan(Path.Combine(_overlayRoot, "personas", id, "memory-seed"), id);
|
||||||
}
|
}
|
||||||
return docs;
|
return docs;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Mode: author_persona
|
# 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
|
## 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.
|
- Ask **by shelf groups**, not one giant questionnaire.
|
||||||
- Until you have enough answers, **do not write to disk** — only ask.
|
- Until you have enough answers, **do not write to disk** — only ask.
|
||||||
- Explicit “just copy as-is” → clone with no shelf edits.
|
- 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)
|
## 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",
|
"pack": "author_persona",
|
||||||
"actions": ["persona_clone", "persona_write", "persona_switch"],
|
"actions": ["persona_clone", "persona_write", "persona_switch"],
|
||||||
"persona_clone": {
|
"persona_clone": {
|
||||||
"from": "leonid",
|
"from": "neutral",
|
||||||
"to": "leonid_calm",
|
"to": "neutral_calm",
|
||||||
"title": "Леонид спокойный",
|
"title": "Нормальный спокойный",
|
||||||
"overwrite": false
|
"overwrite": false
|
||||||
},
|
},
|
||||||
"persona_shelves": {
|
"persona_shelves": {
|
||||||
"voice.json": { "tone": ["calm", "dry"] },
|
"voice.json": { "tone": ["calm", "dry"] },
|
||||||
"humor.json": { "frequency": "rare" }
|
"humor.json": { "frequency": "rare" }
|
||||||
},
|
},
|
||||||
"persona": "leonid_calm",
|
"persona": "neutral_calm",
|
||||||
"notes": "cloned and toned down"
|
"notes": "cloned and toned down"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
- `persona_clone` first (materialize snapshot), then `persona_write` for sparse shelf edits.
|
- `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.
|
- `persona_switch` / `"persona": "<id>"` — client switches the dropdown after save.
|
||||||
- Never emit `persona_delete` or any delete action.
|
- Never emit `persona_delete` or any delete action.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Personas (shelves)
|
# 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.
|
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.
|
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.
|
- **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
|
## Controls
|
||||||
|
|
||||||
@@ -37,4 +38,4 @@ If `controls.json` exists, Exact stores values under `exact.controls`. UI shows
|
|||||||
|
|
||||||
## Authoring via chat
|
## 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",
|
"tagline": "Calm practical art director",
|
||||||
"accent": "#8b949e"
|
"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.
|
**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.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`.
|
**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
|
- **Right:** Chat | Cards | Settings; persona / pack / Ollama chat model; **Ollama health** badge
|
||||||
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
|
- **Chips / slash:** loaded from `Config/_base/ui.json` (persona can override)
|
||||||
|
|
||||||
## Config (bundled + overlay)
|
## Config (bundled + overlay + packs)
|
||||||
|
|
||||||
```
|
```
|
||||||
Config/
|
Config/
|
||||||
@@ -61,11 +63,12 @@ Config/
|
|||||||
personas/<id>/ # sparse shelves: persona/bio/voice/humor/… + optional controls.json / exact.json / memory-seed
|
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/
|
Assistent/
|
||||||
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
|
_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
|
settings.json # embed_model, base_url, per-persona skills
|
||||||
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
|
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
|
||||||
memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state
|
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`.
|
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)
|
## 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.
|
**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
|
## API routes
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,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.14.1";
|
Version = "0.15.0";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
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(AssistentLinkTrainSampleToAgent, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse);
|
API.RegisterAPICall(AssistentUnlinkTrainSampleFromAgent, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentSyncDatasetToAgent, 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)
|
int CfgInt(string key, int fallback)
|
||||||
|
|||||||
+30
-25
@@ -5320,9 +5320,9 @@
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
title: (known && known.title) || ({
|
title: (known && known.title) || ({
|
||||||
neutral: 'Нейтральный',
|
neutral: 'Нормальный',
|
||||||
lewd: 'Пошляк',
|
|
||||||
aggressive: 'Агрессивный',
|
aggressive: 'Агрессивный',
|
||||||
|
dreamer: 'Мечтатель',
|
||||||
}[id] || id),
|
}[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);
|
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) {
|
function syncPersonaDeleteButton(source) {
|
||||||
const btn = $('sa_persona_delete');
|
const btn = $('sa_persona_delete');
|
||||||
if (!btn) {
|
if (!btn) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const src = String(source || '');
|
const canDelete = isDeletablePersonaSource(source);
|
||||||
const canDelete = src === 'overlay' || src === 'overlay+bundled';
|
|
||||||
btn.hidden = !canDelete;
|
btn.hidden = !canDelete;
|
||||||
btn.disabled = !canDelete;
|
btn.disabled = !canDelete;
|
||||||
}
|
}
|
||||||
@@ -6212,17 +6216,12 @@ if (!(meta && meta.historical)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function coolDownHorny() {
|
function coolDownHorny() {
|
||||||
const persona = $('sa_persona')?.value || '';
|
const schema = state.config?.controls || {};
|
||||||
if (persona !== 'leonid') {
|
if (!schema.horny) {
|
||||||
setStatus('/остынь только для Leonid');
|
setStatus('/остынь: у этой личности нет слайдера Хорни');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
state.lastUserControlIntent = true;
|
state.lastUserControlIntent = true;
|
||||||
const schema = state.config?.controls || {};
|
|
||||||
if (!schema.horny) {
|
|
||||||
setStatus('У этой личности нет слайдера Хорни');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const min = Number(schema.horny.min ?? 0);
|
const min = Number(schema.horny.min ?? 0);
|
||||||
const max = Number(schema.horny.max ?? 100);
|
const max = Number(schema.horny.max ?? 100);
|
||||||
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
|
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
|
||||||
@@ -6244,12 +6243,12 @@ if (!(meta && meta.historical)) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function startHornyGame() {
|
async function startHornyGame() {
|
||||||
const persona = $('sa_persona')?.value || '';
|
const schema = state.config?.controls || {};
|
||||||
if (persona !== 'leonid') {
|
if (!schema.horny) {
|
||||||
setStatus('/horny-game только для Leonid');
|
setStatus('/horny-game: у этой личности нет слайдера Хорни');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cur = getControlValue('horny', 35);
|
const cur = getControlValue('horny', Number(schema.horny.default ?? 35));
|
||||||
state.lastUserControlIntent = true;
|
state.lastUserControlIntent = true;
|
||||||
await sendChat({
|
await sendChat({
|
||||||
skipSlash: true,
|
skipSlash: true,
|
||||||
@@ -6336,11 +6335,11 @@ if (!(meta && meta.historical)) {
|
|||||||
const meta = (state.personas || []).find((p) => p.id === id);
|
const meta = (state.personas || []).find((p) => p.id === id);
|
||||||
const title = meta?.title || id;
|
const title = meta?.title || id;
|
||||||
const src = meta?.source || state.config?.persona_source || '';
|
const src = meta?.source || state.config?.persona_source || '';
|
||||||
if (src !== 'overlay' && src !== 'overlay+bundled') {
|
if (src !== 'overlay' && src !== 'overlay+bundled' && src !== 'overlay+pack') {
|
||||||
setStatus('Bundled personas cannot be deleted');
|
setStatus('Можно удалить только overlay-слой (bundled/pack не трогаются)');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled) не трогается.`)) {
|
if (!window.confirm(`Удалить «${title}»?\nПоставка (bundled/pack) не трогается.`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
@@ -7021,6 +7020,12 @@ if (!(meta && meta.historical)) {
|
|||||||
if (source === 'overlay+bundled') {
|
if (source === 'overlay+bundled') {
|
||||||
return 'встроено+правка';
|
return 'встроено+правка';
|
||||||
}
|
}
|
||||||
|
if (source === 'overlay+pack') {
|
||||||
|
return 'пак+правка';
|
||||||
|
}
|
||||||
|
if (source === 'pack') {
|
||||||
|
return 'пак';
|
||||||
|
}
|
||||||
return 'встроено';
|
return 'встроено';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -7055,7 +7060,7 @@ if (!(meta && meta.historical)) {
|
|||||||
function syncPersonaPanelActions() {
|
function syncPersonaPanelActions() {
|
||||||
const id = state.settingsPersonaId;
|
const id = state.settingsPersonaId;
|
||||||
const p = (state.personas || []).find((x) => x.id === id);
|
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');
|
const del = $('sa_btn_persona_delete_panel');
|
||||||
if (del) {
|
if (del) {
|
||||||
del.disabled = !canDelete;
|
del.disabled = !canDelete;
|
||||||
@@ -7125,8 +7130,8 @@ if (!(meta && meta.historical)) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let newId = pack?.id || '';
|
let newId = pack?.id || '';
|
||||||
if ((state.personas || []).some((p) => p.id === newId && (p.source === 'bundled' || p.source === 'overlay+bundled'))) {
|
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 — новый id:', `${newId}_import`) || '';
|
newId = window.prompt('Id занят bundled/pack — новый id:', `${newId}_import`) || '';
|
||||||
}
|
}
|
||||||
genericRequest(
|
genericRequest(
|
||||||
'AssistentImportPersona',
|
'AssistentImportPersona',
|
||||||
@@ -7176,7 +7181,7 @@ if (!(meta && meta.historical)) {
|
|||||||
function deleteSelectedOverlayPersona() {
|
function deleteSelectedOverlayPersona() {
|
||||||
const id = state.settingsPersonaId;
|
const id = state.settingsPersonaId;
|
||||||
const p = (state.personas || []).find((x) => x.id === id);
|
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');
|
setStatus('Можно удалить только overlay');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -7905,8 +7910,8 @@ if (!(meta && meta.historical)) {
|
|||||||
why.push('session_exact пуст — params из Exact + профиль чекпоинта');
|
why.push('session_exact пуст — params из Exact + профиль чекпоинта');
|
||||||
}
|
}
|
||||||
why.push(`профиль чекпоинта: ${profile} (имя/title → turbo|raw)`);
|
why.push(`профиль чекпоинта: ${profile} (имя/title → turbo|raw)`);
|
||||||
if (persona === 'cinema' || state.exact?.generation?.aspect) {
|
if (state.exact?.generation?.aspect) {
|
||||||
why.push(`persona/exact aspect: ${state.exact?.generation?.aspect || exactGen.aspect || '—'}`);
|
why.push(`persona/exact aspect: ${state.exact.generation.aspect || exactGen.aspect || '—'}`);
|
||||||
}
|
}
|
||||||
if (state.lastPatch) {
|
if (state.lastPatch) {
|
||||||
const keys = Object.keys(state.lastPatch).filter((k) => state.lastPatch[k] != null && k !== 'notes');
|
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,
|
steps: 28,
|
||||||
loras: [{ name: 'a', weight: 0.8 }],
|
loras: [{ name: 'a', weight: 0.8 }],
|
||||||
genResults: [{ id: 'var1', src: '/View/x.png' }],
|
genResults: [{ id: 'var1', src: '/View/x.png' }],
|
||||||
persona: 'leonid',
|
persona: 'neutral',
|
||||||
});
|
});
|
||||||
assert.equal(s.gen.prompt, 'fox');
|
assert.equal(s.gen.prompt, 'fox');
|
||||||
assert.equal(s.gen.steps, 28);
|
assert.equal(s.gen.steps, 28);
|
||||||
@@ -126,18 +126,18 @@ describe('session.js', () => {
|
|||||||
{ exact, profiles: exact.profiles, profileName: 'turbo', userParamIntent: true },
|
{ exact, profiles: exact.profiles, profileName: 'turbo', userParamIntent: true },
|
||||||
);
|
);
|
||||||
assert.equal(explicit.patch.steps, 12);
|
assert.equal(explicit.patch.steps, 12);
|
||||||
assert.equal(explicit.patch.cfg, 2);
|
assert.equal(explicit.patch.cfg, 2);
|
||||||
|
|
||||||
const forced = mergeExactParamsForGenerate(
|
const forced = mergeExactParamsForGenerate(
|
||||||
{ generate: true, steps: 20, cfg: 7 },
|
{ generate: true, steps: 20, cfg: 7 },
|
||||||
{
|
{
|
||||||
exact: { generation: { steps: 8, cfg: 1 }, profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } } },
|
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 } },
|
profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } },
|
||||||
profileName: 'turbo',
|
profileName: 'turbo',
|
||||||
userParamIntent: false,
|
userParamIntent: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
assert.equal(forced.patch.steps, 8);
|
assert.equal(forced.patch.steps, 8);
|
||||||
assert.equal(forced.patch.cfg, 1);
|
assert.equal(forced.patch.cfg, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user