Tighten story intelligence evidence visuals
This commit is contained in:
parent
d7fe153816
commit
72148565e2
@ -1044,6 +1044,7 @@ static void Phase21UExtractsMandatoryCharacterAppearanceFixtures()
|
||||
Assert(facts.Any(fact => fact.CanonicalName == "Maggie" && fact.AttributeType == "HairLength" && fact.NormalisedValue == "Long"), "Maggie's long-hair evidence was not extracted.");
|
||||
Assert(facts.Any(fact => fact.CanonicalName == "Beth" && fact.AttributeType == "HairColour" && fact.NormalisedValue == "Blonde"), "Beth's blonde evidence was not extracted.");
|
||||
Assert(facts.Any(fact => fact.CanonicalName == "Rosie" && fact.AttributeType == "HairColour" && fact.NormalisedValue == "Blonde"), "Rosie's blonde evidence was not extracted.");
|
||||
Assert(!CharacterAppearanceExtractionService.Extract("Beth went next door and mumbled, 'Mum.'", ["Beth", "Mum"]).Any(fact => fact.CanonicalName == "Beth" && fact.AttributeType == "AgeBand" && fact.NormalisedValue == StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult), "Nearby Mum text must not age Beth into a mature adult.");
|
||||
Assert(facts.Any(fact => fact.CanonicalName == "Zoe Davies" && fact.AttributeType == "Alias" && fact.NormalisedValue == "miss davies"), "Miss Davies should resolve as an alias of Zoe Davies.");
|
||||
Assert(facts.Any(fact => fact.CanonicalName == "Zoe Davies" && fact.AttributeType == "Alias" && fact.NormalisedValue == "zoe"), "Zoe should resolve as an alias of Zoe Davies.");
|
||||
|
||||
@ -1074,8 +1075,22 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
||||
var redHairedMaggie = StoryMemoryService.ResolveCharacterEvidenceFromAttributes("Maggie", [
|
||||
new StoryMemoryCharacterAttribute { AttributeType = "HairColour", NormalisedValue = StoryIntelligenceIllustrationCompatibility.HairColours.Red, Confidence = 0.95m, IsExplicit = true }
|
||||
]);
|
||||
var bethFromLeakyEvidence = StoryMemoryService.ResolveCharacterEvidenceFromAttributes("Beth", [
|
||||
new StoryMemoryCharacterAttribute { AttributeType = "AgeBand", NormalisedValue = StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult, Confidence = 0.9m, IsExplicit = true, EvidenceSummary = "Beth goes next door and mumbles 'Mum'" },
|
||||
new StoryMemoryCharacterAttribute { AttributeType = "AgeBand", NormalisedValue = StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult, Confidence = 0.9m, IsExplicit = true, EvidenceSummary = "No fifteen-year-old girl with a future." },
|
||||
new StoryMemoryCharacterAttribute { AttributeType = "Presentation", NormalisedValue = StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, Confidence = 0.95m, IsExplicit = true, EvidenceSummary = "I checked my look in the mirror." }
|
||||
]);
|
||||
var mrsWithTeenContext = StoryMemoryService.ApplyImportContextToEvidence(
|
||||
StoryMemoryService.ResolveCharacterEvidenceFromAttributes("Mrs Patterson", [
|
||||
new StoryMemoryCharacterAttribute { AttributeType = "AgeBand", NormalisedValue = StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, Confidence = 0.88m, IsExplicit = true }
|
||||
]),
|
||||
"Mrs Patterson",
|
||||
new StoryMemoryImportContext(StoryMemoryAppearancePreferences.PredominantlyLight, "OlderTeen", "1980s", "UK"));
|
||||
var childCandidate = Metadata("char-child", "Child", "Feminine");
|
||||
var darkRedCandidate = Metadata("char-red-dark", "OlderTeen", "Feminine", hairColour: "Red", skinTone: "Dark");
|
||||
Assert(bethFromLeakyEvidence.AgeBand != StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult, $"Leaked family-role evidence should not age Beth up, got {bethFromLeakyEvidence.AgeBand}.");
|
||||
Assert(bethFromLeakyEvidence.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Name/title guard should repair leaked Beth presentation, got {bethFromLeakyEvidence.Presentation}.");
|
||||
Assert(mrsWithTeenContext.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Adult-role titles must not be overridden by primary age context, got {mrsWithTeenContext.AgeBand}.");
|
||||
Assert(Rejected(mum, childCandidate).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Parent role should reject child portraits.");
|
||||
Assert(Rejected(redHairedMaggie, darkRedCandidate).Any(reason => reason.Contains("skin tone hard mismatch", StringComparison.OrdinalIgnoreCase)), "Red-haired light-skin evidence must reject dark-skin generated portraits.");
|
||||
Assert(examiner.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Driving examiner should infer Adult, got {examiner.AgeBand}.");
|
||||
|
||||
@ -67,6 +67,11 @@ public static class CharacterAppearanceExtractionService
|
||||
foreach (Match match in AgeRegex.Matches(sourceText))
|
||||
{
|
||||
var rawName = match.Groups["name"].Value.Trim();
|
||||
if (LooksLikeLeakedRelationshipAge(rawName, match.Groups["age"].Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var age = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence(rawName, [match.Value], []).AgeBand;
|
||||
if (age != StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown)
|
||||
{
|
||||
@ -161,6 +166,23 @@ public static class CharacterAppearanceExtractionService
|
||||
private static string ShortFragment(string value)
|
||||
=> Regex.Replace(value.Trim(), @"\s+", " ") is var clean && clean.Length > 160 ? clean[..160].TrimEnd() : clean;
|
||||
|
||||
private static bool LooksLikeLeakedRelationshipAge(string rawName, string rawAge)
|
||||
{
|
||||
var name = Normalise(rawName);
|
||||
var age = Normalise(rawAge);
|
||||
var relationshipAge = age is "mother" or "mum" or "father" or "dad" or "aunt" or "uncle";
|
||||
if (!relationshipAge)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return name is not ("mum" or "mother" or "dad" or "father" or "aunt" or "uncle")
|
||||
&& !name.StartsWith("mrs ", StringComparison.OrdinalIgnoreCase)
|
||||
&& !name.StartsWith("miss ", StringComparison.OrdinalIgnoreCase)
|
||||
&& !name.StartsWith("ms ", StringComparison.OrdinalIgnoreCase)
|
||||
&& !name.StartsWith("mr ", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static string Normalise(string? value)
|
||||
{
|
||||
var chars = (value ?? string.Empty).Trim().ToLowerInvariant().Select(ch => char.IsLetterOrDigit(ch) ? ch : ' ').ToArray();
|
||||
|
||||
@ -1618,14 +1618,19 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
var locations = await storyMemoryRepository.ListLocationsAsync(importSessionId);
|
||||
var assets = await storyMemoryRepository.ListAssetsAsync(importSessionId);
|
||||
var attributes = await storyMemoryRepository.ListCharacterAttributesAsync(importSessionId);
|
||||
var importContext = await storyMemoryRepository.GetImportContextAsync(importSessionId);
|
||||
var characterIds = characters.ToDictionary(item => item.StoryMemoryCharacterID, item => item.CanonicalIdentityKey);
|
||||
var locationIds = locations.ToDictionary(item => item.StoryMemoryLocationID, item => item.CanonicalIdentityKey);
|
||||
var assetIds = assets.ToDictionary(item => item.StoryMemoryAssetID, item => item.CanonicalIdentityKey);
|
||||
var characterEvidence = characters.ToDictionary(
|
||||
item => item.CanonicalIdentityKey,
|
||||
item => StoryMemoryService.ResolveCharacterEvidenceFromAttributes(
|
||||
item.DisplayName,
|
||||
attributes.Where(attribute => attribute.StoryMemoryCharacterID == item.StoryMemoryCharacterID)),
|
||||
item =>
|
||||
{
|
||||
var evidence = StoryMemoryService.ResolveCharacterEvidenceFromAttributes(
|
||||
item.DisplayName,
|
||||
attributes.Where(attribute => attribute.StoryMemoryCharacterID == item.StoryMemoryCharacterID));
|
||||
return StoryMemoryService.ApplyImportContextToEvidence(evidence, item.DisplayName, importContext);
|
||||
},
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
var catalogue = (await illustrationRepository.ListAsync(new IllustrationLibraryFilter()))
|
||||
.ToDictionary(item => item.IllustrationLibraryItemID);
|
||||
@ -1673,6 +1678,16 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
|
||||
CharacterEvidenceProfile? evidence = null;
|
||||
characterEvidence?.TryGetValue(entityKey, out evidence);
|
||||
if (entityType == StoryMemoryEntityTypes.Character && evidence is not null)
|
||||
{
|
||||
var metadata = StoryIntelligenceIllustrationCompatibility.CharacterMetadataFrom(item);
|
||||
var rejections = StoryIntelligenceIllustrationCompatibility.CharacterHardRejections(evidence, metadata, true, false);
|
||||
if (rejections.Count > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
var currentEvidence = evidence is null
|
||||
? null
|
||||
: $"age={evidence.AgeBand}; presentation={evidence.Presentation}; hair={evidence.HairColour}; hairLength={evidence.HairLength}; skin={evidence.SkinTone}";
|
||||
|
||||
@ -164,7 +164,7 @@ public sealed class StoryMemoryService(
|
||||
CharacterEvidence(character, attributes.Where(item => item.StoryMemoryCharacterID == character.StoryMemoryCharacterID)),
|
||||
character.CanonicalIdentityKey,
|
||||
options.AppearancePreference);
|
||||
evidence = ApplyPrimaryAgeContext(evidence, options.PrimaryAgeGroup);
|
||||
evidence = ApplyPrimaryAgeContext(evidence, options.PrimaryAgeGroup, character.DisplayName);
|
||||
var existing = assignments.FirstOrDefault(item => item.EntityType == StoryMemoryEntityTypes.Character && item.EntityID == character.StoryMemoryCharacterID);
|
||||
var existingItem = existing?.IllustrationLibraryItemID is int existingId
|
||||
? catalogue.FirstOrDefault(item => item.IllustrationLibraryItemID == existingId)
|
||||
@ -545,7 +545,13 @@ public sealed class StoryMemoryService(
|
||||
: evidence with { SkinTone = skinTone };
|
||||
}
|
||||
|
||||
private static CharacterEvidenceProfile ApplyPrimaryAgeContext(CharacterEvidenceProfile evidence, string primaryAgeGroup)
|
||||
public static CharacterEvidenceProfile ApplyImportContextToEvidence(CharacterEvidenceProfile evidence, string displayName, StoryMemoryImportContext context)
|
||||
{
|
||||
var withAppearance = ApplyAppearancePreference(evidence, Normalise(displayName), context.AppearancePreference);
|
||||
return ApplyPrimaryAgeContext(withAppearance, context.PrimaryAgeGroup, displayName);
|
||||
}
|
||||
|
||||
private static CharacterEvidenceProfile ApplyPrimaryAgeContext(CharacterEvidenceProfile evidence, string primaryAgeGroup, string displayName)
|
||||
{
|
||||
var contextAge = StoryIntelligenceIllustrationCompatibility.NormalizeAgeBand(primaryAgeGroup);
|
||||
if (contextAge == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown)
|
||||
@ -553,8 +559,9 @@ public sealed class StoryMemoryService(
|
||||
return evidence;
|
||||
}
|
||||
|
||||
return evidence.AgeBand is StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown
|
||||
or StoryIntelligenceIllustrationCompatibility.AgeBands.Adult
|
||||
return string.Equals(evidence.AgeBand, StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||
|| (string.Equals(evidence.AgeBand, StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, StringComparison.OrdinalIgnoreCase)
|
||||
&& !NameImpliesAdultRole(displayName))
|
||||
? evidence with { AgeBand = contextAge, AgeConfidence = Math.Max(evidence.AgeConfidence, 0.74m) }
|
||||
: evidence;
|
||||
}
|
||||
@ -580,8 +587,8 @@ public sealed class StoryMemoryService(
|
||||
displayName,
|
||||
[displayName],
|
||||
[]);
|
||||
var age = PickAttribute(attributeList, "AgeBand", inferred.AgeBand, inferred.AgeConfidence);
|
||||
var presentation = PickAttribute(attributeList, "Presentation", inferred.Presentation, inferred.PresentationConfidence);
|
||||
var age = PickAgeAttribute(attributeList, displayName, inferred.AgeBand, inferred.AgeConfidence);
|
||||
var presentation = GuardPresentation(displayName, PickAttribute(attributeList, "Presentation", inferred.Presentation, inferred.PresentationConfidence), inferred);
|
||||
var hairColour = PickAttribute(attributeList, "HairColour", inferred.HairColour, 0.1m);
|
||||
var skinTone = PickAttribute(attributeList, "SkinTone", inferred.SkinTone, 0.1m);
|
||||
var hairLength = PickAttribute(attributeList, "HairLength", inferred.HairLength, 0.1m);
|
||||
@ -624,6 +631,88 @@ public sealed class StoryMemoryService(
|
||||
: (selected.NormalisedValue, selected.Confidence);
|
||||
}
|
||||
|
||||
private static (string Value, decimal Confidence) PickAgeAttribute(
|
||||
IReadOnlyList<StoryMemoryCharacterAttribute> attributes,
|
||||
string displayName,
|
||||
string fallback,
|
||||
decimal fallbackConfidence)
|
||||
{
|
||||
var selected = attributes
|
||||
.Where(item => string.Equals(item.AttributeType, "AgeBand", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(item => !LooksLikeLeakedFamilyAge(displayName, item))
|
||||
.OrderByDescending(item => item.IsExplicit)
|
||||
.ThenByDescending(item => item.Confidence)
|
||||
.ThenByDescending(item => AgeSpecificity(item.NormalisedValue))
|
||||
.ThenByDescending(item => item.SourceSceneResultID ?? 0)
|
||||
.FirstOrDefault();
|
||||
return selected is null
|
||||
? (fallback, fallbackConfidence)
|
||||
: (selected.NormalisedValue, selected.Confidence);
|
||||
}
|
||||
|
||||
private static (string Value, decimal Confidence) GuardPresentation(
|
||||
string displayName,
|
||||
(string Value, decimal Confidence) selected,
|
||||
CharacterEvidenceProfile inferred)
|
||||
{
|
||||
if (string.Equals(inferred.Presentation, StoryIntelligenceIllustrationCompatibility.Presentations.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||
|| inferred.PresentationConfidence < 0.9m
|
||||
|| string.Equals(selected.Value, inferred.Presentation, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return selected;
|
||||
}
|
||||
|
||||
return (inferred.Presentation, Math.Max(selected.Confidence, inferred.PresentationConfidence));
|
||||
}
|
||||
|
||||
private static bool LooksLikeLeakedFamilyAge(string displayName, StoryMemoryCharacterAttribute attribute)
|
||||
{
|
||||
if (!string.Equals(attribute.NormalisedValue, StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (NameImpliesAdultRole(displayName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var evidence = attribute.EvidenceSummary ?? string.Empty;
|
||||
return ContainsAnyLocal(evidence, " mum", "mother", " dad", "father", "aunt", "uncle", "parent");
|
||||
}
|
||||
|
||||
private static bool ContainsAnyLocal(string text, params string[] needles)
|
||||
=> needles.Any(needle => text.Contains(needle, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static int AgeSpecificity(string? ageBand)
|
||||
=> ageBand switch
|
||||
{
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.Child => 5,
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.YoungTeen => 5,
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.OlderTeen => 5,
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult => 4,
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult => 4,
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.Senior => 4,
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.Adult => 1,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
private static bool NameImpliesAdultRole(string? displayName)
|
||||
{
|
||||
var clean = Normalise(displayName);
|
||||
return clean is "mum" or "mother" or "dad" or "father" or "aunt" or "uncle"
|
||||
|| clean.Contains(" mum", StringComparison.Ordinal)
|
||||
|| clean.Contains(" mother", StringComparison.Ordinal)
|
||||
|| clean.Contains(" dad", StringComparison.Ordinal)
|
||||
|| clean.Contains(" father", StringComparison.Ordinal)
|
||||
|| clean.Contains(" aunt", StringComparison.Ordinal)
|
||||
|| clean.Contains(" uncle", StringComparison.Ordinal)
|
||||
|| clean.StartsWith("mrs ", StringComparison.Ordinal)
|
||||
|| clean.StartsWith("mr ", StringComparison.Ordinal)
|
||||
|| clean.StartsWith("dr ", StringComparison.Ordinal)
|
||||
|| clean.StartsWith("professor ", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static IReadOnlyDictionary<string, string> BuildCrossSceneIdentityAliases(IReadOnlyList<SceneResultWithParse> scenes)
|
||||
{
|
||||
var aliases = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@ -898,25 +898,30 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card {
|
||||
width: min(172px, calc(var(--node-size, 150px) + 44px));
|
||||
height: auto;
|
||||
width: var(--node-size, 150px);
|
||||
height: var(--node-size, 150px);
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card .story-character__image {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.story-character.is-evidence-card .story-character__text {
|
||||
position: relative;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: min(172px, calc(var(--node-size, 150px) + 44px));
|
||||
min-height: 86px;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
align-content: center;
|
||||
justify-items: start;
|
||||
gap: 5px;
|
||||
border-color: rgba(169, 205, 240, 0.22);
|
||||
border-radius: 12px;
|
||||
padding: 10px 11px;
|
||||
padding: clamp(8px, calc(var(--node-size, 150px) * 0.07), 12px);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(16, 34, 58, 0.94), rgba(5, 13, 25, 0.9));
|
||||
text-align: left;
|
||||
@ -1066,19 +1071,26 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-asset.is-evidence-card {
|
||||
width: 132px;
|
||||
width: var(--node-size, 78px);
|
||||
height: var(--node-size, 78px);
|
||||
}
|
||||
|
||||
.story-asset.is-evidence-card .story-asset__image {
|
||||
display: none;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.story-asset.is-evidence-card .story-asset__text {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
align-content: center;
|
||||
justify-items: start;
|
||||
width: 132px;
|
||||
min-height: 64px;
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
border-color: rgba(215, 168, 93, 0.18);
|
||||
padding: 8px 9px;
|
||||
padding: 7px 8px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(34, 29, 18, 0.88), rgba(7, 13, 21, 0.84));
|
||||
text-align: left;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user