Improve road locations and age evidence

This commit is contained in:
Nick Beckley 2026-07-26 15:10:13 +00:00
parent 9db13a7fd6
commit 049628b295
4 changed files with 87 additions and 14 deletions

View File

@ -1086,11 +1086,18 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
]),
"Mrs Patterson",
new StoryMemoryImportContext(StoryMemoryAppearancePreferences.PredominantlyLight, "OlderTeen", "1980s", "UK"));
var rosieBirthday = StoryMemoryService.ResolveCharacterEvidenceFromAttributes("Rosie", [
new StoryMemoryCharacterAttribute { AttributeType = "AgeBand", NormalisedValue = StoryIntelligenceIllustrationCompatibility.AgeBands.Child, Confidence = 0.98m, IsExplicit = true, EvidenceSummary = "Deterministic evidence inference" },
new StoryMemoryCharacterAttribute { AttributeType = "AgeBand", NormalisedValue = StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult, Confidence = 0.90m, IsExplicit = true, EvidenceSummary = "She may be eighteen now, but she is still the baby of the group." }
]);
var gregFromRosieAction = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Greg", ["Greg offers to take Rosie to hospital."], []);
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(rosieBirthday.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult, $"Rosie's explicit eighteen-year-old evidence must beat weak child leakage, got {rosieBirthday.AgeBand}.");
Assert(gregFromRosieAction.Presentation != StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, "Greg must not inherit feminine presentation from action text mentioning Rosie.");
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}.");
@ -1098,6 +1105,8 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Cock Hill Lane") == "Lane", "Cock Hill Lane should be Lane.");
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Mount Pleasant Road") == "Road", "Mount Pleasant Road should be Road.");
Assert(StoryIntelligenceIllustrationCompatibility.LocationCompatible("Street", "Road"), "Side streets should be able to reuse road artwork while demand-specific street art is pending.");
Assert(StoryIntelligenceIllustrationCompatibility.LocationCompatible("Road", "Street"), "Named roads should be able to reuse street artwork when it is the closest generated road-family image.");
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("car interior") == "VehicleInterior", "Car interior should be VehicleInterior.");
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("bathroom") == "Bathroom", "Bathroom should be Bathroom.");
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("waiting room") == "WaitingRoom", "Waiting room should be WaitingRoom.");
@ -1136,9 +1145,11 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
Assert(storyMemory.Contains("UpsertPlannedAsync(specification", StringComparison.Ordinal), "Durable Story Memory demand must create reusable illustration library items.");
Assert(storyMemory.Contains("QueueAsync(planned.IllustrationLibraryItemID", StringComparison.Ordinal), "Durable Story Memory demand must queue generated library items for the worker.");
Assert(storyMemory.Contains("demand.IllustrationLibraryItemId", StringComparison.Ordinal), "Demand fallback assignments must retain queued item IDs so generated images can attach later.");
Assert(storyMemory.Contains("categoryCap", StringComparison.Ordinal), "Automatic demand generation must distribute the queue cap across characters, locations and assets.");
Assert(storyMemory.Contains("var categoryCap = maxDemandToQueue;", StringComparison.Ordinal), "Automatic demand generation should cap in-flight work per category, not starve locations and assets behind a divided cap.");
Assert(storyMemory.Contains("or IllustrationLibraryStatuses.Generating);", StringComparison.Ordinal), "Demand generation cap must count only in-flight jobs, not already generated preserved images.");
Assert(!storyMemory.Contains("or IllustrationLibraryStatuses.Generated\r\n or IllustrationLibraryStatuses.Approved", StringComparison.Ordinal), "Generated or approved preserved images must not permanently fill the demand queue cap.");
Assert(storyMemory.Contains("\"Flyover\"", StringComparison.Ordinal) && storyMemory.Contains("concrete overpass", StringComparison.Ordinal), "Location prompts must distinguish road-under-flyover scenes from generic roads.");
Assert(storyMemory.Contains("semanticType is \"Road\" or \"Lane\" or \"Street\"", StringComparison.Ordinal), "Road-family locations must be treated as exterior scenes.");
Assert(storyMemory.Contains("evidence.HairColour", StringComparison.Ordinal), "Character demand keys/specs must include durable hair colour evidence.");
Assert(storyMemory.Contains("Maximum one primary POV", StringComparison.Ordinal) || storyMemory.Contains("ResolveScenePov", StringComparison.Ordinal), "Story Memory must use a single POV resolver.");
Assert(!script.Contains("renderOverflowSummary", StringComparison.Ordinal), "Side panels must not use hidden '+N more' overflow controls.");
@ -1205,7 +1216,7 @@ static void StoryIntelligenceVisualPolishKeepsLabelsFixedAndSemanticsSpecific()
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("wall clock") == "Clock", "Clock should classify as Clock.");
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("park bench") == "Bench", "Bench should classify as Bench.");
Assert(snapshot.Contains("\"loc-generic-road\" => $\"{FallbackRoot}/location-road.svg\"", StringComparison.Ordinal), "Roads need a generic road fallback illustration.");
Assert(snapshot.Contains("type is \"Road\" or \"Lane\" or \"Street\"", StringComparison.Ordinal), "Road-family scene locations need a generic road fallback illustration.");
Assert(snapshot.Contains("\"loc-vehicle-interior\" => $\"{FallbackRoot}/location-vehicle-interior.svg\"", StringComparison.Ordinal), "Vehicle interiors need a non-building fallback illustration.");
Assert(snapshot.Contains("\"asset-document\" => $\"{FallbackRoot}/asset-document.svg\"", StringComparison.Ordinal), "Documents need a recognisable document fallback.");
Assert(snapshot.Contains("\"asset-traffic-sign\" => $\"{FallbackRoot}/asset-traffic-sign.svg\"", StringComparison.Ordinal), "Traffic signs need a recognisable fallback.");

View File

@ -61,7 +61,7 @@ public static class StoryIntelligenceIllustrationCompatibility
var relationshipPresentation = RelationshipPresentation(relationshipSafeText);
var entityPresentation = RelationshipPresentation(scopedText);
var pronounPresentation = PronounPresentation(scopedText);
var namePresentation = NamePresentation(entityText);
var namePresentation = NamePresentation(nameText);
var presentation = FirstKnown(
titlePresentation.Value,
entityPresentation.Value,
@ -395,7 +395,7 @@ public static class StoryIntelligenceIllustrationCompatibility
public static bool LocationCompatible(string requestedType, string candidateType)
=> requestedType == candidateType
|| (requestedType is "Lane" or "Street" or "Avenue" or "Drive" or "Close" && candidateType == "Road")
|| (requestedType is "Road" or "Lane" or "Street" or "Avenue" or "Drive" or "Close" && candidateType is "Road" or "Lane" or "Street")
|| (requestedType is "DriverSeat" or "PassengerSeat" or "RearSeat" && candidateType == "VehicleInterior")
|| (requestedType is "FrontDoor" or "Porch" && candidateType is "HouseExterior")
|| (requestedType is "Kitchen" or "LivingRoom" or "Bedroom" && candidateType is "FlatInterior")

View File

@ -2264,7 +2264,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
if (type == "WaitingRoom") return "loc-generic-waiting-room";
if (type == "DrivingTestCentre") return "loc-driving-test-centre";
if (type == "VehicleInterior") return "loc-vehicle-interior";
if (type == "Road") return "loc-generic-road";
if (type is "Road" or "Lane" or "Street" or "Avenue" or "Drive" or "Close" or "Motorway" or "Bridge" or "Flyover" or "Underpass") return "loc-generic-road";
if (type == "Garage") return "loc-garage-workshop";
if (type is "Hospital" or "PoliceStation" or "School" or "Church" or "Restaurant" or "Pub") return $"loc-generic-{StableKey(type)}";
if (type is "Kitchen" or "Bedroom" or "LivingRoom" or "FlatInterior" or "Hallway") return "loc-small-flat-interior";

View File

@ -637,9 +637,18 @@ public sealed class StoryMemoryService(
string fallback,
decimal fallbackConfidence)
{
var selected = attributes
var ageAttributes = attributes
.Where(item => string.Equals(item.AttributeType, "AgeBand", StringComparison.OrdinalIgnoreCase))
.Where(item => !LooksLikeLeakedFamilyAge(displayName, item))
.ToList();
if (ageAttributes.Any(item => IsTeenOrAdultAge(item.NormalisedValue)))
{
ageAttributes = ageAttributes
.Where(item => !LooksLikeLeakedChildAge(displayName, item))
.ToList();
}
var selected = ageAttributes
.OrderByDescending(item => item.IsExplicit)
.ThenByDescending(item => item.Confidence)
.ThenByDescending(item => AgeSpecificity(item.NormalisedValue))
@ -681,6 +690,35 @@ public sealed class StoryMemoryService(
return ContainsAnyLocal(evidence, " mum", "mother", " dad", "father", "aunt", "uncle", "parent");
}
private static bool LooksLikeLeakedChildAge(string displayName, StoryMemoryCharacterAttribute attribute)
{
if (!string.Equals(attribute.NormalisedValue, StoryIntelligenceIllustrationCompatibility.AgeBands.Child, StringComparison.OrdinalIgnoreCase))
{
return false;
}
var evidence = $"{displayName} {attribute.EvidenceSummary}";
if (ContainsAnyLocal(evidence, "toddler", "infant", "newborn", "schoolboy", "schoolgirl", "ten-year-old", "ten year old", "aged 10", "aged 11", "aged 12"))
{
return false;
}
if (ContainsAnyLocal(evidence, "baby of the group", "baby of group"))
{
return true;
}
return !ContainsAnyLocal(evidence, " child", " child ", "young child", " boy", " girl aged", "age 10", "age 11", "age 12");
}
private static bool IsTeenOrAdultAge(string? ageBand)
=> ageBand is StoryIntelligenceIllustrationCompatibility.AgeBands.YoungTeen
or StoryIntelligenceIllustrationCompatibility.AgeBands.OlderTeen
or StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult
or StoryIntelligenceIllustrationCompatibility.AgeBands.Adult
or StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult
or StoryIntelligenceIllustrationCompatibility.AgeBands.Senior;
private static bool ContainsAnyLocal(string text, params string[] needles)
=> needles.Any(needle => text.Contains(needle, StringComparison.OrdinalIgnoreCase));
@ -911,9 +949,7 @@ public sealed class StoryMemoryService(
if (options.MaxDemandToQueue is int maxDemandToQueue && maxDemandToQueue > 0)
{
var categoryCap = specification.Category == IllustrationLibraryCategories.Character
? maxDemandToQueue
: Math.Max(1, maxDemandToQueue / 3);
var categoryCap = maxDemandToQueue;
var queuedDemandItems = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter()))
.Count(item => string.Equals(item.Category, specification.Category, StringComparison.OrdinalIgnoreCase)
&& item.IsActive
@ -980,18 +1016,21 @@ public sealed class StoryMemoryService(
{
var code = DemandStableCode(StoryMemoryEntityTypes.Location, location.CanonicalIdentityKey, requestedType);
var locationType = UnknownToDefault(requestedType, "UnknownInterior");
var era = UnknownToDefault(options.StoryEra, "1980s");
var storyLocation = UnknownToDefault(options.StoryLocation, "UK");
var subject = LocationVisualSubject(location.DisplayName, locationType, era, storyLocation);
return new IllustrationGenerationSpecification
{
Category = IllustrationLibraryCategories.Location,
Code = code,
Title = $"{SplitWords(locationType)} location",
Description = "Reusable location illustration generated from durable Story Memory semantic type. Not canonical to a specific manuscript.",
VisualSubject = $"A clear, reusable {SplitWords(locationType).ToLowerInvariant()} environment.",
Composition = "one readable focal environment, no prominent people, cinematic but bright enough for a small interface thumbnail",
VisualSubject = subject,
Composition = LocationComposition(locationType),
Mood = "grounded, atmospheric, legible",
LocationType = locationType,
Era = "1980s",
Setting = location.InteriorExterior ?? "Story setting",
Era = era,
Setting = location.InteriorExterior ?? InferInteriorExterior(locationType),
Palette = ["clear navy", "warm light", "balanced natural colour"],
MetadataTags = ["location", "story-memory-demand"],
Metadata = DemandMetadata("Location", location.CanonicalIdentityKey, options)
@ -1240,8 +1279,31 @@ public sealed class StoryMemoryService(
private static int CharacterSignificance(SceneIntelligenceCharacter character)
=> character.MentionedOnly == true ? 25 : string.Equals(character.RoleInScene, "POV", StringComparison.OrdinalIgnoreCase) ? 100 : 60;
private static string LocationVisualSubject(string displayName, string locationType, string era, string storyLocation)
{
var nameHint = string.IsNullOrWhiteSpace(displayName) ? SplitWords(locationType).ToLowerInvariant() : displayName.Trim();
var setting = $"{era} {storyLocation}".Trim();
return locationType switch
{
"Lane" => $"A clear {setting} narrow lane or side road matching '{nameHint}', readable road surface, kerbs, houses or hedges, no prominent people.",
"Street" => $"A clear {setting} side street matching '{nameHint}', readable street layout, pavements and buildings, no prominent people.",
"Road" => $"A clear {setting} road scene matching '{nameHint}', readable carriageway, kerbs, junction markings or suburban road context, no prominent people.",
"Motorway" => $"A clear {setting} motorway or major road scene matching '{nameHint}', multiple lanes, slip road or roundabout context, no prominent people.",
"Flyover" => $"A clear {setting} road under or beside a flyover matching '{nameHint}', concrete overpass structure, road shadows and urban edge detail, no prominent people.",
"Underpass" => $"A clear {setting} road or pedestrian underpass matching '{nameHint}', concrete structure and readable path or carriageway, no prominent people.",
"Bridge" => $"A clear {setting} road bridge scene matching '{nameHint}', bridge structure and readable road context, no prominent people.",
"CarPark" => $"A clear {setting} car park scene matching '{nameHint}', marked bays and parked cars, no prominent people.",
_ => $"A clear, reusable {SplitWords(locationType).ToLowerInvariant()} environment matching '{nameHint}'."
};
}
private static string LocationComposition(string locationType)
=> locationType is "Road" or "Lane" or "Street" or "Motorway" or "Flyover" or "Underpass" or "Bridge"
? "one readable exterior transport environment, road type obvious at thumbnail size, landscape-friendly crop, no text or readable signage"
: "one readable focal environment, no prominent people, cinematic but bright enough for a small interface thumbnail";
private static string InferInteriorExterior(string semanticType)
=> semanticType is "Road" or "CarPark" or "Beach" or "Canal" or "HouseExterior" or "StreetFurniture" ? "Exterior" : "Interior";
=> semanticType is "Road" or "Lane" or "Street" or "Avenue" or "Drive" or "Close" or "Motorway" or "Bridge" or "Flyover" or "Underpass" or "CarPark" or "Beach" or "Canal" or "HouseExterior" or "StreetFurniture" ? "Exterior" : "Interior";
private static string SourceSectionType(StoryIntelligenceSavedSceneResult result, SceneIntelligenceScene scene)
{