From b188deef943d7e53daafbabdca6fa1c150b7346a Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 17:09:13 +0300 Subject: [PATCH 1/2] Record phase 62 portrait-prompt-craft as reviewed. Co-authored-by: Cursor --- docs/phases/off-queue/reviewed.md | 17 ++- docs/phases/reviewed.md | 1 + .../HSchool.AppHost.Tests/PortraitApiTests.cs | 3 +- .../PortraitPromptBuilderTests.cs | 2 +- .../SwarmUiSettingsStoreTests.cs | 115 ++++++++++++++++++ 5 files changed, 135 insertions(+), 3 deletions(-) diff --git a/docs/phases/off-queue/reviewed.md b/docs/phases/off-queue/reviewed.md index f64be68..a282028 100644 --- a/docs/phases/off-queue/reviewed.md +++ b/docs/phases/off-queue/reviewed.md @@ -1,6 +1,6 @@ # Проверенные фазы «Вне очереди» -Только портреты (60–61). Соседей по папке этот журнал не закрывает. +Портреты 60–62. Соседей по папке (уроки, DX) этот журнал не закрывает. ## Портреты. Модели, слои промпта, LoRA и embeddings @@ -20,3 +20,18 @@ Discovery читает `models.Embedding` (офлайн — пустой спи ### Открытое Ничего, что требовало бы решения. `swarmui.json` на диске не пишет ключи embeddings — после `NormalizeAfterLoad` списки пустые, как в `Catalog()`. + +## Портреты. Промпт и Krea-модели + +- **Фазы:** 62 +- **Проверен на:** `cd68d2e`, 2026-08-20 +- **Пути:** `src/HSchool.Server/Game/PortraitPromptBuilder.cs`, `src/HSchool.Server/Game/SwarmUiSettingsStore.cs`, `src/HSchool.Server/swarmui.json`, `tests/HSchool.Server.Tests/PortraitPromptBuilderTests.cs`, `tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs`, `tests/HSchool.AppHost.Tests/PortraitApiTests.cs`, `docs/design/off-queue/portrait-prompt-craft.md` +- **Итог:** обещания фазы 62 совпадают с кодом. Сокет не бампили (`ProtocolConstants.Version` / `PROTOCOL_VERSION` = 9). Realism v2.0 в каталог не входит. Живые сейвы при `Clone` не получают Muse/v2.5. Позы в UI нет. Дописаны сверка `swarmui.json` с `Catalog()`, отказ лифтить старую копию и проза `with black hair`. Хостовые `PortraitApiTests` в этом проходе не гонялись (общий AppHost / живой сервер). Расхождений с дизайном нет. + +### Фаза 62 + +Каталог: четыре SDXL плюс Muse GGUF и Realism v2.5 INT8 с knobs 8 / CFG 1 / euler / simple; `swarmui.json` совпадает с `CreateDefault()`. Пресет `default` — v2.5, стиль и shotType прозой, negative без `(word:1.4)`; child — DreamShaper. Внешность и одежда: `N years old`, `with black hair`, `wearing a white shirt`; слои и custom extra прежние. + +### Открытое + +Хостовый `GetPortraitPrompt_ReturnsBuiltPositiveAndNegative` больше не ищет подстроку `age ` (она есть в `teenage`); проверка `\bage \d+` в этом проходе не гонялась. diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index 16ae638..63474dc 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -34,3 +34,4 @@ | Срез 10. Этап A — страховка | `e2e7d30` | [10-craft](10-craft/reviewed.md) | | Срез 10. Этапы B–C — работник, карточка и дамп | `f4f726a` | [10-craft](10-craft/reviewed.md) | | Портреты (вне очереди) 60–61 | `5e23d1f` | [off-queue](off-queue/reviewed.md) | +| Портреты (вне очереди) 62 | `cd68d2e` | [off-queue](off-queue/reviewed.md) | diff --git a/tests/HSchool.AppHost.Tests/PortraitApiTests.cs b/tests/HSchool.AppHost.Tests/PortraitApiTests.cs index 268ba3c..5f67cad 100644 --- a/tests/HSchool.AppHost.Tests/PortraitApiTests.cs +++ b/tests/HSchool.AppHost.Tests/PortraitApiTests.cs @@ -1,6 +1,7 @@ using System.Net; using System.Net.Http.Json; using System.Text.Json; +using System.Text.RegularExpressions; namespace HSchool.AppHost.Tests; @@ -86,7 +87,7 @@ public class PortraitApiTests(AppHostFixture fixture) Assert.NotNull(payload); Assert.Equal("avatar", payload!.Kind); Assert.Contains("years old", payload.Positive, StringComparison.OrdinalIgnoreCase); - Assert.DoesNotContain("age ", payload.Positive, StringComparison.OrdinalIgnoreCase); + Assert.False(Regex.IsMatch(payload.Positive, @"\bage \d+")); Assert.False(string.IsNullOrWhiteSpace(payload.Negative)); Assert.False(string.IsNullOrWhiteSpace(payload.PresetId)); } diff --git a/tests/HSchool.Server.Tests/PortraitPromptBuilderTests.cs b/tests/HSchool.Server.Tests/PortraitPromptBuilderTests.cs index 56a9b36..0c695c4 100644 --- a/tests/HSchool.Server.Tests/PortraitPromptBuilderTests.cs +++ b/tests/HSchool.Server.Tests/PortraitPromptBuilderTests.cs @@ -15,7 +15,7 @@ public class PortraitPromptBuilderTests var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar); Assert.Contains("12 years old", positive, StringComparison.Ordinal); - Assert.Contains("black hair", positive, StringComparison.OrdinalIgnoreCase); + Assert.Contains("with black hair", positive, StringComparison.OrdinalIgnoreCase); Assert.Contains("wearing a white shirt", positive, StringComparison.OrdinalIgnoreCase); Assert.Contains("Head and shoulders.", positive, StringComparison.Ordinal); Assert.DoesNotContain("age 12", positive, StringComparison.OrdinalIgnoreCase); diff --git a/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs b/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs index fa028ed..402e6f5 100644 --- a/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs +++ b/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs @@ -1,3 +1,4 @@ +using System.Text.Json; using HSchool.Server.Game; namespace HSchool.Server.Tests; @@ -222,5 +223,119 @@ public class SwarmUiSettingsStoreTests Assert.Equal(1, muse.CfgScale); Assert.Equal("euler", muse.Sampler); Assert.Equal("simple", muse.Scheduler); + + var adult = Assert.Single(config.Presets, preset => preset.Id == "default"); + Assert.Equal(SwarmUiModelDefinition.RealismV25Id, adult.Model); + Assert.DoesNotContain(":1.4)", adult.Negative, StringComparison.Ordinal); + Assert.Contains("photograph", adult.Style, StringComparison.OrdinalIgnoreCase); + + var child = Assert.Single(config.Presets, preset => preset.Id == "child"); + Assert.Equal(SwarmUiModelDefinition.DreamShaperId, child.Model); } + + [Fact] + public void SwarmUiJson_MatchesCatalogAndDefaultPresets() + { + var path = Path.Combine(RepoRoot(), "src", "HSchool.Server", "swarmui.json"); + Assert.True(File.Exists(path), path); + var loaded = JsonSerializer.Deserialize(File.ReadAllText(path), JsonOptions); + if (loaded is null) + { + Assert.Fail("swarmui.json did not deserialize."); + return; + } + + loaded.NormalizeAfterLoad(); + + var expected = SwarmUiConfigFile.CreateDefault(); + Assert.Equal(expected.Models.Select(model => model.Id), loaded.Models.Select(model => model.Id)); + Assert.DoesNotContain(loaded.Models, model => model.Id.Contains("v20Turbo", StringComparison.OrdinalIgnoreCase)); + + for (var i = 0; i < expected.Models.Count; i++) + { + Assert.Equal(expected.Models[i].Label, loaded.Models[i].Label); + Assert.Equal(expected.Models[i].Steps, loaded.Models[i].Steps); + Assert.Equal(expected.Models[i].CfgScale, loaded.Models[i].CfgScale); + Assert.Equal(expected.Models[i].Sampler, loaded.Models[i].Sampler); + Assert.Equal(expected.Models[i].Scheduler, loaded.Models[i].Scheduler); + Assert.Equal(expected.Models[i].Seed, loaded.Models[i].Seed); + } + + Assert.Equal(expected.Presets.Select(preset => preset.Id), loaded.Presets.Select(preset => preset.Id)); + Assert.Equal(expected.Presets[0].Model, loaded.Presets[0].Model); + Assert.Equal(expected.Presets[0].Style, loaded.Presets[0].Style); + 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); + } + + [Fact] + public void Clone_DoesNotInjectKreaModelsIntoAnExistingSchoolCopy() + { + var living = new SwarmUiConfigFile + { + ActivePresetId = "default", + Models = + [ + new SwarmUiModelDefinition + { + Id = SwarmUiModelDefinition.BabesId, + Steps = 7, + CfgScale = 1.5, + Sampler = "euler", + Scheduler = "normal", + }, + new SwarmUiModelDefinition + { + Id = SwarmUiModelDefinition.DreamShaperId, + 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 }, + }, + ], + }; + + var copy = SwarmUiConfigFile.Clone(living); + + Assert.Equal(2, copy.Models.Count); + Assert.DoesNotContain(copy.Models, model => model.Id == SwarmUiModelDefinition.MuseId); + Assert.DoesNotContain(copy.Models, model => model.Id == SwarmUiModelDefinition.RealismV25Id); + Assert.Equal(SwarmUiModelDefinition.BabesId, copy.Presets[0].Model); + Assert.Equal("default", copy.ActivePresetId); + } + + private static string RepoRoot() + { + var dir = new DirectoryInfo(AppContext.BaseDirectory); + while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "h-school.sln"))) + { + dir = dir.Parent; + } + + if (dir is null) + { + throw new InvalidOperationException("Could not find h-school.sln from the test output directory."); + } + + return dir.FullName; + } + + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + PropertyNameCaseInsensitive = true, + }; } From b2b56d7af91283d2f09c51e2e7bc00a1d8e01a4d Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Thu, 20 Aug 2026 17:11:27 +0300 Subject: [PATCH 2/2] Point phase 62 review journal at the merge. Co-authored-by: Cursor --- docs/phases/off-queue/reviewed.md | 2 +- docs/phases/reviewed.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/phases/off-queue/reviewed.md b/docs/phases/off-queue/reviewed.md index a282028..4955696 100644 --- a/docs/phases/off-queue/reviewed.md +++ b/docs/phases/off-queue/reviewed.md @@ -24,7 +24,7 @@ Discovery читает `models.Embedding` (офлайн — пустой спи ## Портреты. Промпт и Krea-модели - **Фазы:** 62 -- **Проверен на:** `cd68d2e`, 2026-08-20 +- **Проверен на:** `d093ea3`, 2026-08-20 - **Пути:** `src/HSchool.Server/Game/PortraitPromptBuilder.cs`, `src/HSchool.Server/Game/SwarmUiSettingsStore.cs`, `src/HSchool.Server/swarmui.json`, `tests/HSchool.Server.Tests/PortraitPromptBuilderTests.cs`, `tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs`, `tests/HSchool.AppHost.Tests/PortraitApiTests.cs`, `docs/design/off-queue/portrait-prompt-craft.md` - **Итог:** обещания фазы 62 совпадают с кодом. Сокет не бампили (`ProtocolConstants.Version` / `PROTOCOL_VERSION` = 9). Realism v2.0 в каталог не входит. Живые сейвы при `Clone` не получают Muse/v2.5. Позы в UI нет. Дописаны сверка `swarmui.json` с `Catalog()`, отказ лифтить старую копию и проза `with black hair`. Хостовые `PortraitApiTests` в этом проходе не гонялись (общий AppHost / живой сервер). Расхождений с дизайном нет. diff --git a/docs/phases/reviewed.md b/docs/phases/reviewed.md index 63474dc..2be9336 100644 --- a/docs/phases/reviewed.md +++ b/docs/phases/reviewed.md @@ -34,4 +34,4 @@ | Срез 10. Этап A — страховка | `e2e7d30` | [10-craft](10-craft/reviewed.md) | | Срез 10. Этапы B–C — работник, карточка и дамп | `f4f726a` | [10-craft](10-craft/reviewed.md) | | Портреты (вне очереди) 60–61 | `5e23d1f` | [off-queue](off-queue/reviewed.md) | -| Портреты (вне очереди) 62 | `cd68d2e` | [off-queue](off-queue/reviewed.md) | +| Портреты (вне очереди) 62 | `d093ea3` | [off-queue](off-queue/reviewed.md) |