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:
Leonid Pershin
2026-08-20 16:45:40 +03:00
co-authored by Cursor
parent 8c89f605de
commit ed015b9d6b
9 changed files with 298 additions and 42 deletions
+116 -21
View File
@@ -1,10 +1,14 @@
using HSchool.Content;
using HSchool.Server.Api;
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
{
private const string HairColorId = "HairColor";
private const string EyeColorId = "EyeColor";
public static (string Positive, string Negative) Build(
PersonCardResponse card,
SwarmUiResolvedProfile profile,
@@ -22,26 +26,8 @@ internal static class PortraitPromptBuilder
}
Add(parts, profile.Pose);
Add(parts, DescribeSubject(card));
parts.Add($"age {card.Age}");
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()}");
}
}
Add(parts, DescribeAppearance(card));
Add(parts, DescribeClothing(card));
var positive = SwarmUiLoraFormatter.AppendEmbedTags(
string.Join(", ", parts),
@@ -63,6 +49,83 @@ internal static class PortraitPromptBuilder
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)
{
if (card.Age <= 11)
@@ -77,4 +140,36 @@ internal static class PortraitPromptBuilder
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 EpicRealismId = "epicrealismXL_VXIAbeast4SLightning.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; } = "";
@@ -529,6 +531,8 @@ internal sealed class SwarmUiModelDefinition
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(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(
@@ -736,23 +740,23 @@ internal sealed class SwarmUiPresetDefinition
{
Id = "default",
Label = "Default",
Model = SwarmUiModelDefinition.BabesId,
Model = SwarmUiModelDefinition.RealismV25Id,
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 =
"(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
{
Width = 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 },
FullBody = new SwarmUiKindPreset
{
Width = 896,
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": "",
"positiveLoras": [],
"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": [
{
"id": "default",
"label": "Default",
"model": "babesByStableYogi_v4XLLightning.safetensors",
"style": "cinematic photo, realist detail, detailed character expressions, amazing quality,",
"negative": "(low quality, worst quality:1.4), cgi, text, signature, watermark, extra limbs, censored, explicit, blurry, deformed, bad anatomy, logo",
"model": "realismByStableYogi_v25INT8Turbo.safetensors",
"style": "A cinematic school portrait photograph with natural window light, a neutral background, realistic skin, and sharp focus",
"negative": "text, signature, watermark, extra limbs, deformed anatomy, extra fingers",
"avatar": {
"width": 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": {
"width": 896,
@@ -78,7 +106,7 @@
"fullBody": {
"width": 896,
"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"
}
},
{