Ship Assistent 0.15.3: user-owned Generate, SwarmUI aspects, HF import UX.
Chips and quick patches apply params without auto-Generate; aspect sizes match Swarm Side Length 1024. HF import shows drafts after switching filter from approved-only; training status and job polling improved. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+468
-134
File diff suppressed because it is too large
Load Diff
@@ -343,6 +343,10 @@
|
|||||||
min-height: 7.5rem;
|
min-height: 7.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-slot.sa-has-image img {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
.sa-slot img {
|
.sa-slot img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
@@ -455,6 +459,16 @@
|
|||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-composer-params {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
opacity: 0.82;
|
||||||
|
padding: 0.05rem 0 0.2rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
line-height: 1.35;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
color: color-mix(in srgb, currentColor 88%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.sa-more-wrap {
|
.sa-more-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1712,6 +1726,12 @@
|
|||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-btn-gen-primary {
|
||||||
|
font-weight: 650;
|
||||||
|
border-color: color-mix(in srgb, #3fb950 70%, currentColor);
|
||||||
|
background: color-mix(in srgb, #3fb950 20%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.sa-btn-gen:disabled {
|
.sa-btn-gen:disabled {
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -2482,6 +2502,13 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-train-global-status {
|
||||||
|
display: block;
|
||||||
|
margin: 0.25rem 0.55rem 0.4rem;
|
||||||
|
min-height: 1.1em;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
.sa-ttab {
|
.sa-ttab {
|
||||||
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
|
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -2620,6 +2647,12 @@
|
|||||||
min-height: 1.1rem;
|
min-height: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-hf-status.sa-hf-busy {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--emphasis, #8ab4ff);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
.sa-hf-list {
|
.sa-hf-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -506,7 +506,14 @@ public partial class SwarmAssistentExtension
|
|||||||
{
|
{
|
||||||
return new JObject { ["success"] = true, ["imported"] = 0, ["runner_only"] = true, ["id"] = id, ["note"] = "Большой набор — используй HF id в QLoRA-раннере" };
|
return new JObject { ["success"] = true, ["imported"] = 0, ["runner_only"] = true, ["id"] = id, ["note"] = "Большой набор — используй HF id в QLoRA-раннере" };
|
||||||
}
|
}
|
||||||
return new JObject { ["success"] = true, ["imported"] = imported, ["id"] = id };
|
return new JObject
|
||||||
|
{
|
||||||
|
["success"] = true,
|
||||||
|
["imported"] = imported,
|
||||||
|
["id"] = id,
|
||||||
|
["rows_fetched"] = rows.Count,
|
||||||
|
["status"] = "draft",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static JArray ConvertHfRowToMessages(JObject row, JObject schema, JObject mapping)
|
static JArray ConvertHfRowToMessages(JObject row, JObject schema, JObject mapping)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ public partial class SwarmAssistentExtension
|
|||||||
["success"] = true,
|
["success"] = true,
|
||||||
["samples"] = new JArray(list),
|
["samples"] = new JArray(list),
|
||||||
["approved"] = Memory.CountTrainSamples("approved"),
|
["approved"] = Memory.CountTrainSamples("approved"),
|
||||||
|
["draft"] = Memory.CountTrainSamples("draft"),
|
||||||
["total"] = Memory.CountTrainSamples(null),
|
["total"] = Memory.CountTrainSamples(null),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ Several options (still one fence):
|
|||||||
|
|
||||||
### Commands
|
### Commands
|
||||||
|
|
||||||
- `"generate": true` — merge this delta into the chat session and run Generate (new/updated frame). **Omit** for chat, opinions, remember/save, look-only. Legacy `actions:["generate"]` is accepted as the same.
|
- `"generate": true` — you believe this turn is a draw request. The client **does not** start Swarm from this flag alone (small models often set it on chat questions). Set it **only** when the user asked to draw / generate / «нарисуй» / «сгенерируй». They can always click **Сгенерировать**. **Omit** the JSON for chat, opinions, Civitai/knowledge Q&A, remember/save, look-only. Legacy `actions:["generate"]` is the same advisory flag.
|
||||||
- If the user says not to generate / only remember / only answer — **omit** `generate` and do not look.
|
- If the user says not to generate / only remember / only answer — **omit** `generate` and do not look.
|
||||||
- `"look_at": ["generate"|"ref1"|…]` — vision hop (JPEG on follow-up). Use when you need pixels; do not pair with `generate` on the same normal write turn.
|
- `"look_at": ["generate"|"ref1"|…]` — vision hop (JPEG on follow-up). Use when you need pixels; do not pair with `generate` on the same normal write turn.
|
||||||
- `"ask": ["settings"]` — request full settings dump (Exact + all fields).
|
- `"ask": ["settings"]` — request full settings dump (Exact + all fields).
|
||||||
|
|||||||
@@ -21,13 +21,14 @@
|
|||||||
},
|
},
|
||||||
"aspect_table": {
|
"aspect_table": {
|
||||||
"1:1": [1024, 1024],
|
"1:1": [1024, 1024],
|
||||||
"4:3": [1184, 896],
|
"4:3": [1152, 896],
|
||||||
"3:2": [1248, 832],
|
"3:2": [1216, 832],
|
||||||
"16:9": [1376, 768],
|
"16:9": [1344, 768],
|
||||||
"2.35:1": [1568, 672],
|
"21:9": [1536, 640],
|
||||||
|
"2.35:1": [1536, 640],
|
||||||
"4:5": [928, 1152],
|
"4:5": [928, 1152],
|
||||||
"2:3": [832, 1248],
|
"2:3": [832, 1216],
|
||||||
"9:16": [768, 1376]
|
"9:16": [768, 1344]
|
||||||
},
|
},
|
||||||
"facts": {
|
"facts": {
|
||||||
"architecture": "Krea 2 is a 12B DiT architecture. Not FLUX, not SDXL, not FLUX.1-Krea. Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. Use only Krea2-trained LoRAs — never suggest FLUX/SDXL LoRAs.",
|
"architecture": "Krea 2 is a 12B DiT architecture. Not FLUX, not SDXL, not FLUX.1-Krea. Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. Use only Krea2-trained LoRAs — never suggest FLUX/SDXL LoRAs.",
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
"kind": "aspect",
|
"kind": "aspect",
|
||||||
"key": "table",
|
"key": "table",
|
||||||
"tags": ["aspect", "1k"],
|
"tags": ["aspect", "1k"],
|
||||||
"text": "Official Krea 1K aspect → width/height table lives in Exact memory aspect_table. Prefer patch field aspect over raw width/height. UI fills empties from Exact."
|
"text": "Official sizes: SwarmUI Side Length 1024 (ResolutionAspectReferences ×2, round 16). Krea API docs differ on 16:9 — follow Swarm dropdown. Prefer patch field aspect over raw width/height."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -3,12 +3,14 @@
|
|||||||
"Match the user's language (RU or EN) in chat — Generate prompt stays English",
|
"Match the user's language (RU or EN) in chat — Generate prompt stays English",
|
||||||
"Use only inventory / memory_hits / cards for LoRA names and triggers",
|
"Use only inventory / memory_hits / cards for LoRA names and triggers",
|
||||||
"Emit valid JSON patches when changing generation state",
|
"Emit valid JSON patches when changing generation state",
|
||||||
"Greet or dump your bio only on the first assistant turn; later turns answer directly"
|
"Greet or dump your bio only on the first assistant turn; later turns answer directly",
|
||||||
|
"JSON patch + generate:true only when the user asked to draw or change generation this turn; pure Q&A / Civitai / opinions = prose only, no JSON"
|
||||||
],
|
],
|
||||||
"never": [
|
"never": [
|
||||||
"Re-introduce yourself every turn or start with «О, привет» when history already has your replies",
|
"Re-introduce yourself every turn or start with «О, привет» when history already has your replies",
|
||||||
"Invent LoRA filenames or trigger words",
|
"Invent LoRA filenames or trigger words",
|
||||||
"Lecture or moralize about NSFW",
|
"Lecture or moralize about NSFW",
|
||||||
"Dump all installed LoRAs — use memory_hits and enabled ones"
|
"Dump all installed LoRAs — use memory_hits and enabled ones",
|
||||||
|
"Emit generate:true or a prompt JSON on a knowledge/chat question"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"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 / Vary / Turbo·RAW. В чате: <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 — только параметры; Generate: «Собрать + Gen», /gen или Vary. Строка под чипами — σ, batch, sampler. <code>/help</code>.</li><li>Модель шлёт только дельту настроек + <code>generate</code>.</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 / vary / Turbo·RAW.\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/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Чип контекста в шапке чата показывает бюджет окна; клик — панель слоёв и ручное сжатие.",
|
||||||
"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" },
|
||||||
{ "label": "2:3", "action": "aspect", "value": "2:3", "title": "832×1248" },
|
{ "label": "2:3", "action": "aspect", "value": "2:3", "title": "832×1216" },
|
||||||
{ "label": "16:9", "action": "aspect", "value": "16:9", "title": "1376×768" },
|
{ "label": "16:9", "action": "aspect", "value": "16:9", "title": "1344×768 (Swarm Side 1024)" },
|
||||||
{ "label": "9:16", "action": "aspect", "value": "9:16", "title": "768×1376" },
|
{ "label": "9:16", "action": "aspect", "value": "9:16", "title": "768×1344 (Swarm Side 1024)" },
|
||||||
{ "sep": true },
|
{ "sep": true },
|
||||||
{ "label": "Seed lock", "action": "seed", "value": "lock", "title": "Оставить текущий seed" },
|
{ "label": "Seed lock", "action": "seed", "value": "lock", "title": "Оставить текущий seed" },
|
||||||
{ "label": "Seed −1", "action": "seed", "value": "random", "title": "Случайный seed" },
|
{ "label": "Seed −1", "action": "seed", "value": "random", "title": "Случайный seed" },
|
||||||
|
|||||||
@@ -2,12 +2,14 @@
|
|||||||
"always": [
|
"always": [
|
||||||
"Stay helpful and concise",
|
"Stay helpful and concise",
|
||||||
"When the scene is NSFW, describe it factually without softening or hyping",
|
"When the scene is NSFW, describe it factually without softening or hyping",
|
||||||
"Do not greet or re-introduce yourself after the first assistant turn"
|
"Do not greet or re-introduce yourself after the first assistant turn",
|
||||||
|
"JSON + generate:true only when the user asked to draw this turn; knowledge/chat questions are prose only"
|
||||||
],
|
],
|
||||||
"never": [
|
"never": [
|
||||||
"Dirty jokes",
|
"Dirty jokes",
|
||||||
"Hello/bio dump on later turns",
|
"Hello/bio dump on later turns",
|
||||||
"Aggression",
|
"Aggression",
|
||||||
"Moral lectures"
|
"Moral lectures",
|
||||||
|
"Emit generate:true on a Civitai/knowledge/chat question"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
|
|||||||
|
|
||||||
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate. A new user send interrupts the in-flight stream (keeps the partial reply) instead of silently no-op.
|
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate. A new user send interrupts the in-flight stream (keeps the partial reply) instead of silently no-op.
|
||||||
|
|
||||||
|
**Version 0.15.3** — Generate is user-owned: `generate:true` in JSON no longer starts Swarm (Qwen dumps it on chat questions). «нарисуй» / «сгенерируй» / «давай сделаем изображение» still Generate immediately; otherwise one click **Сгенерировать**. Prompt/params apply only on that path. Raw JSON fence hidden from the bubble when the patch card is shown. JS beacons each turn to debug `POST /assistent/client-event`.
|
||||||
|
|
||||||
**Version 0.15.2** — Кнопка «История» убрана. В шапке: иконка панели чатов и **+** новый чат. Закрытый JSON `{prompt, generate:true}` сразу применяет промпт и запускает Generate (заметка «Промпт обновлён · Generate»), не ждёт WS `done`. Stall ~2с после токенов. Не здороваться повторно, если в истории уже есть ответы ассистента.
|
**Version 0.15.2** — Кнопка «История» убрана. В шапке: иконка панели чатов и **+** новый чат. Закрытый JSON `{prompt, generate:true}` сразу применяет промпт и запускает Generate (заметка «Промпт обновлён · Generate»), не ждёт WS `done`. Stall ~2с после токенов. Не здороваться повторно, если в истории уже есть ответы ассистента.
|
||||||
|
|
||||||
**Version 0.15.1** — Composer stays writable during `Writing…`: Enter / Отправить прерывает зависший стрим и шлёт новое сообщение; частичный ответ сохраняется. Stall 15с без токена сам завершает ход. Ollama chat `think: false`, чтобы Qwen3-VL instruct не держал сокет после приветствия.
|
**Version 0.15.1** — Composer stays writable during `Writing…`: Enter / Отправить прерывает зависший стрим и шлёт новое сообщение; частичный ответ сохраняется. Stall 15с без токена сам завершает ход. Ollama chat `think: false`, чтобы Qwen3-VL instruct не держал сокет после приветствия.
|
||||||
|
|||||||
@@ -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.2";
|
Version = "0.15.3";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory", "training", "heard", "qlora"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,6 +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-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>
|
||||||
@@ -167,6 +168,7 @@
|
|||||||
<button type="button" class="sa-ttab" data-ttab="train" role="tab" aria-selected="false">Тренировка</button>
|
<button type="button" class="sa-ttab" data-ttab="train" role="tab" aria-selected="false">Тренировка</button>
|
||||||
<button type="button" class="sa-ttab" data-ttab="models" role="tab" aria-selected="false">Модели</button>
|
<button type="button" class="sa-ttab" data-ttab="models" role="tab" aria-selected="false">Модели</button>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="sa-status sa-train-global-status" id="sa_train_status" role="status"></span>
|
||||||
<div class="sa-training-panes">
|
<div class="sa-training-panes">
|
||||||
<div class="sa-tpane" data-tpane="dataset">
|
<div class="sa-tpane" data-tpane="dataset">
|
||||||
<div class="sa-train-toolbar">
|
<div class="sa-train-toolbar">
|
||||||
@@ -275,7 +277,6 @@
|
|||||||
<div class="sa-train-progress-bar"><div class="sa-train-progress-fill" id="sa_train_progress_fill"></div></div>
|
<div class="sa-train-progress-bar"><div class="sa-train-progress-fill" id="sa_train_progress_fill"></div></div>
|
||||||
<pre class="sa-train-log" id="sa_train_log"></pre>
|
<pre class="sa-train-log" id="sa_train_log"></pre>
|
||||||
</div>
|
</div>
|
||||||
<span class="sa-status" id="sa_train_status"></span>
|
|
||||||
</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">Обученные и созданные модели (Ollama tags).</p>
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
"watch": "node scripts/build.mjs --watch",
|
"watch": "node scripts/build.mjs --watch",
|
||||||
"test": "node --test test/intent.test.js test/patch.test.js test/context.test.js test/kreaProfile.test.js"
|
"test": "node --test test/intent.test.js test/patch.test.js test/context.test.js test/kreaProfile.test.js test/aspect.test.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.25.0"
|
"esbuild": "^0.25.0"
|
||||||
|
|||||||
+241
-99
@@ -2,6 +2,8 @@
|
|||||||
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
|
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
|
||||||
* v0.8.0: Split assets — SA.request (assistent.api.js) and SA.*Patch (assistent.patch.js).
|
* v0.8.0: Split assets — SA.request (assistent.api.js) and SA.*Patch (assistent.patch.js).
|
||||||
*/
|
*/
|
||||||
|
import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable } from './aspect.js';
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
const LS_BASE = 'swarm_assistent_base_url';
|
const LS_BASE = 'swarm_assistent_base_url';
|
||||||
const LS_MODEL = 'swarm_assistent_model';
|
const LS_MODEL = 'swarm_assistent_model';
|
||||||
@@ -35,16 +37,7 @@
|
|||||||
let CHARS_PER_TOKEN = 3.2;
|
let CHARS_PER_TOKEN = 3.2;
|
||||||
let COMPRESS_AUTO = true;
|
let COMPRESS_AUTO = true;
|
||||||
|
|
||||||
let ASPECT_TABLE = {
|
let ASPECT_TABLE = { ...DEFAULT_ASPECT_TABLE };
|
||||||
'1:1': [1024, 1024],
|
|
||||||
'4:3': [1184, 896],
|
|
||||||
'3:2': [1248, 832],
|
|
||||||
'16:9': [1376, 768],
|
|
||||||
'2.35:1': [1568, 672],
|
|
||||||
'4:5': [928, 1152],
|
|
||||||
'2:3': [832, 1248],
|
|
||||||
'9:16': [768, 1376],
|
|
||||||
};
|
|
||||||
|
|
||||||
let PACK_ALIASES = {
|
let PACK_ALIASES = {
|
||||||
ordinary: 'ordinary',
|
ordinary: 'ordinary',
|
||||||
@@ -127,6 +120,8 @@
|
|||||||
lastUserControlIntent: false,
|
lastUserControlIntent: false,
|
||||||
lastPatch: null,
|
lastPatch: null,
|
||||||
pendingSilentGen: false,
|
pendingSilentGen: false,
|
||||||
|
/** This user turn asked to draw — hops inherit it; model generate:true does not. */
|
||||||
|
turnUserWantsGenerate: false,
|
||||||
pendingPromptEnMerge: null,
|
pendingPromptEnMerge: null,
|
||||||
enabledSkills: [],
|
enabledSkills: [],
|
||||||
kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } },
|
kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } },
|
||||||
@@ -698,6 +693,7 @@
|
|||||||
let text = String(raw || '').replace(/\r\n/g, '\n');
|
let text = String(raw || '').replace(/\r\n/g, '\n');
|
||||||
text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, '\n');
|
text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, '\n');
|
||||||
text = text.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, '\n');
|
text = text.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, '\n');
|
||||||
|
text = text.replace(/```(?:json)?\s*[\s\S]*?```/gi, '');
|
||||||
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
text = text.replace(/\n{3,}/g, '\n\n').trim();
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return '';
|
return '';
|
||||||
@@ -782,11 +778,32 @@
|
|||||||
if (!el) {
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el.value = value;
|
const next = value == null ? '' : String(value);
|
||||||
|
if (el.value === next) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.value = next;
|
||||||
|
// SwarmUI may auto-Generate on input/change — suppress during param-only chip edits.
|
||||||
|
if ((state._quietParamApply || 0) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
el.dispatchEvent(new Event('input', { bubbles: true }));
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
el.dispatchEvent(new Event('change', { bubbles: true }));
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const QUICK_PARAM_KEYS = new Set([
|
||||||
|
'aspect', 'width', 'height', 'steps', 'cfg', 'sigma_shift', 'seed',
|
||||||
|
'vary', 'lock_seed', 'sampler', 'scheduler', 'batch', 'images',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function isParamOnlyQuickPatch(patch) {
|
||||||
|
if (!patch || typeof patch !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const keys = Object.keys(patch).filter((k) => patch[k] != null && k !== 'notes');
|
||||||
|
return keys.length > 0 && keys.every((k) => QUICK_PARAM_KEYS.has(k));
|
||||||
|
}
|
||||||
|
|
||||||
function liveNegativePrompt() {
|
function liveNegativePrompt() {
|
||||||
return String(val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '').trim();
|
return String(val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '').trim();
|
||||||
}
|
}
|
||||||
@@ -1162,8 +1179,9 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return cyrTokenRe(
|
return cyrTokenRe(
|
||||||
'сгенер[а-яё]*|нарисуй|нарисуйте|'
|
'сгенер[а-яё]*|нарисуй|нарисуйте|нарисуем|'
|
||||||
+ 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
+ 'запусти\\s+генер[а-яё]*|'
|
||||||
|
+ 'сдела(й|ем|йте)\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
||||||
).test(t);
|
).test(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1178,23 +1196,26 @@
|
|||||||
vetoFn: userAsksNoGenerate,
|
vetoFn: userAsksNoGenerate,
|
||||||
askGenerateFn: userAsksGenerate,
|
askGenerateFn: userAsksGenerate,
|
||||||
fromAutoCritique: !!opts.fromAutoCritique,
|
fromAutoCritique: !!opts.fromAutoCritique,
|
||||||
|
sessionPrompt: opts.sessionPrompt || '',
|
||||||
|
userWantsGenerate: !!opts.userWantsGenerate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// generate:true / actions / user «сгенерируй» when a prompt already exists.
|
// User «нарисуй» / hops — never model generate:true alone.
|
||||||
const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
|
const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText);
|
||||||
const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1
|
const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1
|
||||||
|| (typeof patch.generate === 'string' && /^(true|1|yes|on)$/i.test(patch.generate))
|
|| (typeof patch.generate === 'string' && /^(true|1|yes|on)$/i.test(patch.generate))
|
||||||
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'))));
|
|| (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'))));
|
||||||
|
const hasPrompt = !!(String(patch?.prompt || '').trim() || String(opts.sessionPrompt || '').trim());
|
||||||
const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText)
|
const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText)
|
||||||
&& !!(modelAsked || String(patch?.prompt || '').trim());
|
&& (modelAsked || hasPrompt);
|
||||||
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
|
const generate = !vetoed && !opts.fromAutoCritique && (userAsked || !!opts.userWantsGenerate);
|
||||||
const hasLook = !!patch
|
const hasLook = !!patch
|
||||||
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||||
const look = !!(hasLook && !vetoed && !generate);
|
const look = !!(hasLook && !vetoed && !generate);
|
||||||
const ask = Array.isArray(patch?.ask)
|
const ask = Array.isArray(patch?.ask)
|
||||||
? patch.ask.map(String)
|
? patch.ask.map(String)
|
||||||
: (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []);
|
: (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []);
|
||||||
return { generate, look, vetoed, ask };
|
return { generate, look, vetoed, ask, modelAsked };
|
||||||
}
|
}
|
||||||
function stripGenerateAction(patch) {
|
function stripGenerateAction(patch) {
|
||||||
if (!patch || typeof patch !== 'object') {
|
if (!patch || typeof patch !== 'object') {
|
||||||
@@ -1281,32 +1302,55 @@
|
|||||||
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 syncLiveParamsBar() {
|
function formatLiveParamsLine() {
|
||||||
const el = $('sa_live_params');
|
|
||||||
if (!el) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
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 steps = val('input_steps') || '—';
|
||||||
const cfg = val('input_cfgscale') || val('input_cfg') || '—';
|
const cfg = val('input_cfgscale') || val('input_cfg') || '—';
|
||||||
|
const sigma = val('input_sigmashift') || '';
|
||||||
const seed = val('input_seed') || '—';
|
const seed = val('input_seed') || '—';
|
||||||
const profile = detectKreaProfileName();
|
const profile = detectKreaProfileName();
|
||||||
el.textContent = `${aspect} · ${w || '?'}×${h || '?'} · steps ${steps} · cfg ${cfg} · ${profile} · seed ${seed}`;
|
const batch = val('input_images') || val('input_batchsize') || '';
|
||||||
|
const sampler = val('input_sampler') || '';
|
||||||
|
const scheduler = val('input_scheduler') || '';
|
||||||
|
const parts = [
|
||||||
|
aspect,
|
||||||
|
`${w || '?'}×${h || '?'}`,
|
||||||
|
`steps ${steps}`,
|
||||||
|
`cfg ${cfg}`,
|
||||||
|
];
|
||||||
|
if (sigma) {
|
||||||
|
parts.push(`σ ${sigma}`);
|
||||||
|
}
|
||||||
|
parts.push(profile, `seed ${seed}`);
|
||||||
|
if (batch && batch !== '1') {
|
||||||
|
parts.push(`×${batch}`);
|
||||||
|
}
|
||||||
|
if (sampler) {
|
||||||
|
parts.push(sampler);
|
||||||
|
}
|
||||||
|
if (scheduler) {
|
||||||
|
parts.push(scheduler);
|
||||||
|
}
|
||||||
|
return parts.join(' · ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncLiveParamsBar() {
|
||||||
|
const line = formatLiveParamsLine();
|
||||||
|
const boardEl = $('sa_live_params');
|
||||||
|
const composerEl = $('sa_composer_params');
|
||||||
|
if (boardEl) {
|
||||||
|
boardEl.textContent = line;
|
||||||
|
}
|
||||||
|
if (composerEl) {
|
||||||
|
composerEl.textContent = line;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAspectTableFrom(obj) {
|
function applyAspectTableFrom(obj) {
|
||||||
if (!obj || typeof obj !== 'object') {
|
const next = mergeAspectTable(obj);
|
||||||
return false;
|
if (!next) {
|
||||||
}
|
|
||||||
const next = {};
|
|
||||||
for (const [k, v] of Object.entries(obj)) {
|
|
||||||
if (Array.isArray(v) && v.length >= 2) {
|
|
||||||
next[k] = [Number(v[0]), Number(v[1])];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!Object.keys(next).length) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
ASPECT_TABLE = next;
|
ASPECT_TABLE = next;
|
||||||
@@ -2464,7 +2508,7 @@
|
|||||||
actions.appendChild(toSession);
|
actions.appendChild(toSession);
|
||||||
const genBtn = document.createElement('button');
|
const genBtn = document.createElement('button');
|
||||||
genBtn.type = 'button';
|
genBtn.type = 'button';
|
||||||
genBtn.className = 'basic-button sa-btn-gen';
|
genBtn.className = 'basic-button sa-btn-gen sa-btn-gen-primary';
|
||||||
genBtn.textContent = 'Сгенерировать';
|
genBtn.textContent = 'Сгенерировать';
|
||||||
genBtn.addEventListener('click', async () => {
|
genBtn.addEventListener('click', async () => {
|
||||||
if (isGenerateUnavailable()) {
|
if (isGenerateUnavailable()) {
|
||||||
@@ -2477,6 +2521,9 @@
|
|||||||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
|
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch);
|
||||||
}
|
}
|
||||||
await pushSessionToSwarm(state.chatSession);
|
await pushSessionToSwarm(state.chatSession);
|
||||||
|
if (typeof appendSystemNote === 'function') {
|
||||||
|
appendSystemNote('Запускаю Generate');
|
||||||
|
}
|
||||||
await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true, fromSession: true });
|
await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true, fromSession: true });
|
||||||
});
|
});
|
||||||
actions.appendChild(genBtn);
|
actions.appendChild(genBtn);
|
||||||
@@ -2617,6 +2664,24 @@
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Images available in the gen lightbox (variant grid or single live Generate slot). */
|
||||||
|
function genLightboxList() {
|
||||||
|
const fromResults = (state.genResults || []).filter((r) => r && r.src);
|
||||||
|
if (fromResults.length) {
|
||||||
|
return fromResults;
|
||||||
|
}
|
||||||
|
const slot = generateSlot();
|
||||||
|
if (slot?.src) {
|
||||||
|
return [{
|
||||||
|
id: slot.id || GEN_ID,
|
||||||
|
label: slot.label || 'Generate',
|
||||||
|
src: slot.src,
|
||||||
|
patch: state.lastPatch || null,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
function ensureGenLightbox() {
|
function ensureGenLightbox() {
|
||||||
let root = $('sa_gen_lightbox');
|
let root = $('sa_gen_lightbox');
|
||||||
if (root) {
|
if (root) {
|
||||||
@@ -2682,7 +2747,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function currentLightboxRow() {
|
function currentLightboxRow() {
|
||||||
const list = (state.genResults || []).filter((r) => r.src);
|
const list = genLightboxList();
|
||||||
if (!list.length || state.lightboxIndex < 0) {
|
if (!list.length || state.lightboxIndex < 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -2691,7 +2756,7 @@
|
|||||||
|
|
||||||
function syncGenLightbox() {
|
function syncGenLightbox() {
|
||||||
const root = ensureGenLightbox();
|
const root = ensureGenLightbox();
|
||||||
const list = (state.genResults || []).filter((r) => r.src);
|
const list = genLightboxList();
|
||||||
const row = list[state.lightboxIndex];
|
const row = list[state.lightboxIndex];
|
||||||
if (!row) {
|
if (!row) {
|
||||||
root.hidden = true;
|
root.hidden = true;
|
||||||
@@ -2714,7 +2779,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openGenLightbox(id) {
|
function openGenLightbox(id) {
|
||||||
const list = (state.genResults || []).filter((r) => r.src);
|
const list = genLightboxList();
|
||||||
let idx = list.findIndex((r) => r.id === id);
|
let idx = list.findIndex((r) => r.id === id);
|
||||||
if (idx < 0) {
|
if (idx < 0) {
|
||||||
idx = 0;
|
idx = 0;
|
||||||
@@ -2737,7 +2802,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stepGenLightbox(delta) {
|
function stepGenLightbox(delta) {
|
||||||
const list = (state.genResults || []).filter((r) => r.src);
|
const list = genLightboxList();
|
||||||
if (list.length < 2) {
|
if (list.length < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2864,10 +2929,22 @@
|
|||||||
busy.innerHTML = '<span class="sa-spinner" aria-hidden="true"></span>';
|
busy.innerHTML = '<span class="sa-spinner" aria-hidden="true"></span>';
|
||||||
el.appendChild(busy);
|
el.appendChild(busy);
|
||||||
|
|
||||||
el.addEventListener('click', () => {
|
el.addEventListener('click', (e) => {
|
||||||
|
if (slot.type === 'generate' && slot.src && e.target?.tagName === 'IMG') {
|
||||||
|
openGenLightbox(slot.id || GEN_ID);
|
||||||
|
return;
|
||||||
|
}
|
||||||
state.selectedSlotId = slot.id;
|
state.selectedSlotId = slot.id;
|
||||||
renderBoard();
|
renderBoard();
|
||||||
});
|
});
|
||||||
|
if (slot.type === 'generate') {
|
||||||
|
el.addEventListener('dblclick', (e) => {
|
||||||
|
if (slot.src && e.target?.tagName === 'IMG') {
|
||||||
|
e.preventDefault();
|
||||||
|
openGenLightbox(slot.id || GEN_ID);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
el.addEventListener('dragover', (e) => {
|
el.addEventListener('dragover', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -4218,6 +4295,14 @@
|
|||||||
return { prose: text || '', patch: null };
|
return { prose: text || '', patch: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function visibleAssistantProse(text) {
|
||||||
|
if (window.SA && typeof SA.visibleProse === 'function') {
|
||||||
|
return SA.visibleProse(text);
|
||||||
|
}
|
||||||
|
const { prose, patch } = extractPatch(text);
|
||||||
|
return patch ? (prose || '') : String(text || '');
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeAspect(raw) {
|
function normalizeAspect(raw) {
|
||||||
if (raw == null) {
|
if (raw == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -4926,7 +5011,7 @@
|
|||||||
}
|
}
|
||||||
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
|
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
|
||||||
const pack = $('sa_pack')?.value || defaultPackId();
|
const pack = $('sa_pack')?.value || defaultPackId();
|
||||||
const prose = (typeof extractPatch === 'function' ? (extractPatch(text).prose || text) : text);
|
const prose = visibleAssistantProse(text);
|
||||||
if (state.streamEl) {
|
if (state.streamEl) {
|
||||||
finalizeStreamMessage(text, []);
|
finalizeStreamMessage(text, []);
|
||||||
} else {
|
} else {
|
||||||
@@ -5500,7 +5585,7 @@
|
|||||||
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
|
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
|
||||||
const finalPatch = patch || extracted;
|
const finalPatch = patch || extracted;
|
||||||
if (role === 'assistant') {
|
if (role === 'assistant') {
|
||||||
setAssistantBody(div, prose || text || '');
|
setAssistantBody(div, finalPatch ? (prose || '') : (prose || text || ''));
|
||||||
} else {
|
} else {
|
||||||
div.textContent = prose || text || '';
|
div.textContent = prose || text || '';
|
||||||
}
|
}
|
||||||
@@ -5509,7 +5594,7 @@
|
|||||||
const silent = !!(meta && meta.silentPatch);
|
const silent = !!(meta && meta.silentPatch);
|
||||||
mountPatchBlock(div, finalPatch, { silent });
|
mountPatchBlock(div, finalPatch, { silent });
|
||||||
}
|
}
|
||||||
if (role === 'assistant' && !(meta && meta.historical)) {
|
if (role === 'assistant' && !(meta && meta.historical)) {
|
||||||
mountCurateButtons(div, meta);
|
mountCurateButtons(div, meta);
|
||||||
}
|
}
|
||||||
box.appendChild(div);
|
box.appendChild(div);
|
||||||
@@ -5611,7 +5696,7 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
if (streamHasClosedPatchFence(state.streamText)) {
|
if (streamHasClosedPatchFence(state.streamText)) {
|
||||||
state.streamText = trimToClosedPatchFence(state.streamText);
|
state.streamText = trimToClosedPatchFence(state.streamText);
|
||||||
state.streamFenceDone = true;
|
state.streamFenceDone = true;
|
||||||
setAssistantBody(state.streamEl, state.streamText, { live: true });
|
setAssistantBody(state.streamEl, visibleAssistantProse(state.streamText), { live: true });
|
||||||
scrollMessagesToBottom();
|
scrollMessagesToBottom();
|
||||||
const fn = state.onClosedTerminalFence;
|
const fn = state.onClosedTerminalFence;
|
||||||
state.onClosedTerminalFence = null;
|
state.onClosedTerminalFence = null;
|
||||||
@@ -5646,13 +5731,13 @@ if (role === 'assistant' && !(meta && meta.historical)) {
|
|||||||
el.classList.remove('sa-streaming', 'sa-typing');
|
el.classList.remove('sa-streaming', 'sa-typing');
|
||||||
mountAssistantMeta(el, meta || undefined);
|
mountAssistantMeta(el, meta || undefined);
|
||||||
const { prose, patch } = extractPatch(fullReply);
|
const { prose, patch } = extractPatch(fullReply);
|
||||||
setAssistantBody(el, prose || fullReply || '');
|
setAssistantBody(el, patch ? (prose || '') : (prose || fullReply || ''));
|
||||||
el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove());
|
el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove());
|
||||||
if (patch) {
|
if (patch) {
|
||||||
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
|
const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen;
|
||||||
mountPatchBlock(el, patch, { silent });
|
mountPatchBlock(el, patch, { silent });
|
||||||
}
|
}
|
||||||
if (!(meta && meta.historical)) {
|
if (!(meta && meta.historical)) {
|
||||||
mountCurateButtons(el, meta);
|
mountCurateButtons(el, meta);
|
||||||
}
|
}
|
||||||
scrollMessagesToBottom();
|
scrollMessagesToBottom();
|
||||||
@@ -7812,8 +7897,32 @@ if (!(meta && meta.historical)) {
|
|||||||
if (effective && act && typeof act.noteModelCommands === 'function') {
|
if (effective && act && typeof act.noteModelCommands === 'function') {
|
||||||
act.noteModelCommands(effective);
|
act.noteModelCommands(effective);
|
||||||
}
|
}
|
||||||
|
const sessionPrompt = String(state.chatSession?.gen?.prompt || '').trim();
|
||||||
|
const userWantsGenerate = !!opts.userWantsGenerate || !!state.turnUserWantsGenerate;
|
||||||
|
let intent = resolveTurnIntent(effective, opts.userText || '', {
|
||||||
|
...opts,
|
||||||
|
sessionPrompt,
|
||||||
|
userWantsGenerate,
|
||||||
|
});
|
||||||
|
if (userWantsGenerate && !intent.vetoed && !fromAutoCritique) {
|
||||||
|
intent = { ...intent, generate: true };
|
||||||
|
}
|
||||||
|
if (!effective && intent.generate && sessionPrompt) {
|
||||||
|
effective = { generate: true };
|
||||||
|
}
|
||||||
const promptChanged = !!(effective && String(effective.prompt || '').trim());
|
const promptChanged = !!(effective && String(effective.prompt || '').trim());
|
||||||
if (effective && S) {
|
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
|
||||||
|
if (typeof doInterruptNow === 'function') doInterruptNow();
|
||||||
|
}
|
||||||
|
if (effective) {
|
||||||
|
if (intent.generate) {
|
||||||
|
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
|
||||||
|
effective = { ...effective, actions: acts.includes('generate') ? acts : acts.concat('generate'), generate: true };
|
||||||
|
}
|
||||||
|
if (!intent.look && typeof stripLookAt === 'function') effective = stripLookAt(effective);
|
||||||
|
if (typeof rememberLastPatch === 'function') rememberLastPatch(effective);
|
||||||
|
}
|
||||||
|
if (intent.generate && effective && S) {
|
||||||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
||||||
activityDone('delta', {
|
activityDone('delta', {
|
||||||
kind: 'delta',
|
kind: 'delta',
|
||||||
@@ -7828,28 +7937,6 @@ if (!(meta && meta.historical)) {
|
|||||||
if (typeof persistChatsStore === 'function') persistChatsStore();
|
if (typeof persistChatsStore === 'function') persistChatsStore();
|
||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
if (typeof rememberLastPatch === 'function') rememberLastPatch(effective);
|
|
||||||
}
|
|
||||||
if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) {
|
|
||||||
if (typeof doInterruptNow === 'function') doInterruptNow();
|
|
||||||
}
|
|
||||||
let intent = resolveTurnIntent(effective, opts.userText || '', opts);
|
|
||||||
if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) {
|
|
||||||
intent = { ...intent, generate: true };
|
|
||||||
}
|
|
||||||
if (effective) {
|
|
||||||
if (intent.generate) {
|
|
||||||
const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : [];
|
|
||||||
effective = { ...effective, actions: acts.includes('generate') ? acts : acts.concat('generate'), generate: true };
|
|
||||||
} else if (typeof stripGenerateAction === 'function') {
|
|
||||||
effective = stripGenerateAction(effective);
|
|
||||||
if (effective && effective.generate) {
|
|
||||||
effective = { ...effective };
|
|
||||||
delete effective.generate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!intent.look && typeof stripLookAt === 'function') effective = stripLookAt(effective);
|
|
||||||
if (typeof rememberLastPatch === 'function') rememberLastPatch(effective);
|
|
||||||
}
|
}
|
||||||
if (intent.vetoed) {
|
if (intent.vetoed) {
|
||||||
state.pendingSilentGen = false;
|
state.pendingSilentGen = false;
|
||||||
@@ -7962,44 +8049,82 @@ if (!(meta && meta.historical)) {
|
|||||||
if (typeof maybeAutoVisionLook === 'function') await maybeAutoVisionLook(srcOut);
|
if (typeof maybeAutoVisionLook === 'function') await maybeAutoVisionLook(srcOut);
|
||||||
}
|
}
|
||||||
} else if (effective) {
|
} else if (effective) {
|
||||||
if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective);
|
|
||||||
if (promptChanged || $('sa_auto_apply')?.checked) {
|
|
||||||
await pushSessionToSwarm(state.chatSession);
|
|
||||||
if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar();
|
|
||||||
if (promptChanged && typeof appendSystemNote === 'function') {
|
|
||||||
appendSystemNote('Промпт обновлён');
|
|
||||||
}
|
|
||||||
if (promptChanged) {
|
|
||||||
setStatus('Промпт обновлён');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!state.generating && typeof stopBusyUi === 'function') {
|
if (!state.generating && typeof stopBusyUi === 'function') {
|
||||||
stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : (promptChanged ? 'Промпт обновлён' : ''));
|
stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.pendingSilentGen = false;
|
state.pendingSilentGen = false;
|
||||||
|
reportDebugClientTurn({
|
||||||
|
user: opts.userText || '',
|
||||||
|
reply: String(reply || '').slice(0, 8000),
|
||||||
|
patch: effective,
|
||||||
|
intent,
|
||||||
|
generating: !!intent.generate,
|
||||||
|
swarm_prompt: val('alt_prompt_textbox') || val('input_prompt') || '',
|
||||||
|
persona: $('sa_persona')?.value || '',
|
||||||
|
pack: $('sa_pack')?.value || '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
async function applyQuickPatch(patch, note) {
|
|
||||||
|
function reportDebugClientTurn(payload) {
|
||||||
|
try {
|
||||||
|
const body = JSON.stringify({
|
||||||
|
ts: Date.now(),
|
||||||
|
...payload,
|
||||||
|
});
|
||||||
|
fetch('http://127.0.0.1:17821/assistent/client-event', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body,
|
||||||
|
mode: 'cors',
|
||||||
|
}).catch(() => {});
|
||||||
|
} catch (e) { /* sidecar optional */ }
|
||||||
|
}
|
||||||
|
async function applyQuickPatch(patch, note, opts = {}) {
|
||||||
|
const wantGenerate = opts.generate === true;
|
||||||
let withActions = { ...patch };
|
let withActions = { ...patch };
|
||||||
if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) {
|
delete withActions.generate;
|
||||||
withActions.actions = ['generate'];
|
if (Array.isArray(withActions.actions)) {
|
||||||
|
withActions.actions = withActions.actions.filter((a) => String(a) !== 'generate');
|
||||||
|
if (!withActions.actions.length) {
|
||||||
|
delete withActions.actions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (wantGenerate) {
|
||||||
|
withActions.generate = true;
|
||||||
|
withActions.actions = [
|
||||||
|
...(Array.isArray(withActions.actions) ? withActions.actions : []),
|
||||||
|
'generate',
|
||||||
|
];
|
||||||
}
|
}
|
||||||
const prevIntent = state.lastUserParamIntent;
|
const prevIntent = state.lastUserParamIntent;
|
||||||
state.lastUserParamIntent = true;
|
state.lastUserParamIntent = true;
|
||||||
const S = window.SA && window.SA.session;
|
const S = window.SA && window.SA.session;
|
||||||
if (S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) {
|
if (wantGenerate && S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) {
|
||||||
withActions = ensureExactParamsForGenerate(withActions);
|
withActions = ensureExactParamsForGenerate(withActions);
|
||||||
}
|
}
|
||||||
if (S) {
|
if (S) {
|
||||||
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
|
state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions);
|
||||||
}
|
}
|
||||||
await pushSessionToSwarm(state.chatSession);
|
state._quietParamApply = (state._quietParamApply || 0) + 1;
|
||||||
|
try {
|
||||||
|
if (wantGenerate) {
|
||||||
|
await pushSessionToSwarm(state.chatSession);
|
||||||
|
} else if (isParamOnlyQuickPatch(withActions)) {
|
||||||
|
await applyPatch(withActions, 'params');
|
||||||
|
} else {
|
||||||
|
await pushSessionToSwarm(state.chatSession);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
state._quietParamApply = Math.max(0, (state._quietParamApply || 1) - 1);
|
||||||
|
}
|
||||||
state.lastUserParamIntent = prevIntent;
|
state.lastUserParamIntent = prevIntent;
|
||||||
setStatus(note || 'Applied');
|
setStatus(note || (wantGenerate ? 'Applied' : 'Параметры (без Generate)'));
|
||||||
if (patchHasGenTrigger(withActions)) {
|
if (wantGenerate) {
|
||||||
await runGenerateFromPatch(withActions, { force: true, fromSession: true });
|
await runGenerateFromPatch(withActions, { force: true, fromSession: true });
|
||||||
}
|
}
|
||||||
syncChipHighlight();
|
syncChipHighlight();
|
||||||
|
syncLiveParamsBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function syncChipHighlight() {
|
function syncChipHighlight() {
|
||||||
@@ -8275,7 +8400,7 @@ if (!(meta && meta.historical)) {
|
|||||||
setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(', ')}`);
|
setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(', ')}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
await applyQuickPatch({ aspect: key, actions: ['generate'] }, `Aspect ${key}`);
|
await applyQuickPatch({ aspect: key }, `Aspect ${key}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === 'seed') {
|
if (cmd === 'seed') {
|
||||||
@@ -8283,12 +8408,12 @@ if (!(meta && meta.historical)) {
|
|||||||
if (mode === 'lock' || mode === 'keep') {
|
if (mode === 'lock' || mode === 'keep') {
|
||||||
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
|
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
|
||||||
} else {
|
} else {
|
||||||
await applyQuickPatch({ seed: -1, vary: true, actions: ['generate'] }, 'Seed random');
|
await applyQuickPatch({ seed: -1, vary: true }, 'Seed random');
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === 'vary') {
|
if (cmd === 'vary') {
|
||||||
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)');
|
await applyQuickPatch({ vary: true, seed: -1 }, 'Vary (new seed)', { generate: true });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (cmd === 'inventory' || cmd === 'inv') {
|
if (cmd === 'inventory' || cmd === 'inv') {
|
||||||
@@ -8411,6 +8536,7 @@ if (!(meta && meta.historical)) {
|
|||||||
state.lastUserParamIntent = userTextMentionsParams(text);
|
state.lastUserParamIntent = userTextMentionsParams(text);
|
||||||
state.lastUserControlIntent = userTextMentionsControls(text);
|
state.lastUserControlIntent = userTextMentionsControls(text);
|
||||||
state.pendingSilentGen = false; // 0.14
|
state.pendingSilentGen = false; // 0.14
|
||||||
|
state.turnUserWantsGenerate = userAsksGenerate(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isMachineTurn(opts) && !opts.skipSlash) {
|
if (!isMachineTurn(opts) && !opts.skipSlash) {
|
||||||
@@ -8444,7 +8570,7 @@ if (!(meta && meta.historical)) {
|
|||||||
patch.loras = state.lastPatch.loras;
|
patch.loras = state.lastPatch.loras;
|
||||||
}
|
}
|
||||||
appendSystemNote(`Ставлю ${aspect} и Generate (тот же промпт) — без повторной критики.`);
|
appendSystemNote(`Ставлю ${aspect} и Generate (тот же промпт) — без повторной критики.`);
|
||||||
await applyQuickPatch(patch, `Aspect ${aspect}`);
|
await applyQuickPatch(patch, `Aspect ${aspect}`, { generate: true });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8657,7 +8783,7 @@ if (!(meta && meta.historical)) {
|
|||||||
state.lastContextChars = 0;
|
state.lastContextChars = 0;
|
||||||
}
|
}
|
||||||
updateCtxChip();
|
updateCtxChip();
|
||||||
const prose = extractPatch(reply).prose || reply;
|
const prose = visibleAssistantProse(reply);
|
||||||
state.history.push({ role: 'assistant', content: prose, persona, pack });
|
state.history.push({ role: 'assistant', content: prose, persona, pack });
|
||||||
persistHistory();
|
persistHistory();
|
||||||
setBusyPhase(state.pendingSilentGen ? 'silent_gen' : 'thinking');
|
setBusyPhase(state.pendingSilentGen ? 'silent_gen' : 'thinking');
|
||||||
@@ -8666,6 +8792,7 @@ if (!(meta && meta.historical)) {
|
|||||||
...opts,
|
...opts,
|
||||||
userText: text,
|
userText: text,
|
||||||
userWantsGenerate: !!opts.userWantsGenerate
|
userWantsGenerate: !!opts.userWantsGenerate
|
||||||
|
|| !!state.turnUserWantsGenerate
|
||||||
|| (!isMachineTurn(opts) && state.pendingSilentGen),
|
|| (!isMachineTurn(opts) && state.pendingSilentGen),
|
||||||
attachedSlotIds: visionSlots.map((s) => s.id),
|
attachedSlotIds: visionSlots.map((s) => s.id),
|
||||||
});
|
});
|
||||||
@@ -8866,8 +8993,14 @@ if (!(meta && meta.historical)) {
|
|||||||
closeGenLightbox();
|
closeGenLightbox();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ((e.key === 'Enter' || e.key === ' ') && state.boardTab === 'generate' && state.selectedGenResultId) {
|
if ((e.key === 'Enter' || e.key === ' ') && state.boardTab === 'generate') {
|
||||||
const row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId);
|
let row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId);
|
||||||
|
if (!row?.src) {
|
||||||
|
const slot = generateSlot();
|
||||||
|
if (slot?.src && (slot.id === state.selectedSlotId || !state.selectedGenResultId)) {
|
||||||
|
row = { id: slot.id || GEN_ID, src: slot.src };
|
||||||
|
}
|
||||||
|
}
|
||||||
if (row?.src) {
|
if (row?.src) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
openGenLightbox(row.id);
|
openGenLightbox(row.id);
|
||||||
@@ -9031,6 +9164,7 @@ if (!(meta && meta.historical)) {
|
|||||||
window.__swarmAssistentWired = true;
|
window.__swarmAssistentWired = true;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
setView(state.view || 'chat');
|
setView(state.view || 'chat');
|
||||||
|
void window.SA?.training?.resumePolling?.();
|
||||||
updateGate();
|
updateGate();
|
||||||
ensureBoard();
|
ensureBoard();
|
||||||
setBoardTab(state.boardTab || 'generate', { persist: false });
|
setBoardTab(state.boardTab || 'generate', { persist: false });
|
||||||
@@ -9357,19 +9491,27 @@ if (!(meta && meta.historical)) {
|
|||||||
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');
|
||||||
if (aspect) {
|
if (aspect) {
|
||||||
await applyQuickPatch({ aspect, actions: ['generate'] }, `Aspect ${aspect}`);
|
await applyQuickPatch({ aspect }, `Aspect ${aspect}`);
|
||||||
} else if (seed === 'lock') {
|
} else if (seed === 'lock') {
|
||||||
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
|
await applyQuickPatch({ lock_seed: true }, 'Seed locked');
|
||||||
} else if (seed === 'random') {
|
} else if (seed === 'random') {
|
||||||
await applyQuickPatch({ seed: -1, actions: ['generate'] }, 'Seed random');
|
await applyQuickPatch({ seed: -1 }, 'Seed random');
|
||||||
} else if (vary) {
|
} else if (vary) {
|
||||||
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary');
|
await applyQuickPatch({ vary: true, seed: -1 }, 'Vary', { generate: true });
|
||||||
} else if (profile === 'turbo') {
|
} else if (profile === 'turbo') {
|
||||||
const p = state.kreaProfiles?.turbo || mergedGenerationDefaults('turbo');
|
const p = state.kreaProfiles?.turbo || mergedGenerationDefaults('turbo');
|
||||||
await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ['generate'] }, 'Turbo');
|
await applyQuickPatch({
|
||||||
|
steps: p.steps ?? 8,
|
||||||
|
cfg: p.cfg ?? 1,
|
||||||
|
sigma_shift: p.sigma_shift ?? 1.15,
|
||||||
|
}, 'Turbo');
|
||||||
} else if (profile === 'raw') {
|
} else if (profile === 'raw') {
|
||||||
const p = state.kreaProfiles?.raw || mergedGenerationDefaults('raw');
|
const p = state.kreaProfiles?.raw || mergedGenerationDefaults('raw');
|
||||||
await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ['generate'] }, 'RAW');
|
await applyQuickPatch({
|
||||||
|
steps: p.steps ?? 28,
|
||||||
|
cfg: p.cfg ?? 4.5,
|
||||||
|
sigma_shift: p.sigma_shift,
|
||||||
|
}, 'RAW');
|
||||||
}
|
}
|
||||||
renderLoraChips();
|
renderLoraChips();
|
||||||
});
|
});
|
||||||
|
|||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Aspect → pixel sizes aligned with SwarmUI (Side Length 1024).
|
||||||
|
* Ref sheet @ 512px from SwarmUI T2IParamInput.ResolutionAspectReferences;
|
||||||
|
* width/height = round(ref * (sideLen / 512), 16).
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const SWARM_ASPECT_REF_512 = {
|
||||||
|
'1:1': [512, 512],
|
||||||
|
'4:3': [576, 448],
|
||||||
|
'3:2': [608, 416],
|
||||||
|
'8:5': [608, 384],
|
||||||
|
'16:9': [672, 384],
|
||||||
|
'21:9': [768, 320],
|
||||||
|
'2:3': [416, 608],
|
||||||
|
'5:8': [384, 608],
|
||||||
|
'9:16': [384, 672],
|
||||||
|
'9:21': [320, 768],
|
||||||
|
};
|
||||||
|
|
||||||
|
const ASPECT_ALIASES = {
|
||||||
|
'2.35:1': '21:9',
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Krea 1K bucket; not in Swarm aspect dropdown (we keep 4:5 chip). */
|
||||||
|
const KREA_EXTRA = {
|
||||||
|
'4:5': [928, 1152],
|
||||||
|
};
|
||||||
|
|
||||||
|
function roundTo16(n) {
|
||||||
|
return Math.round(n / 16) * 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swarmSizeFromRef(aspectKey, sideLen = 1024) {
|
||||||
|
const key = ASPECT_ALIASES[aspectKey] || aspectKey;
|
||||||
|
if (KREA_EXTRA[key]) {
|
||||||
|
return [...KREA_EXTRA[key]];
|
||||||
|
}
|
||||||
|
const ref = SWARM_ASPECT_REF_512[key];
|
||||||
|
if (!ref) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const scale = sideLen / 512;
|
||||||
|
return [roundTo16(ref[0] * scale), roundTo16(ref[1] * scale)];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildDefaultAspectTable(sideLen = 1024) {
|
||||||
|
const table = {};
|
||||||
|
for (const key of Object.keys(SWARM_ASPECT_REF_512)) {
|
||||||
|
table[key] = swarmSizeFromRef(key, sideLen);
|
||||||
|
}
|
||||||
|
table['2.35:1'] = swarmSizeFromRef('21:9', sideLen);
|
||||||
|
for (const [key, size] of Object.entries(KREA_EXTRA)) {
|
||||||
|
table[key] = [...size];
|
||||||
|
}
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_ASPECT_TABLE = buildDefaultAspectTable(1024);
|
||||||
|
|
||||||
|
export function normalizeAspectKey(raw, table = DEFAULT_ASPECT_TABLE) {
|
||||||
|
let s = String(raw ?? '').trim().toLowerCase().replace(/\s+/g, '');
|
||||||
|
if (!s) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (s === 'square') {
|
||||||
|
s = '1:1';
|
||||||
|
} else if (s === 'portrait' || s === 'vert') {
|
||||||
|
s = '2:3';
|
||||||
|
} else if (s === 'landscape' || s === 'horiz') {
|
||||||
|
s = '16:9';
|
||||||
|
} else if (s === 'cinematic' || s === 'ultrawide') {
|
||||||
|
s = '2.35:1';
|
||||||
|
}
|
||||||
|
return table[s] ? s : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function guessAspectFromDimensions(w, h, table = DEFAULT_ASPECT_TABLE) {
|
||||||
|
const width = parseInt(w, 10);
|
||||||
|
const height = parseInt(h, 10);
|
||||||
|
if (!width || !height) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let best = null;
|
||||||
|
let bestDist = Infinity;
|
||||||
|
for (const [key, [aw, ah]] of Object.entries(table)) {
|
||||||
|
const dist = Math.abs(width / height - aw / ah)
|
||||||
|
+ Math.abs(width - aw) / 4000
|
||||||
|
+ Math.abs(height - ah) / 4000;
|
||||||
|
if (dist < bestDist) {
|
||||||
|
bestDist = dist;
|
||||||
|
best = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return bestDist < 0.12 ? best : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAspectTableFromObject(obj) {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const next = {};
|
||||||
|
for (const [k, v] of Object.entries(obj)) {
|
||||||
|
if (Array.isArray(v) && v.length >= 2) {
|
||||||
|
next[k] = [Number(v[0]), Number(v[1])];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Object.keys(next).length ? next : false;
|
||||||
|
}
|
||||||
+12
-7
@@ -1,4 +1,4 @@
|
|||||||
/** Turn intent — veto + model generate; user «сгенерируй» also counts when a prompt/delta exists. */
|
/** Turn intent — user-owned Generate; model generate:true is advisory only. */
|
||||||
|
|
||||||
export function cyrTokenRe(alts) {
|
export function cyrTokenRe(alts) {
|
||||||
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||||
@@ -32,8 +32,9 @@ export function userAsksGenerate(text) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return cyrTokenRe(
|
return cyrTokenRe(
|
||||||
'сгенер[а-яё]*|нарисуй|нарисуйте|'
|
'сгенер[а-яё]*|нарисуй|нарисуйте|нарисуем|'
|
||||||
+ 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
+ 'запусти\\s+генер[а-яё]*|'
|
||||||
|
+ 'сдела(й|ем|йте)\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)',
|
||||||
).test(t);
|
).test(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,18 +85,22 @@ function generateFlagOn(patch) {
|
|||||||
return Array.isArray(patch.actions) && patch.actions.map(String).includes('generate');
|
return Array.isArray(patch.actions) && patch.actions.map(String).includes('generate');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Model generate flag, or user «сгенерируй» when a prompt/delta already exists. */
|
/**
|
||||||
|
* Generate is user-owned. Model `generate:true` is advisory (Qwen dumps it on Q&A).
|
||||||
|
* User «нарисуй»/«сгенерируй» (or opts.userWantsGenerate from hops/buttons) starts Generate.
|
||||||
|
*/
|
||||||
export function resolveTurnIntent(patch, userText, opts = {}) {
|
export function resolveTurnIntent(patch, userText, opts = {}) {
|
||||||
const vetoed = !opts.machineTurn && userAsksNoGenerate(userText);
|
const vetoed = !opts.machineTurn && userAsksNoGenerate(userText);
|
||||||
const modelAsked = generateFlagOn(patch);
|
const modelAsked = generateFlagOn(patch);
|
||||||
|
const hasPrompt = !!(String(patch?.prompt || '').trim() || String(opts.sessionPrompt || '').trim());
|
||||||
const userAsked = !opts.machineTurn && userAsksGenerate(userText)
|
const userAsked = !opts.machineTurn && userAsksGenerate(userText)
|
||||||
&& !!(modelAsked || String(patch?.prompt || '').trim());
|
&& (modelAsked || hasPrompt);
|
||||||
const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked);
|
const generate = !vetoed && !opts.fromAutoCritique && (userAsked || !!opts.userWantsGenerate);
|
||||||
const hasLook = !!patch
|
const hasLook = !!patch
|
||||||
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||||
const look = !!(hasLook && !vetoed && !generate);
|
const look = !!(hasLook && !vetoed && !generate);
|
||||||
const ask = Array.isArray(patch?.ask)
|
const ask = Array.isArray(patch?.ask)
|
||||||
? patch.ask.map(String)
|
? patch.ask.map(String)
|
||||||
: (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []);
|
: (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []);
|
||||||
return { generate, look, vetoed, ask };
|
return { generate, look, vetoed, ask, modelAsked };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { userAsksGenerate, userAsksNoGenerate, resolveTurnIntent } from './intent.js';
|
||||||
import { attachApi } from './api.js';
|
import { attachApi } from './api.js';
|
||||||
import { attachPatch, setPatchKeys } from './patch.js';
|
import { attachPatch, setPatchKeys } from './patch.js';
|
||||||
import { attachPersist } from './persist.js';
|
import { attachPersist } from './persist.js';
|
||||||
@@ -7,6 +8,9 @@ import { attachActivity } from './activity.js';
|
|||||||
import { attachKreaProfile } from './kreaProfile.js';
|
import { attachKreaProfile } from './kreaProfile.js';
|
||||||
|
|
||||||
window.SA = window.SA || {};
|
window.SA = window.SA || {};
|
||||||
|
window.SA.userAsksGenerate = userAsksGenerate;
|
||||||
|
window.SA.userAsksNoGenerate = userAsksNoGenerate;
|
||||||
|
window.SA.resolveTurnIntent = resolveTurnIntent;
|
||||||
attachApi(window.SA);
|
attachApi(window.SA);
|
||||||
attachPatch(window.SA);
|
attachPatch(window.SA);
|
||||||
attachPersist(window.SA);
|
attachPersist(window.SA);
|
||||||
|
|||||||
@@ -131,6 +131,15 @@ export function extractPatch(text) {
|
|||||||
return { prose: text, patch: null };
|
return { prose: text, patch: null };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Chat body text: never fall back to the raw fence when a patch was extracted. */
|
||||||
|
export function visibleProse(text) {
|
||||||
|
const { prose, patch } = extractPatch(text);
|
||||||
|
if (patch) {
|
||||||
|
return prose || '';
|
||||||
|
}
|
||||||
|
return String(text || '');
|
||||||
|
}
|
||||||
|
|
||||||
export function isTerminalStreamPatch(obj) {
|
export function isTerminalStreamPatch(obj) {
|
||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
@@ -170,4 +179,5 @@ export function attachPatch(SA) {
|
|||||||
SA.normalizePatch = normalizePatch;
|
SA.normalizePatch = normalizePatch;
|
||||||
SA.extractPatch = extractPatch;
|
SA.extractPatch = extractPatch;
|
||||||
SA.generateFlagOn = generateFlagOn;
|
SA.generateFlagOn = generateFlagOn;
|
||||||
|
SA.visibleProse = visibleProse;
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-5
@@ -345,18 +345,24 @@ export function fullSettingsDump(session, extras = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Model generate/actions, or askGenerateFn when a prompt/delta exists. vetoFn / fromAutoCritique cancel. */
|
/**
|
||||||
export function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) {
|
* Generate is user-owned. Model generate:true is advisory (Qwen dumps it on chat questions).
|
||||||
|
* askGenerateFn / userWantsGenerate start Generate; vetoFn / fromAutoCritique cancel.
|
||||||
|
*/
|
||||||
|
export function resolveTurnIntent(patch, userText, {
|
||||||
|
vetoFn, askGenerateFn, fromAutoCritique, sessionPrompt, userWantsGenerate,
|
||||||
|
} = {}) {
|
||||||
const delta = normalizeDelta(patch) || {};
|
const delta = normalizeDelta(patch) || {};
|
||||||
const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false;
|
const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false;
|
||||||
const modelAsked = patchWantsGenerate(delta);
|
const modelAsked = patchWantsGenerate(delta);
|
||||||
|
const hasPrompt = !!(String(delta.prompt || '').trim() || String(sessionPrompt || '').trim());
|
||||||
const userAsked = typeof askGenerateFn === 'function' && !!askGenerateFn(userText)
|
const userAsked = typeof askGenerateFn === 'function' && !!askGenerateFn(userText)
|
||||||
&& !!(modelAsked || String(delta.prompt || '').trim());
|
&& (modelAsked || hasPrompt);
|
||||||
const generate = !vetoed && !fromAutoCritique && (modelAsked || userAsked);
|
const generate = !vetoed && !fromAutoCritique && (userAsked || !!userWantsGenerate);
|
||||||
const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null;
|
||||||
const look = !!(hasLook && !generate && !vetoed);
|
const look = !!(hasLook && !generate && !vetoed);
|
||||||
const ask = patchAskList(delta);
|
const ask = patchAskList(delta);
|
||||||
return { generate, look, vetoed, ask };
|
return { generate, look, vetoed, ask, modelAsked };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 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). */
|
||||||
|
|||||||
+98
-8
@@ -101,6 +101,7 @@ export function attachTraining(SA) {
|
|||||||
if (state.ttab === 'train') {
|
if (state.ttab === 'train') {
|
||||||
syncModelfileModels();
|
syncModelfileModels();
|
||||||
syncQloraModels();
|
syncQloraModels();
|
||||||
|
void resumeTrainJobPolling();
|
||||||
}
|
}
|
||||||
if (state.ttab === 'models') refreshTrainModels();
|
if (state.ttab === 'models') refreshTrainModels();
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,12 @@ export function attachTraining(SA) {
|
|||||||
const data = await SA.request('AssistentListTrainSamples', { status, persona, limit: 300 });
|
const data = await SA.request('AssistentListTrainSamples', { status, persona, limit: 300 });
|
||||||
state.samples = data?.samples || [];
|
state.samples = data?.samples || [];
|
||||||
const stats = $('sa_train_stats');
|
const stats = $('sa_train_stats');
|
||||||
if (stats) stats.textContent = `Одобрено: ${data?.approved ?? '—'} · всего: ${data?.total ?? '—'}`;
|
if (stats) {
|
||||||
|
const appr = data?.approved ?? '—';
|
||||||
|
const draft = data?.draft ?? '—';
|
||||||
|
const total = data?.total ?? '—';
|
||||||
|
stats.textContent = `Одобрено: ${appr} · черновики: ${draft} · всего: ${total}`;
|
||||||
|
}
|
||||||
const personaSel = $('sa_train_filter_persona');
|
const personaSel = $('sa_train_filter_persona');
|
||||||
if (personaSel && $('sa_persona')) {
|
if (personaSel && $('sa_persona')) {
|
||||||
const cur = personaSel.value || 'all';
|
const cur = personaSel.value || 'all';
|
||||||
@@ -134,8 +140,14 @@ export function attachTraining(SA) {
|
|||||||
function renderSamples() {
|
function renderSamples() {
|
||||||
const root = $('sa_train_samples');
|
const root = $('sa_train_samples');
|
||||||
if (!root) return;
|
if (!root) return;
|
||||||
|
const filter = $('sa_train_filter_status')?.value || 'all';
|
||||||
if (!state.samples.length) {
|
if (!state.samples.length) {
|
||||||
root.innerHTML = '<div class="sa-mem-empty">Нет примеров. Отметь ответы в чате или импортируй датасет.</div>';
|
const hint = filter === 'approved'
|
||||||
|
? 'Под фильтром «Одобренные» пусто. HF-импорт создаёт <strong>черновики</strong> — переключи на «Черновики» или «Все статусы».'
|
||||||
|
: filter === 'draft'
|
||||||
|
? 'Нет черновиков. Импортируй HF или отметь примеры в чате.'
|
||||||
|
: 'Нет примеров. Отметь ответы в чате или импортируй датасет (HF / файл).';
|
||||||
|
root.innerHTML = `<div class="sa-mem-empty">${hint}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
root.innerHTML = '';
|
root.innerHTML = '';
|
||||||
@@ -302,19 +314,63 @@ export function attachTraining(SA) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function importHf() {
|
async function importHf() {
|
||||||
|
const link = ($('sa_hf_link')?.value || '').trim();
|
||||||
|
if (!state.hfCheck?.id && link) {
|
||||||
|
await checkHfLink();
|
||||||
|
}
|
||||||
if (!state.hfSelected && !state.hfCheck?.id) {
|
if (!state.hfSelected && !state.hfCheck?.id) {
|
||||||
setTrainStatus('Сначала проверь набор');
|
setTrainStatus('Сначала проверь набор');
|
||||||
|
const hfSt = $('sa_hf_status');
|
||||||
|
if (hfSt) hfSt.textContent = 'Вставь ссылку и нажми «Проверить»';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = state.hfSelected || state.hfCheck.id;
|
const id = state.hfSelected || state.hfCheck.id;
|
||||||
const limit = Number($('sa_hf_import_limit')?.value) || 200;
|
const limit = Number($('sa_hf_import_limit')?.value) || 200;
|
||||||
const mapping = buildHfMappingPayload();
|
const mapping = buildHfMappingPayload();
|
||||||
|
const btn = $('sa_btn_hf_import');
|
||||||
|
const hfSt = $('sa_hf_status');
|
||||||
|
const busy = limit > 400
|
||||||
|
? `Импортирую до ${limit} строк… (1–2 мин)`
|
||||||
|
: `Импортирую до ${limit}…`;
|
||||||
|
setTrainStatus(busy);
|
||||||
|
if (hfSt) {
|
||||||
|
hfSt.textContent = busy;
|
||||||
|
hfSt.classList.add('sa-hf-busy');
|
||||||
|
}
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.dataset.label = btn.textContent;
|
||||||
|
btn.textContent = '…';
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const data = await SA.request('AssistentImportHfDataset', { dataset: id, limit, mapping });
|
const data = await SA.request('AssistentImportHfDataset', { dataset: id, limit, mapping });
|
||||||
setTrainStatus(`Импортировано: ${data.imported}${data.runner_only ? ' (runner-only)' : ''}`);
|
const n = Number(data?.imported) || 0;
|
||||||
|
let msg;
|
||||||
|
if (data?.runner_only) {
|
||||||
|
msg = `Runner-only: ${data.note || id} (в sqlite не импортировали)`;
|
||||||
|
} else if (n > 0) {
|
||||||
|
const filt = $('sa_train_filter_status');
|
||||||
|
if (filt && filt.value === 'approved') {
|
||||||
|
filt.value = 'draft';
|
||||||
|
}
|
||||||
|
msg = `Импортировано: ${n} черновик(ов) — список ниже (фильтр → черновики)`;
|
||||||
|
} else {
|
||||||
|
msg = 'Импортировано: 0 — HF token в User Settings, маппинг или gated-набор';
|
||||||
|
}
|
||||||
|
setTrainStatus(msg);
|
||||||
|
if (hfSt) hfSt.textContent = msg;
|
||||||
await refreshSamples();
|
await refreshSamples();
|
||||||
|
$('sa_train_samples')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setTrainStatus(String(e.message || e));
|
const err = String(e.message || e);
|
||||||
|
setTrainStatus(err);
|
||||||
|
if (hfSt) hfSt.textContent = err;
|
||||||
|
} finally {
|
||||||
|
if (hfSt) hfSt.classList.remove('sa-hf-busy');
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = btn.dataset.label || 'Импортировать';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -345,7 +401,9 @@ export function attachTraining(SA) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createModelfile() {
|
async function createModelfile() {
|
||||||
setTrainStatus('Создаю модель…');
|
const btn = $('sa_btn_modelfile_create');
|
||||||
|
btn?.setAttribute('disabled', 'disabled');
|
||||||
|
setTrainStatus('Создаю Modelfile в Ollama…');
|
||||||
try {
|
try {
|
||||||
const data = await SA.request('AssistentCreateOllamaModel', {
|
const data = await SA.request('AssistentCreateOllamaModel', {
|
||||||
base_url: $('sa_base_url')?.value,
|
base_url: $('sa_base_url')?.value,
|
||||||
@@ -361,12 +419,16 @@ export function attachTraining(SA) {
|
|||||||
SA.app?.refreshModels?.();
|
SA.app?.refreshModels?.();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setTrainStatus(String(e.message || e));
|
setTrainStatus(String(e.message || e));
|
||||||
|
} finally {
|
||||||
|
btn?.removeAttribute('disabled');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTrainMode(mode) {
|
function setTrainMode(mode) {
|
||||||
$('sa_train_form_modelfile').hidden = mode !== 'modelfile';
|
$('sa_train_form_modelfile').hidden = mode !== 'modelfile';
|
||||||
$('sa_train_form_qlora').hidden = mode !== 'qlora';
|
$('sa_train_form_qlora').hidden = mode !== 'qlora';
|
||||||
|
const radio = document.querySelector(`input[name="sa_train_mode"][value="${mode}"]`);
|
||||||
|
if (radio) radio.checked = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setTrainingLock(on, text) {
|
function setTrainingLock(on, text) {
|
||||||
@@ -387,15 +449,26 @@ export function attachTraining(SA) {
|
|||||||
const prog = data?.progress || (data?.job?.progress_json ? JSON.parse(data.job.progress_json) : null);
|
const prog = data?.progress || (data?.job?.progress_json ? JSON.parse(data.job.progress_json) : null);
|
||||||
const active = data?.training_active || data?.job?.status === 'running';
|
const active = data?.training_active || data?.job?.status === 'running';
|
||||||
const status = data?.job?.status || prog?.status;
|
const status = data?.job?.status || prog?.status;
|
||||||
setTrainingLock(active, prog?.status === 'running' ? `Тренировка · ${prog?.percent ?? 0}%` : 'Идёт тренировка…');
|
const pct = prog?.percent;
|
||||||
|
const bannerText = active && pct != null
|
||||||
|
? `QLoRA · ${pct}%`
|
||||||
|
: active
|
||||||
|
? 'Идёт QLoRA…'
|
||||||
|
: 'Идёт тренировка…';
|
||||||
|
setTrainingLock(active, bannerText);
|
||||||
const logEl = $('sa_train_log');
|
const logEl = $('sa_train_log');
|
||||||
const bar = $('sa_train_progress_fill');
|
const bar = $('sa_train_progress_fill');
|
||||||
const box = $('sa_train_progress');
|
const box = $('sa_train_progress');
|
||||||
if (prog) {
|
if (prog) {
|
||||||
if (box) box.hidden = false;
|
if (box) box.hidden = false;
|
||||||
if (bar && prog.percent != null) bar.style.width = `${prog.percent}%`;
|
if (bar && pct != null) bar.style.width = `${pct}%`;
|
||||||
if (logEl && prog.log) logEl.textContent = prog.log;
|
if (logEl && prog.log) logEl.textContent = prog.log;
|
||||||
}
|
}
|
||||||
|
if (active) {
|
||||||
|
setTrainStatus(pct != null
|
||||||
|
? `QLoRA · ${pct}% — полный лог на вкладке «Тренировка»`
|
||||||
|
: 'QLoRA запущена — полный лог на вкладке «Тренировка»');
|
||||||
|
}
|
||||||
if (!active) {
|
if (!active) {
|
||||||
clearInterval(state.polling);
|
clearInterval(state.polling);
|
||||||
state.polling = null;
|
state.polling = null;
|
||||||
@@ -422,6 +495,19 @@ export function attachTraining(SA) {
|
|||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resumeTrainJobPolling() {
|
||||||
|
try {
|
||||||
|
const data = await SA.request('AssistentGetTrainJob', {});
|
||||||
|
const active = data?.training_active || data?.job?.status === 'running';
|
||||||
|
await pollTrainJob();
|
||||||
|
if (!active) return;
|
||||||
|
setTrainMode('qlora');
|
||||||
|
$('sa_btn_qlora_cancel').hidden = false;
|
||||||
|
if (state.polling) clearInterval(state.polling);
|
||||||
|
state.polling = setInterval(pollTrainJob, 1500);
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
async function startQlora() {
|
async function startQlora() {
|
||||||
setTrainStatus('Запуск…');
|
setTrainStatus('Запуск…');
|
||||||
try {
|
try {
|
||||||
@@ -448,7 +534,9 @@ export function attachTraining(SA) {
|
|||||||
if (state.polling) clearInterval(state.polling);
|
if (state.polling) clearInterval(state.polling);
|
||||||
state.polling = setInterval(pollTrainJob, 1500);
|
state.polling = setInterval(pollTrainJob, 1500);
|
||||||
pollTrainJob();
|
pollTrainJob();
|
||||||
setTrainStatus('Тренировка запущена');
|
setTrainMode('qlora');
|
||||||
|
setTrainingTab('train');
|
||||||
|
setTrainStatus('QLoRA запущена — прогресс ниже');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setTrainStatus(String(e.message || e));
|
setTrainStatus(String(e.message || e));
|
||||||
}
|
}
|
||||||
@@ -614,6 +702,7 @@ export function attachTraining(SA) {
|
|||||||
$('sa_btn_save_runner')?.addEventListener('click', saveRunner);
|
$('sa_btn_save_runner')?.addEventListener('click', saveRunner);
|
||||||
loadRunner();
|
loadRunner();
|
||||||
setTrainMode('modelfile');
|
setTrainMode('modelfile');
|
||||||
|
void resumeTrainJobPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
SA.training = {
|
SA.training = {
|
||||||
@@ -621,6 +710,7 @@ export function attachTraining(SA) {
|
|||||||
wireTraining();
|
wireTraining();
|
||||||
setTrainingTab(state.ttab);
|
setTrainingTab(state.ttab);
|
||||||
},
|
},
|
||||||
|
resumePolling: resumeTrainJobPolling,
|
||||||
async curateFromChat(messages, meta) {
|
async curateFromChat(messages, meta) {
|
||||||
try {
|
try {
|
||||||
await SA.request('AssistentUpsertTrainSample', {
|
await SA.request('AssistentUpsertTrainSample', {
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import {
|
||||||
|
DEFAULT_ASPECT_TABLE,
|
||||||
|
buildDefaultAspectTable,
|
||||||
|
swarmSizeFromRef,
|
||||||
|
} from '../src/aspect.js';
|
||||||
|
|
||||||
|
describe('aspect.js (SwarmUI side 1024)', () => {
|
||||||
|
it('16:9 and 9:16 match Swarm Resolution dropdown', () => {
|
||||||
|
assert.deepEqual(swarmSizeFromRef('16:9'), [1344, 768]);
|
||||||
|
assert.deepEqual(swarmSizeFromRef('9:16'), [768, 1344]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('default table uses Swarm formula for landscape/portrait chips', () => {
|
||||||
|
assert.deepEqual(DEFAULT_ASPECT_TABLE['16:9'], [1344, 768]);
|
||||||
|
assert.deepEqual(DEFAULT_ASPECT_TABLE['9:16'], [768, 1344]);
|
||||||
|
assert.deepEqual(DEFAULT_ASPECT_TABLE['2:3'], [832, 1216]);
|
||||||
|
assert.deepEqual(DEFAULT_ASPECT_TABLE['1:1'], [1024, 1024]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('2.35:1 aliases 21:9', () => {
|
||||||
|
assert.deepEqual(DEFAULT_ASPECT_TABLE['2.35:1'], DEFAULT_ASPECT_TABLE['21:9']);
|
||||||
|
assert.deepEqual(DEFAULT_ASPECT_TABLE['21:9'], [1536, 640]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scales with side length', () => {
|
||||||
|
const t = buildDefaultAspectTable(512);
|
||||||
|
assert.deepEqual(t['16:9'], [672, 384]);
|
||||||
|
});
|
||||||
|
});
|
||||||
+18
-1
@@ -23,11 +23,13 @@ describe('intent.js', () => {
|
|||||||
it('userAsksGenerate detects сгенерируй', () => {
|
it('userAsksGenerate detects сгенерируй', () => {
|
||||||
assert.equal(userAsksGenerate('сгенерируй'), true);
|
assert.equal(userAsksGenerate('сгенерируй'), true);
|
||||||
assert.equal(userAsksGenerate('нарисуй лису'), true);
|
assert.equal(userAsksGenerate('нарисуй лису'), true);
|
||||||
|
assert.equal(userAsksGenerate('Давай сделаем изображение красивой рыжеволосой девушки'), true);
|
||||||
|
assert.equal(userAsksGenerate('а ты знаешь какие то промпты с civitai'), false);
|
||||||
assert.equal(userAsksGenerate('что ты умеешь'), false);
|
assert.equal(userAsksGenerate('что ты умеешь'), false);
|
||||||
assert.equal(userAsksGenerate('не генерируй'), false);
|
assert.equal(userAsksGenerate('не генерируй'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('resolveTurnIntent uses model generate + user generate ask + veto', () => {
|
it('resolveTurnIntent uses user generate ask + veto, not model flag alone', () => {
|
||||||
const lookOnly = resolveTurnIntent({ look_at: ['generate'] }, 'что не так с кадром?', {});
|
const lookOnly = resolveTurnIntent({ look_at: ['generate'] }, 'что не так с кадром?', {});
|
||||||
assert.equal(lookOnly.generate, false);
|
assert.equal(lookOnly.generate, false);
|
||||||
assert.equal(lookOnly.look, true);
|
assert.equal(lookOnly.look, true);
|
||||||
@@ -44,6 +46,21 @@ describe('intent.js', () => {
|
|||||||
|
|
||||||
const chatOnly = resolveTurnIntent({ prompt: 'fox' }, 'что ты умеешь про генерацию?', {});
|
const chatOnly = resolveTurnIntent({ prompt: 'fox' }, 'что ты умеешь про генерацию?', {});
|
||||||
assert.equal(chatOnly.generate, false);
|
assert.equal(chatOnly.generate, false);
|
||||||
|
|
||||||
|
const falseDump = resolveTurnIntent(
|
||||||
|
{ prompt: 'civitai example', generate: true },
|
||||||
|
'а ты знаешь какие то промпты с civitai',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
assert.equal(falseDump.generate, false);
|
||||||
|
assert.equal(falseDump.modelAsked, true);
|
||||||
|
|
||||||
|
const sdelаем = resolveTurnIntent(
|
||||||
|
{ prompt: 'A 19-year-old redhead', generate: true },
|
||||||
|
'Давай сделаем изображение красивой рыжеволосой девушки',
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
assert.equal(sdelаем.generate, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('packWantsVision for critique pack', () => {
|
it('packWantsVision for critique pack', () => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
isPatchObject,
|
isPatchObject,
|
||||||
normalizePatch,
|
normalizePatch,
|
||||||
setPatchKeys,
|
setPatchKeys,
|
||||||
|
visibleProse,
|
||||||
} from '../src/patch.js';
|
} from '../src/patch.js';
|
||||||
import {
|
import {
|
||||||
mergeDelta,
|
mergeDelta,
|
||||||
@@ -56,6 +57,12 @@ describe('patch.js', () => {
|
|||||||
assert.equal(raw.prose, 'done');
|
assert.equal(raw.prose, 'done');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('visibleProse hides the JSON fence when chat text is empty', () => {
|
||||||
|
assert.equal(visibleProse('```json\n{"prompt":"fox","generate":true}\n```'), '');
|
||||||
|
assert.equal(visibleProse('Ок, держи\n```json\n{"prompt":"fox"}\n```'), 'Ок, держи');
|
||||||
|
assert.equal(visibleProse('просто чат без патча'), 'просто чат без патча');
|
||||||
|
});
|
||||||
|
|
||||||
it('scheduler-only patch is detected with full key list', () => {
|
it('scheduler-only patch is detected with full key list', () => {
|
||||||
setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']);
|
setPatchKeys(['prompt', 'scheduler', 'generate', 'ask']);
|
||||||
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
|
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
|
||||||
@@ -112,6 +119,16 @@ describe('session.js', () => {
|
|||||||
assert.equal(patchWantsGenerate({ generate: true }), true);
|
assert.equal(patchWantsGenerate({ generate: true }), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('resolveTurnIntent ignores model generate:true on a knowledge question', () => {
|
||||||
|
const intent = resolveTurnIntent(
|
||||||
|
{ prompt: 'civitai example prompt', generate: true },
|
||||||
|
'а ты знаешь какие то промпты с civitai',
|
||||||
|
{ askGenerateFn: (t) => /сгенер|нарисуй/i.test(t) },
|
||||||
|
);
|
||||||
|
assert.equal(intent.generate, false);
|
||||||
|
assert.equal(intent.modelAsked, true);
|
||||||
|
});
|
||||||
|
|
||||||
it('mergeExactParamsForGenerate fills turbo profile when LLM omits steps/cfg', () => {
|
it('mergeExactParamsForGenerate fills turbo profile when LLM omits steps/cfg', () => {
|
||||||
const exact = {
|
const exact = {
|
||||||
generation: { profile: 'turbo', steps: 8, cfg: 1, sigma_shift: 1.15 },
|
generation: { profile: 'turbo', steps: 8, cfg: 1, sigma_shift: 1.15 },
|
||||||
|
|||||||
Reference in New Issue
Block a user