Files
h-school/src/HSchool.Server/Game/PortraitPromptBuilder.cs
T
Leonid PershinandCursor ed015b9d6b 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>
2026-08-20 16:45:40 +03:00

176 lines
4.8 KiB
C#

using HSchool.Content;
using HSchool.Server.Api;
namespace HSchool.Server.Game;
/// <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,
PortraitKind kind,
string? promptExtra = null)
{
var parts = new List<string>();
Add(parts, profile.ModelPositive);
Add(parts, profile.Style);
Add(parts, profile.ShotType);
if (kind == PortraitKind.Custom)
{
Add(parts, promptExtra);
}
Add(parts, profile.Pose);
Add(parts, DescribeAppearance(card));
Add(parts, DescribeClothing(card));
var positive = SwarmUiLoraFormatter.AppendEmbedTags(
string.Join(", ", parts),
profile.PositiveEmbeddings);
var negative = SwarmUiLoraFormatter.AppendEmbedTags(
profile.Negative.Trim(),
profile.NegativeEmbeddings);
return (positive, negative);
}
private static void Add(List<string> parts, string? value)
{
if (string.IsNullOrWhiteSpace(value))
{
return;
}
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)
{
return card.Female ? "A young girl" : "A young boy";
}
if (card.Age <= 17)
{
return card.Female ? "A teenage girl" : "A teenage boy";
}
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]}",
};
}