using HSchool.Content; using HSchool.Server.Api; namespace HSchool.Server.Game; /// Turns a person card into an English prompt for SwarmUI. 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, PortraitScene? scene = null) { var parts = new List(); 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)); scene ??= PortraitScene.Empty; foreach (var fragment in scene.PositiveFragments) { Add(parts, fragment); } var positive = SwarmUiLoraFormatter.AppendEmbedTags( string.Join(", ", parts), SwarmUiLoraFormatter.Concat(profile.PositiveEmbeddings, scene.PositiveEmbeddings)); var negativeParts = new List(); Add(negativeParts, profile.Negative); foreach (var fragment in scene.NegativeFragments) { Add(negativeParts, fragment); } var negative = SwarmUiLoraFormatter.AppendEmbedTags( string.Join(", ", negativeParts), SwarmUiLoraFormatter.Concat(profile.NegativeEmbeddings, scene.NegativeEmbeddings)); return (positive, negative); } public static SwarmUiResolvedProfile ApplyScene(SwarmUiResolvedProfile profile, PortraitScene scene) => profile with { PositiveLoras = SwarmUiLoraFormatter.Concat(profile.PositiveLoras, scene.PositiveLoras), NegativeLoras = SwarmUiLoraFormatter.Concat(profile.NegativeLoras, scene.NegativeLoras), }; private static void Add(List parts, string? value) { if (string.IsNullOrWhiteSpace(value)) { return; } parts.Add(value.Trim()); } private static string DescribeAppearance(PersonCardResponse card) { var parts = new List { 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 items) => items.Count switch { 1 => items[0], 2 => $"{items[0]} and {items[1]}", _ => $"{string.Join(", ", items.Take(items.Count - 1))}, and {items[^1]}", }; }