Merge branch 'phase/60-portrait-models'
ci / server (push) Failing after 3m46s
ci / client (push) Successful in 21s

# Conflicts:
#	docs/phases/off-queue/README.md
This commit is contained in:
Leonid Pershin
2026-08-20 14:46:20 +03:00
20 changed files with 1142 additions and 180 deletions
+1
View File
@@ -9,6 +9,7 @@
| [Пропуск сборки](skip-stale-build.md) | 49 |
| [Что нового](whats-new.md) | 54 |
| [Состояние экрана](view-state.md) | 55 |
| [Модели портретов](portrait-models.md) | 60 |
Люди: [`../02-people/people.md`](../02-people/people.md). Штат: [`../03-staffing/staffing.md`](../03-staffing/staffing.md).
Расписание: [`../04-schedule/schedule.md`](../04-schedule/schedule.md). Присутствие: [`../05-ai/ai.md`](../05-ai/ai.md).
+43
View File
@@ -0,0 +1,43 @@
Часть [Вне очереди](README.md). Соседей по папке не читать, если задача не про них.
# Модели Swarm и слои промпта
### Зачем
Пикер моделей не должен предлагать то, чего нет в нашем конфиге. Дефолты генерации живут
на модели; пресет задаёт стиль и тип кадра и может переопределить дефолты. Промпт собирается
слоями, а не одним «базовым» текстом пресета.
### Было / Стало / Почему
**Было.** Пресет сам держал модель, steps/cfg/sampler и `positive`. Список Swarm только
наполнял комбобоксы. «Свой промпт» у custom **заменял** текст кадра. `portraitSettings` в
сейве — копия этого файла.
**Стало.** В конфиге каталог `models` (whitelist) и пресеты. Пикер = Swarm ∩ каталог; при
офлайне Swarm — все id из каталога. У модели: закрытый набор настроек генерации (steps, cfg,
clip skip, sampler, scheduler, seed, LoRA) и обычно пустые base positive/negative. У пресета:
модель, **стиль** (бывший `positive`), доп. negative, размеры и **тип кадра** на avatar/custom/full.
Оверрайды генерации у пресета — nullable, в UI под спойлером. Промпт:
1. base positive модели
2. стиль пресета
3. тип кадра
4. «Свой промпт» (`promptExtra`, только custom; после кадра, не вместо)
5. поза (слот пустой, без поля в UI)
6. внешность: кто это, возраст, тело
7. одежда
8. хвост пустой
Negative: модель + необязательное дополнение пресета. Ширина/высота остаются на виде кадра.
**Почему.** Модель задаёт «как считать картинку»; пресет — «какой это школьный снимок».
Гость по-прежнему рисует с копии школы, не с глобального файла.
### Что не входит
Зеркало всех `ListT2IParams`. Кнопка настроек в меню. Редактор каталога «добавить модель из
Swarm». Поза в UI. Бамп версии сокета. Перегенерация уже лежащих PNG.
Живые школы: `NormalizeAfterLoad` поднимает старый пресет (`positive``style`, kind
`positive``shotType`, уникальные `model``models[]`). Старые картинки не трогаем.
@@ -0,0 +1,45 @@
# Фаза 60. Модели портретов
## Зависимости
Нет. Портреты и `swarmui.json` уже живут вне среза.
## Зачем
Пикер показывает только модели из нашего каталога, которые есть в Swarm. Дефолты генерации
на модели; пресет даёт стиль и тип кадра и может переопределить дефолты под спойлером.
Промпт собирается слоями.
## Задачи
- [x] `swarmui.json`: каталог `models` на все четыре модели из текущего Swarm; пресеты ссылаются
на модель, `style` вместо бывшего base `positive`, `shotType` на виде кадра
- [x] Пикер = Swarm ∩ каталог; Swarm офлайн — id из каталога. Чего нет в конфиге, с пикера нет
- [x] Сборка промпта: модель → стиль → кадр → свой промпт → пустая поза → внешность (кто, возраст,
тело) → одежда. Custom **не** заменяет тип кадра. Negative: модель + доп. пресета
- [x] UI диалога пресетов (создание школы): стиль и кадр сверху; дефолты модели видны; оверрайды
пресета под `<details>`. Строки через `t(...)`
- [x] Живой сейв поднимается в `NormalizeAfterLoad`. HTTP `GET`/`PUT /api/settings/swarmui` и
`portraitSettings` — новая форма. Сокет не бампить. [`protocol.md`](../../protocol.md) тем же
коммитом
## Тесты, без которых фаза не закрыта
- [x] Лифт старого пресета: `positive``style`, kind `positive``shotType`, модель в `models`
- [x] Resolve без оверрайда берёт steps/cfg с модели; с оверрайдом — с пресета
- [x] Пикер: пересечение Swarm ∩ каталог; офлайн — каталог; лишняя модель Swarm отброшена
- [x] Промпт: порядок слоёв; custom держит тип кадра и `promptExtra`; пустая поза не попадает в текст
- [x] `GET /api/settings/swarmui` отдаёт `models`; create копирует их в сейв
- [x] Диалог: поле стиля, спойлер оверрайдов, в селекте модели нет id вне каталога при живом Swarm
## Критерий готовности
- В диалоге создания школы в пикере только модели из `swarmui.json`, которые ответил Swarm
- Смена модели подставляет её дефолты; пресет может перебить их под спойлером
- «Показать промпт» на карточке: стиль, затем кадр, затем тело и одежда; свой промпт после кадра
- Старая школа открывается и генерирует без ручного правок сейва
## Стоп
Не бампить сокет. Не зеркалить все параметры Swarm. Не возвращать кнопку настроек в меню.
Не перегенерировать PNG. Не писать позу в UI.
+1 -1
View File
@@ -19,5 +19,5 @@
| [53. Погода на дороге](53-weather-commute.md) | ✅ | Снег и дождь добавляют минуты к приходу |
| [54. Что нового](54-whats-new.md) | ✅ | После входа — окно коммитов с прошлого визита |
| [55. Состояние экрана](55-view-state.md) | ✅ | F5 и переходы не сбрасывают школу, вкладки и фильтры |
| [60. Модели портретов](60-portrait-models.md) | 🔄 | Каталог моделей Swarm ∩ конфиг, слои промпта, спойлер оверрайдов |
| [60. Модели портретов](60-portrait-models.md) | | Каталог моделей Swarm ∩ конфиг, слои промпта, спойлер оверрайдов |
| [61. LoRA и embeddings](61-portrait-loras-embeds.md) | 🔄 | LoRA и embeddings на модели, пресете и типе кадра |
+16 -12
View File
@@ -495,9 +495,9 @@ is `image/png`. Opening the card does not generate; use POST when the player ask
### `POST /api/schools/{id}/people/{personId}/portrait`
Generates (or regenerates) a portrait through SwarmUI on the server. Same `kind` query as GET.
For `kind=custom` the body is `{ "promptExtra": "..." }` — appended to the base SwarmUI prompt and
the person's body/clothing; required, non-empty, at most 2000 characters. Avatar and full-body POST
need no body.
For `kind=custom` the body is `{ "promptExtra": "..." }` — appended after the shot type (and the
model/style layers), then the person's appearance and clothing; required, non-empty, at most 2000
characters. Avatar and full-body POST need no body.
Success is `201` with `{ "kind", "hasAvatar", "hasCustom", "hasFullBody", "customPortraitPrompt" }` and a `Location` header pointing
at GET. SwarmUI is not configured when `SwarmUi:BaseUrl` is empty — `503` `swarmui-not-configured`.
Swarm errors are `502` `swarmui-unavailable`; a slow backend is `504` `swarmui-timeout`. Files
@@ -506,8 +506,8 @@ land under `saves/{id}.portraits/` and survive until the school is deleted.
### `GET /api/schools/{id}/people/{personId}/portrait/prompt`
Returns the positive and negative prompts SwarmUI would receive, without generating an image.
Same `kind` query as GET portrait. For `kind=custom`, optional query `promptExtra` is appended to
the base prompt; when omitted, the last saved custom prompt is used if one exists. Unknown school
Same `kind` query as GET portrait. For `kind=custom`, optional query `promptExtra` is appended after
the shot type; when omitted, the last saved custom prompt is used if one exists. Unknown school
is `404` `unknown-school`; unknown person is `404` `unknown-person`. Invalid `kind` is
`400` `invalid-query`; custom without a usable prompt is `400` `invalid-body`.
@@ -527,21 +527,25 @@ so the client can disable generate buttons and show reachability without trying
### `GET /api/settings/swarmui`
Returns the **default** SwarmUI preset template (`swarmui.json`): named presets (model, steps,
sampler, LoRA lists, per-kind sizes/prompts), `activePresetId` and `ageRules`. A new school copies
this into its save as `portraitSettings`. Living schools generate from that copy, not from this
file.
Returns the **default** SwarmUI template (`swarmui.json`): a `models` catalog (id, default
generation knobs, usually empty base positive/negative), named presets (model id, `style`, extra
negative, optional generation overrides, per-kind size/`shotType`), `activePresetId` and `ageRules`.
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`).
### `PUT /api/settings/swarmui`
Replaces the default template after validation. Invalid preset ids, age rules or numeric ranges
Replaces the default template after validation. Invalid model/preset ids, age rules or numeric ranges
return `400` `invalid-body`. Already-created schools keep the copy they were created with.
### `GET /api/settings/swarmui/discovery`
When SwarmUI is configured and reachable, proxies `ListT2IParams` and returns
`{ connected, models, loras, samplers, schedulers }` for the settings UI comboboxes. When SwarmUI
is off or unreachable, `connected` is false and the lists are empty.
`{ connected, models, loras, samplers, schedulers }` for the settings UI. The client hides Swarm
models that are missing from the template catalog. When SwarmUI is off or unreachable, `connected`
is false and the lists are empty; the picker then shows the catalog ids so fields can be filled
manually.
### `GET /api/schools/{id}/dress-rules`