Drop SDXL from the vanilla Swarm catalog so new schools draw only Krea 2.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-20 20:40:50 +03:00
co-authored by Cursor
parent 9c0735b988
commit 6fb6efbd45
6 changed files with 146 additions and 100 deletions
+10 -10
View File
@@ -11,22 +11,22 @@ DreamShaper.
## Задачи
- [ ] В `swarmui.json` только Muse GGUF и Realism v2.5 INT8 (8 / CFG 1 / euler / simple).
- [x] В `swarmui.json` только Muse GGUF и Realism v2.5 INT8 (8 / CFG 1 / euler / simple).
babes, epicrealism, lustify и DreamShaper убрать. `Catalog()` совпадает с файлом
- [ ] Пресеты `default` и `child` — Realism v2.5. Стиль и shotType `child` — проза без
- [x] Пресеты `default` и `child` — Realism v2.5. Стиль и shotType `child` — проза без
`(word:1.4)` и SD-весов
- [ ] `ageRules` 6–11 по-прежнему ведут на `child`, но модель у child — v2.5
- [ ] Живые школы не лифтить. Сокет не бампить. PNG не перегенерировать
- [ ] [`protocol.md`](../../protocol.md) — описание каталога без четырёх SDXL, тем же коммитом
- [x] `ageRules` 6–11 по-прежнему ведут на `child`, но модель у child — v2.5
- [x] Живые школы не лифтить. Сокет не бампить. PNG не перегенерировать
- [x] [`protocol.md`](../../protocol.md) — описание каталога без четырёх SDXL, тем же коммитом
(версия сокета не меняется)
## Тесты, без которых фаза не закрыта
- [ ] `CreateDefault` содержит ровно два id: Muse и Realism v2.5; нет DreamShaper и babes
- [ ] Resolve взрослого и ребёнка (6–11) берут Realism v2.5
- [ ] Стиль/негатив child без `(low quality` и без `:1.4)`
- [ ] `GET /api/settings/swarmui` отдаёт только Krea-id; create копирует их в сейв новой школы
- [ ] Нормализация загрузки не переписывает `models` уже лежащей школы с SDXL в каталоге
- [x] `CreateDefault` содержит ровно два id: Muse и Realism v2.5; нет DreamShaper и babes
- [x] Resolve взрослого и ребёнка (6–11) берут Realism v2.5
- [x] Стиль/негатив child без `(low quality` и без `:1.4)`
- [x] `GET /api/settings/swarmui` отдаёт только Krea-id; create копирует их в сейв новой школы
- [x] Нормализация загрузки не переписывает `models` уже лежащей школы с SDXL в каталоге
## Критерий готовности
+7 -2
View File
@@ -533,9 +533,14 @@ generation knobs, usually empty base positive/negative, LoRA and embedding lists
(model id, `style`, extra negative, optional generation overrides, per-kind size/`shotType`, LoRA
and embedding lists), `activePresetId` and `ageRules`. LoRA and embeddings on the model, preset and
shot kind concatenate at generate time.
The vanilla catalog is Krea 2 only: Muse GGUF (`museByStableYogi_v30TurboGguf.gguf`) and Realism
v2.5 INT8 (`realismByStableYogi_v25INT8Turbo.safetensors`), both 8 / CFG 1 / euler / simple. The
four SDXL Lightning checkpoints (babes, DreamShaper, epicrealism, lustify) are not in the template.
Presets `default` and `child` both use Realism v2.5; `ageRules` still send ages 611 to `child`.
The model picker in the UI is Swarm's list intersected with `models`. A new school copies this into
its save as `portraitSettings`. Living schools generate from that copy, not from this file. Older
saves without `models` lift on load (`positive` `style`, kind `positive``shotType`).
its save as `portraitSettings`. Living schools generate from that copy, not from this file — a save
that still lists SDXL models keeps them. Older saves without `models` lift on load (`positive`
`style`, kind `positive``shotType`).
### `PUT /api/settings/swarmui`
@@ -527,10 +527,6 @@ internal sealed class SwarmUiModelDefinition
public static List<SwarmUiModelDefinition> Catalog() =>
[
Lightning(BabesId, "babesByStableYogi v4 XL Lightning", 7, 1.5, 0, "euler", "normal", 0),
Lightning(DreamShaperId, "DreamShaper XL Lightning", 4, 2, 2, "dpmpp_sde", "karras", 3346112079),
Lightning(EpicRealismId, "epicrealism XL Lightning", 7, 1.5, 0, "euler", "normal", 0),
Lightning(LustifyId, "lustify NSFW v40 DMD2", 7, 1.5, 0, "euler", "normal", 0),
Lightning(MuseId, "museByStableYogi v3.0 Turbo GGUF", 8, 1, 0, "euler", "simple", 0),
Lightning(RealismV25Id, "realismByStableYogi v2.5 INT8 Turbo", 8, 1, 0, "euler", "simple", 0),
];
@@ -765,23 +761,25 @@ internal sealed class SwarmUiPresetDefinition
{
Id = "child",
Label = "Children",
Model = SwarmUiModelDefinition.DreamShaperId,
Model = SwarmUiModelDefinition.RealismV25Id,
Style =
"cinematic photo, child-friendly school portrait, soft natural features, gentle expression, neutral background, natural lighting, realistic, sharp focus",
"A cinematic school portrait photograph of a child with natural window light, a neutral background, soft natural features, a gentle expression, realistic skin, and sharp focus",
Negative =
"(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, nsfw, nude, naked, explicit, blurry, deformed, bad anatomy, logo",
"text, signature, watermark, extra limbs, deformed anatomy, extra fingers, cgi, nsfw, nude, naked, explicit",
Avatar = new SwarmUiKindPreset
{
Width = 1024,
Height = 1024,
ShotType = "close up, head and shoulders portrait of a child, facing the camera, upper body visible, soft features.",
ShotType =
"Close-up head and shoulders portrait of a child, facing the camera, upper body visible, with soft features",
},
Custom = new SwarmUiKindPreset { Width = 896, Height = 1152 },
FullBody = new SwarmUiKindPreset
{
Width = 896,
Height = 1152,
ShotType = "full body standing portrait of a child, head to toe visible, neutral pose, current outfit clearly visible.",
ShotType =
"Full-body standing portrait of a child from head to toe, neutral pose, current outfit clearly visible",
},
};
}
+5 -61
View File
@@ -1,62 +1,6 @@
{
"activePresetId": "default",
"models": [
{
"id": "babesByStableYogi_v4XLLightning.safetensors",
"label": "babesByStableYogi v4 XL Lightning",
"steps": 7,
"cfgScale": 1.5,
"clipSkip": 0,
"sampler": "euler",
"scheduler": "normal",
"seed": 0,
"positive": "",
"negative": "",
"positiveLoras": [],
"negativeLoras": []
},
{
"id": "DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors",
"label": "DreamShaper XL Lightning",
"steps": 4,
"cfgScale": 2,
"clipSkip": 2,
"sampler": "dpmpp_sde",
"scheduler": "karras",
"seed": 3346112079,
"positive": "",
"negative": "",
"positiveLoras": [],
"negativeLoras": []
},
{
"id": "epicrealismXL_VXIAbeast4SLightning.safetensors",
"label": "epicrealism XL Lightning",
"steps": 7,
"cfgScale": 1.5,
"clipSkip": 0,
"sampler": "euler",
"scheduler": "normal",
"seed": 0,
"positive": "",
"negative": "",
"positiveLoras": [],
"negativeLoras": []
},
{
"id": "lustifyNSFWCheckpoint_v40DMD2.safetensors",
"label": "lustify NSFW v40 DMD2",
"steps": 7,
"cfgScale": 1.5,
"clipSkip": 0,
"sampler": "euler",
"scheduler": "normal",
"seed": 0,
"positive": "",
"negative": "",
"positiveLoras": [],
"negativeLoras": []
},
{
"id": "museByStableYogi_v30TurboGguf.gguf",
"label": "museByStableYogi v3.0 Turbo GGUF",
@@ -112,13 +56,13 @@
{
"id": "child",
"label": "Children",
"model": "DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors",
"style": "cinematic photo, child-friendly school portrait, soft natural features, gentle expression, neutral background, natural lighting, realistic, sharp focus",
"negative": "(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, nsfw, nude, naked, explicit, blurry, deformed, bad anatomy, logo",
"model": "realismByStableYogi_v25INT8Turbo.safetensors",
"style": "A cinematic school portrait photograph of a child with natural window light, a neutral background, soft natural features, a gentle expression, realistic skin, and sharp focus",
"negative": "text, signature, watermark, extra limbs, deformed anatomy, extra fingers, cgi, nsfw, nude, naked, explicit",
"avatar": {
"width": 1024,
"height": 1024,
"shotType": "close up, head and shoulders portrait of a child, facing the camera, upper body visible, soft features."
"shotType": "Close-up head and shoulders portrait of a child, facing the camera, upper body visible, with soft features"
},
"custom": {
"width": 896,
@@ -128,7 +72,7 @@
"fullBody": {
"width": 896,
"height": 1152,
"shotType": "full body standing portrait of a child, head to toe visible, neutral pose, current outfit clearly visible."
"shotType": "Full-body standing portrait of a child from head to toe, neutral pose, current outfit clearly visible"
}
}
],
@@ -105,8 +105,14 @@ public class PortraitApiTests(AppHostFixture fixture)
Assert.NotNull(settings);
Assert.NotEmpty(settings!.Presets);
Assert.NotEmpty(settings.Models);
Assert.Contains(settings.Models, model => model.Id == "museByStableYogi_v30TurboGguf.gguf");
Assert.Contains(settings.Models, model => model.Id == "realismByStableYogi_v25INT8Turbo.safetensors");
var ids = settings.Models.Select(model => model.Id).ToList();
Assert.Equal(
["museByStableYogi_v30TurboGguf.gguf", "realismByStableYogi_v25INT8Turbo.safetensors"],
ids);
Assert.DoesNotContain("babesByStableYogi_v4XLLightning.safetensors", ids);
Assert.DoesNotContain("DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors", ids);
Assert.DoesNotContain("epicrealismXL_VXIAbeast4SLightning.safetensors", ids);
Assert.DoesNotContain("lustifyNSFWCheckpoint_v40DMD2.safetensors", ids);
Assert.False(string.IsNullOrWhiteSpace(settings.ActivePresetId));
}
@@ -127,13 +133,21 @@ public class PortraitApiTests(AppHostFixture fixture)
Assert.True(presets.TryGetProperty("models", out var models));
Assert.True(models.GetArrayLength() > 0);
var ids = models.EnumerateArray().Select(model => model.GetProperty("id").GetString()).ToList();
Assert.Contains("museByStableYogi_v30TurboGguf.gguf", ids);
Assert.Contains("realismByStableYogi_v25INT8Turbo.safetensors", ids);
Assert.Equal(
["museByStableYogi_v30TurboGguf.gguf", "realismByStableYogi_v25INT8Turbo.safetensors"],
ids);
Assert.DoesNotContain("babesByStableYogi_v4XLLightning.safetensors", ids);
Assert.DoesNotContain("DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors", ids);
var defaultPreset = presets.GetProperty("presets").EnumerateArray()
.First(preset => preset.GetProperty("id").GetString() == "default");
Assert.Equal(
"realismByStableYogi_v25INT8Turbo.safetensors",
defaultPreset.GetProperty("model").GetString());
var childPreset = presets.GetProperty("presets").EnumerateArray()
.First(preset => preset.GetProperty("id").GetString() == "child");
Assert.Equal(
"realismByStableYogi_v25INT8Turbo.safetensors",
childPreset.GetProperty("model").GetString());
}
[Fact]
@@ -12,9 +12,11 @@ public class SwarmUiSettingsStoreTests
var profile = config.Resolve(8, PortraitKind.Avatar);
Assert.Equal("child", profile.PresetId);
Assert.Equal(SwarmUiModelDefinition.DreamShaperId, profile.Model);
Assert.Equal(4, profile.Steps);
Assert.Equal(2, profile.CfgScale);
Assert.Equal(SwarmUiModelDefinition.RealismV25Id, profile.Model);
Assert.Equal(8, profile.Steps);
Assert.Equal(1, profile.CfgScale);
Assert.Equal("euler", profile.Sampler);
Assert.Equal("simple", profile.Scheduler);
}
[Fact]
@@ -157,8 +159,8 @@ public class SwarmUiSettingsStoreTests
true,
[
SwarmUiModelDefinition.BabesId,
SwarmUiModelDefinition.MuseId,
"not-in-config.safetensors",
SwarmUiModelDefinition.EpicRealismId,
],
[],
[],
@@ -167,10 +169,9 @@ public class SwarmUiSettingsStoreTests
var allowed = config.AllowedModelIds(discovery);
Assert.Equal(
[SwarmUiModelDefinition.BabesId, SwarmUiModelDefinition.EpicRealismId],
allowed);
Assert.Equal([SwarmUiModelDefinition.MuseId], allowed);
Assert.DoesNotContain("not-in-config.safetensors", allowed);
Assert.DoesNotContain(SwarmUiModelDefinition.BabesId, allowed);
Assert.DoesNotContain(SwarmUiModelDefinition.DreamShaperId, allowed);
}
@@ -204,12 +205,13 @@ public class SwarmUiSettingsStoreTests
var config = SwarmUiConfigFile.CreateDefault();
var ids = config.Models.Select(model => model.Id).ToList();
Assert.Contains(SwarmUiModelDefinition.BabesId, ids);
Assert.Contains(SwarmUiModelDefinition.DreamShaperId, ids);
Assert.Contains(SwarmUiModelDefinition.EpicRealismId, ids);
Assert.Contains(SwarmUiModelDefinition.LustifyId, ids);
Assert.Contains(SwarmUiModelDefinition.MuseId, ids);
Assert.Contains(SwarmUiModelDefinition.RealismV25Id, ids);
Assert.Equal(
[SwarmUiModelDefinition.MuseId, SwarmUiModelDefinition.RealismV25Id],
ids);
Assert.DoesNotContain(SwarmUiModelDefinition.BabesId, ids);
Assert.DoesNotContain(SwarmUiModelDefinition.DreamShaperId, ids);
Assert.DoesNotContain(SwarmUiModelDefinition.EpicRealismId, ids);
Assert.DoesNotContain(SwarmUiModelDefinition.LustifyId, ids);
Assert.DoesNotContain("realismByStableYogi_v20TurboGGUF.gguf", ids);
var realism = Assert.Single(config.Models, model => model.Id == SwarmUiModelDefinition.RealismV25Id);
@@ -230,7 +232,24 @@ public class SwarmUiSettingsStoreTests
Assert.Contains("photograph", adult.Style, StringComparison.OrdinalIgnoreCase);
var child = Assert.Single(config.Presets, preset => preset.Id == "child");
Assert.Equal(SwarmUiModelDefinition.DreamShaperId, child.Model);
Assert.Equal(SwarmUiModelDefinition.RealismV25Id, child.Model);
}
[Fact]
public void CreateChild_StyleAndNegativeAreProseWithoutSdWeights()
{
var child = SwarmUiPresetDefinition.CreateChild();
Assert.NotNull(child.Avatar);
Assert.NotNull(child.FullBody);
Assert.DoesNotContain("(low quality", child.Style, StringComparison.Ordinal);
Assert.DoesNotContain(":1.4)", child.Style, StringComparison.Ordinal);
Assert.DoesNotContain("(low quality", child.Negative, StringComparison.Ordinal);
Assert.DoesNotContain(":1.4)", child.Negative, StringComparison.Ordinal);
Assert.DoesNotContain("(low quality", child.Avatar.ShotType, StringComparison.Ordinal);
Assert.DoesNotContain(":1.4)", child.Avatar.ShotType, StringComparison.Ordinal);
Assert.DoesNotContain("(low quality", child.FullBody.ShotType, StringComparison.Ordinal);
Assert.DoesNotContain(":1.4)", child.FullBody.ShotType, StringComparison.Ordinal);
}
[Fact]
@@ -267,6 +286,72 @@ public class SwarmUiSettingsStoreTests
Assert.Equal(expected.Presets[0].Negative, loaded.Presets[0].Negative);
Assert.Equal(expected.Presets[0].Avatar?.ShotType, loaded.Presets[0].Avatar?.ShotType);
Assert.Equal(expected.Presets[1].Model, loaded.Presets[1].Model);
Assert.Equal(expected.Presets[1].Style, loaded.Presets[1].Style);
Assert.Equal(expected.Presets[1].Negative, loaded.Presets[1].Negative);
}
[Fact]
public void NormalizeAfterLoad_DoesNotRewriteExistingSdxlCatalog()
{
var living = new SwarmUiConfigFile
{
ActivePresetId = "default",
Models =
[
new SwarmUiModelDefinition
{
Id = SwarmUiModelDefinition.BabesId,
Label = "babes",
Steps = 7,
CfgScale = 1.5,
Sampler = "euler",
Scheduler = "normal",
},
new SwarmUiModelDefinition
{
Id = SwarmUiModelDefinition.DreamShaperId,
Label = "DreamShaper",
Steps = 4,
CfgScale = 2,
Sampler = "dpmpp_sde",
Scheduler = "karras",
},
],
Presets =
[
new SwarmUiPresetDefinition
{
Id = "default",
Label = "Default",
Model = SwarmUiModelDefinition.BabesId,
Avatar = new SwarmUiKindPreset { Width = 1024, Height = 1024 },
Custom = new SwarmUiKindPreset { Width = 896, Height = 1152 },
FullBody = new SwarmUiKindPreset { Width = 896, Height = 1152 },
},
new SwarmUiPresetDefinition
{
Id = "child",
Label = "Children",
Model = SwarmUiModelDefinition.DreamShaperId,
Avatar = new SwarmUiKindPreset { Width = 1024, Height = 1024 },
Custom = new SwarmUiKindPreset { Width = 896, Height = 1152 },
FullBody = new SwarmUiKindPreset { Width = 896, Height = 1152 },
},
],
AgeRules = [new SwarmUiAgeRule { MinAge = 6, MaxAge = 11, PresetId = "child" }],
};
living.NormalizeAfterLoad();
Assert.Equal(
[SwarmUiModelDefinition.BabesId, SwarmUiModelDefinition.DreamShaperId],
living.Models.Select(model => model.Id));
Assert.DoesNotContain(living.Models, model => model.Id == SwarmUiModelDefinition.MuseId);
Assert.DoesNotContain(living.Models, model => model.Id == SwarmUiModelDefinition.RealismV25Id);
Assert.Equal(SwarmUiModelDefinition.BabesId, living.Presets[0].Model);
Assert.Equal(SwarmUiModelDefinition.DreamShaperId, living.Presets[1].Model);
Assert.Equal("child", living.Resolve(8, PortraitKind.Avatar).PresetId);
Assert.Equal(SwarmUiModelDefinition.DreamShaperId, living.Resolve(8, PortraitKind.Avatar).Model);
}
[Fact]