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]}",
};
}