checkpoint before checking out main

This commit is contained in:
Leonid Pershin
2026-08-20 14:43:00 +03:00
parent 477f8cf6c9
commit 5d46550f12
18 changed files with 429 additions and 64 deletions
@@ -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);
}
+5 -3
View File
@@ -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");
+4 -2
View File
@@ -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)
+56 -23
View File
@@ -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() =>
@@ -566,6 +584,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 +624,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 +657,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 +697,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 +798,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 +844,6 @@ internal sealed record SwarmUiResolvedProfile(
string Negative,
IReadOnlyList<SwarmUiLoraEntry> PositiveLoras,
IReadOnlyList<SwarmUiLoraEntry> NegativeLoras,
IReadOnlyList<SwarmUiLoraEntry> PositiveEmbeddings,
IReadOnlyList<SwarmUiLoraEntry> NegativeEmbeddings,
SwarmUiKindPreset KindPreset);