Merge branch 'phase/63-krea-only'

# Conflicts:
#	docs/phases/11-events/README.md
This commit is contained in:
Leonid Pershin
2026-08-21 01:04:49 +03:00
7 changed files with 147 additions and 101 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()` совпадает с файлом babes, epicrealism, lustify и DreamShaper убрать. `Catalog()` совпадает с файлом
- [ ] Пресеты `default` и `child` — Realism v2.5. Стиль и shotType `child` — проза без - [x] Пресеты `default` и `child` — Realism v2.5. Стиль и shotType `child` — проза без
`(word:1.4)` и SD-весов `(word:1.4)` и SD-весов
- [ ] `ageRules` 6–11 по-прежнему ведут на `child`, но модель у child — v2.5 - [x] `ageRules` 6–11 по-прежнему ведут на `child`, но модель у child — v2.5
- [ ] Живые школы не лифтить. Сокет не бампить. PNG не перегенерировать - [x] Живые школы не лифтить. Сокет не бампить. PNG не перегенерировать
- [ ] [`protocol.md`](../../protocol.md) — описание каталога без четырёх SDXL, тем же коммитом - [x] [`protocol.md`](../../protocol.md) — описание каталога без четырёх SDXL, тем же коммитом
(версия сокета не меняется) (версия сокета не меняется)
## Тесты, без которых фаза не закрыта ## Тесты, без которых фаза не закрыта
- [ ] `CreateDefault` содержит ровно два id: Muse и Realism v2.5; нет DreamShaper и babes - [x] `CreateDefault` содержит ровно два id: Muse и Realism v2.5; нет DreamShaper и babes
- [ ] Resolve взрослого и ребёнка (6–11) берут Realism v2.5 - [x] Resolve взрослого и ребёнка (6–11) берут Realism v2.5
- [ ] Стиль/негатив child без `(low quality` и без `:1.4)` - [x] Стиль/негатив child без `(low quality` и без `:1.4)`
- [ ] `GET /api/settings/swarmui` отдаёт только Krea-id; create копирует их в сейв новой школы - [x] `GET /api/settings/swarmui` отдаёт только Krea-id; create копирует их в сейв новой школы
- [ ] Нормализация загрузки не переписывает `models` уже лежащей школы с SDXL в каталоге - [x] Нормализация загрузки не переписывает `models` уже лежащей школы с SDXL в каталоге
## Критерий готовности ## Критерий готовности
+1 -1
View File
@@ -12,7 +12,7 @@
| Фаза | Статус | Зачем | | Фаза | Статус | Зачем |
| --- | --- | --- | | --- | --- | --- |
| [63. Каталог Krea 2](63-krea-only.md) | 🔄 | Muse + v2.5, child на v2.5, без лифта | | [63. Каталог Krea 2](63-krea-only.md) | | Muse + v2.5, child на v2.5, без лифта |
**Этап B — лента.** Факт, тост, пауза. **Этап B — лента.** Факт, тост, пауза.
+7 -2
View File
@@ -539,9 +539,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 (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 and embedding lists), `activePresetId` and `ageRules`. LoRA and embeddings on the model, preset and
shot kind concatenate at generate time. 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 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 its save as `portraitSettings`. Living schools generate from that copy, not from this file — a save
saves without `models` lift on load (`positive` `style`, kind `positive``shotType`). that still lists SDXL models keeps them. Older saves without `models` lift on load (`positive`
`style`, kind `positive``shotType`).
### `PUT /api/settings/swarmui` ### `PUT /api/settings/swarmui`
@@ -527,10 +527,6 @@ internal sealed class SwarmUiModelDefinition
public static List<SwarmUiModelDefinition> Catalog() => 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(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), Lightning(RealismV25Id, "realismByStableYogi v2.5 INT8 Turbo", 8, 1, 0, "euler", "simple", 0),
]; ];
@@ -765,23 +761,25 @@ internal sealed class SwarmUiPresetDefinition
{ {
Id = "child", Id = "child",
Label = "Children", Label = "Children",
Model = SwarmUiModelDefinition.DreamShaperId, Model = SwarmUiModelDefinition.RealismV25Id,
Style = 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 = 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 Avatar = new SwarmUiKindPreset
{ {
Width = 1024, Width = 1024,
Height = 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 }, Custom = new SwarmUiKindPreset { Width = 896, Height = 1152 },
FullBody = new SwarmUiKindPreset FullBody = new SwarmUiKindPreset
{ {
Width = 896, Width = 896,
Height = 1152, 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", "activePresetId": "default",
"models": [ "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", "id": "museByStableYogi_v30TurboGguf.gguf",
"label": "museByStableYogi v3.0 Turbo GGUF", "label": "museByStableYogi v3.0 Turbo GGUF",
@@ -112,13 +56,13 @@
{ {
"id": "child", "id": "child",
"label": "Children", "label": "Children",
"model": "DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors", "model": "realismByStableYogi_v25INT8Turbo.safetensors",
"style": "cinematic photo, child-friendly school portrait, soft natural features, gentle expression, neutral background, natural lighting, realistic, sharp focus", "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": "(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, nsfw, nude, naked, explicit, blurry, deformed, bad anatomy, logo", "negative": "text, signature, watermark, extra limbs, deformed anatomy, extra fingers, cgi, nsfw, nude, naked, explicit",
"avatar": { "avatar": {
"width": 1024, "width": 1024,
"height": 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": { "custom": {
"width": 896, "width": 896,
@@ -128,7 +72,7 @@
"fullBody": { "fullBody": {
"width": 896, "width": 896,
"height": 1152, "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.NotNull(settings);
Assert.NotEmpty(settings!.Presets); Assert.NotEmpty(settings!.Presets);
Assert.NotEmpty(settings.Models); Assert.NotEmpty(settings.Models);
Assert.Contains(settings.Models, model => model.Id == "museByStableYogi_v30TurboGguf.gguf"); var ids = settings.Models.Select(model => model.Id).ToList();
Assert.Contains(settings.Models, model => model.Id == "realismByStableYogi_v25INT8Turbo.safetensors"); 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)); 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(presets.TryGetProperty("models", out var models));
Assert.True(models.GetArrayLength() > 0); Assert.True(models.GetArrayLength() > 0);
var ids = models.EnumerateArray().Select(model => model.GetProperty("id").GetString()).ToList(); var ids = models.EnumerateArray().Select(model => model.GetProperty("id").GetString()).ToList();
Assert.Contains("museByStableYogi_v30TurboGguf.gguf", ids); Assert.Equal(
Assert.Contains("realismByStableYogi_v25INT8Turbo.safetensors", ids); ["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() var defaultPreset = presets.GetProperty("presets").EnumerateArray()
.First(preset => preset.GetProperty("id").GetString() == "default"); .First(preset => preset.GetProperty("id").GetString() == "default");
Assert.Equal( Assert.Equal(
"realismByStableYogi_v25INT8Turbo.safetensors", "realismByStableYogi_v25INT8Turbo.safetensors",
defaultPreset.GetProperty("model").GetString()); 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] [Fact]
@@ -12,9 +12,11 @@ public class SwarmUiSettingsStoreTests
var profile = config.Resolve(8, PortraitKind.Avatar); var profile = config.Resolve(8, PortraitKind.Avatar);
Assert.Equal("child", profile.PresetId); Assert.Equal("child", profile.PresetId);
Assert.Equal(SwarmUiModelDefinition.DreamShaperId, profile.Model); Assert.Equal(SwarmUiModelDefinition.RealismV25Id, profile.Model);
Assert.Equal(4, profile.Steps); Assert.Equal(8, profile.Steps);
Assert.Equal(2, profile.CfgScale); Assert.Equal(1, profile.CfgScale);
Assert.Equal("euler", profile.Sampler);
Assert.Equal("simple", profile.Scheduler);
} }
[Fact] [Fact]
@@ -157,8 +159,8 @@ public class SwarmUiSettingsStoreTests
true, true,
[ [
SwarmUiModelDefinition.BabesId, SwarmUiModelDefinition.BabesId,
SwarmUiModelDefinition.MuseId,
"not-in-config.safetensors", "not-in-config.safetensors",
SwarmUiModelDefinition.EpicRealismId,
], ],
[], [],
[], [],
@@ -167,10 +169,9 @@ public class SwarmUiSettingsStoreTests
var allowed = config.AllowedModelIds(discovery); var allowed = config.AllowedModelIds(discovery);
Assert.Equal( Assert.Equal([SwarmUiModelDefinition.MuseId], allowed);
[SwarmUiModelDefinition.BabesId, SwarmUiModelDefinition.EpicRealismId],
allowed);
Assert.DoesNotContain("not-in-config.safetensors", allowed); Assert.DoesNotContain("not-in-config.safetensors", allowed);
Assert.DoesNotContain(SwarmUiModelDefinition.BabesId, allowed);
Assert.DoesNotContain(SwarmUiModelDefinition.DreamShaperId, allowed); Assert.DoesNotContain(SwarmUiModelDefinition.DreamShaperId, allowed);
} }
@@ -204,12 +205,13 @@ public class SwarmUiSettingsStoreTests
var config = SwarmUiConfigFile.CreateDefault(); var config = SwarmUiConfigFile.CreateDefault();
var ids = config.Models.Select(model => model.Id).ToList(); var ids = config.Models.Select(model => model.Id).ToList();
Assert.Contains(SwarmUiModelDefinition.BabesId, ids); Assert.Equal(
Assert.Contains(SwarmUiModelDefinition.DreamShaperId, ids); [SwarmUiModelDefinition.MuseId, SwarmUiModelDefinition.RealismV25Id],
Assert.Contains(SwarmUiModelDefinition.EpicRealismId, ids); ids);
Assert.Contains(SwarmUiModelDefinition.LustifyId, ids); Assert.DoesNotContain(SwarmUiModelDefinition.BabesId, ids);
Assert.Contains(SwarmUiModelDefinition.MuseId, ids); Assert.DoesNotContain(SwarmUiModelDefinition.DreamShaperId, ids);
Assert.Contains(SwarmUiModelDefinition.RealismV25Id, ids); Assert.DoesNotContain(SwarmUiModelDefinition.EpicRealismId, ids);
Assert.DoesNotContain(SwarmUiModelDefinition.LustifyId, ids);
Assert.DoesNotContain("realismByStableYogi_v20TurboGGUF.gguf", ids); Assert.DoesNotContain("realismByStableYogi_v20TurboGGUF.gguf", ids);
var realism = Assert.Single(config.Models, model => model.Id == SwarmUiModelDefinition.RealismV25Id); 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); Assert.Contains("photograph", adult.Style, StringComparison.OrdinalIgnoreCase);
var child = Assert.Single(config.Presets, preset => preset.Id == "child"); 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] [Fact]
@@ -267,6 +286,72 @@ public class SwarmUiSettingsStoreTests
Assert.Equal(expected.Presets[0].Negative, loaded.Presets[0].Negative); 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[0].Avatar?.ShotType, loaded.Presets[0].Avatar?.ShotType);
Assert.Equal(expected.Presets[1].Model, loaded.Presets[1].Model); 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] [Fact]