Merge branch 'review/off-queue-62'

This commit is contained in:
Leonid Pershin
2026-08-20 17:10:49 +03:00
5 changed files with 135 additions and 3 deletions
+16 -1
View File
@@ -1,6 +1,6 @@
# Проверенные фазы «Вне очереди» # Проверенные фазы «Вне очереди»
Только портреты (6061). Соседей по папке этот журнал не закрывает. Портреты 6062. Соседей по папке (уроки, DX) этот журнал не закрывает.
## Портреты. Модели, слои промпта, LoRA и embeddings ## Портреты. Модели, слои промпта, LoRA и embeddings
@@ -20,3 +20,18 @@ Discovery читает `models.Embedding` (офлайн — пустой спи
### Открытое ### Открытое
Ничего, что требовало бы решения. `swarmui.json` на диске не пишет ключи embeddings — после `NormalizeAfterLoad` списки пустые, как в `Catalog()`. Ничего, что требовало бы решения. `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+` в этом проходе не гонялась.
+1
View File
@@ -34,3 +34,4 @@
| Срез 10. Этап A — страховка | `e2e7d30` | [10-craft](10-craft/reviewed.md) | | Срез 10. Этап A — страховка | `e2e7d30` | [10-craft](10-craft/reviewed.md) |
| Срез 10. Этапы B–C — работник, карточка и дамп | `f4f726a` | [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) | | Портреты (вне очереди) 60–61 | `5e23d1f` | [off-queue](off-queue/reviewed.md) |
| Портреты (вне очереди) 62 | `cd68d2e` | [off-queue](off-queue/reviewed.md) |
@@ -1,6 +1,7 @@
using System.Net; using System.Net;
using System.Net.Http.Json; using System.Net.Http.Json;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
namespace HSchool.AppHost.Tests; namespace HSchool.AppHost.Tests;
@@ -86,7 +87,7 @@ public class PortraitApiTests(AppHostFixture fixture)
Assert.NotNull(payload); Assert.NotNull(payload);
Assert.Equal("avatar", payload!.Kind); Assert.Equal("avatar", payload!.Kind);
Assert.Contains("years old", payload.Positive, StringComparison.OrdinalIgnoreCase); 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.Negative));
Assert.False(string.IsNullOrWhiteSpace(payload.PresetId)); Assert.False(string.IsNullOrWhiteSpace(payload.PresetId));
} }
@@ -15,7 +15,7 @@ public class PortraitPromptBuilderTests
var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar); var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar);
Assert.Contains("12 years old", positive, StringComparison.Ordinal); 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("wearing a white shirt", positive, StringComparison.OrdinalIgnoreCase);
Assert.Contains("Head and shoulders.", positive, StringComparison.Ordinal); Assert.Contains("Head and shoulders.", positive, StringComparison.Ordinal);
Assert.DoesNotContain("age 12", positive, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("age 12", positive, StringComparison.OrdinalIgnoreCase);
@@ -1,3 +1,4 @@
using System.Text.Json;
using HSchool.Server.Game; using HSchool.Server.Game;
namespace HSchool.Server.Tests; namespace HSchool.Server.Tests;
@@ -222,5 +223,119 @@ public class SwarmUiSettingsStoreTests
Assert.Equal(1, muse.CfgScale); Assert.Equal(1, muse.CfgScale);
Assert.Equal("euler", muse.Sampler); Assert.Equal("euler", muse.Sampler);
Assert.Equal("simple", muse.Scheduler); 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<SwarmUiConfigFile>(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,
};
} }