New schools default to Realism v2.5; living saves keep their catalog. Co-authored-by: Cursor <cursoragent@cursor.com>
176 lines
4.8 KiB
C#
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]}",
|
|
};
|
|
}
|