Phase 21M - Deterministic Illustration Assignment Repair
This commit is contained in:
parent
948ae2e54d
commit
7001359281
@ -47,7 +47,8 @@ var tests = new (string Name, Action Test)[]
|
||||
("Illustration matching rejects duplicate named portrait allocation", IllustrationMatchingRejectsDuplicateNamedPortraitAllocation),
|
||||
("Illustration semantic types prevent bathroom office and ambulance car mismatches", IllustrationSemanticTypesPreventObviousMismatches),
|
||||
("Illustration demand archetypes stay broad and use active template", IllustrationDemandArchetypesStayBroadAndUseActiveTemplate),
|
||||
("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage)
|
||||
("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage),
|
||||
("Illustration assignment repair enforces explicit evidence groups and strict subtypes", IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes)
|
||||
};
|
||||
|
||||
foreach (var test in tests)
|
||||
@ -347,13 +348,13 @@ static void IllustrationPromptBuilderSeparatesSpecAndPrompt()
|
||||
var result = builder.Build(spec);
|
||||
|
||||
Assert(result.TemplateVersion == IllustrationPromptBuilder.CurrentTemplateVersion, "Template version was not recorded.");
|
||||
Assert(result.TemplateVersion == "21D.2", "Illustration prompt template version should reflect the vivid art-direction reset.");
|
||||
Assert(result.TemplateVersion == "21J.3", "Illustration prompt template version should reflect the current approved art direction.");
|
||||
Assert(result.Prompt.Contains("no visible text", StringComparison.OrdinalIgnoreCase), "Prompt did not include visual safety guidance.");
|
||||
Assert(result.Prompt.Contains("Category composition rules", StringComparison.Ordinal), "Prompt did not include category composition rules.");
|
||||
Assert(result.Prompt.Contains("Structured details", StringComparison.Ordinal), "Prompt did not include structured details.");
|
||||
Assert(result.Prompt.Contains("modern cinematic digital illustration", StringComparison.OrdinalIgnoreCase), "Prompt did not include the new cinematic digital art direction.");
|
||||
Assert(result.Prompt.Contains("premium narrative game concept art", StringComparison.OrdinalIgnoreCase), "Prompt did not include premium game concept-art guidance.");
|
||||
Assert(result.Prompt.Contains("bright balanced lighting", StringComparison.OrdinalIgnoreCase), "Prompt did not include brighter lighting guidance.");
|
||||
Assert(result.Prompt.Contains("balanced cinematic lighting", StringComparison.OrdinalIgnoreCase), "Prompt did not include current lighting guidance.");
|
||||
Assert(result.Prompt.Contains("avoid muddy olive and brown grading", StringComparison.OrdinalIgnoreCase), "Prompt did not explicitly avoid the previous muddy grading failure mode.");
|
||||
Assert(result.Prompt.Contains(spec.Code, StringComparison.Ordinal), "Prompt did not include stable code.");
|
||||
Assert(result.SpecificationJson.Contains("\"code\": \"asset-folded-letter\"", StringComparison.Ordinal), "Specification JSON did not preserve the stable code.");
|
||||
@ -560,6 +561,40 @@ static void IllustrationDemandArchetypesStayBroadAndUseActiveTemplate()
|
||||
Assert(IllustrationPromptBuilder.CurrentTemplateVersion == "21J.3", $"Demand generation should use the active shared template, got {IllustrationPromptBuilder.CurrentTemplateVersion}.");
|
||||
}
|
||||
|
||||
static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes()
|
||||
{
|
||||
var femaleInstructor = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("female instructor", ["female instructor"], []);
|
||||
var maleExaminers = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("younger male examiners", ["younger male examiners"], []);
|
||||
var lad = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("tubby lad", ["tubby lad"], []);
|
||||
var girl = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("girl", ["schoolgirl"], []);
|
||||
var kevin = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Kevin", ["Kevin", "female instructor (Kevin's)"], ["female instructor", "Kevin's examiner"]);
|
||||
|
||||
Assert(femaleInstructor.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"female instructor should be Feminine, got {femaleInstructor.Presentation}.");
|
||||
Assert(maleExaminers.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"male examiners should be Masculine, got {maleExaminers.Presentation}.");
|
||||
Assert(lad.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"lad should be Masculine, got {lad.Presentation}.");
|
||||
Assert(lad.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"lad should infer a young age band, got {lad.AgeBand}.");
|
||||
Assert(girl.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"girl should be Feminine, got {girl.Presentation}.");
|
||||
Assert(girl.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"schoolgirl should infer Child, got {girl.AgeBand}.");
|
||||
Assert(kevin.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"Kevin should not inherit feminine presentation from his instructor, got {kevin.Presentation}.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.IsGroupEntity("social workers"), "Social workers should be treated as a group entity.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.IsGroupEntity("younger male examiners"), "Plural examiners should be treated as a group entity.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.IsGroupEntity("uniformed people"), "Uniformed people should be treated as a group entity.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.IsGroupEntity("social worker"), "Singular social worker should remain an individual entity.");
|
||||
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Bathroom", "LaundryUtilityRoom"), "Bathroom must not match laundry room.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Road", "CarPark"), "Road must not match car park.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("WaitingRoom", "FlatInterior"), "Waiting room must not match flat interior.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Ford Fiesta") == "Car", "Ford Fiesta should classify as Car.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Car") == "Car", "Car should classify as Car.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("supercharger") == "CarPart", "Supercharger should not classify as full car.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("biscuit tin") == "Tin", "Biscuit tin should not classify as Photograph.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("parcel") == "Parcel", "Parcel should not classify as Letter.");
|
||||
|
||||
var beth = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Beth", ["Beth is fifteen."], []);
|
||||
var adult = Metadata("char-family-relative", "MatureAdult", "Feminine");
|
||||
Assert(Rejected(beth, adult).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Existing adult assignment must be discarded when Beth evidence says YoungTeen.");
|
||||
}
|
||||
|
||||
static IllustrationCharacterMetadata Metadata(string code, string ageBand, string presentation, bool isUnknownFigure = false)
|
||||
=> new(
|
||||
ageBand,
|
||||
|
||||
@ -48,11 +48,12 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
public static CharacterEvidenceProfile InferCharacterEvidence(string name, IEnumerable<string> identityValues, IEnumerable<string> relationshipValues)
|
||||
{
|
||||
var identityText = $" {string.Join(' ', identityValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} ";
|
||||
var relationshipText = $" {string.Join(' ', relationshipValues.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(entityText, name);
|
||||
var relationshipSafeText = $"{nameText} {relationshipText}";
|
||||
var scopedText = EntityScopedText($"{nameText} {identityScopedText}", name);
|
||||
var relationshipSafeText = RelationshipScopedText(relationshipText, name);
|
||||
var titlePresentation = TitlePresentation(scopedText);
|
||||
var relationshipPresentation = RelationshipPresentation(relationshipSafeText);
|
||||
var entityPresentation = RelationshipPresentation(scopedText);
|
||||
@ -251,6 +252,7 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
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";
|
||||
@ -260,6 +262,10 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
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";
|
||||
@ -268,7 +274,7 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
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", " car ", "vehicle")) return "Car";
|
||||
if (ContainsAny(text, "tr6")) return "Car";
|
||||
if (ContainsAny(text, "photo", "photograph")) return "Photograph";
|
||||
if (ContainsAny(text, "document", "paper")) return "Document";
|
||||
return "UnknownObject";
|
||||
@ -277,7 +283,7 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
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" or "WaitingRoom" && candidateType is "FlatInterior")
|
||||
|| (requestedType is "Kitchen" or "LivingRoom" or "Bedroom" && candidateType is "FlatInterior")
|
||||
|| (requestedType == "Bathroom" && candidateType == "Bathroom");
|
||||
|
||||
public static bool AssetCompatible(string requestedType, string candidateType)
|
||||
@ -285,12 +291,29 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
|| (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, "child", "young girl", "young boy")) return new(AgeBands.Child, 0.86m);
|
||||
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);
|
||||
@ -327,8 +350,8 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
|
||||
private static EvidenceSignal RelationshipPresentation(string text)
|
||||
{
|
||||
if (ContainsAny(text, "mother", " mum", "mummy", "aunt", "sister", "daughter", "wife", "grandmother", " niece", "woman", "girl", "lady", "queen")) return new(Presentations.Feminine, 0.92m);
|
||||
if (ContainsAny(text, "father", " dad", "daddy", "uncle", "brother", "son", "husband", "grandfather", " nephew", "man", "boy", "lad", "king")) return new(Presentations.Masculine, 0.92m);
|
||||
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;
|
||||
}
|
||||
|
||||
@ -413,10 +436,39 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@ -68,6 +68,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
|
||||
foreach (var observation in observations.Values.OrderByDescending(item => item.Significance).ThenBy(item => item.Name))
|
||||
{
|
||||
if (StoryIntelligenceIllustrationCompatibility.IsGroupEntity(observation.Name))
|
||||
{
|
||||
ApplyGroupDiagnostics(prototype, observation);
|
||||
continue;
|
||||
}
|
||||
|
||||
var existing = existingAssignments.GetValueOrDefault(observation.CharacterKey);
|
||||
var ownExistingItemId = existing?.IllustrationLibraryItemID;
|
||||
if (ownExistingItemId.HasValue && duplicateExistingItemIds.Contains(ownExistingItemId.Value) && !retainedExistingItemIds.Add(ownExistingItemId.Value))
|
||||
@ -119,10 +125,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
|
||||
var reasonReassigned = existingScore is not null && chosen.Candidate.Item.IllustrationLibraryItemID != existing?.IllustrationLibraryItemID
|
||||
? ReassignmentReason(observation, existingScore, chosen)
|
||||
: existing is { IllustrationLibraryItemID: null }
|
||||
? $"Assigned {chosen.Candidate.Item.StableCode} because generated demand or newly valid library art replaced a fallback assignment."
|
||||
: null;
|
||||
var reason = reasonReassigned ?? (existing is null
|
||||
? $"Assigned {chosen.Candidate.Item.StableCode} from structured character evidence."
|
||||
: "Preserved existing Story Intelligence character illustration assignment.");
|
||||
: $"Retained {chosen.Candidate.Item.StableCode}; existing assignment passed hard revalidation with score {chosen.Score:N0}.");
|
||||
var candidateDiagnostics = JsonSerializer.Serialize(allScores.Take(6).Select(MatchDiagnostics.From), JsonOptions);
|
||||
var evidenceJson = JsonSerializer.Serialize(observation.Evidence, JsonOptions);
|
||||
|
||||
@ -140,7 +148,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
reasonReassigned));
|
||||
|
||||
assignedItemIds.Add(chosen.Candidate.Item.IllustrationLibraryItemID);
|
||||
ApplyAssignment(prototype, observation, chosen, saved, reason, reasonReassigned, candidateDiagnostics);
|
||||
ApplyAssignment(prototype, observation, existing, chosen, saved, reason, reasonReassigned, evidenceJson, candidateDiagnostics);
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,7 +167,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
|
||||
if (best is null)
|
||||
{
|
||||
return existingScore;
|
||||
return existingScore.Score >= SuitableScore ? existingScore : null;
|
||||
}
|
||||
|
||||
var materiallyWrong = MateriallyWrong(existingScore.Candidate.Metadata, observation.Evidence);
|
||||
@ -170,7 +178,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
|
||||
if (!materiallyWrong || best.Score < existingScore.Score + ReassignmentMargin)
|
||||
{
|
||||
return existingScore;
|
||||
return existingScore.Score >= SuitableScore ? existingScore : best;
|
||||
}
|
||||
}
|
||||
|
||||
@ -441,10 +449,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
private void ApplyAssignment(
|
||||
StoryIntelligenceExperiencePrototypeViewModel prototype,
|
||||
CharacterObservation observation,
|
||||
StoryIntelligenceCharacterIllustrationAssignment? existing,
|
||||
MatchScore chosen,
|
||||
StoryIntelligenceCharacterIllustrationAssignment saved,
|
||||
string reason,
|
||||
string? reasonReassigned,
|
||||
string evidenceJson,
|
||||
string candidateDiagnostics)
|
||||
{
|
||||
foreach (var character in prototype.Scenes.SelectMany(scene => scene.Characters).Where(character => string.Equals(character.Id, observation.CharacterKey, StringComparison.OrdinalIgnoreCase)))
|
||||
@ -468,6 +478,10 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
RejectedCandidates = candidateDiagnostics,
|
||||
ReasonChosen = reason,
|
||||
ReasonReassigned = reasonReassigned,
|
||||
PreviousIllustration = existing?.StableCode,
|
||||
PreviousEvidence = existing?.EvidenceJson,
|
||||
CurrentEvidence = evidenceJson,
|
||||
DemandKey = DemandKey(observation.Evidence),
|
||||
DemandStatus = "Satisfied",
|
||||
ResolvedPresentation = observation.Evidence.Presentation,
|
||||
ResolvedAgeBand = observation.Evidence.AgeBand,
|
||||
@ -506,6 +520,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
MatchingScore = allScores.FirstOrDefault()?.Score,
|
||||
RejectedCandidates = diagnostics,
|
||||
ReasonChosen = "No suitable unused illustration matched structured character evidence.",
|
||||
CurrentEvidence = JsonSerializer.Serialize(observation.Evidence, JsonOptions),
|
||||
DemandKey = DemandKey(observation.Evidence),
|
||||
DemandStatus = demandStatus,
|
||||
ResolvedPresentation = observation.Evidence.Presentation,
|
||||
ResolvedAgeBand = observation.Evidence.AgeBand,
|
||||
@ -552,6 +568,31 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
|| clean.StartsWith("unidentified ", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void ApplyGroupDiagnostics(StoryIntelligenceExperiencePrototypeViewModel prototype, CharacterObservation observation)
|
||||
{
|
||||
foreach (var character in prototype.Scenes.SelectMany(scene => scene.Characters).Where(character => string.Equals(character.Id, observation.CharacterKey, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
character.LibraryCode = string.Empty;
|
||||
character.ImagePath = "/images/story-intelligence/prototype/character-group.svg";
|
||||
character.ImageResolution = new StoryIntelligenceExperienceImageResolution
|
||||
{
|
||||
EntityId = character.Id,
|
||||
StableCode = string.Empty,
|
||||
IllustrationLibraryItemId = null,
|
||||
Status = "GroupEntity",
|
||||
FinalImageUrl = null,
|
||||
FallbackImageUrl = character.ImagePath,
|
||||
FallbackUsed = true,
|
||||
ReasonChosen = "Collective group entity; reusable group fallback used without unique portrait assignment or demand.",
|
||||
CurrentEvidence = JsonSerializer.Serialize(observation.Evidence, JsonOptions),
|
||||
DemandStatus = "Not required for group entity",
|
||||
ResolvedPresentation = "Unknown",
|
||||
ResolvedAgeBand = "Unknown",
|
||||
IllustrationAllocationStatus = "Group entity"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormaliseUnknown(string? value, string fallback)
|
||||
=> IsUnknown(value) ? fallback : value!;
|
||||
|
||||
|
||||
@ -1760,11 +1760,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
{
|
||||
var value = $"{name} {location?.LocationType} {location?.GenericRoomType} {setting?.LocationType}".ToLowerInvariant();
|
||||
var type = StoryIntelligenceIllustrationCompatibility.LocationType(value);
|
||||
if (type == "Bathroom") return "loc-laundry-utility-room";
|
||||
if (type is "Kitchen" or "Bedroom" or "LivingRoom" or "WaitingRoom" or "FlatInterior" or "Hallway") return "loc-small-flat-interior";
|
||||
if (type is "Bathroom" or "WaitingRoom" or "Road") return string.Empty;
|
||||
if (type is "Kitchen" or "Bedroom" or "LivingRoom" or "FlatInterior" or "Hallway") return "loc-small-flat-interior";
|
||||
if (type == "Office") return "loc-domestic-desk";
|
||||
if (type == "Stairwell") return "loc-narrow-stairwell";
|
||||
if (type is "CarPark" or "Road") return "loc-wet-car-park";
|
||||
if (type is "CarPark") return "loc-wet-car-park";
|
||||
if (type == "HouseExterior") return "loc-estate-house-exterior";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
@ -151,6 +151,10 @@ public sealed class StoryIntelligenceExperienceImageResolution
|
||||
public string? RejectedCandidates { get; init; }
|
||||
public string? ReasonChosen { get; init; }
|
||||
public string? ReasonReassigned { get; init; }
|
||||
public string? PreviousIllustration { get; init; }
|
||||
public string? PreviousEvidence { get; init; }
|
||||
public string? CurrentEvidence { get; init; }
|
||||
public string? DemandKey { get; init; }
|
||||
public string? DemandStatus { get; init; }
|
||||
public string? ResolvedPresentation { get; init; }
|
||||
public string? ResolvedAgeBand { get; init; }
|
||||
|
||||
@ -24,8 +24,11 @@
|
||||
<th>Identity</th>
|
||||
<th>Illustration</th>
|
||||
<th>Resolved evidence</th>
|
||||
<th>Revalidation</th>
|
||||
<th>Assignment</th>
|
||||
<th>Previous / change</th>
|
||||
<th>Demand</th>
|
||||
<th>Demand key</th>
|
||||
<th>Candidate diagnostics</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -37,8 +40,19 @@
|
||||
<td><strong>@row.Character.Name</strong><br /><span class="muted">@row.Character.Id</span></td>
|
||||
<td><code>@(row.Character.ImageResolution.StableCode ?? row.Character.LibraryCode)</code><br />@row.Character.ImageResolution.Status</td>
|
||||
<td>Age @Display(row.Character.ImageResolution.ResolvedAgeBand)<br />Presentation @Display(row.Character.ImageResolution.ResolvedPresentation)</td>
|
||||
<td>@Revalidation(row.Character.ImageResolution)</td>
|
||||
<td>@Display(row.Character.ImageResolution.ReasonChosen)<br /><span class="muted">@Display(row.Character.ImageResolution.IllustrationAllocationStatus)</span></td>
|
||||
<td>
|
||||
Previous illustration: <code>@Display(row.Character.ImageResolution.PreviousIllustration)</code><br />
|
||||
Change: @Display(row.Character.ImageResolution.ReasonReassigned)<br />
|
||||
<details>
|
||||
<summary>Evidence</summary>
|
||||
<pre class="small mb-0">Previous: @Display(row.Character.ImageResolution.PreviousEvidence)
|
||||
Current: @Display(row.Character.ImageResolution.CurrentEvidence)</pre>
|
||||
</details>
|
||||
</td>
|
||||
<td>@Display(row.Character.ImageResolution.DemandStatus)</td>
|
||||
<td><code>@Display(row.Character.ImageResolution.DemandKey)</code></td>
|
||||
<td><pre class="small mb-0">@Display(row.Character.ImageResolution.RejectedCandidates)</pre></td>
|
||||
</tr>
|
||||
}
|
||||
@ -116,4 +130,20 @@
|
||||
@functions {
|
||||
private static string Display(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) ? "none" : value;
|
||||
|
||||
private static string Revalidation(StoryIntelligenceExperienceImageResolution resolution)
|
||||
{
|
||||
if (string.Equals(resolution.Status, "GroupEntity", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "Group entity";
|
||||
}
|
||||
|
||||
if (resolution.FallbackUsed)
|
||||
{
|
||||
return "No compatible illustration";
|
||||
}
|
||||
|
||||
return string.IsNullOrWhiteSpace(resolution.ReasonReassigned) ? "Passed" : "Reassigned";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,20 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240" role="img" aria-label="Group of people">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#18283d"/>
|
||||
<stop offset="1" stop-color="#07131f"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="ring" x1="0" x2="1" y1="0" y2="1">
|
||||
<stop offset="0" stop-color="#8fb9df"/>
|
||||
<stop offset="1" stop-color="#d9a85f"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="240" height="240" rx="120" fill="url(#bg)"/>
|
||||
<circle cx="120" cy="120" r="86" fill="none" stroke="url(#ring)" stroke-width="6" opacity=".85"/>
|
||||
<circle cx="120" cy="92" r="26" fill="#d8e5f1" opacity=".54"/>
|
||||
<path d="M74 164c8-31 28-48 46-48s38 17 46 48" fill="#d8e5f1" opacity=".32"/>
|
||||
<circle cx="76" cy="112" r="20" fill="#8fb9df" opacity=".38"/>
|
||||
<path d="M42 166c7-25 21-38 34-38 11 0 22 8 29 24" fill="#8fb9df" opacity=".2"/>
|
||||
<circle cx="164" cy="112" r="20" fill="#d9a85f" opacity=".42"/>
|
||||
<path d="M135 152c7-16 18-24 29-24 13 0 27 13 34 38" fill="#d9a85f" opacity=".2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Loading…
x
Reference in New Issue
Block a user