Merge branch 'review/off-queue-portraits'
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
# Проверенные фазы «Вне очереди»
|
||||
|
||||
Только портреты (60–61). Соседей по папке этот журнал не закрывает.
|
||||
|
||||
## Портреты. Модели, слои промпта, LoRA и embeddings
|
||||
|
||||
- **Фазы:** 60–61
|
||||
- **Проверен на:** `94e3e66`, 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 склеиваются модель → пресет → кадр. Промпт содержит `<embed:…>` и не содержит `<lora:`. Generate шлёт `loras` и `loraweights` раздельно. В диалоге поля embeddings есть у модели и у вида кадра.
|
||||
|
||||
### Открытое
|
||||
|
||||
Ничего, что требовало бы решения. `swarmui.json` на диске не пишет ключи embeddings — после `NormalizeAfterLoad` списки пустые, как в `Catalog()`.
|
||||
@@ -33,3 +33,4 @@
|
||||
| Срез 9. Этапы C–D — конфликт, присутствие, речь и romance | `19554d4` | [09-social](09-social/reviewed.md) |
|
||||
| Срез 10. Этап A — страховка | `e2e7d30` | [10-craft](10-craft/reviewed.md) |
|
||||
| Срез 10. Этапы B–C — работник, карточка и дамп | `f4f726a` | [10-craft](10-craft/reviewed.md) |
|
||||
| Портреты (вне очереди) 60–61 | `94e3e66` | [off-queue](off-queue/reviewed.md) |
|
||||
|
||||
@@ -178,4 +178,42 @@ describe('swarmUiSettingsDialog', () => {
|
||||
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;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user