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:
+638
-94
File diff suppressed because it is too large
Load Diff
+140
-10
@@ -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
@@ -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
@@ -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),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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}" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 user’s 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 user’s 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` (2–4). 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` (2–4). 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`.
|
||||||
|
|||||||
@@ -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 (128–4096 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 (128–4096 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" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 2–3 concrete nitpicks; weak frames get 4–6 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. 2–6 short lines in the user's language.
|
1. 2–6 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`.
|
||||||
|
|||||||
@@ -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` (1–4 typical).
|
- **Batch:** `images` or `batch` (1–4 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.
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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
@@ -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" },
|
||||||
|
|||||||
@@ -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"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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 (0–2)';
|
||||||
|
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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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
@@ -10,6 +10,8 @@ function escapeHtml(s) {
|
|||||||
.replace(/"/g, '"');
|
.replace(/"/g, '"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -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 } },
|
||||||
|
|||||||
Reference in New Issue
Block a user