Merge branch 'phase/61-portrait-loras-embeds'
This commit is contained in:
@@ -53,7 +53,10 @@ const ru = {
|
||||
settingsOverrides: 'Настройки генерации пресета',
|
||||
settingsPositiveLoras: 'Positive LoRA',
|
||||
settingsNegativeLoras: 'Negative LoRA',
|
||||
settingsPositiveEmbeddings: 'Positive embeddings',
|
||||
settingsNegativeEmbeddings: 'Negative embeddings',
|
||||
settingsAddLora: 'Добавить LoRA',
|
||||
settingsAddEmbedding: 'Добавить embedding',
|
||||
settingsAvatarPreset: 'Аватар',
|
||||
settingsCustomPreset: 'Свой промпт (размеры)',
|
||||
settingsFullBodyPreset: 'В полный рост',
|
||||
@@ -437,7 +440,10 @@ const en: Messages = {
|
||||
settingsOverrides: 'Preset generation overrides',
|
||||
settingsPositiveLoras: 'Positive LoRA',
|
||||
settingsNegativeLoras: 'Negative LoRA',
|
||||
settingsPositiveEmbeddings: 'Positive embeddings',
|
||||
settingsNegativeEmbeddings: 'Negative embeddings',
|
||||
settingsAddLora: 'Add LoRA',
|
||||
settingsAddEmbedding: 'Add embedding',
|
||||
settingsAvatarPreset: 'Avatar',
|
||||
settingsCustomPreset: 'Custom prompt (size)',
|
||||
settingsFullBodyPreset: 'Full body',
|
||||
|
||||
@@ -498,6 +498,10 @@ export interface SwarmUiKindPreset {
|
||||
width: number;
|
||||
height: number;
|
||||
shotType: string;
|
||||
positiveLoras?: SwarmUiLoraEntry[] | null;
|
||||
negativeLoras?: SwarmUiLoraEntry[] | null;
|
||||
positiveEmbeddings?: SwarmUiLoraEntry[] | null;
|
||||
negativeEmbeddings?: SwarmUiLoraEntry[] | null;
|
||||
}
|
||||
|
||||
export interface SwarmUiLoraEntry {
|
||||
@@ -518,6 +522,8 @@ export interface SwarmUiModelDefinition {
|
||||
negative: string;
|
||||
positiveLoras: SwarmUiLoraEntry[];
|
||||
negativeLoras: SwarmUiLoraEntry[];
|
||||
positiveEmbeddings: SwarmUiLoraEntry[];
|
||||
negativeEmbeddings: SwarmUiLoraEntry[];
|
||||
}
|
||||
|
||||
export interface SwarmUiPresetDefinition {
|
||||
@@ -534,6 +540,8 @@ export interface SwarmUiPresetDefinition {
|
||||
seed?: number | null;
|
||||
positiveLoras?: SwarmUiLoraEntry[] | null;
|
||||
negativeLoras?: SwarmUiLoraEntry[] | null;
|
||||
positiveEmbeddings?: SwarmUiLoraEntry[] | null;
|
||||
negativeEmbeddings?: SwarmUiLoraEntry[] | null;
|
||||
avatar: SwarmUiKindPreset;
|
||||
custom: SwarmUiKindPreset;
|
||||
fullBody: SwarmUiKindPreset;
|
||||
@@ -556,6 +564,7 @@ export interface SwarmUiDiscovery {
|
||||
connected: boolean;
|
||||
models: readonly string[];
|
||||
loras: readonly string[];
|
||||
embeddings: readonly string[];
|
||||
samplers: readonly string[];
|
||||
schedulers: readonly string[];
|
||||
}
|
||||
|
||||
@@ -128,6 +128,8 @@ describe('createSchoolDialog', () => {
|
||||
negative: '',
|
||||
positiveLoras: [],
|
||||
negativeLoras: [],
|
||||
positiveEmbeddings: [],
|
||||
negativeEmbeddings: [],
|
||||
},
|
||||
],
|
||||
presets: [
|
||||
|
||||
@@ -43,6 +43,8 @@ function settings(): SwarmUiSettingsFile {
|
||||
negative: '',
|
||||
positiveLoras: [],
|
||||
negativeLoras: [],
|
||||
positiveEmbeddings: [],
|
||||
negativeEmbeddings: [],
|
||||
},
|
||||
{
|
||||
id: 'other.safetensors',
|
||||
@@ -57,6 +59,8 @@ function settings(): SwarmUiSettingsFile {
|
||||
negative: '',
|
||||
positiveLoras: [],
|
||||
negativeLoras: [],
|
||||
positiveEmbeddings: [],
|
||||
negativeEmbeddings: [],
|
||||
},
|
||||
],
|
||||
presets: [
|
||||
@@ -80,6 +84,7 @@ function discovery(overrides: Partial<SwarmUiDiscovery> = {}): SwarmUiDiscovery
|
||||
connected: true,
|
||||
models: ['template.safetensors', 'not-in-config.safetensors'],
|
||||
loras: [],
|
||||
embeddings: [],
|
||||
samplers: ['euler'],
|
||||
schedulers: ['normal'],
|
||||
...overrides,
|
||||
@@ -135,7 +140,8 @@ describe('swarmUiSettingsDialog', () => {
|
||||
|
||||
expect([...dialog.querySelectorAll('.field__label')].some((node) => node.textContent === t('settingsStyle'))).toBe(true);
|
||||
expect([...dialog.querySelectorAll('.field__label')].some((node) => node.textContent === t('settingsShotType'))).toBe(true);
|
||||
expect(dialog.querySelector('.settings-model')?.textContent).toContain(t('settingsModelDefaults'));
|
||||
expect(dialog.querySelector('.settings-model')?.textContent).toContain(t('settingsPositiveEmbeddings'));
|
||||
expect([...dialog.querySelectorAll('.settings-kind .settings-subtitle')].some((node) => node.textContent === t('settingsPositiveEmbeddings'))).toBe(true);
|
||||
const spoiler = dialog.querySelector('details.settings-overrides');
|
||||
if (!(spoiler instanceof HTMLDetailsElement)) {
|
||||
throw new Error('overrides spoiler is missing');
|
||||
|
||||
@@ -25,7 +25,7 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
const ageRulesHost = el('div', { class: 'settings-age-rules' });
|
||||
|
||||
let config: SwarmUiSettingsFile | null = null;
|
||||
let discovery: SwarmUiDiscovery = { connected: false, models: [], loras: [], samplers: [], schedulers: [] };
|
||||
let discovery: SwarmUiDiscovery = { connected: false, models: [], loras: [], embeddings: [], samplers: [], schedulers: [] };
|
||||
let editingPresetId = '';
|
||||
|
||||
const cancelButton = el('button', { class: 'button', type: 'button', onClick: () => modal.close(null) });
|
||||
@@ -239,6 +239,11 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
return host;
|
||||
}
|
||||
|
||||
model.positiveLoras ??= [];
|
||||
model.negativeLoras ??= [];
|
||||
model.positiveEmbeddings ??= [];
|
||||
model.negativeEmbeddings ??= [];
|
||||
|
||||
host.append(
|
||||
textareaField(t('settingsModelPositive'), model.positive, (value) => {
|
||||
model.positive = value;
|
||||
@@ -264,8 +269,10 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
numberField(t('settingsSeed'), model.seed, (value) => {
|
||||
model.seed = value;
|
||||
}),
|
||||
loraSection(t('settingsPositiveLoras'), model.positiveLoras),
|
||||
loraSection(t('settingsNegativeLoras'), model.negativeLoras),
|
||||
loraSection(t('settingsPositiveLoras'), model.positiveLoras, discovery.loras, t('settingsAddLora')),
|
||||
loraSection(t('settingsNegativeLoras'), model.negativeLoras, discovery.loras, t('settingsAddLora')),
|
||||
loraSection(t('settingsPositiveEmbeddings'), model.positiveEmbeddings, discovery.embeddings, t('settingsAddEmbedding')),
|
||||
loraSection(t('settingsNegativeEmbeddings'), model.negativeEmbeddings, discovery.embeddings, t('settingsAddEmbedding')),
|
||||
);
|
||||
return host;
|
||||
}
|
||||
@@ -295,16 +302,33 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
}),
|
||||
);
|
||||
|
||||
const positiveLoras = preset.positiveLoras ?? [];
|
||||
const negativeLoras = preset.negativeLoras ?? [];
|
||||
preset.positiveLoras = preset.positiveLoras ?? null;
|
||||
const positiveHost = loraSection(t('settingsPositiveLoras'), positiveLoras, () => {
|
||||
preset.positiveLoras = positiveLoras;
|
||||
});
|
||||
const negativeHost = loraSection(t('settingsNegativeLoras'), negativeLoras, () => {
|
||||
preset.negativeLoras = negativeLoras;
|
||||
});
|
||||
body.append(positiveHost, negativeHost);
|
||||
const bindList = (
|
||||
title: string,
|
||||
current: SwarmUiLoraEntry[] | null | undefined,
|
||||
names: readonly string[],
|
||||
addLabel: string,
|
||||
assign: (items: SwarmUiLoraEntry[]) => void,
|
||||
): HTMLElement => {
|
||||
const items = current ?? [];
|
||||
return loraSection(title, items, names, addLabel, () => {
|
||||
assign(items);
|
||||
});
|
||||
};
|
||||
|
||||
body.append(
|
||||
bindList(t('settingsPositiveLoras'), preset.positiveLoras, discovery.loras, t('settingsAddLora'), (items) => {
|
||||
preset.positiveLoras = items;
|
||||
}),
|
||||
bindList(t('settingsNegativeLoras'), preset.negativeLoras, discovery.loras, t('settingsAddLora'), (items) => {
|
||||
preset.negativeLoras = items;
|
||||
}),
|
||||
bindList(t('settingsPositiveEmbeddings'), preset.positiveEmbeddings, discovery.embeddings, t('settingsAddEmbedding'), (items) => {
|
||||
preset.positiveEmbeddings = items;
|
||||
}),
|
||||
bindList(t('settingsNegativeEmbeddings'), preset.negativeEmbeddings, discovery.embeddings, t('settingsAddEmbedding'), (items) => {
|
||||
preset.negativeEmbeddings = items;
|
||||
}),
|
||||
);
|
||||
details.append(body);
|
||||
return details;
|
||||
}
|
||||
@@ -367,32 +391,38 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
}
|
||||
}
|
||||
|
||||
function loraSection(title: string, loras: SwarmUiLoraEntry[], onMutate?: () => void): HTMLElement {
|
||||
function loraSection(
|
||||
title: string,
|
||||
items: SwarmUiLoraEntry[],
|
||||
names: readonly string[],
|
||||
addLabel: string,
|
||||
onMutate?: () => void,
|
||||
): HTMLElement {
|
||||
const host = el('div', { class: 'settings-loras' });
|
||||
const heading = el('h4', { class: 'settings-subtitle', text: title });
|
||||
const rows = el('div', { class: 'settings-lora-rows' });
|
||||
|
||||
const paintRows = (): void => {
|
||||
rows.replaceChildren();
|
||||
for (const lora of loras) {
|
||||
const nameField = choiceField('', lora.name, discovery.loras, (value) => {
|
||||
lora.name = value;
|
||||
for (const item of items) {
|
||||
const nameField = choiceField('', item.name, names, (value) => {
|
||||
item.name = value;
|
||||
});
|
||||
const weightInput = el('input', { class: 'input', type: 'number', value: String(lora.weight) });
|
||||
const weightInput = el('input', { class: 'input', type: 'number', value: String(item.weight) });
|
||||
weightInput.step = '0.05';
|
||||
weightInput.min = '-4';
|
||||
weightInput.max = '4';
|
||||
weightInput.addEventListener('input', () => {
|
||||
lora.weight = Number(weightInput.value);
|
||||
item.weight = Number(weightInput.value);
|
||||
});
|
||||
const remove = el('button', {
|
||||
class: 'button button--small button--danger',
|
||||
type: 'button',
|
||||
text: '×',
|
||||
onClick: () => {
|
||||
const index = loras.indexOf(lora);
|
||||
const index = items.indexOf(item);
|
||||
if (index >= 0) {
|
||||
loras.splice(index, 1);
|
||||
items.splice(index, 1);
|
||||
}
|
||||
|
||||
paintRows();
|
||||
@@ -406,9 +436,9 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
const add = el('button', {
|
||||
class: 'button button--small',
|
||||
type: 'button',
|
||||
text: t('settingsAddLora'),
|
||||
text: addLabel,
|
||||
onClick: () => {
|
||||
loras.push({ name: discovery.loras[0] ?? '', weight: 1 });
|
||||
items.push({ name: names[0] ?? '', weight: 1 });
|
||||
onMutate?.();
|
||||
paintRows();
|
||||
},
|
||||
@@ -419,6 +449,10 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
}
|
||||
|
||||
function kindSection(title: string, preset: SwarmUiPresetDefinition['avatar']): HTMLElement {
|
||||
preset.positiveLoras ??= [];
|
||||
preset.negativeLoras ??= [];
|
||||
preset.positiveEmbeddings ??= [];
|
||||
preset.negativeEmbeddings ??= [];
|
||||
return el(
|
||||
'div',
|
||||
{ class: 'settings-kind' },
|
||||
@@ -432,6 +466,10 @@ export function swarmUiSettingsDialog(initial?: SwarmUiSettingsFile): Promise<Sw
|
||||
textareaField(t('settingsShotType'), preset.shotType, (value) => {
|
||||
preset.shotType = value;
|
||||
}),
|
||||
loraSection(t('settingsPositiveLoras'), preset.positiveLoras, discovery.loras, t('settingsAddLora')),
|
||||
loraSection(t('settingsNegativeLoras'), preset.negativeLoras, discovery.loras, t('settingsAddLora')),
|
||||
loraSection(t('settingsPositiveEmbeddings'), preset.positiveEmbeddings, discovery.embeddings, t('settingsAddEmbedding')),
|
||||
loraSection(t('settingsNegativeEmbeddings'), preset.negativeEmbeddings, discovery.embeddings, t('settingsAddEmbedding')),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,10 +43,12 @@ internal static class PortraitPromptBuilder
|
||||
}
|
||||
}
|
||||
|
||||
var positive = string.Join(", ", parts);
|
||||
var negative = SwarmUiLoraFormatter.AppendLoraTags(
|
||||
var positive = SwarmUiLoraFormatter.AppendEmbedTags(
|
||||
string.Join(", ", parts),
|
||||
profile.PositiveEmbeddings);
|
||||
var negative = SwarmUiLoraFormatter.AppendEmbedTags(
|
||||
profile.Negative.Trim(),
|
||||
profile.NegativeLoras);
|
||||
profile.NegativeEmbeddings);
|
||||
|
||||
return (positive, negative);
|
||||
}
|
||||
|
||||
@@ -128,10 +128,12 @@ internal sealed class SwarmUiClient
|
||||
body["clipstopatlayer"] = -profile.ClipSkip;
|
||||
}
|
||||
|
||||
var loras = SwarmUiLoraFormatter.FormatForApi(profile.PositiveLoras);
|
||||
if (loras is not null)
|
||||
var (loraNames, loraWeights) = SwarmUiLoraFormatter.FormatForApi(
|
||||
SwarmUiLoraFormatter.Concat(profile.PositiveLoras, profile.NegativeLoras));
|
||||
if (loraNames is not null)
|
||||
{
|
||||
body["loras"] = loras;
|
||||
body["loras"] = loraNames;
|
||||
body["loraweights"] = loraWeights;
|
||||
}
|
||||
|
||||
using var content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||
|
||||
@@ -6,10 +6,11 @@ internal sealed record SwarmUiDiscovery(
|
||||
bool Connected,
|
||||
IReadOnlyList<string> Models,
|
||||
IReadOnlyList<string> Loras,
|
||||
IReadOnlyList<string> Embeddings,
|
||||
IReadOnlyList<string> Samplers,
|
||||
IReadOnlyList<string> Schedulers)
|
||||
{
|
||||
public static SwarmUiDiscovery Offline { get; } = new(false, [], [], [], []);
|
||||
public static SwarmUiDiscovery Offline { get; } = new(false, [], [], [], [], []);
|
||||
}
|
||||
|
||||
internal static class SwarmUiDiscoveryParser
|
||||
@@ -18,9 +19,10 @@ internal static class SwarmUiDiscoveryParser
|
||||
{
|
||||
var models = ReadModelNames(root, "Stable-Diffusion");
|
||||
var loras = ReadModelNames(root, "LoRA");
|
||||
var embeddings = ReadModelNames(root, "Embedding");
|
||||
var samplers = ReadParamValues(root, "sampler");
|
||||
var schedulers = ReadParamValues(root, "scheduler");
|
||||
return new SwarmUiDiscovery(true, models, loras, samplers, schedulers);
|
||||
return new SwarmUiDiscovery(true, models, loras, embeddings, samplers, schedulers);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> ReadModelNames(JsonElement root, string subtype)
|
||||
|
||||
@@ -2,15 +2,11 @@ namespace HSchool.Server.Game;
|
||||
|
||||
internal static class SwarmUiLoraFormatter
|
||||
{
|
||||
/// <summary>SwarmUI comma-separated lora list: name,weight,name,weight,…</summary>
|
||||
public static string? FormatForApi(IReadOnlyList<SwarmUiLoraEntry> loras)
|
||||
/// <summary>SwarmUI comma-separated LoRA names and a matching weights string.</summary>
|
||||
public static (string? Names, string? Weights) FormatForApi(IReadOnlyList<SwarmUiLoraEntry> loras)
|
||||
{
|
||||
if (loras.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parts = new List<string>(loras.Count * 2);
|
||||
var names = new List<string>();
|
||||
var weights = new List<string>();
|
||||
foreach (var lora in loras)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(lora.Name))
|
||||
@@ -18,26 +14,21 @@ internal static class SwarmUiLoraFormatter
|
||||
continue;
|
||||
}
|
||||
|
||||
parts.Add(lora.Name.Trim());
|
||||
parts.Add(lora.Weight.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
names.Add(lora.Name.Trim());
|
||||
weights.Add(lora.Weight.ToString(System.Globalization.CultureInfo.InvariantCulture));
|
||||
}
|
||||
|
||||
return parts.Count == 0 ? null : string.Join(',', parts);
|
||||
if (names.Count == 0)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
return (string.Join(',', names), string.Join(',', weights));
|
||||
}
|
||||
|
||||
public static string AppendLoraTags(string prompt, IReadOnlyList<SwarmUiLoraEntry> loras)
|
||||
public static string AppendEmbedTags(string prompt, IReadOnlyList<SwarmUiLoraEntry> embeddings)
|
||||
{
|
||||
if (loras.Count == 0)
|
||||
{
|
||||
return prompt;
|
||||
}
|
||||
|
||||
var tags = loras
|
||||
.Where(lora => !string.IsNullOrWhiteSpace(lora.Name))
|
||||
.Select(lora =>
|
||||
$"<lora:{lora.Name.Trim()}:{lora.Weight.ToString(System.Globalization.CultureInfo.InvariantCulture)}>")
|
||||
.ToList();
|
||||
|
||||
var tags = Tags(embeddings, "embed");
|
||||
if (tags.Count == 0)
|
||||
{
|
||||
return prompt;
|
||||
@@ -47,4 +38,46 @@ internal static class SwarmUiLoraFormatter
|
||||
? string.Join(' ', tags)
|
||||
: $"{prompt.TrimEnd()} {string.Join(' ', tags)}";
|
||||
}
|
||||
|
||||
public static List<SwarmUiLoraEntry> Concat(params IReadOnlyList<SwarmUiLoraEntry>?[] layers)
|
||||
{
|
||||
var result = new List<SwarmUiLoraEntry>();
|
||||
foreach (var layer in layers)
|
||||
{
|
||||
if (layer is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var entry in layer)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(entry.Name))
|
||||
{
|
||||
result.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<string> Tags(IReadOnlyList<SwarmUiLoraEntry> entries, string kind)
|
||||
{
|
||||
var tags = new List<string>();
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(entry.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = entry.Name.Trim();
|
||||
tags.Add(
|
||||
entry.Weight == 1
|
||||
? $"<{kind}:{name}>"
|
||||
: $"<{kind}:{name}:{entry.Weight.ToString(System.Globalization.CultureInfo.InvariantCulture)}>");
|
||||
}
|
||||
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,8 @@ internal sealed class SwarmUiConfigFile
|
||||
{
|
||||
model.PositiveLoras ??= [];
|
||||
model.NegativeLoras ??= [];
|
||||
model.PositiveEmbeddings ??= [];
|
||||
model.NegativeEmbeddings ??= [];
|
||||
if (string.IsNullOrWhiteSpace(model.Label))
|
||||
{
|
||||
model.Label = LabelFromId(model.Id);
|
||||
@@ -359,6 +361,16 @@ internal sealed class SwarmUiConfigFile
|
||||
{
|
||||
preset.NegativeLoras = null;
|
||||
}
|
||||
|
||||
if (SameLoras(preset.PositiveEmbeddings, model.PositiveEmbeddings))
|
||||
{
|
||||
preset.PositiveEmbeddings = null;
|
||||
}
|
||||
|
||||
if (SameLoras(preset.NegativeEmbeddings, model.NegativeEmbeddings))
|
||||
{
|
||||
preset.NegativeEmbeddings = null;
|
||||
}
|
||||
}
|
||||
|
||||
public SwarmUiModelDefinition? FindModel(string id) =>
|
||||
@@ -484,6 +496,10 @@ internal sealed class SwarmUiModelDefinition
|
||||
|
||||
public List<SwarmUiLoraEntry>? NegativeLoras { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? PositiveEmbeddings { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? NegativeEmbeddings { get; set; }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
if (Steps is < 1 or > 200)
|
||||
@@ -501,8 +517,10 @@ internal sealed class SwarmUiModelDefinition
|
||||
throw new InvalidOperationException($"Model '{Id}' clipSkip must be between 0 and 12.");
|
||||
}
|
||||
|
||||
SwarmUiPresetDefinition.ValidateLoras(Id, PositiveLoras, "positive");
|
||||
SwarmUiPresetDefinition.ValidateLoras(Id, NegativeLoras, "negative");
|
||||
SwarmUiPresetDefinition.ValidateLoras(Id, PositiveLoras, "positive LoRA");
|
||||
SwarmUiPresetDefinition.ValidateLoras(Id, NegativeLoras, "negative LoRA");
|
||||
SwarmUiPresetDefinition.ValidateLoras(Id, PositiveEmbeddings, "positive embedding");
|
||||
SwarmUiPresetDefinition.ValidateLoras(Id, NegativeEmbeddings, "negative embedding");
|
||||
}
|
||||
|
||||
public static List<SwarmUiModelDefinition> Catalog() =>
|
||||
@@ -532,6 +550,10 @@ internal sealed class SwarmUiModelDefinition
|
||||
Sampler = sampler,
|
||||
Scheduler = scheduler,
|
||||
Seed = seed,
|
||||
PositiveLoras = [],
|
||||
NegativeLoras = [],
|
||||
PositiveEmbeddings = [],
|
||||
NegativeEmbeddings = [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -566,6 +588,10 @@ internal sealed class SwarmUiPresetDefinition
|
||||
|
||||
public List<SwarmUiLoraEntry>? NegativeLoras { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? PositiveEmbeddings { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? NegativeEmbeddings { get; set; }
|
||||
|
||||
public SwarmUiKindPreset? Avatar { get; set; }
|
||||
|
||||
public SwarmUiKindPreset? Custom { get; set; }
|
||||
@@ -602,8 +628,26 @@ internal sealed class SwarmUiPresetDefinition
|
||||
throw new InvalidOperationException($"Preset '{Id}' clipSkip must be between 0 and 12.");
|
||||
}
|
||||
|
||||
ValidateLoras(Id, PositiveLoras, "positive");
|
||||
ValidateLoras(Id, NegativeLoras, "negative");
|
||||
ValidateLoras(Id, PositiveLoras, "positive LoRA");
|
||||
ValidateLoras(Id, NegativeLoras, "negative LoRA");
|
||||
ValidateLoras(Id, PositiveEmbeddings, "positive embedding");
|
||||
ValidateLoras(Id, NegativeEmbeddings, "negative embedding");
|
||||
ValidateKind(Avatar, "avatar");
|
||||
ValidateKind(Custom, "custom");
|
||||
ValidateKind(FullBody, "fullBody");
|
||||
}
|
||||
|
||||
private void ValidateKind(SwarmUiKindPreset? kind, string name)
|
||||
{
|
||||
if (kind is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ValidateLoras($"{Id}.{name}", kind.PositiveLoras, "positive LoRA");
|
||||
ValidateLoras($"{Id}.{name}", kind.NegativeLoras, "negative LoRA");
|
||||
ValidateLoras($"{Id}.{name}", kind.PositiveEmbeddings, "positive embedding");
|
||||
ValidateLoras($"{Id}.{name}", kind.NegativeEmbeddings, "negative embedding");
|
||||
}
|
||||
|
||||
internal static void ValidateLoras(string ownerId, IReadOnlyList<SwarmUiLoraEntry>? loras, string side)
|
||||
@@ -617,12 +661,12 @@ internal sealed class SwarmUiPresetDefinition
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(lora.Name))
|
||||
{
|
||||
throw new InvalidOperationException($"'{ownerId}' has an empty {side} LoRA name.");
|
||||
throw new InvalidOperationException($"'{ownerId}' has an empty {side} name.");
|
||||
}
|
||||
|
||||
if (lora.Weight is < -4 or > 4)
|
||||
{
|
||||
throw new InvalidOperationException($"'{ownerId}' LoRA '{lora.Name}' weight is out of range.");
|
||||
throw new InvalidOperationException($"'{ownerId}' {side} '{lora.Name}' weight is out of range.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -657,8 +701,10 @@ internal sealed class SwarmUiPresetDefinition
|
||||
kindPreset.ResolvedShotType(),
|
||||
"",
|
||||
negative,
|
||||
PositiveLoras ?? model.PositiveLoras ?? [],
|
||||
NegativeLoras ?? model.NegativeLoras ?? [],
|
||||
SwarmUiLoraFormatter.Concat(model.PositiveLoras, PositiveLoras, kindPreset.PositiveLoras),
|
||||
SwarmUiLoraFormatter.Concat(model.NegativeLoras, NegativeLoras, kindPreset.NegativeLoras),
|
||||
SwarmUiLoraFormatter.Concat(model.PositiveEmbeddings, PositiveEmbeddings, kindPreset.PositiveEmbeddings),
|
||||
SwarmUiLoraFormatter.Concat(model.NegativeEmbeddings, NegativeEmbeddings, kindPreset.NegativeEmbeddings),
|
||||
kindPreset);
|
||||
}
|
||||
|
||||
@@ -756,6 +802,14 @@ internal sealed class SwarmUiKindPreset
|
||||
/// <summary>Legacy kind positive; copied into <see cref="ShotType"/> on load.</summary>
|
||||
public string? Positive { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? PositiveLoras { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? NegativeLoras { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? PositiveEmbeddings { get; set; }
|
||||
|
||||
public List<SwarmUiLoraEntry>? NegativeEmbeddings { get; set; }
|
||||
|
||||
public void LiftShotType()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(ShotType) && !string.IsNullOrWhiteSpace(Positive))
|
||||
@@ -794,4 +848,6 @@ internal sealed record SwarmUiResolvedProfile(
|
||||
string Negative,
|
||||
IReadOnlyList<SwarmUiLoraEntry> PositiveLoras,
|
||||
IReadOnlyList<SwarmUiLoraEntry> NegativeLoras,
|
||||
IReadOnlyList<SwarmUiLoraEntry> PositiveEmbeddings,
|
||||
IReadOnlyList<SwarmUiLoraEntry> NegativeEmbeddings,
|
||||
SwarmUiKindPreset KindPreset);
|
||||
|
||||
Reference in New Issue
Block a user