diff --git a/docs/phases/off-queue/reviewed.md b/docs/phases/off-queue/reviewed.md new file mode 100644 index 0000000..a24cdfe --- /dev/null +++ b/docs/phases/off-queue/reviewed.md @@ -0,0 +1,22 @@ +# Проверенные фазы «Вне очереди» + +Только портреты (60–61). Соседей по папке этот журнал не закрывает. + +## Портреты. Модели, слои промпта, LoRA и embeddings + +- **Фазы:** 60–61 +- **Проверен на:** `d42af25`, 2026-08-20 +- **Пути:** `src/HSchool.Server/Game/SwarmUiSettingsStore.cs`, `src/HSchool.Server/Game/SwarmUiLoraFormatter.cs`, `src/HSchool.Server/Game/SwarmUiClient.cs`, `src/HSchool.Server/Game/SwarmUiDiscovery.cs`, `src/HSchool.Server/Game/PortraitPromptBuilder.cs`, `src/HSchool.Server/Game/PortraitService.cs`, `src/HSchool.Server/Api/SettingsEndpoints.cs`, `src/HSchool.Server/swarmui.json`, `src/HSchool.Client/src/ui/swarmUiSettingsDialog.ts`, `src/HSchool.Client/src/net/api.ts`, `tests/HSchool.Server.Tests`, `tests/HSchool.AppHost.Tests/PortraitApiTests.cs`, `docs/protocol.md`, `docs/design/off-queue/portrait-models.md`, `docs/design/off-queue/portrait-loras-embeds.md` +- **Итог:** обещания фаз 60–61 совпадают с кодом и тестами; сокет не бампили (`ProtocolConstants.Version` / `PROTOCOL_VERSION` = 9). Дописаны проверки cfg без оверрайда, склейки negative и смены модели в диалоге. Хостовые `PortraitApiTests` в этом проходе не гонялись (общий AppHost / живой сервер). Расхождений с дизайном нет. + +### Фаза 60 + +Лифт `positive` → `style` и kind `positive` → `shotType`, пикер Swarm ∩ каталог, слои промпта (модель → стиль → кадр → `promptExtra` → пустая поза → внешность → одежда), HTTP `GET`/`PUT /api/settings/swarmui` с `models` и копией в сейв при create — на месте. Resolve без оверрайда берёт steps/cfg с модели; с оверрайдом — с пресета. + +### Фаза 61 + +Discovery читает `models.Embedding` (офлайн — пустой список). LoRA и embeddings склеиваются модель → пресет → кадр. Промпт содержит `` и не содержит ` { expect(values).not.toContain('other.safetensors'); void opened; }); + + it('shows the selected model generation defaults after a model change', async () => { + vi.mocked(fetchSwarmUiDiscovery).mockResolvedValue( + discovery({ models: ['template.safetensors', 'other.safetensors'] }), + ); + const opened = swarmUiSettingsDialog(settings()); + const dialog = await vi.waitFor(() => { + const node = document.querySelector('dialog'); + const label = node === null + ? undefined + : [...node.querySelectorAll('.field__label')].find((entry) => entry.textContent === t('settingsModel')); + if (node === null || label === undefined) { + throw new Error('model field is not painted'); + } + + return node; + }); + + const label = [...dialog.querySelectorAll('.field__label')].find((node) => node.textContent === t('settingsModel')); + const select = label?.parentElement?.querySelector('select'); + if (!(select instanceof HTMLSelectElement)) { + throw new Error('model select is missing'); + } + + select.value = 'other.safetensors'; + select.dispatchEvent(new Event('change')); + + const stepsLabel = [...dialog.querySelectorAll('.settings-model .field__label')].find( + (node) => node.textContent === t('settingsSteps'), + ); + const stepsInput = stepsLabel?.parentElement?.querySelector('input'); + if (!(stepsInput instanceof HTMLInputElement)) { + throw new Error('model steps field is missing'); + } + + expect(stepsInput.value).toBe('8'); + void opened; + }); }); diff --git a/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs b/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs index 601b082..9fad570 100644 --- a/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs +++ b/tests/HSchool.Server.Tests/SwarmUiSettingsStoreTests.cs @@ -13,6 +13,7 @@ public class SwarmUiSettingsStoreTests Assert.Equal("child", profile.PresetId); Assert.Equal(SwarmUiModelDefinition.DreamShaperId, profile.Model); Assert.Equal(4, profile.Steps); + Assert.Equal(2, profile.CfgScale); } [Fact] @@ -24,6 +25,54 @@ public class SwarmUiSettingsStoreTests Assert.Equal("default", profile.PresetId); Assert.Equal(SwarmUiModelDefinition.BabesId, profile.Model); Assert.Equal(7, profile.Steps); + Assert.Equal(1.5, profile.CfgScale); + } + + [Fact] + public void Resolve_WithoutOverride_TakesStepsAndCfgFromModel() + { + var config = new SwarmUiConfigFile + { + Models = + [ + new SwarmUiModelDefinition { Id = "m.safetensors", Steps = 9, CfgScale = 2.25 }, + ], + Presets = + [ + new SwarmUiPresetDefinition + { + Id = "default", + Label = "Default", + Model = "m.safetensors", + Avatar = new SwarmUiKindPreset { Width = 512, Height = 512 }, + Custom = new SwarmUiKindPreset { Width = 512, Height = 512 }, + FullBody = new SwarmUiKindPreset { Width = 512, Height = 512 }, + }, + ], + }; + + var profile = config.Resolve(20, PortraitKind.Avatar); + + Assert.Equal(9, profile.Steps); + Assert.Equal(2.25, profile.CfgScale); + } + + [Fact] + public void ToProfile_JoinsModelAndPresetNegative() + { + var model = new SwarmUiModelDefinition { Id = "m.safetensors", Negative = "model-neg" }; + var preset = new SwarmUiPresetDefinition + { + Id = "default", + Label = "Default", + Model = "m.safetensors", + Negative = "preset-neg", + Avatar = new SwarmUiKindPreset { Width = 512, Height = 512 }, + }; + + var profile = preset.ToProfile(PortraitKind.Avatar, model); + + Assert.Equal("model-neg, preset-neg", profile.Negative); } [Fact]