550 lines
29 KiB
C#
550 lines
29 KiB
C#
using System.Text.Json;
|
|
using PlotLine.Models;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public static class StoryIntelligenceIllustrationCompatibility
|
|
{
|
|
public static class AgeBands
|
|
{
|
|
public const string Child = "Child";
|
|
public const string YoungTeen = "YoungTeen";
|
|
public const string OlderTeen = "OlderTeen";
|
|
public const string YoungAdult = "YoungAdult";
|
|
public const string Adult = "Adult";
|
|
public const string MatureAdult = "MatureAdult";
|
|
public const string Senior = "Senior";
|
|
public const string Unknown = "Unknown";
|
|
}
|
|
|
|
public static class Presentations
|
|
{
|
|
public const string Feminine = "Feminine";
|
|
public const string Masculine = "Masculine";
|
|
public const string Androgynous = "Androgynous";
|
|
public const string Unknown = "Unknown";
|
|
}
|
|
|
|
public static class HairColours
|
|
{
|
|
public const string Black = "Black";
|
|
public const string Brown = "Brown";
|
|
public const string Blonde = "Blonde";
|
|
public const string Red = "Red";
|
|
public const string Grey = "Grey";
|
|
public const string White = "White";
|
|
public const string Bald = "Bald";
|
|
public const string Unknown = "Unknown";
|
|
}
|
|
|
|
public static class SkinTones
|
|
{
|
|
public const string Light = "Light";
|
|
public const string Medium = "Medium";
|
|
public const string Dark = "Dark";
|
|
public const string Unknown = "Unknown";
|
|
}
|
|
|
|
public static CharacterEvidenceProfile InferCharacterEvidence(string name, IEnumerable<string> identityValues, IEnumerable<string> relationshipValues)
|
|
{
|
|
var identityText = $" {string.Join(' ', identityValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} ";
|
|
var identityScopedText = $" {string.Join(". ", identityValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} ";
|
|
var relationshipText = $" {string.Join(". ", relationshipValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} ";
|
|
var nameText = $" {name.ToLowerInvariant()} ";
|
|
var entityText = $"{nameText} {identityText}";
|
|
var scopedText = EntityScopedText($"{nameText} {identityScopedText}", name);
|
|
var relationshipSafeText = RelationshipScopedText(relationshipText, name);
|
|
var titlePresentation = TitlePresentation(scopedText);
|
|
var relationshipPresentation = RelationshipPresentation(relationshipSafeText);
|
|
var entityPresentation = RelationshipPresentation(scopedText);
|
|
var pronounPresentation = PronounPresentation(scopedText);
|
|
var namePresentation = NamePresentation(entityText);
|
|
var presentation = FirstKnown(
|
|
titlePresentation.Value,
|
|
entityPresentation.Value,
|
|
relationshipPresentation.Value,
|
|
pronounPresentation.Value,
|
|
namePresentation.Value,
|
|
Presentations.Unknown);
|
|
var presentationConfidence = new[] { titlePresentation.Confidence, entityPresentation.Confidence, relationshipPresentation.Confidence, pronounPresentation.Confidence, namePresentation.Confidence }.Max();
|
|
var explicitAge = ExplicitAgeBand(scopedText);
|
|
var relationshipAge = RelationshipAgeBand(name, scopedText);
|
|
var namedAge = NameAgeBand(name);
|
|
var age = FirstKnown(relationshipAge.Value, explicitAge.Value, namedAge.Value, AgeBands.Unknown);
|
|
var ageConfidence = new[] { explicitAge.Confidence, relationshipAge.Confidence, namedAge.Confidence }.Max();
|
|
var attributeText = EntityAttributeText(entityText, name);
|
|
var hairColour = HairColour(attributeText);
|
|
var skinTone = SkinTone(attributeText);
|
|
var hairLength = HairLength(attributeText);
|
|
|
|
return new(
|
|
age,
|
|
presentation,
|
|
hairColour,
|
|
skinTone,
|
|
hairLength,
|
|
ContainsAny(attributeText, "glasses", "spectacles") ? "Yes" : "Unknown",
|
|
ContainsAny(attributeText, "moustache", "mustache") ? "Moustache" : ContainsAny(attributeText, "beard", "bearded") ? "Beard" : "Unknown",
|
|
ageConfidence,
|
|
presentationConfidence,
|
|
relationshipPresentation.Confidence,
|
|
namePresentation.Confidence,
|
|
pronounPresentation.Confidence);
|
|
}
|
|
|
|
public static IllustrationCharacterMetadata CharacterMetadataFrom(IllustrationLibraryItem item)
|
|
{
|
|
IllustrationGenerationSpecification? specification = null;
|
|
Dictionary<string, string>? metadata = null;
|
|
if (!string.IsNullOrWhiteSpace(item.MetadataJson))
|
|
{
|
|
try
|
|
{
|
|
metadata = JsonSerializer.Deserialize<Dictionary<string, string>>(item.MetadataJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(item.SpecificationJson))
|
|
{
|
|
try
|
|
{
|
|
specification = JsonSerializer.Deserialize<IllustrationGenerationSpecification>(item.SpecificationJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
}
|
|
}
|
|
|
|
var stableCode = item.StableCode ?? string.Empty;
|
|
var isUnknownFigure = BoolMetadata(metadata, "isUnknownFigure") || stableCode.Contains("unknown-figure", StringComparison.OrdinalIgnoreCase);
|
|
var isGenericFallback = BoolMetadata(metadata, "isGenericFallback") || stableCode.Contains("fallback", StringComparison.OrdinalIgnoreCase);
|
|
var suitableForNamed = !isUnknownFigure && !isGenericFallback && !BoolMetadata(metadata, "notSuitableForNamedCharacter");
|
|
return new(
|
|
NormalizeAgeBand(FirstMetadata(metadata, "ageBand", "apparentAgeBand") ?? specification?.ApparentAgeBand ?? stableCode),
|
|
NormalizePresentation(FirstMetadata(metadata, "presentation") ?? specification?.Presentation ?? stableCode),
|
|
NormalizeHairColour(FirstMetadata(metadata, "hairColour") ?? specification?.HairColour ?? stableCode),
|
|
NormalizeSkinTone(FirstMetadata(metadata, "skinTone") ?? specification?.SkinTone ?? stableCode),
|
|
NormalizeHairLength(FirstMetadata(metadata, "hairLength") ?? specification?.HairLength ?? stableCode),
|
|
FirstMetadata(metadata, "glasses") ?? (FeatureValue(specification?.Features, "Glasses") ? "Yes" : "Unknown"),
|
|
FirstMetadata(metadata, "facialHair") ?? (FeatureValue(specification?.Features, "Beard") ? "Beard" : FeatureValue(specification?.Features, "Moustache") ? "Moustache" : "Unknown"),
|
|
isUnknownFigure,
|
|
isGenericFallback,
|
|
suitableForNamed,
|
|
item.PromptTemplateVersion,
|
|
FirstMetadata(metadata, "generationSource", "source") ?? string.Empty);
|
|
}
|
|
|
|
public static IReadOnlyList<string> CharacterHardRejections(CharacterEvidenceProfile evidence, IllustrationCharacterMetadata candidate, bool isNamedCharacter, bool alreadyAssignedToAnotherSignificantCharacter)
|
|
{
|
|
var rejected = new List<string>();
|
|
if (isNamedCharacter && candidate.IsUnknownFigure)
|
|
{
|
|
rejected.Add("named character cannot use Unknown Figure artwork");
|
|
}
|
|
|
|
if (isNamedCharacter && !candidate.IsSuitableForNamedCharacter)
|
|
{
|
|
rejected.Add("candidate is not suitable for strict named-character matching");
|
|
}
|
|
|
|
if (AgeIncompatible(evidence.AgeBand, candidate.AgeBand))
|
|
{
|
|
rejected.Add($"age hard mismatch: wanted {evidence.AgeBand}, candidate {candidate.AgeBand}");
|
|
}
|
|
|
|
if (evidence.HasStrongPresentationEvidence
|
|
&& PresentationIncompatible(evidence.Presentation, candidate.Presentation))
|
|
{
|
|
rejected.Add($"presentation hard mismatch: wanted {evidence.Presentation}, candidate {candidate.Presentation}");
|
|
}
|
|
|
|
if (ExplicitKnown(evidence.HairColour, evidence.AgeConfidence) && KnownMismatch(evidence.HairColour, candidate.HairColour))
|
|
{
|
|
rejected.Add($"hair colour hard mismatch: wanted {evidence.HairColour}, candidate {candidate.HairColour}");
|
|
}
|
|
|
|
if (alreadyAssignedToAnotherSignificantCharacter && isNamedCharacter)
|
|
{
|
|
rejected.Add("already assigned to another significant named character");
|
|
}
|
|
|
|
return rejected;
|
|
}
|
|
|
|
public static string NormalizeAgeBand(string? value)
|
|
{
|
|
var text = (value ?? string.Empty).Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase).ToLowerInvariant();
|
|
if (text.Contains("youngteen") || text.Contains("13") || text.Contains("14") || text.Contains("15") || text.Contains("fifteen")) return AgeBands.YoungTeen;
|
|
if (text.Contains("olderteen") || text.Contains("teen") || text.Contains("16") || text.Contains("17") || text.Contains("18") || text.Contains("19")) return AgeBands.OlderTeen;
|
|
if (text.Contains("child") || text.Contains("boy") || text.Contains("girl")) return AgeBands.Child;
|
|
if (text.Contains("youngadult")) return AgeBands.YoungAdult;
|
|
if (text.Contains("middleaged") || text.Contains("matureadult") || text.Contains("parent") || text.Contains("mother") || text.Contains("father") || text.Contains("aunt") || text.Contains("uncle")) return AgeBands.MatureAdult;
|
|
if (text.Contains("adult")) return AgeBands.Adult;
|
|
if (text.Contains("senior") || text.Contains("elderly") || text.Contains("older") || text.Contains("grandmother") || text.Contains("grandfather")) return AgeBands.Senior;
|
|
return AgeBands.Unknown;
|
|
}
|
|
|
|
public static string NormalizePresentation(string? value)
|
|
{
|
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
|
if (ContainsAny(text, "feminine", "female", "woman", "girl", "mother", "mum", "mrs", "miss", "ms", "aunt", "sister", "daughter", "wife", "grandmother")) return Presentations.Feminine;
|
|
if (ContainsAny(text, "masculine", "male", "man", "boy", "father", "dad", "mr", "uncle", "brother", "son", "husband", "grandfather")) return Presentations.Masculine;
|
|
if (text.Contains("androgynous", StringComparison.OrdinalIgnoreCase)) return Presentations.Androgynous;
|
|
return Presentations.Unknown;
|
|
}
|
|
|
|
public static string NormalizeHairColour(string? value)
|
|
{
|
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
|
if (text.Contains("bald", StringComparison.OrdinalIgnoreCase)) return HairColours.Bald;
|
|
if (text.Contains("black", StringComparison.OrdinalIgnoreCase)) return HairColours.Black;
|
|
if (ContainsAny(text, "brown", "brunette", "darkbrown", "dark hair")) return HairColours.Brown;
|
|
if (ContainsAny(text, "blonde", "fair hair")) return HairColours.Blonde;
|
|
if (ContainsAny(text, "red", "auburn", "ginger")) return HairColours.Red;
|
|
if (ContainsAny(text, "grey", "gray")) return HairColours.Grey;
|
|
if (text.Contains("white", StringComparison.OrdinalIgnoreCase)) return HairColours.White;
|
|
return HairColours.Unknown;
|
|
}
|
|
|
|
public static string NormalizeSkinTone(string? value)
|
|
{
|
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
|
if (ContainsAny(text, "deep", "dark", "black skin")) return SkinTones.Dark;
|
|
if (ContainsAny(text, "medium", "lightmedium", "olive", "brown skin")) return SkinTones.Medium;
|
|
if (ContainsAny(text, "light", "pale", "fair skin")) return SkinTones.Light;
|
|
return SkinTones.Unknown;
|
|
}
|
|
|
|
public static string NormalizeHairLength(string? value)
|
|
{
|
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
|
if (text.Contains("bald", StringComparison.OrdinalIgnoreCase)) return "Bald";
|
|
if (text.Contains("short", StringComparison.OrdinalIgnoreCase)) return "Short";
|
|
if (text.Contains("long", StringComparison.OrdinalIgnoreCase)) return "Long";
|
|
if (text.Contains("medium", StringComparison.OrdinalIgnoreCase) || text.Contains("shoulder", StringComparison.OrdinalIgnoreCase)) return "Medium";
|
|
return "Unknown";
|
|
}
|
|
|
|
public static string LocationType(string? value)
|
|
{
|
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
|
if (ContainsAny(text, "bathroom", "toilet", "washroom")) return "Bathroom";
|
|
if (text.Contains("kitchen", StringComparison.OrdinalIgnoreCase)) return "Kitchen";
|
|
if (text.Contains("bedroom", StringComparison.OrdinalIgnoreCase)) return "Bedroom";
|
|
if (ContainsAny(text, "living room", "sitting room", "lounge")) return "LivingRoom";
|
|
if (ContainsAny(text, "hall", "hallway", "corridor")) return "Hallway";
|
|
if (text.Contains("stair", StringComparison.OrdinalIgnoreCase)) return "Stairwell";
|
|
if (ContainsAny(text, "front door", "porch", "entrance", "house", "exterior")) return "HouseExterior";
|
|
if (ContainsAny(text, "office", "desk", "study", "library")) return "Office";
|
|
if (text.Contains("school", StringComparison.OrdinalIgnoreCase)) return "School";
|
|
if (text.Contains("waiting room", StringComparison.OrdinalIgnoreCase)) return "WaitingRoom";
|
|
if (ContainsAny(text, "road", "street", "lane", "drive", "motorway", "bridge")) return "Road";
|
|
if (ContainsAny(text, "car park", "parking")) return "CarPark";
|
|
if (text.Contains("shop", StringComparison.OrdinalIgnoreCase)) return "Shop";
|
|
if (text.Contains("hospital", StringComparison.OrdinalIgnoreCase)) return "Hospital";
|
|
if (ContainsAny(text, "flat", "apartment", "room")) return "FlatInterior";
|
|
return "UnknownInterior";
|
|
}
|
|
|
|
public static string AssetType(string? value)
|
|
{
|
|
var text = (value ?? string.Empty).ToLowerInvariant();
|
|
var padded = $" {text} ";
|
|
if (text.Contains("ambulance", StringComparison.OrdinalIgnoreCase)) return "Ambulance";
|
|
if (text.Contains("stretcher", StringComparison.OrdinalIgnoreCase)) return "Stretcher";
|
|
if (text.Contains("body bag", StringComparison.OrdinalIgnoreCase)) return "BodyBag";
|
|
if (text.Contains("bottle", StringComparison.OrdinalIgnoreCase)) return "Bottle";
|
|
if (ContainsAny(text, "money", "cash", "coins", "banknote")) return "Money";
|
|
if (ContainsAny(text, "scarf", "scarves")) return "Clothing";
|
|
if (ContainsAny(text, "driving licence", "driver licence", "license", "licence")) return "Document";
|
|
if (ContainsAny(text, "ring", "necklace", "jewellery", "jewelry")) return "Jewellery";
|
|
if (ContainsAny(text, "gun", "pistol", "rifle", "weapon")) return "Weapon";
|
|
if (ContainsAny(padded, " ford fiesta ", " fiesta ", " motor car ", " car ", " vehicle ")) return "Car";
|
|
if (ContainsAny(text, "supercharger", "engine part")) return "CarPart";
|
|
if (ContainsAny(text, "biscuit tin", "tin")) return "Tin";
|
|
if (ContainsAny(text, "parcel", "package")) return "Parcel";
|
|
if (text.Contains("rucksack", StringComparison.OrdinalIgnoreCase)) return "Rucksack";
|
|
if (text.Contains("suitcase", StringComparison.OrdinalIgnoreCase)) return "Suitcase";
|
|
if (text.Contains("note", StringComparison.OrdinalIgnoreCase)) return "Note";
|
|
if (text.Contains("letter", StringComparison.OrdinalIgnoreCase)) return "Letter";
|
|
if (text.Contains("notebook", StringComparison.OrdinalIgnoreCase)) return "Notebook";
|
|
if (text.Contains("book", StringComparison.OrdinalIgnoreCase)) return "Book";
|
|
if (ContainsAny(text, "telephone", "phone", "recorder")) return "Telephone";
|
|
if (text.Contains("key", StringComparison.OrdinalIgnoreCase)) return "Keys";
|
|
if (ContainsAny(text, "tr6")) return "Car";
|
|
if (ContainsAny(text, "photo", "photograph")) return "Photograph";
|
|
if (ContainsAny(text, "document", "paper")) return "Document";
|
|
return "UnknownObject";
|
|
}
|
|
|
|
public static bool LocationCompatible(string requestedType, string candidateType)
|
|
=> requestedType == candidateType
|
|
|| (requestedType is "FrontDoor" or "Porch" && candidateType is "HouseExterior")
|
|
|| (requestedType is "Kitchen" or "LivingRoom" or "Bedroom" && candidateType is "FlatInterior")
|
|
|| (requestedType == "Bathroom" && candidateType == "Bathroom");
|
|
|
|
public static bool AssetCompatible(string requestedType, string candidateType)
|
|
=> requestedType == candidateType
|
|
|| (requestedType == "Note" && candidateType is "Letter" or "Document")
|
|
|| (requestedType == "Rucksack" && candidateType == "Rucksack");
|
|
|
|
public static bool IsGroupEntity(string? value)
|
|
{
|
|
var clean = (value ?? string.Empty).Trim().ToLowerInvariant();
|
|
return clean.EndsWith('s')
|
|
&& (clean.Contains("social worker", StringComparison.Ordinal)
|
|
|| clean.Contains("neighbour", StringComparison.Ordinal)
|
|
|| clean.Contains("neighbor", StringComparison.Ordinal)
|
|
|| clean.Contains("police officer", StringComparison.Ordinal)
|
|
|| clean.Contains("student", StringComparison.Ordinal)
|
|
|| clean.Contains("examiner", StringComparison.Ordinal)
|
|
|| clean.Contains("uniformed", StringComparison.Ordinal)
|
|
|| clean.Contains("responder", StringComparison.Ordinal))
|
|
|| clean is "crowd" or "uniformed people" or "police" or "social services";
|
|
}
|
|
|
|
private static EvidenceSignal ExplicitAgeBand(string text)
|
|
{
|
|
if (ContainsAny(text, "15", "fifteen")) return new(AgeBands.YoungTeen, 0.98m);
|
|
if (ContainsAny(text, "13", "thirteen", "14", "fourteen")) return new(AgeBands.YoungTeen, 0.98m);
|
|
if (ContainsAny(text, "16", "sixteen", "17", "seventeen", "18", "eighteen", "19", "nineteen")) return new(AgeBands.OlderTeen, 0.98m);
|
|
if (ContainsAny(text, "toddler", "baby", "infant", "newborn")) return new(AgeBands.Child, 0.98m);
|
|
if (ContainsAny(text, "child", "young girl", "young boy", "schoolboy", "schoolgirl", " boy ", " girl ", " lad ")) return new(AgeBands.Child, 0.86m);
|
|
if (ContainsAny(text, "teenager", "teenage")) return new(AgeBands.OlderTeen, 0.86m);
|
|
if (ContainsAny(text, "young adult", "young woman", "young man")) return new(AgeBands.YoungAdult, 0.82m);
|
|
if (ContainsAny(text, "middle-aged", "middle aged", "mature adult")) return new(AgeBands.MatureAdult, 0.86m);
|
|
if (ContainsAny(text, "elderly", "senior", "old woman", "old man")) return new(AgeBands.Senior, 0.9m);
|
|
return EvidenceSignal.Unknown;
|
|
}
|
|
|
|
private static EvidenceSignal RelationshipAgeBand(string name, string text)
|
|
{
|
|
if (ContainsAny(text, "toddler", "baby", "newborn")) return new(AgeBands.Child, 0.96m);
|
|
var identity = $"{name} {text}";
|
|
if (ContainsAny(identity, "mother", " mum", "father", " dad", "parent", "aunt", "uncle", "teacher")
|
|
&& !PossessiveOfOtherIdentity(name, text))
|
|
{
|
|
return new(AgeBands.MatureAdult, 0.74m);
|
|
}
|
|
|
|
if (ContainsAny(text, "grandmother", "grandfather")) return new(AgeBands.Senior, 0.82m);
|
|
if (ContainsAny(text, "daughter", "son", "sister", "brother")) return new(AgeBands.Unknown, 0.3m);
|
|
return EvidenceSignal.Unknown;
|
|
}
|
|
|
|
private static EvidenceSignal NameAgeBand(string name)
|
|
{
|
|
var clean = name.Trim().ToLowerInvariant();
|
|
return clean is "beth" ? new(AgeBands.YoungTeen, 0.86m) : EvidenceSignal.Unknown;
|
|
}
|
|
|
|
private static EvidenceSignal TitlePresentation(string text)
|
|
{
|
|
if (ContainsAny(text, " mrs ", " mrs. ", " miss ", " miss. ", " ms ", " ms. ", " lady ")) return new(Presentations.Feminine, 0.96m);
|
|
if (ContainsAny(text, " mr ", " mr. ", " sir ")) return new(Presentations.Masculine, 0.96m);
|
|
return EvidenceSignal.Unknown;
|
|
}
|
|
|
|
private static EvidenceSignal RelationshipPresentation(string text)
|
|
{
|
|
if (ContainsAny(text, "female", "mother", " mum", "mummy", "aunt", "sister", "daughter", "wife", "widow", "grandmother", " niece", "woman", "girl", "lady", "queen")) return new(Presentations.Feminine, 0.92m);
|
|
if (ContainsAny(text, "male", "gentleman", "father", " dad", "daddy", "uncle", "brother", "son", "husband", "widower", "grandfather", " nephew", "man", "boy", "lad", "bloke", "chap", "king")) return new(Presentations.Masculine, 0.92m);
|
|
return EvidenceSignal.Unknown;
|
|
}
|
|
|
|
private static EvidenceSignal PronounPresentation(string text)
|
|
{
|
|
if (ContainsAny(text, " she ", " her ", " hers ", " herself ")) return new(Presentations.Feminine, 0.86m);
|
|
if (ContainsAny(text, " he ", " him ", " his ", " himself ")) return new(Presentations.Masculine, 0.86m);
|
|
return EvidenceSignal.Unknown;
|
|
}
|
|
|
|
private static EvidenceSignal NamePresentation(string text)
|
|
{
|
|
if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " annie ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m);
|
|
if (ContainsAny(text, " graham ", " george ", " john ", " david ", " simon ", " kevin ", " colin ")) return new(Presentations.Masculine, 0.82m);
|
|
return EvidenceSignal.Unknown;
|
|
}
|
|
|
|
private static string HairColour(string text) => NormalizeHairColour(text);
|
|
private static string SkinTone(string text) => NormalizeSkinTone(text);
|
|
private static string HairLength(string text) => NormalizeHairLength(text);
|
|
|
|
private static bool AgeIncompatible(string evidence, string candidate)
|
|
=> (evidence, candidate) switch
|
|
{
|
|
(AgeBands.Child, not AgeBands.Child) => true,
|
|
(AgeBands.YoungTeen, AgeBands.YoungAdult or AgeBands.Adult or AgeBands.MatureAdult or AgeBands.Senior) => true,
|
|
(AgeBands.OlderTeen, AgeBands.MatureAdult or AgeBands.Senior) => true,
|
|
(AgeBands.Adult, AgeBands.Child or AgeBands.YoungTeen or AgeBands.OlderTeen) => true,
|
|
(AgeBands.MatureAdult, AgeBands.Child or AgeBands.YoungTeen or AgeBands.OlderTeen) => true,
|
|
(AgeBands.Senior, AgeBands.Child or AgeBands.YoungTeen or AgeBands.OlderTeen or AgeBands.YoungAdult) => true,
|
|
_ => false
|
|
};
|
|
|
|
private static bool PresentationIncompatible(string evidence, string candidate)
|
|
=> (evidence, candidate) is (Presentations.Feminine, Presentations.Masculine) or (Presentations.Masculine, Presentations.Feminine);
|
|
|
|
private static bool ExplicitKnown(string value, decimal confidence)
|
|
=> !string.IsNullOrWhiteSpace(value) && value != "Unknown" && confidence >= 0.85m;
|
|
|
|
private static bool KnownMismatch(string evidence, string candidate)
|
|
=> !string.Equals(evidence, "Unknown", StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(candidate, "Unknown", StringComparison.OrdinalIgnoreCase)
|
|
&& !string.Equals(evidence, candidate, StringComparison.OrdinalIgnoreCase);
|
|
|
|
private static string FirstKnown(params string[] values)
|
|
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value) && !string.Equals(value, "Unknown", StringComparison.OrdinalIgnoreCase)) ?? "Unknown";
|
|
|
|
private static string? FirstMetadata(Dictionary<string, string>? metadata, params string[] keys)
|
|
=> keys.Select(key => metadata?.GetValueOrDefault(key)).FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
|
|
|
private static bool BoolMetadata(Dictionary<string, string>? metadata, string key)
|
|
=> bool.TryParse(metadata?.GetValueOrDefault(key), out var value) && value;
|
|
|
|
private static bool FeatureValue(IReadOnlyList<string>? features, string value)
|
|
=> features?.Any(feature => feature.Contains(value, StringComparison.OrdinalIgnoreCase)) == true;
|
|
|
|
private static bool ContainsAny(string text, params string[] needles)
|
|
=> needles.Any(needle => text.Contains(needle, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private static string EntityAttributeText(string text, string entityName)
|
|
{
|
|
var cleanName = (entityName ?? string.Empty).Trim().ToLowerInvariant();
|
|
if (string.IsNullOrWhiteSpace(cleanName))
|
|
{
|
|
return text;
|
|
}
|
|
|
|
var sentences = text.Split(['.', '!', '?', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
var scoped = sentences
|
|
.Where(sentence => sentence.Contains(cleanName, StringComparison.OrdinalIgnoreCase))
|
|
.Where(sentence => AttributeNearEntity(sentence, cleanName))
|
|
.ToList();
|
|
return scoped.Count == 0 ? $" {cleanName} " : $" {string.Join(' ', scoped)} ";
|
|
}
|
|
|
|
private static string EntityScopedText(string text, string entityName)
|
|
{
|
|
var cleanName = (entityName ?? string.Empty).Trim().ToLowerInvariant();
|
|
if (string.IsNullOrWhiteSpace(cleanName))
|
|
{
|
|
return text;
|
|
}
|
|
|
|
var sentences = text.Split(['.', '!', '?', ';'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
var possessiveAscii = $"{cleanName}'s";
|
|
var possessiveCurly = $"{cleanName}\u2019s";
|
|
var scoped = sentences.Where(sentence => sentence.Contains(cleanName, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
scoped = scoped
|
|
.Where(sentence => !sentence.Contains(possessiveAscii, StringComparison.OrdinalIgnoreCase)
|
|
&& !sentence.Contains(possessiveCurly, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
return scoped.Count == 0 ? $" {cleanName} " : $" {string.Join(' ', scoped)} ";
|
|
}
|
|
|
|
private static string RelationshipScopedText(string text, string entityName)
|
|
{
|
|
var cleanName = (entityName ?? string.Empty).Trim().ToLowerInvariant();
|
|
if (string.IsNullOrWhiteSpace(cleanName))
|
|
{
|
|
return text;
|
|
}
|
|
|
|
var possessiveAscii = $"{cleanName}'s";
|
|
var possessiveCurly = $"{cleanName}\u2019s";
|
|
if (text.Contains(possessiveAscii, StringComparison.OrdinalIgnoreCase)
|
|
|| text.Contains(possessiveCurly, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var fragments = text.Split(['.', '!', '?', ';', ',', '|'], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
|
.Where(fragment => !fragment.Contains(possessiveAscii, StringComparison.OrdinalIgnoreCase)
|
|
&& !fragment.Contains(possessiveCurly, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
return fragments.Count == 0 ? string.Empty : $" {string.Join(' ', fragments)} ";
|
|
}
|
|
|
|
private static bool AttributeNearEntity(string sentence, string cleanName)
|
|
{
|
|
var lower = sentence.ToLowerInvariant();
|
|
var entityIndex = lower.IndexOf(cleanName, StringComparison.Ordinal);
|
|
if (entityIndex < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var attributeIndex = new[]
|
|
{
|
|
lower.IndexOf("hair", StringComparison.Ordinal),
|
|
lower.IndexOf("skin", StringComparison.Ordinal),
|
|
lower.IndexOf("glasses", StringComparison.Ordinal),
|
|
lower.IndexOf("spectacles", StringComparison.Ordinal),
|
|
lower.IndexOf("beard", StringComparison.Ordinal),
|
|
lower.IndexOf("moustache", StringComparison.Ordinal),
|
|
lower.IndexOf("mustache", StringComparison.Ordinal)
|
|
}
|
|
.Where(index => index >= 0)
|
|
.DefaultIfEmpty(-1)
|
|
.Min();
|
|
return attributeIndex >= 0 && Math.Abs(attributeIndex - entityIndex) <= 48;
|
|
}
|
|
|
|
private static bool PossessiveOfOtherIdentity(string name, string text)
|
|
{
|
|
var cleanName = (name ?? string.Empty).Trim().ToLowerInvariant();
|
|
if (cleanName.Contains("mother", StringComparison.Ordinal)
|
|
|| cleanName.Contains("mum", StringComparison.Ordinal)
|
|
|| cleanName.Contains("father", StringComparison.Ordinal)
|
|
|| cleanName.Contains("dad", StringComparison.Ordinal)
|
|
|| cleanName.Contains("aunt", StringComparison.Ordinal)
|
|
|| cleanName.Contains("uncle", StringComparison.Ordinal)
|
|
|| cleanName.Contains("teacher", StringComparison.Ordinal))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return ContainsAny(text, "'s mother", "'s mum", "'s father", "'s dad", "'s aunt", "'s uncle", " her mother", " his mother", " her mum", " his dad");
|
|
}
|
|
|
|
private sealed record EvidenceSignal(string Value, decimal Confidence)
|
|
{
|
|
public static EvidenceSignal Unknown { get; } = new("Unknown", 0);
|
|
}
|
|
}
|
|
|
|
public sealed record CharacterEvidenceProfile(
|
|
string AgeBand,
|
|
string Presentation,
|
|
string HairColour,
|
|
string SkinTone,
|
|
string HairLength,
|
|
string Glasses,
|
|
string FacialHair,
|
|
decimal AgeConfidence,
|
|
decimal PresentationConfidence,
|
|
decimal RelationshipConfidence,
|
|
decimal NameConfidence,
|
|
decimal PronounConfidence)
|
|
{
|
|
public bool HasStrongPresentationEvidence => PresentationConfidence >= 0.8m;
|
|
}
|
|
|
|
public sealed record IllustrationCharacterMetadata(
|
|
string AgeBand,
|
|
string Presentation,
|
|
string HairColour,
|
|
string SkinTone,
|
|
string HairLength,
|
|
string Glasses,
|
|
string FacialHair,
|
|
bool IsUnknownFigure,
|
|
bool IsGenericFallback,
|
|
bool IsSuitableForNamedCharacter,
|
|
string? PromptTemplateVersion,
|
|
string GenerationSource);
|