diff --git a/docs/phases/11-events/63-krea-only.md b/docs/phases/11-events/63-krea-only.md index cd7ac8e..8b864af 100644 --- a/docs/phases/11-events/63-krea-only.md +++ b/docs/phases/11-events/63-krea-only.md @@ -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 в каталоге ## Критерий готовности diff --git a/docs/phases/11-events/README.md b/docs/phases/11-events/README.md index f28196c..eeab07a 100644 --- a/docs/phases/11-events/README.md +++ b/docs/phases/11-events/README.md @@ -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 — лента.** Факт, тост, пауза. diff --git a/docs/protocol.md b/docs/protocol.md index c148269..1c45235 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -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 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 6–11 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` diff --git a/src/HSchool.Server/Game/SwarmUiSettingsStore.cs b/src/HSchool.Server/Game/SwarmUiSettingsStore.cs index 295a439..93792b9 100644 --- a/src/HSchool.Server/Game/SwarmUiSettingsStore.cs +++ b/src/HSchool.Server/Game/SwarmUiSettingsStore.cs @@ -527,10 +527,6 @@ internal sealed class SwarmUiModelDefinition public static List 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", }, }; } diff --git a/src/HSchool.Server/swarmui.json b/src/HSchool.Server/swarmui.json index 6b165e1..61479ce 100644 --- a/src/HSchool.Server/swarmui.json +++ b/src/HSchool.Server/swarmui.json @@ -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" } } ], diff --git a/tests/HSchool.AppHost.Tests/PortraitApiTests.cs b/tests/HSchool.AppHost.Tests/PortraitApiTests.cs index 5f67cad..83ed029 100644 --- a/tests/HSchool.AppHost.Tests/PortraitApiTests.cs +++ b/tests/HSchool.AppHost.Tests/PortraitApiTests.cs @@ -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] diff --git a/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs b/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs index 402e6f5..2128a78 100644 --- a/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs +++ b/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs @@ -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]