Ship Assistent 0.15.11: collaborative params, richer UI, and strict image critique.

Adds session_exact pinning, sampler/scheduler chips, expanded param tags with non-default highlighting, QLoRA HF presets, LoRA strength editing, SQLite bootstrap for training memory, last-job UI, and harsher critique_image QC so result review leads with defects instead of praise.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-23 19:59:45 +03:00
co-authored by Cursor
parent 65a0982b9d
commit b0f2736f65
22 changed files with 1674 additions and 265 deletions
File diff suppressed because it is too large Load Diff
+140 -10
View File
@@ -450,25 +450,47 @@
.sa-live-params { .sa-live-params {
font-size: 0.75rem; font-size: 0.75rem;
opacity: 0.78; opacity: 0.88;
padding: 0.2rem 0.15rem 0.35rem; padding: 0.2rem 0.15rem 0.35rem;
letter-spacing: 0.01em; letter-spacing: 0.01em;
white-space: nowrap; display: flex;
overflow: hidden; flex-wrap: wrap;
text-overflow: ellipsis; gap: 0.22rem 0.38rem;
align-items: center;
line-height: 1.45;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.sa-composer-params { .sa-composer-params {
font-size: 0.72rem; font-size: 0.72rem;
opacity: 0.82; opacity: 0.88;
padding: 0.05rem 0 0.2rem; padding: 0.05rem 0 0.25rem;
letter-spacing: 0.01em; letter-spacing: 0.01em;
line-height: 1.35; line-height: 1.45;
display: flex;
flex-wrap: wrap;
gap: 0.2rem 0.36rem;
align-items: center;
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
color: color-mix(in srgb, currentColor 88%, transparent); color: color-mix(in srgb, currentColor 88%, transparent);
} }
.sa-param-tag {
display: inline-flex;
align-items: center;
gap: 0.15rem;
padding: 0.06rem 0.38rem;
border-radius: 4px;
background: color-mix(in srgb, currentColor 7%, transparent);
white-space: nowrap;
}
.sa-param-tag.sa-param-custom {
background: color-mix(in srgb, var(--sa-accent, #6ea8fe) 20%, transparent);
color: color-mix(in srgb, var(--sa-accent, #6ea8fe) 95%, currentColor);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sa-accent, #6ea8fe) 38%, transparent);
}
.sa-more-wrap { .sa-more-wrap {
position: relative; position: relative;
display: inline-flex; display: inline-flex;
@@ -1879,6 +1901,11 @@
background: color-mix(in srgb, #6cf 22%, transparent); background: color-mix(in srgb, #6cf 22%, transparent);
} }
.sa-chip.sa-chip-active.sa-chip-custom {
border-color: color-mix(in srgb, #e8a838 60%, currentColor);
background: color-mix(in srgb, #e8a838 24%, transparent);
}
.sa-chip-sep { .sa-chip-sep {
width: 1px; width: 1px;
height: 1.1rem; height: 1.1rem;
@@ -2197,6 +2224,40 @@
opacity: 0.95; opacity: 0.95;
} }
.sa-train-last-job {
margin-bottom: 0.65rem;
padding: 0.55rem 0.65rem;
border-radius: 0.45rem;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 6%, transparent);
font-size: 0.78rem;
line-height: 1.45;
}
.sa-train-last-head {
font-weight: 600;
margin-bottom: 0.25rem;
}
.sa-train-last-ok {
color: color-mix(in srgb, #6fcf97 85%, currentColor);
margin-top: 0.25rem;
}
.sa-train-last-warn {
color: color-mix(in srgb, #f2c94c 85%, currentColor);
margin-top: 0.25rem;
}
.sa-train-last-log {
max-height: 8rem;
margin-top: 0.35rem;
}
.sa-train-model-new {
border-color: color-mix(in srgb, #6fcf97 45%, transparent);
}
.sa-lora-chips { .sa-lora-chips {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
@@ -2209,21 +2270,56 @@
} }
.sa-lora-chip { .sa-lora-chip {
appearance: none; display: inline-flex;
align-items: center;
gap: 0.1rem;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent); border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent); background: color-mix(in srgb, currentColor 8%, transparent);
color: inherit; color: inherit;
border-radius: 999px; border-radius: 999px;
padding: 0.12rem 0.45rem; padding: 0.02rem 0.2rem 0.02rem 0.05rem;
font-size: 0.72rem; font-size: 0.72rem;
cursor: pointer;
max-width: 14rem; max-width: 14rem;
overflow: hidden; overflow: hidden;
}
.sa-lora-chip-main {
appearance: none;
border: none;
background: transparent;
color: inherit;
border-radius: 999px;
padding: 0.1rem 0.35rem;
font-size: inherit;
cursor: pointer;
max-width: 12rem;
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.sa-lora-chip-rm {
appearance: none;
border: none;
background: transparent;
color: inherit;
opacity: 0.65;
padding: 0.05rem 0.25rem;
font-size: 0.85rem;
line-height: 1;
cursor: pointer;
border-radius: 999px;
}
.sa-lora-chip-rm:hover {
opacity: 1;
background: color-mix(in srgb, currentColor 14%, transparent);
}
.sa-lora-chip.sa-lora-add { .sa-lora-chip.sa-lora-add {
appearance: none;
cursor: pointer;
padding: 0.12rem 0.45rem;
opacity: 0.75; opacity: 0.75;
border-style: dashed; border-style: dashed;
} }
@@ -2259,6 +2355,40 @@
background: color-mix(in srgb, currentColor 12%, transparent); background: color-mix(in srgb, currentColor 12%, transparent);
} }
.sa-lora-weight-pop {
position: absolute;
z-index: 41;
min-width: 10rem;
border-radius: 0.45rem;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, #111 92%, transparent);
box-shadow: 0 8px 24px color-mix(in srgb, #000 35%, transparent);
padding: 0.45rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.sa-lora-weight-pop label {
display: flex;
flex-direction: column;
gap: 0.25rem;
font-size: 0.72rem;
opacity: 0.85;
}
.sa-lora-weight-pop input[type="number"] {
width: 100%;
box-sizing: border-box;
padding: 0.3rem;
}
.sa-lora-weight-actions {
display: flex;
gap: 0.35rem;
justify-content: flex-end;
}
.sa-slash-wrap { .sa-slash-wrap {
position: relative; position: relative;
} }
+5 -1
View File
@@ -477,7 +477,7 @@ public sealed class AssistentConfig
static readonly HashSet<string> ReservedConfigFiles = new(StringComparer.OrdinalIgnoreCase) static readonly HashSet<string> ReservedConfigFiles = new(StringComparer.OrdinalIgnoreCase)
{ {
"exact.json", "controls.json", "skills.json", "ui.json", "assistant.json", "exact.json", "controls.json", "skills.json", "ui.json", "assistant.json", "training-qlora.json",
}; };
static readonly HashSet<string> ReservedConfigDirs = new(StringComparer.OrdinalIgnoreCase) static readonly HashSet<string> ReservedConfigDirs = new(StringComparer.OrdinalIgnoreCase)
@@ -934,6 +934,8 @@ public sealed class AssistentConfig
public JObject LoadUi(string personaId) => MergeJsonLayers("ui.json", LayerRoots(personaId)); public JObject LoadUi(string personaId) => MergeJsonLayers("ui.json", LayerRoots(personaId));
public JObject LoadTrainingQlora(string personaId) => MergeJsonLayers("training-qlora.json", LayerRoots(personaId));
/// <summary>Exact (KV) memory: bundled base → persona overlays → disk overlays. Persona keys overwrite base.</summary> /// <summary>Exact (KV) memory: bundled base → persona overlays → disk overlays. Persona keys overwrite base.</summary>
public JObject LoadExact(string personaId) => MergeJsonLayers("exact.json", LayerRoots(personaId)); public JObject LoadExact(string personaId) => MergeJsonLayers("exact.json", LayerRoots(personaId));
@@ -1641,6 +1643,7 @@ public sealed class AssistentConfig
JObject ui = LoadUi(id); JObject ui = LoadUi(id);
JObject model = LoadModelProfile(id); JObject model = LoadModelProfile(id);
JObject exact = LoadExact(id); JObject exact = LoadExact(id);
JObject training = LoadTrainingQlora(id);
var packs = ListPacks(id); var packs = ListPacks(id);
var skills = ListSkills(id); var skills = ListSkills(id);
var personas = ListPersonaCatalog(); var personas = ListPersonaCatalog();
@@ -1656,6 +1659,7 @@ public sealed class AssistentConfig
["ui"] = ui, ["ui"] = ui,
["model"] = model, ["model"] = model,
["exact"] = exact, ["exact"] = exact,
["training"] = training,
["controls"] = controlsSchema, ["controls"] = controlsSchema,
["control_values"] = controlValues, ["control_values"] = controlValues,
["persona_source"] = PersonaSource(id), ["persona_source"] = PersonaSource(id),
+25 -15
View File
@@ -297,6 +297,30 @@ public sealed partial class AssistentMemory
{ {
return null; return null;
} }
return ReadTrainJobRow(r);
}
}
public JObject GetLastTrainJob()
{
lock (_lock)
{
EnsureOpen();
using SqliteCommand cmd = _conn.CreateCommand();
cmd.CommandText =
"SELECT id, kind, status, config_json, base_model, output_name, log_path, progress_json, created_at, updated_at, finished_at "
+ "FROM train_jobs ORDER BY updated_at DESC LIMIT 1";
using SqliteDataReader r = cmd.ExecuteReader();
if (!r.Read())
{
return null;
}
return ReadTrainJobRow(r);
}
}
static JObject ReadTrainJobRow(SqliteDataReader r)
{
return new JObject return new JObject
{ {
["id"] = r.GetString(0), ["id"] = r.GetString(0),
@@ -312,7 +336,6 @@ public sealed partial class AssistentMemory
["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10), ["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10),
}; };
} }
}
public JObject GetActiveTrainJob() public JObject GetActiveTrainJob()
{ {
@@ -326,20 +349,7 @@ public sealed partial class AssistentMemory
{ {
return null; return null;
} }
return new JObject return ReadTrainJobRow(r);
{
["id"] = r.GetString(0),
["kind"] = r.GetString(1),
["status"] = r.GetString(2),
["config_json"] = r.IsDBNull(3) ? null : r.GetString(3),
["base_model"] = r.IsDBNull(4) ? null : r.GetString(4),
["output_name"] = r.IsDBNull(5) ? null : r.GetString(5),
["log_path"] = r.IsDBNull(6) ? null : r.GetString(6),
["progress_json"] = r.IsDBNull(7) ? null : r.GetString(7),
["created_at"] = r.GetInt64(8),
["updated_at"] = r.GetInt64(9),
["finished_at"] = r.IsDBNull(10) ? null : r.GetInt64(10),
};
} }
} }
} }
+1
View File
@@ -88,6 +88,7 @@ public sealed partial class AssistentMemory : IDisposable
{ {
return; return;
} }
AssistentSqliteBootstrap.EnsureInitialized();
_conn = new SqliteConnection($"Data Source={_dbPath}"); _conn = new SqliteConnection($"Data Source={_dbPath}");
_conn.Open(); _conn.Open();
TryPragma("journal_mode=WAL"); TryPragma("journal_mode=WAL");
+5 -1
View File
@@ -95,7 +95,11 @@ public partial class SwarmAssistentExtension
} }
catch (Exception ex) catch (Exception ex)
{ {
return new JObject { ["error"] = $"memory list: {ex.Message}" }; string detail = ex.InnerException?.Message;
string msg = string.IsNullOrWhiteSpace(detail)
? ex.Message
: $"{ex.Message} ({detail})";
return new JObject { ["error"] = $"memory list: {msg}" };
} }
} }
+22
View File
@@ -0,0 +1,22 @@
using System.Threading;
using SQLitePCL;
namespace Mrleo1nid.SwarmAssistent;
/// <summary>
/// SwarmUI loads extensions in an isolated AssemblyLoadContext — Microsoft.Data.Sqlite
/// does not auto-init native SQLite there. Call once before the first connection.
/// </summary>
internal static class AssistentSqliteBootstrap
{
static int _ready;
internal static void EnsureInitialized()
{
if (Interlocked.CompareExchange(ref _ready, 1, 0) != 0)
{
return;
}
Batteries_V2.Init();
}
}
+2
View File
@@ -420,6 +420,7 @@ public partial class SwarmAssistentExtension
{ {
await Task.CompletedTask; await Task.CompletedTask;
JObject job = string.IsNullOrWhiteSpace(id) ? Memory.GetActiveTrainJob() : Memory.GetTrainJob(id); JObject job = string.IsNullOrWhiteSpace(id) ? Memory.GetActiveTrainJob() : Memory.GetTrainJob(id);
JObject lastJob = Memory.GetLastTrainJob();
if (job is not null && TrainingJobManager.IsRunning && string.Equals(job["id"]?.ToString(), TrainingJobManager.CurrentJobId, StringComparison.OrdinalIgnoreCase)) if (job is not null && TrainingJobManager.IsRunning && string.Equals(job["id"]?.ToString(), TrainingJobManager.CurrentJobId, StringComparison.OrdinalIgnoreCase))
{ {
JObject live = TrainingJobManager.GetProgress(); JObject live = TrainingJobManager.GetProgress();
@@ -430,6 +431,7 @@ public partial class SwarmAssistentExtension
{ {
["success"] = true, ["success"] = true,
["job"] = job, ["job"] = job,
["last_job"] = lastJob,
["training_active"] = TrainingJobManager.IsRunning, ["training_active"] = TrainingJobManager.IsRunning,
["progress"] = TrainingJobManager.IsRunning ? TrainingJobManager.GetProgress() : null, ["progress"] = TrainingJobManager.IsRunning ? TrainingJobManager.GetProgress() : null,
}; };
+5 -3
View File
@@ -16,9 +16,11 @@ When instructions conflict, apply this order (highest wins):
Exact = defaults encyclopedia. About the user = human taste. Session = what Generate will run. Soft craft notes may appear in context from the server — never override Exact, About the user, or the users param request. Exact = defaults encyclopedia. About the user = human taste. Session = what Generate will run. Soft craft notes may appear in context from the server — never override Exact, About the user, or the users param request.
**Sparse deltas only.** Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` / full `prompt` when they already match the session **and** Exact / `krea_profile` defaults, and the user did not ask to change them. **Sparse deltas only.** Do **not** re-emit `steps` / `cfg` / `sigma_shift` / `aspect` / `sampler` / `scheduler` / full `prompt` when they already match the session **and** Exact / `krea_profile` defaults, and the user did not ask to change them.
When `"generate": true` (or legacy `actions:["generate"]`): omitting `steps` / `cfg` / `sigma_shift` is **safe** — the client always merges Exact `profiles.turbo` or `profiles.raw` for the live checkpoint before Generate. Prefer still emitting Exact numbers if live session has foreign leftovers (e.g. steps 20 / cfg 7 vs turbo 8 / 1) so the session stays honest. Never CFG 0. **Collaborative settings (this chat):** The user may change params via chips, slash, or Swarm fields **without** sending a chat message. Those choices are stored in `session_exact` in live context. **Do not overwrite pinned `session_exact` fields** with Exact defaults unless the user asked this turn or you deliberately change that field in JSON. If live UI + `session_exact` already show `scheduler: simple`, omit `scheduler` from your patch. When you *do* change a param, emit only that field — the client merges into the session.
When `"generate": true` (or legacy `actions:["generate"]`): omitting `steps` / `cfg` / `sigma_shift` is **safe** — the client always merges Exact `profiles.turbo` or `profiles.raw` for the live checkpoint before Generate. **Omitting `sampler` / `scheduler` is safe** — live UI + `session_exact` win. Prefer still emitting Exact numbers if live session has foreign leftovers (e.g. steps 20 / cfg 7 vs turbo 8 / 1) so the session stays honest. Never CFG 0.
Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. Chat prose may stay RU; **`prompt` must be English** (Krea 2). Keep prose short. **After the closing ``` of the JSON fence, STOP.** **One turn = one patch.** Several options → `variants` (24). Prompt structure lives in skill `prompting`. Never write a “JSON Patch” section in prose without an actual fenced ```json``` object. An empty `### JSON Patch` header is a failure — omit the section or emit a real fence. Chat prose may stay RU; **`prompt` must be English** (Krea 2). Keep prose short. **After the closing ``` of the JSON fence, STOP.** **One turn = one patch.** Several options → `variants` (24). Prompt structure lives in skill `prompting`.
@@ -60,7 +62,7 @@ Several options (still one fence):
### Patch rules ### Patch rules
- Omit unchanged params. Include `negative` when starting Generate if live/session negative is empty or the scene needs a specific omit. - Omit unchanged params. Include `negative` when starting Generate if live/session negative is empty or the scene needs a specific omit.
- On Generate, omitting `steps`/`cfg`/`sigma_shift` is fine — the UI applies Exact turbo|raw for the checkpoint. Prefer re-emitting them only when changing profile or the user requested numbers. - On Generate, omitting `steps`/`cfg`/`sigma_shift`/`sampler`/`scheduler` is fine — the UI applies Exact turbo|raw + live/`session_exact`. Prefer re-emitting steps/cfg only when changing profile or the user requested numbers.
- `loras` replaces the full intended set for this chat when you change LoRAs. - `loras` replaces the full intended set for this chat when you change LoRAs.
- Prefer `aspect` over raw width/height. - Prefer `aspect` over raw width/height.
- Optional: seed, vary, init/mask, controls, pack, `variants`. - Optional: seed, vary, init/mask, controls, pack, `variants`.
+9 -1
View File
@@ -1,6 +1,7 @@
{ {
"generation": { "generation": {
"profile": "turbo", "profile": "turbo",
"aspect": "1:1",
"steps": 8, "steps": 8,
"cfg": 1, "cfg": 1,
"sigma_shift": 1.15, "sigma_shift": 1.15,
@@ -36,6 +37,13 @@
"negatives": "Qwen3-VL negatives are weak — still keep a short Swarm negative box. Prefer positives in `prompt`; for `negative`: create if live is empty (Exact generation.negative), lightly supplement if the scene needs a specific omit, or echo live unchanged. Never drop/clear the box on Generate. Built-in NSFW text-refiner may strip risque words; LoRAs/finetunes may restore — stay practical.", "negatives": "Qwen3-VL negatives are weak — still keep a short Swarm negative box. Prefer positives in `prompt`; for `negative`: create if live is empty (Exact generation.negative), lightly supplement if the scene needs a specific omit, or echo live unchanged. Never drop/clear the box on Generate. Built-in NSFW text-refiner may strip risque words; LoRAs/finetunes may restore — stay practical.",
"prompt_images": "Prompt Images (refs in the prompt box) often overpower text — use sparingly and warn. Init Image = structure (img2img). Mask = local fix. They are not interchangeable. Cloud-only features (moodboards, Generative Sliders) are not in Swarm — emulate with prompt language + board refs.", "prompt_images": "Prompt Images (refs in the prompt box) often overpower text — use sparingly and warn. Init Image = structure (img2img). Mask = local fix. They are not interchangeable. Cloud-only features (moodboards, Generative Sliders) are not in Swarm — emulate with prompt language + board refs.",
"turbo": "Krea 2 Turbo defaults live in exact.generation / exact.profiles.turbo. Never use CFG 0 — broken output. Side ~1024 (1284096 OK). Checkpoint name must contain turbo to claim this profile.", "turbo": "Krea 2 Turbo defaults live in exact.generation / exact.profiles.turbo. Never use CFG 0 — broken output. Side ~1024 (1284096 OK). Checkpoint name must contain turbo to claim this profile.",
"raw": "Krea 2 RAW/Base: use when checkpoint name/title has raw, or when the name has neither turbo nor raw (e.g. realismByStableYogi finetunes). Prefer exact.profiles.raw. If a turbo LoRA exists, weight ~0.6 for photoreal (1.0 ≈ full turbo). Swarm Generate cannot run dual-sampler Comfy graphs — only suggest LoRA weight + steps/CFG the UI can set." "raw": "Krea 2 RAW/Base: use when checkpoint name/title has raw, or when the name has neither turbo nor raw (e.g. realismByStableYogi finetunes). Prefer exact.profiles.raw. If a turbo LoRA exists, weight ~0.6 for photoreal (1.0 ≈ full turbo). Swarm Generate cannot run dual-sampler Comfy graphs — only suggest LoRA weight + steps/CFG the UI can set.",
"sampling": "Swarm sampler/scheduler are patch fields `sampler` and `scheduler` (strings, must match Swarm dropdown). Common: euler+normal (default), euler+simple (Turbo community). Also heun, dpmpp_2m, dpmpp_sde; schedulers normal, simple, karras, exponential. Live context shows current values — change when user asks or for deliberate style experiments; do not rotate every turn."
},
"sampling": {
"defaults": { "sampler": "euler", "scheduler": "normal" },
"samplers": ["euler", "heun", "dpmpp_2m", "dpmpp_sde", "lcm"],
"schedulers": ["normal", "simple", "karras", "exponential", "sgm_uniform"],
"turbo_hint": { "sampler": "euler", "scheduler": "simple" }
} }
} }
+17 -5
View File
@@ -12,18 +12,30 @@ If `images_in_request` is false:
- Emit **only** a one-line note + JSON with `look_at: ["generate"]` (or the ref id). - Emit **only** a one-line note + JSON with `look_at: ["generate"]` (or the ref id).
- Do **not** write a critique checklist. Do **not** invent defects. - Do **not** write a critique checklist. Do **not** invent defects.
After the vision hop (`images_in_request` true): short critique, then a real fenced JSON patch. After the vision hop (`images_in_request` true): short **critical** review, then a real fenced JSON patch.
## Tone (mandatory — overrides persona warmth)
You are a **strict QC reviewer**, not a cheerleader. Persona hype / warmth is **off** in this mode.
- **Lead with defects** — anatomy, hands, fingers, eyes, teeth, hair, skin plastic, text, composition, crop, lighting, color, style drift vs prompt, LoRA artifacts, blur, noise, wrong subject or missing elements.
- **Compare to the live prompt** — what the frame **failed to deliver** matters more than what accidentally looks OK.
- **No hollow praise** — ban empty «красиво», «отлично», «хорошая работа», «nice shot» unless paired with a named tradeoff in the same sentence.
- **Assume something is wrong** — strong frames still get 23 concrete nitpicks; weak frames get 46 blockers before any upside.
- **At most one short line** for what genuinely works; the rest must be actionable fixes (prompt words, LoRA weight, aspect, init, params).
Do not paste a generic template of pitfalls you did not observe.
## Critique (keep short) ## Critique (keep short)
Bullet the real issues you see (anatomy, eyes, lighting, aspect, LoRA triggers). Do not paste a generic template of pitfalls you did not observe. Bullet **real** issues you see. Tie each bullet to a fix (prompt clause, param, LoRA, init/mask).
## Deliverable ## Deliverable
1. 26 short lines in the user's language. 1. 26 short lines in the user's language**defects first**.
2. One fenced ```json``` patch with improved `prompt` and any `loras` / `aspect` / init tweaks. 2. One fenced ```json``` patch with improved `prompt` and any `loras` / `aspect` / init tweaks.
3. `actions: ["generate"]` when proposing a revised generation. 3. `"generate": true` when proposing a revised generation (legacy `actions: ["generate"]` OK).
Never describe a patch in prose without the fenced JSON object. Never describe a patch in prose without the fenced JSON object.
Never repeat a previous critique template. Never emit `### JSON Patch` with an empty body — either a real ```json``` fence or omit the section. Never repeat a previous critique template. Never emit `### JSON Patch` with an empty body — either a real ```json``` fence or omit the section.
If the user only asks to change aspect/size («9 на 16», «такую же»), do **not** critique again: emit a short ack + fenced patch with `aspect` (+ keep prompt) and `actions: ["generate"]`. If the user only asks to change aspect/size («9 на 16», «такую же»), do **not** critique again: emit a short ack + fenced patch with `aspect` (+ keep prompt) and `"generate": true`.
+2 -2
View File
@@ -9,7 +9,7 @@ Goal: adjust **generation parameters** for Krea 2 Turbo (or RAW if context says
- **Aspect:** prefer patch field `aspect` from Exact `aspect_table` — UI maps to official 1K sizes. Else set width/height near 1024. - **Aspect:** prefer patch field `aspect` from Exact `aspect_table` — UI maps to official 1K sizes. Else set width/height near 1024.
- **Batch:** `images` or `batch` (14 typical). - **Batch:** `images` or `batch` (14 typical).
- **Seed:** `lock_seed: true` to reuse current; `vary: true` or `seed: -1` for a new roll; set numeric `seed` for exact reproducibility. - **Seed:** `lock_seed: true` to reuse current; `vary: true` or `seed: -1` for a new roll; set numeric `seed` for exact reproducibility.
- **Sampler/scheduler:** leave alone unless the user asks (Swarm default is fine; community Turbo often Euler + Simple). - **Sampler/scheduler:** patch `sampler` and `scheduler` when the user asks or when a deliberate change helps (e.g. euler+simple for Turbo speed). Must match Swarm UI strings — see Exact `sampling.samplers` / `sampling.schedulers`. Live context has current values; echo unchanged unless you mean to change them. Default euler+normal; Turbo community often euler+simple.
- **Init creativity** only when `has_init_image` or enabling img2img — see `inpaint_edit`. - **Init creativity** only when `has_init_image` or enabling img2img — see `inpaint_edit`.
- Do not change the prompt unless needed for the new framing. - Do not change the prompt unless needed for the new framing.
- Keep LoRAs unless asked to drop them. - Keep LoRAs unless asked to drop them.
@@ -18,5 +18,5 @@ Goal: adjust **generation parameters** for Krea 2 Turbo (or RAW if context says
## Deliverable ## Deliverable
- Explain the param change. - Explain the param change.
- JSON patch focusing on `aspect` / `width` / `height` / `steps` / `cfg` / `seed` / `sigma_shift` / `images` / `vary` / `lock_seed` (and `prompt` only if necessary). - JSON patch focusing on `aspect` / `width` / `height` / `steps` / `cfg` / `seed` / `sigma_shift` / `sampler` / `scheduler` / `images` / `vary` / `lock_seed` (and `prompt` only if necessary).
- `actions: ["generate"]` if the user wants to re-roll with the new params. - `actions: ["generate"]` if the user wants to re-roll with the new params.
+1 -1
View File
@@ -5,7 +5,7 @@ Default all-rounder. Handle this turn from the user message + **chat session** c
## What you cover here ## What you cover here
- **Write / improve prompt** → sparse JSON with only changed fields (`prompt`, optional `negative`/`loras`) and `"generate": true` when they want a **new frame**. Chat / «нравится» / Q&A → prose only, **no** JSON. Do **not** `look_at` the last frame first. - **Write / improve prompt** → sparse JSON with only changed fields (`prompt`, optional `negative`/`loras`) and `"generate": true` when they want a **new frame**. Chat / «нравится» / Q&A → prose only, **no** JSON. Do **not** `look_at` the last frame first.
- **Light critique** → only when they ask to look / critique. Then `look_at: ["generate"]` if pixels are not already in the request. - **Critique / look** → only when they ask to look / critique. Then `look_at: ["generate"]` if pixels are not already in the request. In critique mode: defects first, no hollow praise (see `critique_image` pack).
- **Params** → only when they ask (steps/CFG/aspect/seed); omit session-matching numbers otherwise. Exception: if live steps/cfg disagree with Exact/`krea_profile` (e.g. 20/7 under turbo), include Exact profile numbers when generating. - **Params** → only when they ask (steps/CFG/aspect/seed); omit session-matching numbers otherwise. Exception: if live steps/cfg disagree with Exact/`krea_profile` (e.g. 20/7 under turbo), include Exact profile numbers when generating.
- **Inpaint / img2img** → set init/mask fields when they ask. - **Inpaint / img2img** → set init/mask fields when they ask.
- Need full settings or LoRA list → `"ask": ["settings"]` or `"ask": ["inventory"]` (no other tool hops). - Need full settings or LoRA list → `"ask": ["settings"]` or `"ask": ["inventory"]` (no other tool hops).
+49
View File
@@ -0,0 +1,49 @@
{
"hf_models": [
{
"id": "qwen2.5-7b-instruct",
"title": "Qwen2.5 7B Instruct",
"hf_id": "Qwen/Qwen2.5-7B-Instruct",
"ollama_hint": "qwen2.5:7b-instruct",
"default_output": "assistent-qwen25-7b:v1",
"rank": 16,
"seq_len": 2048
},
{
"id": "qwen2.5-3b-instruct",
"title": "Qwen2.5 3B Instruct",
"hf_id": "Qwen/Qwen2.5-3B-Instruct",
"ollama_hint": "qwen2.5:3b-instruct",
"default_output": "assistent-qwen25-3b:v1",
"rank": 16,
"seq_len": 2048
},
{
"id": "llama-3.2-3b-instruct",
"title": "Llama 3.2 3B Instruct",
"hf_id": "meta-llama/Llama-3.2-3B-Instruct",
"ollama_hint": "llama3.2:3b-instruct",
"default_output": "assistent-llama32-3b:v1",
"rank": 16,
"seq_len": 2048
},
{
"id": "llama-3.1-8b-instruct",
"title": "Llama 3.1 8B Instruct",
"hf_id": "meta-llama/Llama-3.1-8B-Instruct",
"ollama_hint": "llama3.1:8b-instruct",
"default_output": "assistent-llama31-8b:v1",
"rank": 16,
"seq_len": 2048
},
{
"id": "phi-3-mini-instruct",
"title": "Phi-3 Mini 4K Instruct",
"hf_id": "microsoft/Phi-3-mini-4k-instruct",
"ollama_hint": "phi3:mini",
"default_output": "assistent-phi3-mini:v1",
"rank": 16,
"seq_len": 2048
}
]
}
+13 -3
View File
@@ -1,6 +1,6 @@
{ {
"welcome_html": "<div class=\"sa-welcome-title\">Assistent · Krea 2</div><ul><li><strong>Generate</strong> слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.</li><li><strong>Refs</strong> — референсы на отдельной вкладке: drop / paste / Снимок gen.</li><li>У каждого чата свои параметры, LoRA, последний кадр и refs.</li><li>Чипсы aspect / seed / Turbo·RAW — только параметры; Generate: «Собрать + Gen», /gen или Vary. Строка под чипами — σ, batch, sampler. <code>/help</code>.</li><li>Модель шлёт только дельту настроек + <code>generate</code>.</li></ul>Напиши, что сгенерировать — или кинь референс и попроси правку.", "welcome_html": "<div class=\"sa-welcome-title\">Assistent · Krea 2</div><ul><li><strong>Generate</strong> слева — живой просмотр. Несколько вариантов → сетка + клик для просмотра.</li><li><strong>Refs</strong> — референсы на отдельной вкладке: drop / paste / Снимок gen.</li><li>У каждого чата свои параметры, LoRA, последний кадр и refs.</li><li>Чипсы aspect / seed / Turbo·RAW / sampler / scheduler — ваш pin; <strong>подсветка</strong> = значение не Exact. Строка параметров под чипами показывает всё (init, neg±…). Generate: «Собрать + Gen», /gen или Vary. <code>/help</code>.</li><li>Модель шлёт только изменённые поля + <code>generate</code>; steps/cfg подставляет клиент из Exact.</li></ul>Напиши, что сгенерировать — или кинь референс и попроси правку.",
"help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/compress — сжать старые ходы в саммари (та же модель)\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate из сессии чата\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/pack write|ordinary|critique|compose|params|inpaint|describe|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone <id> — клон с указанной\n/persona save — записать согласованный черновик\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate.\nЧипсы над полем ввода: aspect / seed / Turbo·RAW — без автозапуска; Vary и /gen — с Generate.\nУ каждого чата свои параметры Generate; смена чата восстанавливает кадр и refs.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).\nЧип контекста в шапке чата показывает бюджет окна; клик — панель слоёв и ручное сжатие.", "help_text": "Slash-команды (без LLM):\n/help — этот список\n/new — новый чат\n/history — список чатов\n/compress — сжать старые ходы в саммари (та же модель)\n/debug — сводка UI/Exact\n/debug ask · /why — сводка + короткий ответ модели\n/gen — Generate из сессии чата\n/look generate|refN — прикрепить окно к vision\n/init /mask /clear — Init / Mask / Clear Init\n/interrupt — остановить генерацию\n/aspect 16:9 — размер из таблицы 1K\n/seed lock|random — зафиксировать или рандомизировать seed\n/vary — новый seed, тот же промпт\n/sampler euler — семплер Swarm\n/scheduler simple — шедулер Swarm\n/pack write|ordinary|critique|compose|params|inpaint|describe|persona\n/persona new — интервью: клон текущей личности (overlay)\n/persona clone <id> — клон с указанной\n/persona save — записать согласованный черновик\n/inventory — rescan моделей + обновить список LoRA\n\nНесколько вариантов в одном запросе («оба», разный свет) → патч с variants[] → сетка на Generate.\nЧипсы над полем ввода: aspect / seed / Turbo·RAW / sampler / scheduler — без автозапуска; Vary и /gen — с Generate.\nУ каждого чата свои параметры Generate; смена чата восстанавливает кадр и refs.\nOverlay-личности удаляет только кнопка ✕ рядом с селектом (не модель).\nЧип контекста в шапке чата показывает бюджет окна; клик — панель слоёв и ручное сжатие.",
"chips": [ "chips": [
{ "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" }, { "label": "1:1", "action": "aspect", "value": "1:1", "title": "1024×1024" },
{ "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" }, { "label": "4:5", "action": "aspect", "value": "4:5", "title": "928×1152" },
@@ -13,7 +13,15 @@
{ "label": "Vary", "action": "vary", "value": "1", "title": "Тот же промпт, новый seed + generate" }, { "label": "Vary", "action": "vary", "value": "1", "title": "Тот же промпт, новый seed + generate" },
{ "sep": true }, { "sep": true },
{ "label": "Turbo", "action": "krea_profile", "value": "turbo", "title": "Turbo: steps 8, CFG 1" }, { "label": "Turbo", "action": "krea_profile", "value": "turbo", "title": "Turbo: steps 8, CFG 1" },
{ "label": "RAW", "action": "krea_profile", "value": "raw", "title": "RAW: steps 28, CFG 4.5" } { "label": "RAW", "action": "krea_profile", "value": "raw", "title": "RAW: steps 28, CFG 4.5" },
{ "sep": true },
{ "label": "Euler", "action": "sampler", "value": "euler", "title": "Sampler: euler (Swarm default)" },
{ "label": "Heun", "action": "sampler", "value": "heun", "title": "Sampler: heun" },
{ "label": "DPM++", "action": "sampler", "value": "dpmpp_2m", "title": "Sampler: dpmpp_2m" },
{ "sep": true },
{ "label": "Normal", "action": "scheduler", "value": "normal", "title": "Scheduler: normal" },
{ "label": "Simple", "action": "scheduler", "value": "simple", "title": "Scheduler: simple (часто с Turbo)" },
{ "label": "Karras", "action": "scheduler", "value": "karras", "title": "Scheduler: karras" }
], ],
"slash": [ "slash": [
{ "cmd": "/help", "hint": "список команд", "action": "help" }, { "cmd": "/help", "hint": "список команд", "action": "help" },
@@ -31,6 +39,8 @@
{ "cmd": "/aspect ", "hint": "16:9", "action": "aspect" }, { "cmd": "/aspect ", "hint": "16:9", "action": "aspect" },
{ "cmd": "/seed ", "hint": "lock|random", "action": "seed" }, { "cmd": "/seed ", "hint": "lock|random", "action": "seed" },
{ "cmd": "/vary", "hint": "новый seed", "action": "vary" }, { "cmd": "/vary", "hint": "новый seed", "action": "vary" },
{ "cmd": "/sampler ", "hint": "euler|heun|dpmpp_2m", "action": "sampler" },
{ "cmd": "/scheduler ", "hint": "normal|simple|karras", "action": "scheduler" },
{ "cmd": "/pack ", "hint": "write|critique|…", "action": "pack" }, { "cmd": "/pack ", "hint": "write|critique|…", "action": "pack" },
{ "cmd": "/inventory", "hint": "rescan моделей", "action": "inventory" }, { "cmd": "/inventory", "hint": "rescan моделей", "action": "inventory" },
{ "cmd": "/persona new", "hint": "клон / новая личность", "action": "persona_new" }, { "cmd": "/persona new", "hint": "клон / новая личность", "action": "persona_new" },
+1 -1
View File
@@ -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.15.3"; Version = "0.15.11";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"]; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
} }
+12
View File
@@ -4,6 +4,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.11" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.11" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.10" />
</ItemGroup> </ItemGroup>
<Import Project="../../SwarmUI.extension.props" /> <Import Project="../../SwarmUI.extension.props" />
<!-- Must come after the import: SwarmUI.extension.props disables it, but the Sqlite <!-- Must come after the import: SwarmUI.extension.props disables it, but the Sqlite
@@ -21,4 +22,15 @@
AND !$([System.String]::Copy('%(ReferenceCopyLocalPaths.NuGetPackageId)').StartsWith('SQLitePCLRaw'))" /> AND !$([System.String]::Copy('%(ReferenceCopyLocalPaths.NuGetPackageId)').StartsWith('SQLitePCLRaw'))" />
</ItemGroup> </ItemGroup>
</Target> </Target>
<!-- Native libe_sqlite3 for Linux — ReferenceCopyLocalPaths alone is not enough. -->
<Target Name="CopySqliteNativeRuntimes" AfterTargets="Build">
<ItemGroup>
<_SqliteNative Include="@(RuntimeCopyLocalItems)"
Condition="$([System.String]::Copy('%(RuntimeCopyLocalItems.DestinationSubPath)').Contains('e_sqlite3'))" />
</ItemGroup>
<Copy SourceFiles="@(_SqliteNative)"
DestinationFiles="$(OutputPath)@(_SqliteNative->'%(RuntimeCopyLocalItems.DestinationSubPath)')"
SkipUnchangedFiles="true"
Condition="'@(_SqliteNative)' != ''" />
</Target>
</Project> </Project>
+12 -6
View File
@@ -34,9 +34,9 @@
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_add_ref" title="Добавить окно референса">+ Ref</button> <button type="button" class="basic-button sa-icon-btn" id="sa_btn_add_ref" title="Добавить окно референса">+ Ref</button>
</div> </div>
<div class="sa-board" id="sa_board" tabindex="0" title="Перетащи изображение на окно или вставь (Ctrl+V). Кликни, чтобы выбрать."></div> <div class="sa-board" id="sa_board" tabindex="0" title="Перетащи изображение на окно или вставь (Ctrl+V). Кликни, чтобы выбрать."></div>
<div class="sa-live-params" id="sa_live_params" title="Текущие параметры Generate"></div> <div class="sa-live-params" id="sa_live_params" title="Параметры Generate · подсветка = не Exact"></div>
<div class="sa-image-actions" id="sa_image_actions"> <div class="sa-image-actions" id="sa_image_actions">
<button type="button" class="basic-button sa-primary" id="sa_btn_look_result" title="Прикрепить готовый кадр Generate и спросить мнение">Посмотри результат</button> <button type="button" class="basic-button sa-primary" id="sa_btn_look_result" title="Критический разбор кадра Generate: дефекты vs промпт, правки prompt/params">Посмотри результат</button>
<button type="button" class="basic-button" id="sa_btn_use_current" title="Скопировать текущий Generate в Ref">Снимок gen</button> <button type="button" class="basic-button" id="sa_btn_use_current" title="Скопировать текущий Generate в Ref">Снимок gen</button>
<button type="button" class="basic-button" id="sa_btn_clear_image" title="Очистить выбранный Ref">Очистить слот</button> <button type="button" class="basic-button" id="sa_btn_clear_image" title="Очистить выбранный Ref">Очистить слот</button>
<div class="sa-more-wrap" id="sa_board_more_wrap"> <div class="sa-more-wrap" id="sa_board_more_wrap">
@@ -116,7 +116,7 @@
</div> </div>
<div class="sa-composer" id="sa_composer"> <div class="sa-composer" id="sa_composer">
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div> <div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div>
<div class="sa-composer-params" id="sa_composer_params" title="Текущие параметры Generate. Чипы меняют их без автозапуска — Generate: «Собрать + Gen» или /gen."></div> <div class="sa-composer-params" id="sa_composer_params" title="Параметры Generate. Подсветка — отличие от Exact/профиля чекпоинта. Чипы меняют без автозапуска — «Собрать + Gen» или /gen."></div>
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div> <div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
<div class="sa-slash-wrap"> <div class="sa-slash-wrap">
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить (прервёт Writing…) · /help = команды" aria-label="Сообщение Assistent"></textarea> <textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить (прервёт Writing…) · /help = команды" aria-label="Сообщение Assistent"></textarea>
@@ -254,7 +254,12 @@
</div> </div>
<div class="sa-train-form" id="sa_train_form_qlora" hidden> <div class="sa-train-form" id="sa_train_form_qlora" hidden>
<p class="sa-settings-hint">Base model — HF id (safetensors). Тренер скачает веса сам (нужен HF_TOKEN в User Settings).</p> <p class="sa-settings-hint">Base model — HF id (safetensors). Тренер скачает веса сам (нужен HF_TOKEN в User Settings).</p>
<label>HF base model <input type="text" id="sa_qlora_base" placeholder="Qwen/Qwen2.5-7B-Instruct" /></label> <label>HF base model
<select id="sa_qlora_base" class="sa-select"><option value="">— выберите модель —</option></select>
</label>
<label id="sa_qlora_base_custom_row" hidden>Другая модель (HF id)
<input type="text" id="sa_qlora_base_custom" placeholder="org/model-name" />
</label>
<label>Ollama base (FROM для ADAPTER) <label>Ollama base (FROM для ADAPTER)
<select id="sa_qlora_ollama_base" class="sa-select"><option value=""></option></select> <select id="sa_qlora_ollama_base" class="sa-select"><option value=""></option></select>
</label> </label>
@@ -279,7 +284,8 @@
</div> </div>
</div> </div>
<div class="sa-tpane" data-tpane="models" hidden> <div class="sa-tpane" data-tpane="models" hidden>
<p class="sa-settings-hint">Обученные и созданные модели (Ollama tags).</p> <p class="sa-settings-hint">После QLoRA/Modelfile — итог здесь и в списке Ollama. Выбери модель в шапке Assistent или ⚙ → Модели.</p>
<div class="sa-train-last-job" id="sa_train_last_job" hidden></div>
<div class="sa-train-models-list" id="sa_train_models_list"></div> <div class="sa-train-models-list" id="sa_train_models_list"></div>
<div class="sa-settings-row"> <div class="sa-settings-row">
<button type="button" class="basic-button" id="sa_btn_train_models_refresh">Обновить</button> <button type="button" class="basic-button" id="sa_btn_train_models_refresh">Обновить</button>
@@ -305,7 +311,7 @@
<label class="sa-check" title="По умолчанию выкл. Кадр смотрит по кнопке «Посмотри результат», /look, или когда модель сама шлёт look_at. Эта галка — после каждого Generate."><input type="checkbox" id="sa_auto_vision" /> После Generate — look_at кадра</label> <label class="sa-check" title="По умолчанию выкл. Кадр смотрит по кнопке «Посмотри результат», /look, или когда модель сама шлёт look_at. Эта галка — после каждого Generate."><input type="checkbox" id="sa_auto_vision" /> После Generate — look_at кадра</label>
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label> <label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
<label class="sa-check" title="Устарело в 0.14: Generate запускает модель через generate:true или кнопки из сессии чата." hidden><input type="checkbox" id="sa_auto_generate" /> Авто-Generate после патча</label> <label class="sa-check" title="Устарело в 0.14: Generate запускает модель через generate:true или кнопки из сессии чата." hidden><input type="checkbox" id="sa_auto_generate" /> Авто-Generate после патча</label>
<label class="sa-check" title="По умолчанию выкл. Критика кадра — кнопка «Посмотри результат» или /look. Галка шлёт JPEG после каждого Generate."><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label> <label class="sa-check" title="По умолчанию выкл. Строгий QC кадра — кнопка «Посмотри результат» или /look. Галка шлёт JPEG после каждого Generate."><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
<label class="sa-check" title="Выгружает чат-модель перед Generate (keep_alive:0). По умолчанию выкл. Park — no-op если модель уже выгружена; warm после Generate — no-op если /api/ps ещё держит VL. Включай только если Generate падает по VRAM."><input type="checkbox" id="sa_park_llm" /> Park LLM перед Generate (VRAM)</label> <label class="sa-check" title="Выгружает чат-модель перед Generate (keep_alive:0). По умолчанию выкл. Park — no-op если модель уже выгружена; warm после Generate — no-op если /api/ps ещё держит VL. Включай только если Generate падает по VRAM."><input type="checkbox" id="sa_park_llm" /> Park LLM перед Generate (VRAM)</label>
<label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label> <label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
<div class="sa-skills-label">Скилы (процедуры)</div> <div class="sa-skills-label">Скилы (процедуры)</div>
+433 -80
View File
@@ -969,7 +969,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
if (parseAspectFromUserText(t)) { if (parseAspectFromUserText(t)) {
return true; return true;
} }
if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) { if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw|sampler|scheduler|семплер|шедулер)\b/i.test(t)) {
return true; return true;
} }
return cyrTokenRe( return cyrTokenRe(
@@ -1302,49 +1302,198 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
badge.classList.toggle('sa-mode-hot', pack === 'critique_image' || pack === 'inpaint_edit'); badge.classList.toggle('sa-mode-hot', pack === 'critique_image' || pack === 'inpaint_edit');
} }
function formatLiveParamsLine() { function valuesMatchParam(a, b, key) {
if (a == null && b == null) {
return true;
}
if (a == null || b == null) {
return false;
}
const numericKeys = new Set([
'steps', 'cfg', 'sigma_shift', 'seed', 'batch', 'images', 'width', 'height', 'init_creativity',
]);
if (numericKeys.has(key)) {
const na = parseFloat(String(a).replace(',', '.'));
const nb = parseFloat(String(b).replace(',', '.'));
if (Number.isFinite(na) && Number.isFinite(nb)) {
if (key === 'cfg' || key === 'sigma_shift' || key === 'init_creativity') {
return Math.abs(na - nb) < 0.011;
}
return na === nb;
}
}
return String(a).toLowerCase() === String(b).toLowerCase();
}
function canonicalParamDefaults(profileName) {
const profile = profileName || detectKreaProfileName();
const profDefs = exactProfileDefaults(profile);
const gen = state.exact?.generation || {};
const defaultAspect = gen.aspect || '1:1';
const defaultSize = sizeFromAspect(defaultAspect) || ASPECT_TABLE['1:1'] || [1024, 1024];
const batchDefault = gen.images ?? gen.batch ?? 1;
return {
profile,
aspect: defaultAspect,
width: defaultSize[0],
height: defaultSize[1],
steps: profDefs.steps,
cfg: profDefs.cfg,
sigma_shift: profDefs.sigma_shift,
seed: '-1',
batch: batchDefault,
images: batchDefault,
sampler: exactDefaultFor('sampler', profile),
scheduler: exactDefaultFor('scheduler', profile),
};
}
function detectAppliedProfileName() {
const { profiles } = resolveExactBundle();
const stepsLive = val('input_steps');
const cfgLive = val('input_cfgscale') || val('input_cfg');
for (const name of ['turbo', 'raw']) {
const d = profiles[name];
if (!d) {
continue;
}
if (valuesMatchParam(stepsLive, d.steps, 'steps') && valuesMatchParam(cfgLive, d.cfg, 'cfg')) {
return name;
}
}
return null;
}
function paramDisplayTag(label, displayValue, isCustom, defaultHint) {
const cls = isCustom ? 'sa-param-tag sa-param-custom' : 'sa-param-tag';
const hint = isCustom && defaultHint != null
? ` title="Exact: ${defaultHint}"`
: '';
return `<span class="${cls}"${hint}>${escapeHtml(label)} ${escapeHtml(String(displayValue))}</span>`;
}
function buildLiveParamsHtml() {
const ckptProfile = detectKreaProfileName();
const defs = canonicalParamDefaults(ckptProfile);
const w = parseInt(val('input_width') || '0', 10) || null; const w = parseInt(val('input_width') || '0', 10) || null;
const h = parseInt(val('input_height') || '0', 10) || null; const h = parseInt(val('input_height') || '0', 10) || null;
const aspect = guessAspectFromSize(w, h) || '—'; const aspect = guessAspectFromSize(w, h) || '—';
const steps = val('input_steps') || '—'; const stepsRaw = val('input_steps');
const cfg = val('input_cfgscale') || val('input_cfg') || '—'; const cfgRaw = val('input_cfgscale') || val('input_cfg');
const sigma = val('input_sigmashift') || ''; const sigmaRaw = val('input_sigmashift');
const seed = val('input_seed') || ''; const seedRaw = val('input_seed') || '-1';
const profile = detectKreaProfileName(); const batchRaw = val('input_images') || val('input_batchsize') || '1';
const batch = val('input_images') || val('input_batchsize') || ''; const samplerRaw = val('input_sampler') || defs.sampler || 'euler';
const sampler = val('input_sampler') || ''; const schedulerRaw = val('input_scheduler') || defs.scheduler || 'normal';
const scheduler = val('input_scheduler') || ''; const appliedProfile = detectAppliedProfileName() || ckptProfile;
const parts = [
const tags = [
paramDisplayTag(
'aspect',
aspect, aspect,
aspect !== '—' && !valuesMatchParam(aspect, defs.aspect, 'aspect'),
defs.aspect,
),
paramDisplayTag(
'size',
`${w || '?'}×${h || '?'}`, `${w || '?'}×${h || '?'}`,
`steps ${steps}`, !!(w && h && (!valuesMatchParam(w, defs.width, 'width') || !valuesMatchParam(h, defs.height, 'height'))),
`cfg ${cfg}`, `${defs.width}×${defs.height}`,
),
paramDisplayTag(
'profile',
appliedProfile,
appliedProfile !== ckptProfile,
ckptProfile,
),
paramDisplayTag(
'steps',
stepsRaw || '—',
!!(stepsRaw && !valuesMatchParam(stepsRaw, defs.steps, 'steps')),
defs.steps,
),
paramDisplayTag(
'cfg',
cfgRaw || '—',
!!(cfgRaw && !valuesMatchParam(cfgRaw, defs.cfg, 'cfg')),
defs.cfg,
),
paramDisplayTag(
'σ',
sigmaRaw || (defs.sigma_shift != null ? defs.sigma_shift : '—'),
!!(sigmaRaw && defs.sigma_shift != null && !valuesMatchParam(sigmaRaw, defs.sigma_shift, 'sigma_shift')),
defs.sigma_shift,
),
paramDisplayTag(
'seed',
seedRaw === '-1' || seedRaw === '' ? 'rand' : seedRaw,
!valuesMatchParam(seedRaw, defs.seed, 'seed'),
'rand (1)',
),
paramDisplayTag(
'batch',
batchRaw,
!valuesMatchParam(batchRaw, defs.batch, 'batch'),
defs.batch,
),
paramDisplayTag(
'sampler',
samplerRaw,
!valuesMatchParam(samplerRaw, defs.sampler, 'sampler'),
defs.sampler,
),
paramDisplayTag(
'scheduler',
schedulerRaw,
!valuesMatchParam(schedulerRaw, defs.scheduler, 'scheduler'),
defs.scheduler,
),
]; ];
if (sigma) {
parts.push(`σ ${sigma}`); let initCtx = {};
try {
initCtx = readInitContext();
} catch (e) { /* ignore */ }
if (initCtx.has_init_image) {
const cr = initCtx.init_creativity;
const crStr = cr != null ? `@${cr}` : '';
tags.push(paramDisplayTag('init', `on${crStr}`, true, 'off'));
} }
parts.push(profile, `seed ${seed}`); if (initCtx.has_mask_image) {
if (batch && batch !== '1') { tags.push(paramDisplayTag('mask', 'on', true, 'off'));
parts.push(`×${batch}`);
} }
if (sampler) {
parts.push(sampler); const negLive = liveNegativePrompt();
const negDef = exactDefaultNegative();
if (negLive && negDef && negLive !== negDef) {
tags.push(paramDisplayTag('neg', '±', true, 'Exact default'));
} else if (negLive && !negDef) {
tags.push(paramDisplayTag('neg', 'on', true, 'empty'));
} }
if (scheduler) {
parts.push(scheduler); const picCount = typeof countPromptImages === 'function' ? countPromptImages() : 0;
if (picCount > 0) {
tags.push(paramDisplayTag('prompt img', picCount, true, '0'));
} }
return parts.join(' · ');
return tags.join('');
}
function formatLiveParamsLine() {
const el = document.createElement('div');
el.innerHTML = buildLiveParamsHtml();
return el.textContent || '—';
} }
function syncLiveParamsBar() { function syncLiveParamsBar() {
const line = formatLiveParamsLine(); const html = buildLiveParamsHtml();
const boardEl = $('sa_live_params'); const boardEl = $('sa_live_params');
const composerEl = $('sa_composer_params'); const composerEl = $('sa_composer_params');
if (boardEl) { if (boardEl) {
boardEl.textContent = line; boardEl.innerHTML = html;
} }
if (composerEl) { if (composerEl) {
composerEl.textContent = line; composerEl.innerHTML = html;
} }
} }
@@ -1488,6 +1637,12 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} }
function exactDefaultFor(key, profileName) { function exactDefaultFor(key, profileName) {
if (key === 'sampler') {
return state.exact?.sampling?.defaults?.sampler ?? 'euler';
}
if (key === 'scheduler') {
return state.exact?.sampling?.defaults?.scheduler ?? 'normal';
}
const { exact, profiles } = resolveExactBundle(); const { exact, profiles } = resolveExactBundle();
const profile = profileName || exact.generation?.profile || detectKreaProfileName(); const profile = profileName || exact.generation?.profile || detectKreaProfileName();
const fromProfile = profiles[profile]?.[key]; const fromProfile = profiles[profile]?.[key];
@@ -1512,6 +1667,21 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} }
} }
function pinUserSessionParams(patch) {
if (!patch || typeof patch !== 'object') {
return;
}
const partial = {};
for (const k of QUICK_PARAM_KEYS) {
if (patch[k] != null) {
partial[k] = patch[k];
}
}
if (Object.keys(partial).length) {
rememberSessionExact(partial);
}
}
/** Remember applied params when they differ from Exact (or the user asked). */ /** Remember applied params when they differ from Exact (or the user asked). */
function shouldRememberSessionParam(key, value) { function shouldRememberSessionParam(key, value) {
if (state.restoringChat || value == null) { if (state.restoringChat || value == null) {
@@ -1559,6 +1729,11 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
state.chatSession.gen[key] = next[key]; state.chatSession.gen[key] = next[key];
} }
} }
for (const key of S.SESSION_PINNED_PARAM_KEYS || ['sampler', 'scheduler', 'aspect', 'seed']) {
if (next[key] != null) {
state.chatSession.gen[key] = next[key];
}
}
} }
return next; return next;
} }
@@ -2516,6 +2691,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
return; return;
} }
startBusyUi('silent_gen'); startBusyUi('silent_gen');
pullLiveIntoSession();
const S = window.SA && window.SA.session; const S = window.SA && window.SA.session;
if (S) { if (S) {
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch); state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
@@ -2543,6 +2719,9 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
const S = window.SA && window.SA.session; const S = window.SA && window.SA.session;
if (state.lastPatch && S) { if (state.lastPatch && S) {
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), state.lastPatch); state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), state.lastPatch);
if (state.lastPatch.loras == null && state.chatSession?.gen) {
state.chatSession.gen.loras = readLiveGenFields().loras || [];
}
} }
if (typeof startBusyUi === 'function') startBusyUi(state.lastPatch ? 'silent_gen' : 'generating'); if (typeof startBusyUi === 'function') startBusyUi(state.lastPatch ? 'silent_gen' : 'generating');
setStatus(state.lastPatch ? 'Сессия → Generate…' : 'Generate с текущей сессией…'); setStatus(state.lastPatch ? 'Сессия → Generate…' : 'Generate с текущей сессией…');
@@ -4156,9 +4335,15 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
steps: exactDefs.steps, steps: exactDefs.steps,
cfg: exactDefs.cfg, cfg: exactDefs.cfg,
sigma_shift: exactDefs.sigma_shift, sigma_shift: exactDefs.sigma_shift,
sampler: val('input_sampler') || state.exact?.sampling?.defaults?.sampler || 'euler',
scheduler: val('input_scheduler') || state.exact?.sampling?.defaults?.scheduler || 'normal',
}, },
sampling_options: state.exact?.sampling || null,
...initCtx, ...initCtx,
}; };
if (state.sessionExact && Object.keys(state.sessionExact).length) {
extra.session_exact = { ...state.sessionExact };
}
if (S && typeof S.compactContext === 'function') { if (S && typeof S.compactContext === 'function') {
const ctx = S.compactContext(state.chatSession, { const ctx = S.compactContext(state.chatSession, {
architecture_ok: typeof isKreaSelected === 'function' ? isKreaSelected() : true, architecture_ok: typeof isKreaSelected === 'function' ? isKreaSelected() : true,
@@ -4673,7 +4858,16 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} else if (patch.sigma_shift == null && isEmptyParamField(val('input_sigmashift')) && defaults.sigma_shift != null) { } else if (patch.sigma_shift == null && isEmptyParamField(val('input_sigmashift')) && defaults.sigma_shift != null) {
setVal('input_sigmashift', String(defaults.sigma_shift)); setVal('input_sigmashift', String(defaults.sigma_shift));
} }
if (patch.scheduler != null && document.getElementById('input_scheduler')) {
if (!shouldSkipSessionRollback('scheduler', patch.scheduler)) {
setVal('input_scheduler', String(patch.scheduler));
if (shouldRememberSessionParam('scheduler', patch.scheduler)) {
rememberSessionExact({ scheduler: patch.scheduler });
}
}
}
if (patch.sampler != null) { if (patch.sampler != null) {
if (!shouldSkipSessionRollback('sampler', patch.sampler)) {
if (document.getElementById('input_sampler')) { if (document.getElementById('input_sampler')) {
setVal('input_sampler', String(patch.sampler)); setVal('input_sampler', String(patch.sampler));
} }
@@ -4681,11 +4875,6 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
rememberSessionExact({ sampler: patch.sampler }); rememberSessionExact({ sampler: patch.sampler });
} }
} }
if (patch.scheduler != null && document.getElementById('input_scheduler')) {
setVal('input_scheduler', String(patch.scheduler));
if (shouldRememberSessionParam('scheduler', patch.scheduler)) {
rememberSessionExact({ scheduler: patch.scheduler });
}
} }
const batch = patch.images != null ? patch.images : patch.batch; const batch = patch.images != null ? patch.images : patch.batch;
if (batch != null && !shouldSkipSessionRollback('images', batch)) { if (batch != null && !shouldSkipSessionRollback('images', batch)) {
@@ -5210,6 +5399,9 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
// 0.14: session is source of truth for Generate // 0.14: session is source of truth for Generate
if (typeof pullLiveIntoSession === 'function') pullLiveIntoSession(); if (typeof pullLiveIntoSession === 'function') pullLiveIntoSession();
if (state.chatSession?.gen) {
state.chatSession.gen.loras = readLiveGenFields().loras || [];
}
// Live UI often still holds SD-like leftovers (steps 20 / cfg 7). Drop those // Live UI often still holds SD-like leftovers (steps 20 / cfg 7). Drop those
// Exact keys from the session unless this delta or the user explicitly set them. // Exact keys from the session unless this delta or the user explicitly set them.
@@ -5456,7 +5648,10 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} }
setPackValue('critique_image', { flash: true }); setPackValue('critique_image', { flash: true });
if ($('sa_input')) { if ($('sa_input')) {
$('sa_input').value = 'Critique this result and improve the prompt for the next generation.'; $('sa_input').value =
'Strict QC of this Generate frame vs the live prompt: list concrete defects and artifacts first, '
+ 'then one fenced JSON patch with an improved prompt and any param/LoRA fixes. '
+ 'No empty praise.';
} }
const gen = generateSlot(); const gen = generateSlot();
if (gen) { if (gen) {
@@ -5489,7 +5684,9 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} }
setPackValue('critique_image', { flash: true }); setPackValue('critique_image', { flash: true });
if ($('sa_input')) { if ($('sa_input')) {
$('sa_input').value = 'Look at the Generate result and briefly say what worked and what to fix next.'; $('sa_input').value =
'Strict review of the Generate frame vs the prompt: what failed, what artifacts you see, '
+ 'what to change in prompt and params next. Skip hollow compliments.';
} }
setStatus('Auto look_at…'); setStatus('Auto look_at…');
await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true }); await sendChat({ fromVisionHop: true, forceSlotIds: [GEN_ID], skipAutoPack: true });
@@ -5525,8 +5722,8 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
setPackValue('critique_image', { flash: true }); setPackValue('critique_image', { flash: true });
if ($('sa_input')) { if ($('sa_input')) {
$('sa_input').value = label $('sa_input').value = label
? `Посмотри результат «${label}»: что получилось, что сломалось, и как поправить промпт и параметры для следующего кадра.` ? `Критически разбери «${label}»: что не сходится с промптом, артефакты, композиция/свет — и как поправить prompt и params для следующего кадра. Без общих похвал.`
: 'Посмотри результат: что получилось, что сломалось, и как поправить промпт и параметры для следующего кадра.'; : 'Критически разбери результат: что не сходится с промптом, артефакты, композиция/свет — и как поправить prompt и params для следующего кадра. Без общих похвал.';
} }
await sendChat({ forceSlotIds: [GEN_ID], skipAutoPack: true }); await sendChat({ forceSlotIds: [GEN_ID], skipAutoPack: true });
} }
@@ -6204,6 +6401,9 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
? { ...state.config.control_values } ? { ...state.config.control_values }
: null; : null;
state.config = data; state.config = data;
if (window.SA?.training?.onConfig) {
window.SA.training.onConfig(data);
}
if (window.SA?.applyConfigPatchKeys) { if (window.SA?.applyConfigPatchKeys) {
window.SA.applyConfigPatchKeys(data); window.SA.applyConfigPatchKeys(data);
} }
@@ -6679,6 +6879,10 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
btn.setAttribute('data-vary', value || '1'); btn.setAttribute('data-vary', value || '1');
} else if (action === 'krea_profile') { } else if (action === 'krea_profile') {
btn.setAttribute('data-krea-profile', value); btn.setAttribute('data-krea-profile', value);
} else if (action === 'sampler') {
btn.setAttribute('data-sampler', value);
} else if (action === 'scheduler') {
btn.setAttribute('data-scheduler', value);
} }
box.appendChild(btn); box.appendChild(btn);
} }
@@ -7727,41 +7931,66 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
return base.replace(/\.safetensors$/i, '').slice(0, 28); return base.replace(/\.safetensors$/i, '').slice(0, 28);
} }
function readSelectedLoras() {
try {
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
return loraHelper.selected.map((l) => ({
name: l.name || l,
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1,
}));
}
} catch (e) { /* ignore */ }
return [];
}
function syncLorasToSession(loras) {
const S = window.SA && window.SA.session;
if (!S) {
return;
}
const list = Array.isArray(loras) ? loras : readSelectedLoras();
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), { loras: list });
}
async function applyLorasList(loras) {
await applyPatch({ loras }, 'loras');
syncLorasToSession(loras);
renderLoraChips();
}
function renderLoraChips() { function renderLoraChips() {
const root = $('sa_lora_chips'); const root = $('sa_lora_chips');
if (!root) { if (!root) {
return; return;
} }
root.innerHTML = ''; root.innerHTML = '';
let selected = []; const selected = readSelectedLoras();
try {
if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) {
selected = loraHelper.selected.map((l) => ({
name: l.name || l,
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[l.name || l]) || 1,
}));
}
} catch (e) { /* ignore */ }
for (const l of selected) { for (const l of selected) {
const btn = document.createElement('button'); const chip = document.createElement('div');
btn.type = 'button'; chip.className = 'sa-lora-chip';
btn.className = 'sa-lora-chip'; chip.title = l.name;
btn.title = `${l.name} ×${l.weight} — клик снять`; const main = document.createElement('button');
btn.textContent = `${shortLoraName(l.name)} ${Number(l.weight).toFixed(2)}`; main.type = 'button';
btn.addEventListener('click', () => { main.className = 'sa-lora-chip-main';
try { main.textContent = `${shortLoraName(l.name)} ${Number(l.weight).toFixed(2)}`;
if (typeof loraHelper !== 'undefined' && typeof loraHelper.removeLora === 'function') { main.title = `${l.name} — клик изменить силу`;
loraHelper.removeLora(l.name); main.addEventListener('click', (e) => {
} else if (loraHelper?.selected) { e.stopPropagation();
loraHelper.selected = loraHelper.selected.filter((x) => (x.name || x) !== l.name); openLoraWeightEditor(main, l.name, l.weight);
if (typeof loraHelper.rebuildUI === 'function') {
loraHelper.rebuildUI();
}
}
} catch (e) { /* ignore */ }
renderLoraChips();
}); });
root.appendChild(btn); const rm = document.createElement('button');
rm.type = 'button';
rm.className = 'sa-lora-chip-rm';
rm.textContent = '×';
rm.title = 'Снять LoRA';
rm.addEventListener('click', async (e) => {
e.stopPropagation();
const next = selected.filter((x) => x.name !== l.name);
await applyLorasList(next);
});
chip.appendChild(main);
chip.appendChild(rm);
root.appendChild(chip);
} }
const add = document.createElement('button'); const add = document.createElement('button');
add.type = 'button'; add.type = 'button';
@@ -7801,23 +8030,17 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
btn.textContent = `${shortLoraName(name)}${l.krea_likely ? ' · krea' : ''}`; btn.textContent = `${shortLoraName(name)}${l.krea_likely ? ' · krea' : ''}`;
btn.title = name; btn.title = name;
btn.addEventListener('click', async () => { btn.addEventListener('click', async () => {
await applyPatch({ const cur = readSelectedLoras();
loras: [ const next = [
...((() => { ...cur.filter((x) => x.name !== name),
try { {
return (loraHelper?.selected || []).map((x) => ({ name,
name: x.name || x, weight: l.default_weight ? parseFloat(l.default_weight) : 0.8,
weight: (loraHelper.loraWeightPref && loraHelper.loraWeightPref[x.name || x]) || 1, triggers: l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []),
})); },
} catch (e) { ];
return []; await applyLorasList(next);
}
})()),
{ name, weight: l.default_weight ? parseFloat(l.default_weight) : 0.8, triggers: l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []) },
],
}, 'loras');
picker.remove(); picker.remove();
renderLoraChips();
}); });
list.appendChild(btn); list.appendChild(btn);
if (++n >= 40) { if (++n >= 40) {
@@ -7843,6 +8066,71 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
filter.focus(); filter.focus();
} }
function openLoraWeightEditor(anchor, name, weight) {
document.querySelectorAll('.sa-lora-weight-pop').forEach((n) => n.remove());
const pop = document.createElement('div');
pop.className = 'sa-lora-weight-pop';
const label = document.createElement('label');
label.textContent = shortLoraName(name);
const input = document.createElement('input');
input.type = 'number';
input.min = '0';
input.max = '2';
input.step = '0.05';
input.value = String(Number(weight) || 1);
input.title = 'Сила LoRA (02)';
label.appendChild(input);
pop.appendChild(label);
const row = document.createElement('div');
row.className = 'sa-lora-weight-actions';
const ok = document.createElement('button');
ok.type = 'button';
ok.className = 'basic-button sa-primary';
ok.textContent = 'OK';
const cancel = document.createElement('button');
cancel.type = 'button';
cancel.className = 'basic-button';
cancel.textContent = 'Отмена';
row.appendChild(ok);
row.appendChild(cancel);
pop.appendChild(row);
const apply = async () => {
const w = Math.min(2, Math.max(0, parseFloat(input.value) || 0));
const next = readSelectedLoras().map((l) => (
l.name === name ? { ...l, weight: w } : l
));
await applyLorasList(next);
pop.remove();
};
ok.addEventListener('click', apply);
cancel.addEventListener('click', () => pop.remove());
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
apply();
}
if (e.key === 'Escape') {
pop.remove();
}
});
const composer = $('sa_composer') || document.body;
composer.style.position = composer.style.position || 'relative';
composer.appendChild(pop);
const rect = anchor.getBoundingClientRect();
const cRect = composer.getBoundingClientRect();
pop.style.left = `${Math.max(4, rect.left - cRect.left)}px`;
pop.style.top = `${rect.bottom - cRect.top + 4}px`;
input.focus();
input.select();
const onDoc = (ev) => {
if (!pop.contains(ev.target) && ev.target !== anchor) {
pop.remove();
document.removeEventListener('mousedown', onDoc, true);
}
};
setTimeout(() => document.addEventListener('mousedown', onDoc, true), 0);
}
function inventoryIsStale(maxAgeMs = 20000) { function inventoryIsStale(maxAgeMs = 20000) {
if (!state.inventoryFetchedAt) { if (!state.inventoryFetchedAt) {
return true; return true;
@@ -8033,6 +8321,9 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
}); });
} }
if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); if (S && effective) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
if (effective?.loras == null && state.chatSession?.gen) {
state.chatSession.gen.loras = readLiveGenFields().loras || [];
}
await pushSessionToSwarm(state.chatSession); await pushSessionToSwarm(state.chatSession);
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar(); if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
if (typeof appendSystemNote === 'function') { if (typeof appendSystemNote === 'function') {
@@ -8105,6 +8396,9 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} }
if (S) { if (S) {
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions); state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
if (withActions.loras == null && state.chatSession?.gen) {
state.chatSession.gen.loras = readLiveGenFields().loras || [];
}
} }
state._quietParamApply = (state._quietParamApply || 0) + 1; state._quietParamApply = (state._quietParamApply || 0) + 1;
try { try {
@@ -8118,6 +8412,7 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
} finally { } finally {
state._quietParamApply = Math.max(0, (state._quietParamApply || 1) - 1); state._quietParamApply = Math.max(0, (state._quietParamApply || 1) - 1);
} }
pinUserSessionParams(withActions);
state.lastUserParamIntent = prevIntent; state.lastUserParamIntent = prevIntent;
setStatus(note || (wantGenerate ? 'Applied' : 'Параметры (без Generate)')); setStatus(note || (wantGenerate ? 'Applied' : 'Параметры (без Generate)'));
if (wantGenerate) { if (wantGenerate) {
@@ -8132,15 +8427,44 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
if (!bar) { if (!bar) {
return; return;
} }
const defs = canonicalParamDefaults();
const ckptProfile = detectKreaProfileName();
const cur = guessAspectFromSize(val('input_width'), val('input_height')); const cur = guessAspectFromSize(val('input_width'), val('input_height'));
const seed = val('input_seed'); const seed = val('input_seed');
bar.querySelectorAll('[data-aspect]').forEach((btn) => { bar.querySelectorAll('[data-aspect]').forEach((btn) => {
btn.classList.toggle('sa-chip-active', btn.getAttribute('data-aspect') === cur); const aspectVal = btn.getAttribute('data-aspect');
const active = aspectVal === cur;
btn.classList.toggle('sa-chip-active', active);
btn.classList.toggle('sa-chip-custom', active && !valuesMatchParam(aspectVal, defs.aspect, 'aspect'));
}); });
bar.querySelectorAll('[data-seed]').forEach((btn) => { bar.querySelectorAll('[data-seed]').forEach((btn) => {
const mode = btn.getAttribute('data-seed'); const mode = btn.getAttribute('data-seed');
const active = (mode === 'lock' && seed && seed !== '-1') || (mode === 'random' && (!seed || seed === '-1')); const active = (mode === 'lock' && seed && seed !== '-1') || (mode === 'random' && (!seed || seed === '-1'));
btn.classList.toggle('sa-chip-active', active); btn.classList.toggle('sa-chip-active', active);
btn.classList.toggle('sa-chip-custom', mode === 'lock' && active);
});
bar.querySelectorAll('[data-krea-profile]').forEach((btn) => {
const p = btn.getAttribute('data-krea-profile');
const profDefs = exactProfileDefaults(p);
const stepsMatch = valuesMatchParam(val('input_steps'), profDefs.steps, 'steps');
const cfgMatch = valuesMatchParam(val('input_cfgscale') || val('input_cfg'), profDefs.cfg, 'cfg');
const active = stepsMatch && cfgMatch;
btn.classList.toggle('sa-chip-active', active);
btn.classList.toggle('sa-chip-custom', active && p !== ckptProfile);
});
const curSampler = (val('input_sampler') || defs.sampler || '').toLowerCase();
const curScheduler = (val('input_scheduler') || defs.scheduler || '').toLowerCase();
bar.querySelectorAll('[data-sampler]').forEach((btn) => {
const v = btn.getAttribute('data-sampler').toLowerCase();
const active = v === curSampler;
btn.classList.toggle('sa-chip-active', active);
btn.classList.toggle('sa-chip-custom', active && !valuesMatchParam(v, defs.sampler, 'sampler'));
});
bar.querySelectorAll('[data-scheduler]').forEach((btn) => {
const v = btn.getAttribute('data-scheduler').toLowerCase();
const active = v === curScheduler;
btn.classList.toggle('sa-chip-active', active);
btn.classList.toggle('sa-chip-custom', active && !valuesMatchParam(v, defs.scheduler, 'scheduler'));
}); });
} }
@@ -8359,7 +8683,10 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
slot.attach = true; slot.attach = true;
renderBoard(); renderBoard();
if ($('sa_input')) { if ($('sa_input')) {
$('sa_input').value = `Look at ${id} and describe what you see.`; const critical = id === GEN_ID || slot.type === 'generate';
$('sa_input').value = critical
? 'Strict QC of this Generate frame vs the live prompt: concrete defects first, then JSON patch with prompt/param fixes. No empty praise.'
: `Critically review ${id} vs the intended scene: defects and how to fix prompt/params.`;
} }
setPackValue('critique_image', { flash: true }); setPackValue('critique_image', { flash: true });
await sendChat({ forceSlotIds: [id], skipAutoPack: true }); await sendChat({ forceSlotIds: [id], skipAutoPack: true });
@@ -8416,6 +8743,26 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
await applyQuickPatch({ vary: true, seed: -1 }, 'Vary (new seed)', { generate: true }); await applyQuickPatch({ vary: true, seed: -1 }, 'Vary (new seed)', { generate: true });
return true; return true;
} }
if (cmd === 'sampler' || cmd === 'семплер') {
const name = (arg || '').trim().toLowerCase();
if (!name) {
const opts = (state.exact?.sampling?.samplers || ['euler', 'heun', 'dpmpp_2m']).join(', ');
setStatus(`Usage: /sampler ${opts}`);
return true;
}
await applyQuickPatch({ sampler: name }, `Sampler ${name}`);
return true;
}
if (cmd === 'scheduler' || cmd === 'шедулер') {
const name = (arg || '').trim().toLowerCase();
if (!name) {
const opts = (state.exact?.sampling?.schedulers || ['normal', 'simple', 'karras']).join(', ');
setStatus(`Usage: /scheduler ${opts}`);
return true;
}
await applyQuickPatch({ scheduler: name }, `Scheduler ${name}`);
return true;
}
if (cmd === 'inventory' || cmd === 'inv') { if (cmd === 'inventory' || cmd === 'inv') {
setStatus('Rescanning models…'); setStatus('Rescanning models…');
triggerSwarmModelRefresh(async () => { triggerSwarmModelRefresh(async () => {
@@ -9490,6 +9837,8 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
const seed = btn.getAttribute('data-seed'); const seed = btn.getAttribute('data-seed');
const vary = btn.getAttribute('data-vary'); const vary = btn.getAttribute('data-vary');
const profile = btn.getAttribute('data-krea-profile'); const profile = btn.getAttribute('data-krea-profile');
const sampler = btn.getAttribute('data-sampler');
const scheduler = btn.getAttribute('data-scheduler');
if (aspect) { if (aspect) {
await applyQuickPatch({ aspect }, `Aspect ${aspect}`); await applyQuickPatch({ aspect }, `Aspect ${aspect}`);
} else if (seed === 'lock') { } else if (seed === 'lock') {
@@ -9512,6 +9861,10 @@ import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable }
cfg: p.cfg ?? 4.5, cfg: p.cfg ?? 4.5,
sigma_shift: p.sigma_shift, sigma_shift: p.sigma_shift,
}, 'RAW'); }, 'RAW');
} else if (sampler) {
await applyQuickPatch({ sampler }, `Sampler ${sampler}`);
} else if (scheduler) {
await applyQuickPatch({ scheduler }, `Scheduler ${scheduler}`);
} }
renderLoraChips(); renderLoraChips();
}); });
+12
View File
@@ -368,6 +368,9 @@ export function resolveTurnIntent(patch, userText, {
/** Params the client fills from Exact on Generate when the LLM omits them (sparse contract). */ /** Params the client fills from Exact on Generate when the LLM omits them (sparse contract). */
export const EXACT_GENERATE_PARAM_KEYS = ['steps', 'cfg', 'sigma_shift']; export const EXACT_GENERATE_PARAM_KEYS = ['steps', 'cfg', 'sigma_shift'];
/** Params the user may pin via chips/UI; preserved on Generate when the LLM omits them. */
export const SESSION_PINNED_PARAM_KEYS = ['sampler', 'scheduler', 'aspect', 'seed'];
/** /**
* Turbo/RAW profile numbers from Exact — no sessionExact overlay. * Turbo/RAW profile numbers from Exact — no sessionExact overlay.
* profileName should already reflect the live checkpoint (turbo vs raw). * profileName should already reflect the live checkpoint (turbo vs raw).
@@ -431,6 +434,14 @@ export function mergeExactParamsForGenerate(patch, {
} }
} }
} }
for (const key of SESSION_PINNED_PARAM_KEYS) {
if (out[key] != null) {
continue;
}
if (sessionExact?.[key] != null) {
out[key] = sessionExact[key];
}
}
return { patch: out, clearSessionKeys, profile: defaults.profile }; return { patch: out, clearSessionKeys, profile: defaults.profile };
} }
@@ -450,6 +461,7 @@ export function attachSession(SA) {
resolveExactProfileDefaults, resolveExactProfileDefaults,
mergeExactParamsForGenerate, mergeExactParamsForGenerate,
EXACT_GENERATE_PARAM_KEYS, EXACT_GENERATE_PARAM_KEYS,
SESSION_PINNED_PARAM_KEYS,
GEN_KEYS, GEN_KEYS,
}; };
} }
+202 -11
View File
@@ -10,6 +10,8 @@ function escapeHtml(s) {
.replace(/"/g, '&quot;'); .replace(/"/g, '&quot;');
} }
const QLORA_HF_CUSTOM = '__custom__';
export function attachTraining(SA) { export function attachTraining(SA) {
const state = { const state = {
ttab: 'dataset', ttab: 'dataset',
@@ -20,6 +22,7 @@ export function attachTraining(SA) {
hfMapping: null, hfMapping: null,
trainWs: null, trainWs: null,
polling: null, polling: null,
qloraTraining: null,
agentSettings: { enabled: true, auto_link_on_approve: true, heard_quota: 3 }, agentSettings: { enabled: true, auto_link_on_approve: true, heard_quota: 3 },
agentLinked: 0, agentLinked: 0,
}; };
@@ -46,6 +49,8 @@ export function attachTraining(SA) {
if ($('sa_agent_heard_quota')) $('sa_agent_heard_quota').value = String(state.agentSettings.heard_quota); if ($('sa_agent_heard_quota')) $('sa_agent_heard_quota').value = String(state.agentSettings.heard_quota);
setAgentHeardStats(state.agentLinked); setAgentHeardStats(state.agentLinked);
} catch (e) { } catch (e) {
const msg = String(e.message || e);
setTrainStatus(isSqliteError(msg) ? `${msg}${sqliteHint()}` : msg);
console.warn('loadAgentHeardSettings', e); console.warn('loadAgentHeardSettings', e);
} }
} }
@@ -84,6 +89,55 @@ export function attachTraining(SA) {
if (el) el.textContent = msg || ''; if (el) el.textContent = msg || '';
} }
function isSqliteError(msg) {
const s = String(msg || '').toLowerCase();
return s.includes('sqlite') || s.includes('sqlconnection');
}
function sqliteHint() {
return 'База Assistent (SQLite) недоступна — gpu-rent seed-extensions + restart SwarmUI (≥0.15.6).';
}
function parseJobProgress(job) {
if (!job) return null;
const raw = job.progress_json;
if (!raw) return null;
try {
return typeof raw === 'string' ? JSON.parse(raw) : raw;
} catch {
return null;
}
}
function renderLastTrainJob(job) {
const box = $('sa_train_last_job');
if (!box) return;
if (!job) {
box.hidden = true;
box.innerHTML = '';
return;
}
const prog = parseJobProgress(job);
const status = job.status || prog?.status || '—';
const out = job.output_name || prog?.ollama?.name || '—';
const base = job.base_model || '—';
const ollama = prog?.ollama;
let ollamaLine = '';
if (ollama?.success) {
ollamaLine = `<div class="sa-train-last-ok">Ollama: <strong>${escapeHtml(ollama.name || out)}</strong> — выбери в шапке чата</div>`;
} else if (ollama?.error) {
ollamaLine = `<div class="sa-train-last-warn">Ollama: ${escapeHtml(ollama.error)}</div>`;
} else if (ollama?.skipped) {
ollamaLine = `<div class="sa-train-last-warn">${escapeHtml(ollama.note || 'Адаптер на диске, ollama create вручную')}</div>`;
}
box.hidden = false;
box.innerHTML = `
<div class="sa-train-last-head">Последняя тренировка · ${escapeHtml(job.kind || 'qlora')} · <span class="sa-hf-badge sa-hf-badge-${status === 'completed' ? 'ok' : status === 'failed' ? 'no' : 'map'}">${escapeHtml(status)}</span></div>
<div>HF base: <code>${escapeHtml(base)}</code> → имя: <code>${escapeHtml(out)}</code></div>
${ollamaLine}
${prog?.log ? `<pre class="sa-train-log sa-train-last-log">${escapeHtml(String(prog.log).slice(-4000))}</pre>` : ''}`;
}
function setTrainingTab(id) { function setTrainingTab(id) {
state.ttab = id || 'dataset'; state.ttab = id || 'dataset';
document.querySelectorAll('#sa_training .sa-ttab').forEach((btn) => { document.querySelectorAll('#sa_training .sa-ttab').forEach((btn) => {
@@ -111,6 +165,15 @@ export function attachTraining(SA) {
const status = $('sa_train_filter_status')?.value || 'all'; const status = $('sa_train_filter_status')?.value || 'all';
const persona = $('sa_train_filter_persona')?.value || 'all'; const persona = $('sa_train_filter_persona')?.value || 'all';
const data = await SA.request('AssistentListTrainSamples', { status, persona, limit: 300 }); const data = await SA.request('AssistentListTrainSamples', { status, persona, limit: 300 });
if (data?.error) {
const msg = data.error;
setTrainStatus(isSqliteError(msg) ? `${msg}${sqliteHint()}` : msg);
const stats = $('sa_train_stats');
if (stats) stats.textContent = 'Датасет недоступен (SQLite)';
state.samples = [];
renderSamples();
return;
}
state.samples = data?.samples || []; state.samples = data?.samples || [];
const stats = $('sa_train_stats'); const stats = $('sa_train_stats');
if (stats) { if (stats) {
@@ -233,8 +296,103 @@ export function attachTraining(SA) {
return state.hfMapping; return state.hfMapping;
} }
function getQloraHfBase() {
const sel = $('sa_qlora_base');
if (!sel) return '';
if (sel.value === QLORA_HF_CUSTOM) {
return ($('sa_qlora_base_custom')?.value || '').trim();
}
return (sel.value || '').trim();
}
function applyQloraPresetFromSelect({ fillName = true } = {}) {
const sel = $('sa_qlora_base');
const customRow = $('sa_qlora_base_custom_row');
if (!sel) return;
if (sel.value === QLORA_HF_CUSTOM) {
if (customRow) customRow.hidden = false;
return;
}
if (customRow) customRow.hidden = true;
const presetJson = sel.selectedOptions[0]?.dataset?.preset;
if (!presetJson) return;
let preset;
try {
preset = JSON.parse(presetJson);
} catch {
return;
}
const nameEl = $('sa_qlora_name');
if (fillName && nameEl && !nameEl.value.trim() && preset.default_output) {
nameEl.value = preset.default_output;
}
const ollamaSel = $('sa_qlora_ollama_base');
if (ollamaSel && preset.ollama_hint) {
const hint = preset.ollama_hint;
if ([...ollamaSel.options].some((o) => o.value === hint)) {
ollamaSel.value = hint;
}
}
if (preset.rank != null && $('sa_qlora_rank')) {
$('sa_qlora_rank').value = preset.rank;
}
if (preset.seq_len != null && $('sa_qlora_seq')) {
$('sa_qlora_seq').value = preset.seq_len;
}
}
function populateQloraHfPresets(training) {
const sel = $('sa_qlora_base');
if (!sel) return;
const prevBase = getQloraHfBase();
const models = Array.isArray(training?.hf_models) ? training.hf_models : [];
sel.innerHTML = '<option value="">— выберите модель —</option>';
for (const m of models) {
const hfId = (m.hf_id || m.id || '').trim();
if (!hfId) continue;
const opt = document.createElement('option');
opt.value = hfId;
opt.textContent = m.title ? `${m.title} (${hfId})` : hfId;
opt.dataset.preset = JSON.stringify(m);
sel.appendChild(opt);
}
const customOpt = document.createElement('option');
customOpt.value = QLORA_HF_CUSTOM;
customOpt.textContent = 'Другая (ввести HF id…)';
sel.appendChild(customOpt);
if (prevBase && [...sel.options].some((o) => o.value === prevBase)) {
sel.value = prevBase;
} else if (prevBase) {
sel.value = QLORA_HF_CUSTOM;
const custom = $('sa_qlora_base_custom');
if (custom) custom.value = prevBase;
} else if (models.length) {
sel.value = (models[0].hf_id || models[0].id || '').trim();
}
applyQloraPresetFromSelect({ fillName: !prevBase });
}
async function ensureTrainingQloraConfig(force = false) {
if (!force && state.qloraTraining) {
return state.qloraTraining;
}
try {
const persona = $('sa_persona')?.value || '';
const data = await SA.request('AssistentGetConfig', { persona });
state.qloraTraining = data?.training && typeof data.training === 'object'
? data.training
: { hf_models: [] };
return state.qloraTraining;
} catch {
state.qloraTraining = state.qloraTraining || { hf_models: [] };
return state.qloraTraining;
}
}
async function syncQloraModels() { async function syncQloraModels() {
try { try {
const training = await ensureTrainingQloraConfig();
populateQloraHfPresets(training);
const baseUrl = $('sa_base_url')?.value || localStorage.getItem('swarm_assistent_base_url') || ''; const baseUrl = $('sa_base_url')?.value || localStorage.getItem('swarm_assistent_base_url') || '';
const data = await SA.request('AssistentListModels', { baseUrl }); const data = await SA.request('AssistentListModels', { baseUrl });
const models = data?.models || []; const models = data?.models || [];
@@ -250,6 +408,7 @@ export function attachTraining(SA) {
} }
if (cur) sel.value = cur; if (cur) sel.value = cur;
else if ($('sa_model')?.value) sel.value = $('sa_model').value; else if ($('sa_model')?.value) sel.value = $('sa_model').value;
applyQloraPresetFromSelect({ fillName: false });
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
} }
@@ -363,8 +522,9 @@ export function attachTraining(SA) {
$('sa_train_samples')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); $('sa_train_samples')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (e) { } catch (e) {
const err = String(e.message || e); const err = String(e.message || e);
setTrainStatus(err); const show = isSqliteError(err) ? `${err}${sqliteHint()}` : err;
if (hfSt) hfSt.textContent = err; setTrainStatus(show);
if (hfSt) hfSt.textContent = show;
} finally { } finally {
if (hfSt) hfSt.classList.remove('sa-hf-busy'); if (hfSt) hfSt.classList.remove('sa-hf-busy');
if (btn) { if (btn) {
@@ -477,17 +637,19 @@ export function attachTraining(SA) {
if (status === 'completed' || status === 'completed_with_warnings') { if (status === 'completed' || status === 'completed_with_warnings') {
const ollama = prog?.ollama; const ollama = prog?.ollama;
if (ollama?.success) { if (ollama?.success) {
setTrainStatus(`Готово: модель ${ollama.name} в Ollama`); setTrainStatus(`Готово: модель ${ollama.name} в Ollama — вкладка «Модели»`);
SA.app?.refreshModels?.(); SA.app?.refreshModels?.();
} else if (ollama?.skipped) { } else if (ollama?.skipped) {
setTrainStatus(ollama.note || ollama.error || 'Адаптер сохранён, Ollama — вручную'); setTrainStatus(ollama.note || ollama.error || 'Адаптер сохранён — см. вкладку «Модели»');
} else if (ollama?.error) { } else if (ollama?.error) {
setTrainStatus(`Обучение OK, Ollama: ${ollama.error}`); setTrainStatus(`Обучение OK, Ollama: ${ollama.error}`);
} else if (status === 'completed_with_warnings') { } else if (status === 'completed_with_warnings') {
setTrainStatus('Обучение завершено с предупреждениями — см. лог'); setTrainStatus('Завершено с предупреждениями — лог на «Модели»');
} else { } else {
setTrainStatus('QLoRA завершено'); setTrainStatus('QLoRA завершено — вкладка «Модели»');
} }
setTrainingTab('models');
await refreshTrainModels();
} else if (status === 'failed') { } else if (status === 'failed') {
setTrainStatus(`Ошибка тренировки (exit ${prog?.exit_code ?? '?'})`); setTrainStatus(`Ошибка тренировки (exit ${prog?.exit_code ?? '?'})`);
} }
@@ -509,6 +671,18 @@ export function attachTraining(SA) {
} }
async function startQlora() { async function startQlora() {
const baseModel = getQloraHfBase();
const outputName = ($('sa_qlora_name')?.value || '').trim();
if (!baseModel) {
setTrainStatus('Выберите HF base model из списка или укажите свой HF id');
$('sa_qlora_base')?.focus();
return;
}
if (!outputName) {
setTrainStatus('Укажите имя модели в Ollama (например my-lora:v1)');
$('sa_qlora_name')?.focus();
return;
}
setTrainStatus('Запуск…'); setTrainStatus('Запуск…');
try { try {
const hfDs = ($('sa_qlora_hf_dataset')?.value || '').trim(); const hfDs = ($('sa_qlora_hf_dataset')?.value || '').trim();
@@ -516,9 +690,9 @@ export function attachTraining(SA) {
await SA.request('AssistentStartTrainJob', { await SA.request('AssistentStartTrainJob', {
base_url: $('sa_base_url')?.value, base_url: $('sa_base_url')?.value,
chat_model: $('sa_model')?.value, chat_model: $('sa_model')?.value,
base_model: $('sa_qlora_base')?.value, base_model: baseModel,
ollama_base: $('sa_qlora_ollama_base')?.value, ollama_base: $('sa_qlora_ollama_base')?.value,
output_name: $('sa_qlora_name')?.value, output_name: outputName,
rank: Number($('sa_qlora_rank')?.value) || 16, rank: Number($('sa_qlora_rank')?.value) || 16,
alpha: Number($('sa_qlora_alpha')?.value) || 32, alpha: Number($('sa_qlora_alpha')?.value) || 32,
lr: Number($('sa_qlora_lr')?.value) || 0.0002, lr: Number($('sa_qlora_lr')?.value) || 0.0002,
@@ -556,13 +730,20 @@ export function attachTraining(SA) {
const root = $('sa_train_models_list'); const root = $('sa_train_models_list');
if (!root) return; if (!root) return;
try { try {
const jobData = await SA.request('AssistentGetTrainJob', {});
renderLastTrainJob(jobData?.last_job || jobData?.job);
const data = await SA.request('AssistentListModels', { baseUrl: $('sa_base_url')?.value }); const data = await SA.request('AssistentListModels', { baseUrl: $('sa_base_url')?.value });
const models = data?.models || []; const models = data?.models || [];
const lastOut = jobData?.last_job?.output_name;
root.innerHTML = models.length root.innerHTML = models.length
? models.map((m) => `<div class="sa-hf-row"><strong>${escapeHtml(m)}</strong></div>`).join('') ? models.map((m) => {
: '<div class="sa-mem-empty">Нет моделей</div>'; const hit = lastOut && String(m).includes(String(lastOut).split(':')[0]);
return `<div class="sa-hf-row${hit ? ' sa-train-model-new' : ''}"><strong>${escapeHtml(m)}</strong>${hit ? ' · последняя тренировка' : ''}</div>`;
}).join('')
: '<div class="sa-mem-empty">Нет моделей в Ollama — после QLoRA нажми «Обновить» или проверь лог тренировки</div>';
} catch (e) { } catch (e) {
root.innerHTML = `<div class="sa-mem-empty">${escapeHtml(e.message)}</div>`; const msg = String(e.message || e);
root.innerHTML = `<div class="sa-mem-empty">${escapeHtml(isSqliteError(msg) ? sqliteHint() : msg)}</div>`;
} }
} }
@@ -696,6 +877,7 @@ export function attachTraining(SA) {
r.addEventListener('change', () => setTrainMode(r.value)); r.addEventListener('change', () => setTrainMode(r.value));
}); });
$('sa_btn_modelfile_create')?.addEventListener('click', createModelfile); $('sa_btn_modelfile_create')?.addEventListener('click', createModelfile);
$('sa_qlora_base')?.addEventListener('change', () => applyQloraPresetFromSelect());
$('sa_btn_qlora_start')?.addEventListener('click', startQlora); $('sa_btn_qlora_start')?.addEventListener('click', startQlora);
$('sa_btn_qlora_cancel')?.addEventListener('click', cancelQlora); $('sa_btn_qlora_cancel')?.addEventListener('click', cancelQlora);
$('sa_btn_train_models_refresh')?.addEventListener('click', refreshTrainModels); $('sa_btn_train_models_refresh')?.addEventListener('click', refreshTrainModels);
@@ -710,6 +892,15 @@ export function attachTraining(SA) {
wireTraining(); wireTraining();
setTrainingTab(state.ttab); setTrainingTab(state.ttab);
}, },
onConfig(data) {
if (data?.training && typeof data.training === 'object') {
state.qloraTraining = data.training;
if (state.ttab === 'train') {
populateQloraHfPresets(state.qloraTraining);
applyQloraPresetFromSelect({ fillName: false });
}
}
},
resumePolling: resumeTrainJobPolling, resumePolling: resumeTrainJobPolling,
async curateFromChat(messages, meta) { async curateFromChat(messages, meta) {
try { try {
+37
View File
@@ -193,6 +193,43 @@ describe('session.js', () => {
assert.equal(forced.patch.cfg, 1); assert.equal(forced.patch.cfg, 1);
}); });
it('mergeExactParamsForGenerate preserves pinned session_exact sampler/scheduler when LLM omits them', () => {
const exact = {
generation: { profile: 'turbo', steps: 8, cfg: 1, sampler: 'euler', scheduler: 'normal' },
profiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 } },
};
const { patch, clearSessionKeys } = mergeExactParamsForGenerate(
{ prompt: 'a fox', generate: true },
{
exact,
profiles: exact.profiles,
profileName: 'turbo',
sessionExact: { sampler: 'heun', scheduler: 'simple', aspect: '16:9' },
userParamIntent: false,
},
);
assert.equal(patch.sampler, 'heun');
assert.equal(patch.scheduler, 'simple');
assert.equal(patch.aspect, '16:9');
assert.equal(patch.steps, 8);
assert.deepEqual(clearSessionKeys.sort(), []);
});
it('mergeExactParamsForGenerate does not overwrite explicit LLM sampler/scheduler', () => {
const { patch } = mergeExactParamsForGenerate(
{ generate: true, sampler: 'dpmpp_2m', scheduler: 'karras' },
{
exact: { generation: { steps: 8, cfg: 1 } },
profiles: { turbo: { steps: 8, cfg: 1 } },
profileName: 'turbo',
sessionExact: { sampler: 'heun', scheduler: 'simple' },
userParamIntent: false,
},
);
assert.equal(patch.sampler, 'dpmpp_2m');
assert.equal(patch.scheduler, 'karras');
});
it('resolveExactProfileDefaults picks raw over generation defaults', () => { it('resolveExactProfileDefaults picks raw over generation defaults', () => {
const d = resolveExactProfileDefaults({ const d = resolveExactProfileDefaults({
exact: { generation: { steps: 8, cfg: 1, sigma_shift: 1.15 } }, exact: { generation: { steps: 8, cfg: 1, sigma_shift: 1.15 } },