Add Krea portrait models and write appearance as prose.
New schools default to Realism v2.5; living saves keep their catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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,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",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user