Merge branch 'phase/62-portrait-prompt-craft'
ci / server (push) Failing after 3m42s
ci / client (push) Successful in 23s

This commit is contained in:
Leonid Pershin
2026-08-20 16:48:12 +03:00
10 changed files with 299 additions and 43 deletions
+1
View File
@@ -11,6 +11,7 @@
| [Состояние экрана](view-state.md) | 55 | | [Состояние экрана](view-state.md) | 55 |
| [Модели портретов](portrait-models.md) | 60 | | [Модели портретов](portrait-models.md) | 60 |
| [LoRA и embeddings](portrait-loras-embeds.md) | 61 | | [LoRA и embeddings](portrait-loras-embeds.md) | 61 |
| [Промпт и Krea-модели](portrait-prompt-craft.md) | 62 |
Люди: [`../02-people/people.md`](../02-people/people.md). Штат: [`../03-staffing/staffing.md`](../03-staffing/staffing.md). Люди: [`../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). Расписание: [`../04-schedule/schedule.md`](../04-schedule/schedule.md). Присутствие: [`../05-ai/ai.md`](../05-ai/ai.md).
@@ -0,0 +1,32 @@
Часть [Вне очереди](README.md). Соседей по папке не читать, если задача не про них.
# Каталог Krea и проза портрета
### Зачем
В Swarm появились Krea 2 чекпоинты, а пикер по-прежнему показывает только whitelist.
Дефолт новой школы должен считать картинку как Krea turbo. Внешность и одежда в промпте —
связный английский, а не список тегов: иначе Krea рисует хуже, чем SDXL Lightning.
### Было / Стало / Почему
**Было.** В `swarmui.json` четыре SDXL. Пресет `default` — babes XL Lightning.
Сборка: `age 12`, `hair colour black`, `wearing shirt in white`. Живая школа держит копию
шаблона.
**Стало.** В каталог добавлены `museByStableYogi_v30TurboGguf.gguf` и
`realismByStableYogi_v25INT8Turbo.safetensors` (8 шагов, CFG 1, euler, simple). Пресет
`default` ссылается на Realism v2.5; стиль и тип кадра — короткая проза; negative без
SD-весов. Child остаётся DreamShaper. Слои промпта те же (модель → стиль → кадр → extra →
поза → внешность → одежда). Внешность: *who*, *N years old*, *with black hair, brown eyes…*.
Одежда: *wearing a white shirt*. `Realism v2.0` GGUF в каталог не входит. Живые школы не
поднимаем.
**Почему.** Пикер = Swarm ∩ каталог, без строки в JSON модели нет. Krea 2 — не SDXL:
другие knobs и проза вместо тегов. Копия в сейве не должна менять уже созданные школы.
### Что не входит
`realismByStableYogi_v20TurboGGUF.gguf`. Лифт живых `portraitSettings`. Раздельная сборка
SDXL vs Krea. Редактор «добавить из Swarm». Поза в UI. ControlNet. Бамп сокета.
Перегенерация PNG.
@@ -0,0 +1,41 @@
# Фаза 62. Промпт и Krea-модели
## Зависимости
Фазы 60 и 61 (каталог моделей, слои, LoRA/embeddings).
## Зачем
Новые Krea-чекпоинты видны в пикере. Новая школа по умолчанию рисует Realism v2.5.
Внешность и одежда в промпте — проза, не теги.
## Задачи
- [x] Каталог: Muse GGUF и Realism v2.5 INT8, knobs 8 / CFG 1 / euler / simple. Четыре
SDXL остаются. v2.0 GGUF не добавлять. `swarmui.json` совпадает с `Catalog()`
- [x] Пресет `default` — Realism v2.5; стиль и shotType прозой; negative без
`(word:1.4)`. Child — DreamShaper, как сейчас
- [x] Сборка внешности и одежды: `N years old`, `with black hair`, `wearing a white shirt`.
Не писать `age N`, `hair colour …`, `wearing X in Y`. Слои и custom extra не трогать
- [x] Живые школы не лифтить. Сокет не бампить
## Тесты, без которых фаза не закрыта
- [x] `CreateDefault` содержит Muse и Realism v2.5 с 8 / 1 / euler / simple
- [x] Взрослый resolve берёт Realism v2.5; ребёнок — DreamShaper
- [x] Промпт: `years old`, `black hair`, `wearing a white shirt`; нет `age 12`, нет
`Hair colour`, нет `wearing shirt in white`
- [x] Порядок слоёв прежний; custom держит кадр и `promptExtra`
- [x] `GET /api/settings/swarmui` отдаёт оба новых id; create копирует их в сейв
## Критерий готовности
- В диалоге создания школы в пикере есть Muse и Realism v2.5 (если Swarm их отдал)
- Новая школа без правок пресета генерирует с Realism v2.5
- «Показать промпт»: связная внешность и одежда, не список статов карточки
- Уже созданная школа не меняет каталог и дефолтную модель
## Стоп
Не бампить сокет. Не лифтить живые сейвы. Не тащить Realism v2.0. Не писать позу в UI.
Не перегенерировать PNG. Не зеркалить все параметры Swarm.
+1 -1
View File
@@ -21,4 +21,4 @@
| [55. Состояние экрана](55-view-state.md) | ✅ | F5 и переходы не сбрасывают школу, вкладки и фильтры | | [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 на модели, пресете и типе кадра | | [61. LoRA и embeddings](61-portrait-loras-embeds.md) | ✅ | LoRA и embeddings на модели, пресете и типе кадра |
| [62. Промпт и Krea-модели](62-portrait-prompt-craft.md) | 🔄 | Muse и Realism v2.5 в каталоге; внешность и одежда прозой | | [62. Промпт и Krea-модели](62-portrait-prompt-craft.md) | | Muse и Realism v2.5 в каталоге; внешность и одежда прозой |
+116 -21
View File
@@ -1,10 +1,14 @@
using HSchool.Content;
using HSchool.Server.Api; using HSchool.Server.Api;
namespace HSchool.Server.Game; namespace HSchool.Server.Game;
/// <summary>Turns a person card into a Flux-style English prompt for SwarmUI.</summary> /// <summary>Turns a person card into an English prompt for SwarmUI.</summary>
internal static class PortraitPromptBuilder internal static class PortraitPromptBuilder
{ {
private const string HairColorId = "HairColor";
private const string EyeColorId = "EyeColor";
public static (string Positive, string Negative) Build( public static (string Positive, string Negative) Build(
PersonCardResponse card, PersonCardResponse card,
SwarmUiResolvedProfile profile, SwarmUiResolvedProfile profile,
@@ -22,26 +26,8 @@ internal static class PortraitPromptBuilder
} }
Add(parts, profile.Pose); Add(parts, profile.Pose);
Add(parts, DescribeSubject(card)); Add(parts, DescribeAppearance(card));
parts.Add($"age {card.Age}"); Add(parts, DescribeClothing(card));
foreach (var row in card.Body)
{
Add(parts, $"{row.Label.ToLowerInvariant()} {row.Value.ToLowerInvariant()}");
}
foreach (var item in PortraitVisibleWorn.Filter(card.Worn))
{
var color = item.ColorLabel ?? item.Color;
if (!string.IsNullOrWhiteSpace(color))
{
Add(parts, $"wearing {item.Label.ToLowerInvariant()} in {color.ToLowerInvariant()}");
}
else
{
Add(parts, $"wearing {item.Label.ToLowerInvariant()}");
}
}
var positive = SwarmUiLoraFormatter.AppendEmbedTags( var positive = SwarmUiLoraFormatter.AppendEmbedTags(
string.Join(", ", parts), string.Join(", ", parts),
@@ -63,6 +49,83 @@ internal static class PortraitPromptBuilder
parts.Add(value.Trim()); parts.Add(value.Trim());
} }
private static string DescribeAppearance(PersonCardResponse card)
{
var parts = new List<string> { DescribeSubject(card), $"{card.Age} years old" };
var hair = BodyValue(card, HairColorId);
if (hair is not null)
{
parts.Add($"{hair.ToLowerInvariant()} hair");
}
var eyes = BodyValue(card, EyeColorId);
if (eyes is not null)
{
parts.Add($"{eyes.ToLowerInvariant()} eyes");
}
var build = BodyValue(card, BodyBuilds.Attribute);
if (build is not null && !build.Equals(BodyBuilds.Average, StringComparison.OrdinalIgnoreCase))
{
parts.Add($"a {build.ToLowerInvariant()} build");
}
var height = BodyValue(card, BodyBuilds.HeightAttribute);
if (height is not null)
{
parts.Add($"{height} cm tall");
}
if (parts.Count == 2)
{
return $"{parts[0]}, {parts[1]}";
}
return $"{parts[0]}, {parts[1]}, with {string.Join(", ", parts.Skip(2))}";
}
private static string? DescribeClothing(PersonCardResponse card)
{
var items = PortraitVisibleWorn.Filter(card.Worn)
.Select(DescribeWornItem)
.Where(item => !string.IsNullOrWhiteSpace(item))
.ToList();
if (items.Count == 0)
{
return null;
}
return $"wearing {JoinList(items)}";
}
private static string DescribeWornItem(WornItemResponse item)
{
var noun = item.Label.Trim();
if (string.IsNullOrWhiteSpace(noun))
{
noun = item.DefName.Trim();
}
noun = noun.ToLowerInvariant();
var color = (item.ColorLabel ?? item.Color)?.Trim();
if (!string.IsNullOrWhiteSpace(color))
{
noun = $"{color.ToLowerInvariant()} {noun}";
}
var condition = item.ConditionLabel?.Trim().ToLowerInvariant();
noun = condition switch
{
"torn" => $"torn {noun}",
"worn" => $"worn {noun}",
"in rags" => $"{noun} in rags",
_ => noun,
};
return WithArticle(noun);
}
private static string DescribeSubject(PersonCardResponse card) private static string DescribeSubject(PersonCardResponse card)
{ {
if (card.Age <= 11) if (card.Age <= 11)
@@ -77,4 +140,36 @@ internal static class PortraitPromptBuilder
return card.Female ? "A young woman" : "A young man"; return card.Female ? "A young woman" : "A young man";
} }
private static string? BodyValue(PersonCardResponse card, string id)
{
foreach (var row in card.Body)
{
if (row.Id.Equals(id, StringComparison.Ordinal) && !string.IsNullOrWhiteSpace(row.Value))
{
return row.Value.Trim();
}
}
return null;
}
private static string WithArticle(string phrase)
{
if (phrase.Length == 0)
{
return phrase;
}
var first = char.ToLowerInvariant(phrase[0]);
var article = first is 'a' or 'e' or 'i' or 'o' or 'u' ? "an" : "a";
return $"{article} {phrase}";
}
private static string JoinList(IReadOnlyList<string> items) => items.Count switch
{
1 => items[0],
2 => $"{items[0]} and {items[1]}",
_ => $"{string.Join(", ", items.Take(items.Count - 1))}, and {items[^1]}",
};
} }
@@ -471,6 +471,8 @@ internal sealed class SwarmUiModelDefinition
public const string DreamShaperId = "DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors"; public const string DreamShaperId = "DreamShaper_XL_-_Lightning_DPM++_SDE.safetensors";
public const string EpicRealismId = "epicrealismXL_VXIAbeast4SLightning.safetensors"; public const string EpicRealismId = "epicrealismXL_VXIAbeast4SLightning.safetensors";
public const string LustifyId = "lustifyNSFWCheckpoint_v40DMD2.safetensors"; public const string LustifyId = "lustifyNSFWCheckpoint_v40DMD2.safetensors";
public const string MuseId = "museByStableYogi_v30TurboGguf.gguf";
public const string RealismV25Id = "realismByStableYogi_v25INT8Turbo.safetensors";
public string Id { get; set; } = ""; public string Id { get; set; } = "";
@@ -529,6 +531,8 @@ internal sealed class SwarmUiModelDefinition
Lightning(DreamShaperId, "DreamShaper XL Lightning", 4, 2, 2, "dpmpp_sde", "karras", 3346112079), Lightning(DreamShaperId, "DreamShaper XL Lightning", 4, 2, 2, "dpmpp_sde", "karras", 3346112079),
Lightning(EpicRealismId, "epicrealism XL Lightning", 7, 1.5, 0, "euler", "normal", 0), Lightning(EpicRealismId, "epicrealism XL Lightning", 7, 1.5, 0, "euler", "normal", 0),
Lightning(LustifyId, "lustify NSFW v40 DMD2", 7, 1.5, 0, "euler", "normal", 0), Lightning(LustifyId, "lustify NSFW v40 DMD2", 7, 1.5, 0, "euler", "normal", 0),
Lightning(MuseId, "museByStableYogi v3.0 Turbo GGUF", 8, 1, 0, "euler", "simple", 0),
Lightning(RealismV25Id, "realismByStableYogi v2.5 INT8 Turbo", 8, 1, 0, "euler", "simple", 0),
]; ];
private static SwarmUiModelDefinition Lightning( private static SwarmUiModelDefinition Lightning(
@@ -736,23 +740,23 @@ internal sealed class SwarmUiPresetDefinition
{ {
Id = "default", Id = "default",
Label = "Default", Label = "Default",
Model = SwarmUiModelDefinition.BabesId, Model = SwarmUiModelDefinition.RealismV25Id,
Style = Style =
"cinematic photo, realist detail, detailed character expressions, amazing quality, analog film grain, school portrait photograph, neutral background, natural lighting, realistic, sharp focus", "A cinematic school portrait photograph with natural window light, a neutral background, realistic skin, and sharp focus",
Negative = Negative =
"(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, nsfw, nude, naked, explicit, blurry, deformed, bad anatomy, logo", "text, signature, watermark, extra limbs, deformed anatomy, extra fingers",
Avatar = new SwarmUiKindPreset Avatar = new SwarmUiKindPreset
{ {
Width = 1024, Width = 1024,
Height = 1024, Height = 1024,
ShotType = "close up, head and shoulders portrait, facing the camera, upper body visible.", ShotType = "Close-up head and shoulders portrait, facing the camera, upper body visible",
}, },
Custom = new SwarmUiKindPreset { Width = 896, Height = 1152 }, Custom = new SwarmUiKindPreset { Width = 896, Height = 1152 },
FullBody = new SwarmUiKindPreset FullBody = new SwarmUiKindPreset
{ {
Width = 896, Width = 896,
Height = 1152, Height = 1152,
ShotType = "full body standing portrait, head to toe visible, neutral pose, current outfit clearly visible.", ShotType = "Full-body standing portrait from head to toe, neutral pose, current outfit clearly visible",
}, },
}; };
+33 -5
View File
@@ -56,19 +56,47 @@
"negative": "", "negative": "",
"positiveLoras": [], "positiveLoras": [],
"negativeLoras": [] "negativeLoras": []
},
{
"id": "museByStableYogi_v30TurboGguf.gguf",
"label": "museByStableYogi v3.0 Turbo GGUF",
"steps": 8,
"cfgScale": 1,
"clipSkip": 0,
"sampler": "euler",
"scheduler": "simple",
"seed": 0,
"positive": "",
"negative": "",
"positiveLoras": [],
"negativeLoras": []
},
{
"id": "realismByStableYogi_v25INT8Turbo.safetensors",
"label": "realismByStableYogi v2.5 INT8 Turbo",
"steps": 8,
"cfgScale": 1,
"clipSkip": 0,
"sampler": "euler",
"scheduler": "simple",
"seed": 0,
"positive": "",
"negative": "",
"positiveLoras": [],
"negativeLoras": []
} }
], ],
"presets": [ "presets": [
{ {
"id": "default", "id": "default",
"label": "Default", "label": "Default",
"model": "babesByStableYogi_v4XLLightning.safetensors", "model": "realismByStableYogi_v25INT8Turbo.safetensors",
"style": "cinematic photo, realist detail, detailed character expressions, amazing quality,", "style": "A cinematic school portrait photograph with natural window light, a neutral background, realistic skin, and sharp focus",
"negative": "(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, censored, explicit, blurry, deformed, bad anatomy, logo", "negative": "text, signature, watermark, extra limbs, deformed anatomy, extra fingers",
"avatar": { "avatar": {
"width": 1024, "width": 1024,
"height": 1024, "height": 1024,
"shotType": "close up, head and shoulders portrait, facing the camera, upper body visible, avatar," "shotType": "Close-up head and shoulders portrait, facing the camera, upper body visible"
}, },
"custom": { "custom": {
"width": 896, "width": 896,
@@ -78,7 +106,7 @@
"fullBody": { "fullBody": {
"width": 896, "width": 896,
"height": 1152, "height": 1152,
"shotType": "full body standing portrait, head to toe visible, neutral pose, current outfit clearly visible." "shotType": "Full-body standing portrait from head to toe, neutral pose, current outfit clearly visible"
} }
}, },
{ {
@@ -85,7 +85,8 @@ public class PortraitApiTests(AppHostFixture fixture)
var payload = await response.Content.ReadFromJsonAsync<PortraitPromptPayload>(TestContext.Current.CancellationToken); var payload = await response.Content.ReadFromJsonAsync<PortraitPromptPayload>(TestContext.Current.CancellationToken);
Assert.NotNull(payload); Assert.NotNull(payload);
Assert.Equal("avatar", payload!.Kind); Assert.Equal("avatar", payload!.Kind);
Assert.Contains("age", payload.Positive, StringComparison.OrdinalIgnoreCase); Assert.Contains("years old", payload.Positive, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("age ", payload.Positive, StringComparison.OrdinalIgnoreCase);
Assert.False(string.IsNullOrWhiteSpace(payload.Negative)); Assert.False(string.IsNullOrWhiteSpace(payload.Negative));
Assert.False(string.IsNullOrWhiteSpace(payload.PresetId)); Assert.False(string.IsNullOrWhiteSpace(payload.PresetId));
} }
@@ -103,6 +104,8 @@ public class PortraitApiTests(AppHostFixture fixture)
Assert.NotNull(settings); Assert.NotNull(settings);
Assert.NotEmpty(settings!.Presets); Assert.NotEmpty(settings!.Presets);
Assert.NotEmpty(settings.Models); Assert.NotEmpty(settings.Models);
Assert.Contains(settings.Models, model => model.Id == "museByStableYogi_v30TurboGguf.gguf");
Assert.Contains(settings.Models, model => model.Id == "realismByStableYogi_v25INT8Turbo.safetensors");
Assert.False(string.IsNullOrWhiteSpace(settings.ActivePresetId)); Assert.False(string.IsNullOrWhiteSpace(settings.ActivePresetId));
} }
@@ -122,6 +125,14 @@ public class PortraitApiTests(AppHostFixture fixture)
Assert.True(list.GetArrayLength() > 0); Assert.True(list.GetArrayLength() > 0);
Assert.True(presets.TryGetProperty("models", out var models)); Assert.True(presets.TryGetProperty("models", out var models));
Assert.True(models.GetArrayLength() > 0); Assert.True(models.GetArrayLength() > 0);
var ids = models.EnumerateArray().Select(model => model.GetProperty("id").GetString()).ToList();
Assert.Contains("museByStableYogi_v30TurboGguf.gguf", ids);
Assert.Contains("realismByStableYogi_v25INT8Turbo.safetensors", ids);
var defaultPreset = presets.GetProperty("presets").EnumerateArray()
.First(preset => preset.GetProperty("id").GetString() == "default");
Assert.Equal(
"realismByStableYogi_v25INT8Turbo.safetensors",
defaultPreset.GetProperty("model").GetString());
} }
[Fact] [Fact]
@@ -7,18 +7,20 @@ namespace HSchool.Server.Tests;
public class PortraitPromptBuilderTests public class PortraitPromptBuilderTests
{ {
[Fact] [Fact]
public void Build_IncludesAgeHairAndWornClothing() public void Build_DescribesAppearanceAndClothingInProse()
{ {
var profile = SamplePreset().ToProfile(PortraitKind.Avatar); var profile = SamplePreset().ToProfile(PortraitKind.Avatar);
var card = SampleCard(); var card = SampleCard();
var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar); var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar);
Assert.Contains("age 12", positive, StringComparison.OrdinalIgnoreCase); Assert.Contains("12 years old", positive, StringComparison.Ordinal);
Assert.Contains("black", positive, StringComparison.OrdinalIgnoreCase); Assert.Contains("black hair", positive, StringComparison.OrdinalIgnoreCase);
Assert.Contains("shirt", positive, StringComparison.OrdinalIgnoreCase); Assert.Contains("wearing a white shirt", positive, StringComparison.OrdinalIgnoreCase);
Assert.Contains("white", 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("Hair colour", positive, StringComparison.Ordinal);
Assert.DoesNotContain("wearing shirt in white", positive, StringComparison.OrdinalIgnoreCase);
} }
[Fact] [Fact]
@@ -66,7 +68,7 @@ public class PortraitPromptBuilderTests
var styleAt = positive.IndexOf("School photo.", StringComparison.Ordinal); var styleAt = positive.IndexOf("School photo.", StringComparison.Ordinal);
var shotAt = positive.IndexOf("Head and shoulders.", StringComparison.Ordinal); var shotAt = positive.IndexOf("Head and shoulders.", StringComparison.Ordinal);
var subjectAt = positive.IndexOf("teenage girl", StringComparison.OrdinalIgnoreCase); var subjectAt = positive.IndexOf("teenage girl", StringComparison.OrdinalIgnoreCase);
var clothesAt = positive.IndexOf("wearing shirt", StringComparison.OrdinalIgnoreCase); var clothesAt = positive.IndexOf("wearing a white shirt", StringComparison.OrdinalIgnoreCase);
Assert.True(modelAt >= 0 && styleAt > modelAt && shotAt > styleAt && subjectAt > shotAt && clothesAt > subjectAt); Assert.True(modelAt >= 0 && styleAt > modelAt && shotAt > styleAt && subjectAt > shotAt && clothesAt > subjectAt);
Assert.DoesNotContain("pose", positive, StringComparison.OrdinalIgnoreCase); Assert.DoesNotContain("pose", positive, StringComparison.OrdinalIgnoreCase);
@@ -80,6 +82,31 @@ public class PortraitPromptBuilderTests
var (positive, _) = PortraitPromptBuilder.Build(child, profile, PortraitKind.Avatar); var (positive, _) = PortraitPromptBuilder.Build(child, profile, PortraitKind.Avatar);
Assert.Contains("young girl", positive, StringComparison.OrdinalIgnoreCase); Assert.Contains("young girl", positive, StringComparison.OrdinalIgnoreCase);
Assert.Contains("8 years old", positive, StringComparison.Ordinal);
}
[Fact]
public void Build_TornClothing_PrefixesCondition()
{
var profile = SamplePreset().ToProfile(PortraitKind.Avatar);
var card = SampleCard() with
{
Worn =
[
new WornItemResponse(
"Shirt",
"Shirt",
"White",
"White",
[new DefLabelResponse(ApparelLayers.Top, "Top")],
0.2f,
"torn",
[ApparelLayers.Underwear]),
],
};
var (positive, _) = PortraitPromptBuilder.Build(card, profile, PortraitKind.Avatar);
Assert.Contains("wearing a torn white shirt", positive, StringComparison.OrdinalIgnoreCase);
} }
private static SwarmUiPresetDefinition SamplePreset() private static SwarmUiPresetDefinition SamplePreset()
@@ -23,9 +23,11 @@ public class SwarmUiSettingsStoreTests
var profile = config.Resolve(34, PortraitKind.Full); var profile = config.Resolve(34, PortraitKind.Full);
Assert.Equal("default", profile.PresetId); Assert.Equal("default", profile.PresetId);
Assert.Equal(SwarmUiModelDefinition.BabesId, profile.Model); Assert.Equal(SwarmUiModelDefinition.RealismV25Id, profile.Model);
Assert.Equal(7, profile.Steps); Assert.Equal(8, profile.Steps);
Assert.Equal(1.5, profile.CfgScale); Assert.Equal(1, profile.CfgScale);
Assert.Equal("euler", profile.Sampler);
Assert.Equal("simple", profile.Scheduler);
} }
[Fact] [Fact]
@@ -196,7 +198,7 @@ public class SwarmUiSettingsStoreTests
} }
[Fact] [Fact]
public void CreateDefault_SeedsEveryKnownSwarmModel() public void CreateDefault_SeedsCatalogModels()
{ {
var config = SwarmUiConfigFile.CreateDefault(); var config = SwarmUiConfigFile.CreateDefault();
var ids = config.Models.Select(model => model.Id).ToList(); var ids = config.Models.Select(model => model.Id).ToList();
@@ -205,5 +207,20 @@ public class SwarmUiSettingsStoreTests
Assert.Contains(SwarmUiModelDefinition.DreamShaperId, ids); Assert.Contains(SwarmUiModelDefinition.DreamShaperId, ids);
Assert.Contains(SwarmUiModelDefinition.EpicRealismId, ids); Assert.Contains(SwarmUiModelDefinition.EpicRealismId, ids);
Assert.Contains(SwarmUiModelDefinition.LustifyId, ids); Assert.Contains(SwarmUiModelDefinition.LustifyId, ids);
Assert.Contains(SwarmUiModelDefinition.MuseId, ids);
Assert.Contains(SwarmUiModelDefinition.RealismV25Id, ids);
Assert.DoesNotContain("realismByStableYogi_v20TurboGGUF.gguf", ids);
var realism = Assert.Single(config.Models, model => model.Id == SwarmUiModelDefinition.RealismV25Id);
Assert.Equal(8, realism.Steps);
Assert.Equal(1, realism.CfgScale);
Assert.Equal("euler", realism.Sampler);
Assert.Equal("simple", realism.Scheduler);
var muse = Assert.Single(config.Models, model => model.Id == SwarmUiModelDefinition.MuseId);
Assert.Equal(8, muse.Steps);
Assert.Equal(1, muse.CfgScale);
Assert.Equal("euler", muse.Sampler);
Assert.Equal("simple", muse.Scheduler);
} }
} }