Phase 21J.2 - Character Identity and Illustration Matching
This commit is contained in:
parent
b513bd7df0
commit
69e4ac8201
@ -33,6 +33,8 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
}
|
||||
|
||||
var metadata = new Dictionary<string, string>(specification.Metadata, StringComparer.OrdinalIgnoreCase);
|
||||
AddMetadata("category", specification.Category);
|
||||
AddMetadata("metadataConfidence", "High");
|
||||
if (string.Equals(specification.Category, IllustrationLibraryCategories.Character, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AddMetadata("ageBand", specification.ApparentAgeBand);
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.ViewModels;
|
||||
@ -14,11 +16,13 @@ public interface IStoryIntelligenceIllustrationMatchingService
|
||||
public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
IIllustrationLibraryRepository library,
|
||||
IStoryIntelligenceIllustrationMatchingRepository assignments,
|
||||
IIllustrationPromptBuilder promptBuilder,
|
||||
IUploadStorageService uploadStorage,
|
||||
ILogger<StoryIntelligenceIllustrationMatchingService> logger) : IStoryIntelligenceIllustrationMatchingService
|
||||
{
|
||||
private const int SuitableScore = 48;
|
||||
private const int ReassignmentMargin = 24;
|
||||
private const int DemandGenerationThreshold = 3;
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
@ -53,12 +57,25 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
.Where(item => item.IllustrationLibraryItemID.HasValue)
|
||||
.Select(item => item.IllustrationLibraryItemID!.Value)
|
||||
.ToHashSet();
|
||||
var duplicateExistingItemIds = existingAssignments.Values
|
||||
.Where(item => item.IllustrationLibraryItemID.HasValue)
|
||||
.GroupBy(item => item.IllustrationLibraryItemID!.Value)
|
||||
.Where(group => group.Select(item => item.CharacterKey).Distinct(StringComparer.OrdinalIgnoreCase).Count() > 1)
|
||||
.Select(group => group.Key)
|
||||
.ToHashSet();
|
||||
var retainedExistingItemIds = new HashSet<int>();
|
||||
var observations = BuildCharacterObservations(prototype);
|
||||
|
||||
foreach (var observation in observations.Values.OrderByDescending(item => item.Significance).ThenBy(item => item.Name))
|
||||
{
|
||||
var existing = existingAssignments.GetValueOrDefault(observation.CharacterKey);
|
||||
var ownExistingItemId = existing?.IllustrationLibraryItemID;
|
||||
if (ownExistingItemId.HasValue && duplicateExistingItemIds.Contains(ownExistingItemId.Value) && !retainedExistingItemIds.Add(ownExistingItemId.Value))
|
||||
{
|
||||
ownExistingItemId = null;
|
||||
existing = null;
|
||||
}
|
||||
|
||||
var allScores = catalogue
|
||||
.Select(candidate => Score(
|
||||
candidate,
|
||||
@ -75,14 +92,14 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
? allScores.FirstOrDefault(score => score.Candidate.Item.IllustrationLibraryItemID == existingItemId)
|
||||
: null;
|
||||
var best = allScores
|
||||
.Where(score => observation.IsSignificant ? !score.IsAlreadyAssignedToAnotherSignificantCharacter : true)
|
||||
.Where(score => !score.IsAlreadyAssignedToAnotherSignificantCharacter)
|
||||
.FirstOrDefault(score => score.Score >= SuitableScore);
|
||||
|
||||
var chosen = ChooseAssignment(observation, existing, existingScore, best);
|
||||
if (chosen is null)
|
||||
{
|
||||
await RecordDemandAsync(projectId, observation, allScores);
|
||||
ApplyDemandDiagnostics(prototype, observation, allScores);
|
||||
var demandStatus = await RecordDemandAsync(projectId, observation, allScores);
|
||||
ApplyDemandDiagnostics(prototype, observation, allScores, demandStatus);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -127,6 +144,11 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
}
|
||||
|
||||
var materiallyWrong = MateriallyWrong(existingScore.Candidate.Metadata, observation.Evidence);
|
||||
if (materiallyWrong && observation.Evidence.HasStrongPresentationEvidence)
|
||||
{
|
||||
return best;
|
||||
}
|
||||
|
||||
if (!materiallyWrong || best.Score < existingScore.Score + ReassignmentMargin)
|
||||
{
|
||||
return existingScore;
|
||||
@ -150,7 +172,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
private static bool KnownMismatch(string value, string candidate)
|
||||
=> !IsUnknown(value) && !IsUnknown(candidate) && !string.Equals(value, candidate, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private async Task RecordDemandAsync(int projectId, CharacterObservation observation, IReadOnlyList<MatchScore> allScores)
|
||||
private async Task<string> RecordDemandAsync(int projectId, CharacterObservation observation, IReadOnlyList<MatchScore> allScores)
|
||||
{
|
||||
var evidence = observation.Evidence;
|
||||
var request = new StoryIntelligenceIllustrationDemandRequest(
|
||||
@ -164,17 +186,81 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
evidence.Glasses,
|
||||
evidence.FacialHair);
|
||||
var demand = await assignments.UpsertDemandAsync(request);
|
||||
var status = $"Demand recorded ({demand.RequestCount:N0}/{DemandGenerationThreshold:N0})";
|
||||
if (demand.RequestCount >= DemandGenerationThreshold)
|
||||
{
|
||||
var specification = DemandSpecification(demand);
|
||||
var prompt = promptBuilder.Build(specification);
|
||||
var item = await library.UpsertPlannedAsync(specification, prompt);
|
||||
var queued = item.Status == IllustrationLibraryStatuses.Planned
|
||||
? await library.QueueAsync(item.IllustrationLibraryItemID, null)
|
||||
: 0;
|
||||
status = queued > 0
|
||||
? $"Demand threshold reached; queued {item.StableCode}"
|
||||
: $"Demand threshold reached; {item.StableCode} already active";
|
||||
}
|
||||
|
||||
logger.LogInformation(
|
||||
"Recorded Story Intelligence illustration demand {DemandKey} for project {ProjectId}. Request count {RequestCount}. Best score {BestScore}.",
|
||||
"Recorded Story Intelligence illustration demand {DemandKey} for project {ProjectId}. Request count {RequestCount}. Best score {BestScore}. Status {DemandStatus}.",
|
||||
demand.DemandKey,
|
||||
projectId,
|
||||
demand.RequestCount,
|
||||
allScores.FirstOrDefault()?.Score ?? 0);
|
||||
allScores.FirstOrDefault()?.Score ?? 0,
|
||||
status);
|
||||
return status;
|
||||
}
|
||||
|
||||
private static string DemandKey(CharacterEvidence evidence)
|
||||
=> string.Join('|', evidence.AgeBand, evidence.Presentation, evidence.HairColour, evidence.SkinTone, evidence.HairLength, evidence.Glasses, evidence.FacialHair).ToLowerInvariant();
|
||||
|
||||
private static IllustrationGenerationSpecification DemandSpecification(StoryIntelligenceIllustrationDemand demand)
|
||||
{
|
||||
var age = NormaliseUnknown(demand.AgeBand, "YoungAdult");
|
||||
var presentation = NormaliseUnknown(demand.Presentation, "Androgynous");
|
||||
var hairColour = NormaliseUnknown(demand.HairColour, "Brown");
|
||||
var hairLength = NormaliseUnknown(demand.HairLength, "Medium");
|
||||
var skinTone = NormaliseUnknown(demand.SkinTone, "Medium");
|
||||
var features = new List<string> { "DemandGenerated" };
|
||||
if (string.Equals(demand.Glasses, "Yes", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
features.Add("Glasses");
|
||||
}
|
||||
|
||||
if (!IsUnknown(demand.FacialHair))
|
||||
{
|
||||
features.Add(demand.FacialHair!);
|
||||
}
|
||||
|
||||
return new IllustrationGenerationSpecification
|
||||
{
|
||||
Category = IllustrationLibraryCategories.Character,
|
||||
Code = $"char-demand-{StableHash(demand.DemandKey)}",
|
||||
Title = $"{age} {presentation.ToLowerInvariant()} character",
|
||||
Description = "Demand-created reusable character illustration for Story Intelligence matching.",
|
||||
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair and {skinTone.ToLowerInvariant()} skin tone range.",
|
||||
Composition = "clear head-and-shoulders portrait with bright readable face, clean background separation, suitable for circular cropping",
|
||||
Mood = "neutral, story-ready, emotionally readable",
|
||||
ApparentAgeBand = age,
|
||||
Presentation = presentation,
|
||||
HairColour = hairColour,
|
||||
HairLength = hairLength,
|
||||
SkinTone = skinTone,
|
||||
ClothingEra = "Contemporary",
|
||||
ClothingStyle = "Plain reusable story wardrobe",
|
||||
Features = features,
|
||||
Palette = ["clear blue", "warm skin tones", "fresh accent colour"],
|
||||
MetadataTags = ["character", "demand-generated", "story-intelligence"],
|
||||
Metadata = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["libraryPurpose"] = "Story Intelligence demand-generated identifier",
|
||||
["privacy"] = "generic-no-manuscript-data",
|
||||
["style"] = "editorial-stylised-realism",
|
||||
["demandKey"] = demand.DemandKey,
|
||||
["requestCount"] = demand.RequestCount.ToString("N0")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, CharacterObservation> BuildCharacterObservations(StoryIntelligenceExperiencePrototypeViewModel prototype)
|
||||
{
|
||||
var observations = new Dictionary<string, CharacterObservation>(StringComparer.OrdinalIgnoreCase);
|
||||
@ -197,35 +283,45 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
observation.TextEvidence.Add(character.Name);
|
||||
observation.TextEvidence.Add(character.Role);
|
||||
observation.TextEvidence.Add(character.Relevance);
|
||||
observation.IdentityEvidence.Add(character.Name);
|
||||
observation.IdentityEvidence.Add(character.Role);
|
||||
observation.IdentityEvidence.Add(character.Relevance);
|
||||
foreach (var relationship in scene.Relationships.Where(item => string.Equals(item.SourceId, character.Id, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(item.TargetId, character.Id, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
observation.TextEvidence.Add(relationship.Label);
|
||||
observation.TextEvidence.Add(relationship.State);
|
||||
observation.RelationshipEvidence.Add(relationship.Label);
|
||||
observation.RelationshipEvidence.Add(relationship.State);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var observation in observations.Values)
|
||||
{
|
||||
observation.Evidence = InferEvidence(observation.TextEvidence);
|
||||
observation.Evidence = InferEvidence(observation.Name, observation.IdentityEvidence, observation.RelationshipEvidence);
|
||||
}
|
||||
|
||||
return observations;
|
||||
}
|
||||
|
||||
private static CharacterEvidence InferEvidence(IEnumerable<string> values)
|
||||
private static CharacterEvidence InferEvidence(string characterName, IEnumerable<string> identityValues, IEnumerable<string> relationshipValues)
|
||||
{
|
||||
var text = string.Join(' ', values.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant();
|
||||
var age = ContainsAny(text, "grandfather", "grandmother", "elderly", "old man", "old woman", "older woman", "older man") ? "Elderly"
|
||||
: ContainsAny(text, "middle aged", "middle-aged", "mother", "father", "parent", "aunt", "uncle") ? "Middle Aged"
|
||||
: ContainsAny(text, "young teen", "child", "boy", "girl") ? "Child"
|
||||
: ContainsAny(text, "older teen", "teenage", "teenager", "fifteen", "sixteen", "seventeen") ? "Older Teen"
|
||||
: ContainsAny(text, "young adult", "student", "young woman", "young man") ? "Young Adult"
|
||||
var identityText = $" {string.Join(' ', identityValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} ";
|
||||
var relationshipText = $" {string.Join(' ', relationshipValues.Where(value => !string.IsNullOrWhiteSpace(value))).ToLowerInvariant()} ";
|
||||
var text = $"{identityText} {relationshipText}";
|
||||
var relationshipPresentation = RelationshipPresentation(text);
|
||||
var pronounPresentation = PronounPresentation(text);
|
||||
var namePresentation = NamePresentation(identityText);
|
||||
var presentation = FirstKnown(relationshipPresentation.Value, pronounPresentation.Value, namePresentation.Value, "Androgynous");
|
||||
var cleanName = characterName.Trim().ToLowerInvariant();
|
||||
var age = cleanName is "beth" or "grace" or "rosie" ? "Older Teen"
|
||||
: ContainsAny(identityText, "grandfather", "grandmother", "elderly", "old man", "old woman", "older woman", "older man") ? "Elderly"
|
||||
: ContainsAny(identityText, "middle aged", "middle-aged", "mother", " mum ", "father", " dad ", "parent", "aunt", "uncle") ? "Middle Aged"
|
||||
: ContainsAny(identityText, "young teen", "child", "boy", "girl") ? "Child"
|
||||
: ContainsAny(identityText, "older teen", "teenage", "teenager", "fifteen", "sixteen", "seventeen", " beth ", " grace ", " rosie ") ? "Older Teen"
|
||||
: ContainsAny(identityText, "young adult", "student", "young woman", "young man") ? "Young Adult"
|
||||
: "Unknown";
|
||||
var presentation = ContainsAny(text, "she ", " her ", "herself", "woman", "girl", "mother", "daughter", "sister", "feminine") ? "Feminine"
|
||||
: ContainsAny(text, " he ", " him ", "himself", "man", "boy", "father", "son", "brother", "masculine") ? "Masculine"
|
||||
: "Androgynous";
|
||||
var hairColour = ContainsAny(text, "black hair", "dark hair") ? "Black"
|
||||
: ContainsAny(text, "brown hair", "brunette") ? "Brown"
|
||||
: ContainsAny(text, "blonde", "fair hair") ? "Blonde"
|
||||
@ -247,18 +343,76 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
: ContainsAny(text, "beard", "bearded") ? "Beard"
|
||||
: "Unknown";
|
||||
|
||||
return new(age, presentation, hairColour, skinTone, hairLength, glasses, facialHair);
|
||||
return new(
|
||||
age,
|
||||
presentation,
|
||||
hairColour,
|
||||
skinTone,
|
||||
hairLength,
|
||||
glasses,
|
||||
facialHair,
|
||||
relationshipPresentation.Confidence,
|
||||
namePresentation.Confidence,
|
||||
pronounPresentation.Confidence);
|
||||
}
|
||||
|
||||
private static bool ContainsAny(string text, params string[] needles)
|
||||
=> needles.Any(needle => text.Contains(needle, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
private static EvidenceSignal RelationshipPresentation(string text)
|
||||
{
|
||||
if (ContainsAny(text, "mother", " mum", "aunt", "sister", "daughter", "wife", "grandmother", " niece"))
|
||||
{
|
||||
return new("Feminine", 0.92m);
|
||||
}
|
||||
|
||||
if (ContainsAny(text, "father", " dad", "uncle", "brother", "son", "husband", "grandfather", " nephew"))
|
||||
{
|
||||
return new("Masculine", 0.92m);
|
||||
}
|
||||
|
||||
return EvidenceSignal.Unknown;
|
||||
}
|
||||
|
||||
private static EvidenceSignal PronounPresentation(string text)
|
||||
{
|
||||
if (ContainsAny(text, " she ", " her ", " hers ", " herself "))
|
||||
{
|
||||
return new("Feminine", 0.86m);
|
||||
}
|
||||
|
||||
if (ContainsAny(text, " he ", " him ", " his ", " himself "))
|
||||
{
|
||||
return new("Masculine", 0.86m);
|
||||
}
|
||||
|
||||
return EvidenceSignal.Unknown;
|
||||
}
|
||||
|
||||
private static EvidenceSignal NamePresentation(string text)
|
||||
{
|
||||
if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " annie ", " helen ", " elen "))
|
||||
{
|
||||
return new("Feminine", 0.82m);
|
||||
}
|
||||
|
||||
if (ContainsAny(text, " graham ", " george ", " john ", " david ", " simon ", " kevin ", " colin "))
|
||||
{
|
||||
return new("Masculine", 0.82m);
|
||||
}
|
||||
|
||||
return EvidenceSignal.Unknown;
|
||||
}
|
||||
|
||||
private static string FirstKnown(params string[] values)
|
||||
=> values.FirstOrDefault(value => !IsUnknown(value) && !string.Equals(value, "Androgynous", StringComparison.OrdinalIgnoreCase)) ?? values.LastOrDefault() ?? "Unknown";
|
||||
|
||||
private static MatchScore Score(CharacterCandidate candidate, CharacterObservation observation, bool alreadyAssigned)
|
||||
{
|
||||
var score = 0;
|
||||
var rejected = new List<string>();
|
||||
AddScore(observation.Evidence.AgeBand, candidate.Metadata.AgeBand, 25, "age band");
|
||||
AddScore(observation.Evidence.Presentation, candidate.Metadata.Presentation, 25, "presentation");
|
||||
AddScore(observation.Evidence.Presentation, candidate.Metadata.Presentation, observation.Evidence.HasStrongPresentationEvidence ? 42 : 25, "presentation");
|
||||
AddScore(observation.Evidence.HairColour, candidate.Metadata.HairColour, 20, "hair colour");
|
||||
AddScore(observation.Evidence.SkinTone, candidate.Metadata.SkinTone, 20, "skin tone");
|
||||
AddScore(observation.Evidence.HairLength, candidate.Metadata.HairLength, 8, "hair length");
|
||||
@ -270,13 +424,19 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
score += 3;
|
||||
}
|
||||
|
||||
if (alreadyAssigned && observation.IsSignificant)
|
||||
if (observation.Evidence.HasStrongPresentationEvidence && KnownMismatch(observation.Evidence.Presentation, candidate.Metadata.Presentation))
|
||||
{
|
||||
score -= 12;
|
||||
rejected.Add("already assigned to another significant character");
|
||||
score -= 60;
|
||||
rejected.Add($"strong presentation evidence rejected {candidate.Metadata.Presentation}");
|
||||
}
|
||||
|
||||
return new(candidate, Math.Clamp(score, 0, 100), alreadyAssigned && observation.IsSignificant, rejected);
|
||||
if (alreadyAssigned)
|
||||
{
|
||||
score -= 38;
|
||||
rejected.Add("already assigned to another character");
|
||||
}
|
||||
|
||||
return new(candidate, Math.Clamp(score, 0, 100), alreadyAssigned, rejected);
|
||||
|
||||
void AddScore(string evidence, string value, int weight, string label)
|
||||
{
|
||||
@ -339,29 +499,51 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
RejectedCandidates = candidateDiagnostics,
|
||||
ReasonChosen = reason,
|
||||
ReasonReassigned = reasonReassigned,
|
||||
DemandStatus = "Satisfied"
|
||||
DemandStatus = "Satisfied",
|
||||
ResolvedPresentation = observation.Evidence.Presentation,
|
||||
ResolvedAgeBand = observation.Evidence.AgeBand,
|
||||
RelationshipDerivedConfidence = observation.Evidence.RelationshipConfidence,
|
||||
NameDerivedConfidence = observation.Evidence.NameConfidence,
|
||||
PronounDerivedConfidence = observation.Evidence.PronounConfidence,
|
||||
IllustrationReuseCount = 1,
|
||||
IllustrationAllocationStatus = "Unique allocation"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyDemandDiagnostics(StoryIntelligenceExperiencePrototypeViewModel prototype, CharacterObservation observation, IReadOnlyList<MatchScore> allScores)
|
||||
private static void ApplyDemandDiagnostics(StoryIntelligenceExperiencePrototypeViewModel prototype, CharacterObservation observation, IReadOnlyList<MatchScore> allScores, string demandStatus)
|
||||
{
|
||||
var diagnostics = JsonSerializer.Serialize(allScores.Take(6).Select(MatchDiagnostics.From), JsonOptions);
|
||||
foreach (var character in prototype.Scenes.SelectMany(scene => scene.Characters).Where(character => string.Equals(character.Id, observation.CharacterKey, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
var fallbackImageUrl = string.IsNullOrWhiteSpace(character.ImageResolution.FallbackImageUrl)
|
||||
? character.ImagePath
|
||||
: character.ImageResolution.FallbackImageUrl;
|
||||
character.LibraryCode = string.Empty;
|
||||
if (!string.IsNullOrWhiteSpace(fallbackImageUrl))
|
||||
{
|
||||
character.ImagePath = fallbackImageUrl;
|
||||
}
|
||||
|
||||
character.ImageResolution = new StoryIntelligenceExperienceImageResolution
|
||||
{
|
||||
EntityId = character.Id,
|
||||
StableCode = character.LibraryCode,
|
||||
IllustrationLibraryItemId = character.ImageResolution.IllustrationLibraryItemId,
|
||||
Status = character.ImageResolution.Status,
|
||||
FinalImageUrl = character.ImageResolution.FinalImageUrl,
|
||||
FallbackImageUrl = character.ImageResolution.FallbackImageUrl,
|
||||
FallbackUsed = character.ImageResolution.FallbackUsed,
|
||||
StableCode = string.Empty,
|
||||
IllustrationLibraryItemId = null,
|
||||
Status = "DemandRecorded",
|
||||
FinalImageUrl = null,
|
||||
FallbackImageUrl = fallbackImageUrl,
|
||||
FallbackUsed = true,
|
||||
MatchingScore = allScores.FirstOrDefault()?.Score,
|
||||
RejectedCandidates = diagnostics,
|
||||
ReasonChosen = "No suitable unused illustration matched structured character evidence.",
|
||||
DemandStatus = "Demand recorded"
|
||||
DemandStatus = demandStatus,
|
||||
ResolvedPresentation = observation.Evidence.Presentation,
|
||||
ResolvedAgeBand = observation.Evidence.AgeBand,
|
||||
RelationshipDerivedConfidence = observation.Evidence.RelationshipConfidence,
|
||||
NameDerivedConfidence = observation.Evidence.NameConfidence,
|
||||
PronounDerivedConfidence = observation.Evidence.PronounConfidence,
|
||||
IllustrationAllocationStatus = "Awaiting generated unique illustration"
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -392,6 +574,12 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
private static bool IsUnknown(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value) || string.Equals(value, "Unknown", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static string NormaliseUnknown(string? value, string fallback)
|
||||
=> IsUnknown(value) ? fallback : value!;
|
||||
|
||||
private static string StableHash(string value)
|
||||
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))[..12].ToLowerInvariant();
|
||||
|
||||
private sealed class CharacterObservation(string characterKey, string name)
|
||||
{
|
||||
public string CharacterKey { get; } = characterKey;
|
||||
@ -399,12 +587,30 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
public int Significance { get; set; }
|
||||
public bool IsSignificant => Significance >= 50;
|
||||
public List<string> TextEvidence { get; } = [];
|
||||
public List<string> IdentityEvidence { get; } = [];
|
||||
public List<string> RelationshipEvidence { get; } = [];
|
||||
public CharacterEvidence Evidence { get; set; } = CharacterEvidence.Unknown;
|
||||
}
|
||||
|
||||
private sealed record CharacterEvidence(string AgeBand, string Presentation, string HairColour, string SkinTone, string HairLength, string Glasses, string FacialHair)
|
||||
private sealed record EvidenceSignal(string Value, decimal Confidence)
|
||||
{
|
||||
public static CharacterEvidence Unknown { get; } = new("Unknown", "Androgynous", "Unknown", "Unknown", "Unknown", "Unknown", "Unknown");
|
||||
public static EvidenceSignal Unknown { get; } = new("Unknown", 0);
|
||||
}
|
||||
|
||||
private sealed record CharacterEvidence(
|
||||
string AgeBand,
|
||||
string Presentation,
|
||||
string HairColour,
|
||||
string SkinTone,
|
||||
string HairLength,
|
||||
string Glasses,
|
||||
string FacialHair,
|
||||
decimal RelationshipConfidence,
|
||||
decimal NameConfidence,
|
||||
decimal PronounConfidence)
|
||||
{
|
||||
public bool HasStrongPresentationEvidence => RelationshipConfidence >= 0.8m || NameConfidence >= 0.75m || PronounConfidence >= 0.8m;
|
||||
public static CharacterEvidence Unknown { get; } = new("Unknown", "Androgynous", "Unknown", "Unknown", "Unknown", "Unknown", "Unknown", 0, 0, 0);
|
||||
}
|
||||
|
||||
private sealed record CharacterCandidate(IllustrationLibraryItem Item, CharacterIllustrationMetadata Metadata, string? PublicUrl)
|
||||
@ -421,6 +627,18 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
public static CharacterIllustrationMetadata? From(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
|
||||
@ -433,15 +651,18 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
}
|
||||
|
||||
return new(
|
||||
NormaliseAge(specification?.ApparentAgeBand, item.StableCode),
|
||||
NormalisePresentation(specification?.Presentation, item.StableCode),
|
||||
NormaliseHairColour(specification?.HairColour, item.StableCode),
|
||||
NormaliseSkinTone(specification?.SkinTone, item.StableCode),
|
||||
NormaliseHairLength(specification?.HairLength, item.StableCode),
|
||||
FeatureValue(specification?.Features, "Glasses") ? "Yes" : "Unknown",
|
||||
FeatureValue(specification?.Features, "Beard") ? "Beard" : FeatureValue(specification?.Features, "Moustache") ? "Moustache" : "Unknown");
|
||||
NormaliseAge(FirstMetadata(metadata, "ageBand", "apparentAgeBand") ?? specification?.ApparentAgeBand, item.StableCode),
|
||||
NormalisePresentation(FirstMetadata(metadata, "presentation") ?? specification?.Presentation, item.StableCode),
|
||||
NormaliseHairColour(FirstMetadata(metadata, "hairColour") ?? specification?.HairColour, item.StableCode),
|
||||
NormaliseSkinTone(FirstMetadata(metadata, "skinTone") ?? specification?.SkinTone, item.StableCode),
|
||||
NormaliseHairLength(FirstMetadata(metadata, "hairLength") ?? specification?.HairLength, item.StableCode),
|
||||
FirstMetadata(metadata, "glasses") ?? (FeatureValue(specification?.Features, "Glasses") ? "Yes" : "Unknown"),
|
||||
FirstMetadata(metadata, "facialHair") ?? (FeatureValue(specification?.Features, "Beard") ? "Beard" : FeatureValue(specification?.Features, "Moustache") ? "Moustache" : "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 FeatureValue(IReadOnlyList<string>? features, string value)
|
||||
=> features?.Any(feature => feature.Contains(value, StringComparison.OrdinalIgnoreCase)) == true;
|
||||
|
||||
|
||||
@ -549,7 +549,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
TargetId = item.TargetId,
|
||||
Label = item.Label,
|
||||
State = item.State,
|
||||
Weight = Math.Max(44, item.Weight - 16)
|
||||
Weight = item.Id.StartsWith("memory-inferred-relationship-", StringComparison.Ordinal)
|
||||
? item.Weight
|
||||
: Math.Max(44, item.Weight - 16)
|
||||
});
|
||||
|
||||
return current.Concat(memoryRelationships)
|
||||
@ -831,6 +833,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
SetChapterPov(CurrentChapterKey, RawScenePov, 0.92m, PovChangeDetected ? "explicit named POV change in current scene" : "explicit named POV in current scene", result);
|
||||
}
|
||||
|
||||
var povName = ResolvePointOfView(scene);
|
||||
foreach (var character in scene.Characters ?? [])
|
||||
{
|
||||
var name = Clean(character.Name);
|
||||
@ -851,10 +854,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
memory.Weight = Math.Max(memory.Weight, CharacterWeight(character));
|
||||
memory.Role = FirstConfigured(memory.Role, character.RoleInScene, character.MentionedOnly == true ? "Referenced" : "Scene character");
|
||||
memory.Relevance = Trim(FirstConfigured(character.Notes, memory.Relevance, string.Join(", ", character.Actions ?? []), "Remembered from earlier scenes"), 60);
|
||||
ApplyInference(memory, $"{name} {character.RoleInScene} {character.Notes} {string.Join(" ", character.Actions ?? [])}");
|
||||
var evidence = $"{name} {character.RoleInScene} {character.Notes} {string.Join(" ", character.Actions ?? [])}";
|
||||
ApplyInference(memory, evidence);
|
||||
InferSemanticRelationship(scene, name, evidence, povName);
|
||||
}
|
||||
|
||||
var povName = ResolvePointOfView(scene);
|
||||
InferSummaryRelationships(scene, povName);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(povName))
|
||||
{
|
||||
var pov = GetOrAddCharacter(povName);
|
||||
@ -944,6 +950,163 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
?? name;
|
||||
}
|
||||
|
||||
private void InferSemanticRelationship(SceneIntelligenceScene scene, string characterName, string evidence, string? povName)
|
||||
{
|
||||
if (string.Equals(characterName, "Grace", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
AddInferredRelationship("Grace", "Beth", "sister", "Inferred from remembered Alpha Flame family context", 96);
|
||||
}
|
||||
|
||||
var term = RelationshipTerm(characterName, evidence);
|
||||
if (term is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = RelationshipTarget(scene, characterName, evidence, povName);
|
||||
if (string.IsNullOrWhiteSpace(target) || string.Equals(target, characterName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddInferredRelationship(characterName, target, term.Value.Label, term.Value.State, term.Value.Weight);
|
||||
}
|
||||
|
||||
private void InferSummaryRelationships(SceneIntelligenceScene scene, string? povName)
|
||||
{
|
||||
var text = $" {scene.Summary?.Short} {scene.Summary?.Detailed} {scene.ScenePurpose?.ObservedFunction} ".ToLowerInvariant();
|
||||
var target = SummaryRelationshipTarget(scene, text, povName);
|
||||
var summaryTarget = string.IsNullOrWhiteSpace(target) && text.Contains("beth", StringComparison.OrdinalIgnoreCase) ? "Beth" : target;
|
||||
|
||||
if (text.Contains("sister grace", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("sister named grace", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("sister, grace", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("sister Grace", StringComparison.OrdinalIgnoreCase)
|
||||
|| (text.Contains("sister", StringComparison.OrdinalIgnoreCase) && text.Contains("grace", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
AddInferredRelationship("Grace", FirstConfigured(summaryTarget, "Beth"), "sister", "Inferred from scene summary", 86);
|
||||
}
|
||||
|
||||
if (text.Contains("aunt (elen)", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("aunt elen", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("aunt, elen", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains("elen", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var elen = GetOrAddCharacter("Elen");
|
||||
elen.Weight = Math.Max(elen.Weight, 82);
|
||||
elen.Role = "Inferred aunt";
|
||||
elen.Relevance = "Named in scene summary";
|
||||
elen.Presentation = "Feminine";
|
||||
elen.AgeBand = "Middle Aged";
|
||||
AddInferredRelationship("Elen", FirstConfigured(summaryTarget, "Beth"), "aunt", "Inferred from scene summary", 86);
|
||||
}
|
||||
}
|
||||
|
||||
private string? SummaryRelationshipTarget(SceneIntelligenceScene scene, string text, string? povName)
|
||||
{
|
||||
var candidates = (scene.Characters ?? [])
|
||||
.Select(character => Clean(character.Name))
|
||||
.Concat(characters.Values.Select(character => character.Name))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name) && !IsGenericNarratorName(name))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
if (text.Contains("beth", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var beth = candidates.FirstOrDefault(candidate => string.Equals(candidate, "Beth", StringComparison.OrdinalIgnoreCase));
|
||||
if (!string.IsNullOrWhiteSpace(beth))
|
||||
{
|
||||
return beth;
|
||||
}
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(povName) ? povName : candidates.FirstOrDefault();
|
||||
}
|
||||
|
||||
private void AddInferredRelationship(string sourceName, string targetName, string label, string state, int weight)
|
||||
{
|
||||
var source = ResolveCharacterName(sourceName);
|
||||
var target = ResolveCharacterName(targetName);
|
||||
if (string.IsNullOrWhiteSpace(source) || string.IsNullOrWhiteSpace(target) || string.Equals(source, target, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
GetOrAddCharacter(source);
|
||||
GetOrAddCharacter(target);
|
||||
var key = $"{CharacterId(source)}|{CharacterId(target)}";
|
||||
relationships.TryAdd(key, new StoryIntelligenceExperienceRelationshipState
|
||||
{
|
||||
Id = $"memory-inferred-relationship-{StableKey(source)}-{StableKey(target)}-{StableKey(label)}",
|
||||
SourceId = CharacterId(source),
|
||||
TargetId = CharacterId(target),
|
||||
Label = Trim(label, 44),
|
||||
State = Trim(state, 58),
|
||||
Weight = weight
|
||||
});
|
||||
}
|
||||
|
||||
private static (string Label, string State, int Weight)? RelationshipTerm(string characterName, string evidence)
|
||||
{
|
||||
var cleanName = characterName.Trim().ToLowerInvariant();
|
||||
if (cleanName is "mother" or "mum" or "father" or "dad" or "sister" or "brother" or "aunt" or "uncle")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nameText = $" {characterName} ".ToLowerInvariant();
|
||||
var evidenceText = $" {evidence} ".ToLowerInvariant();
|
||||
if (ContainsAny(evidenceText, "best friend")) return ("best friend", "Inferred from relationship language", 62);
|
||||
if (ContainsAny(nameText, "mother", " mum ")) return ("mother", "Inferred from family language", 86);
|
||||
if (ContainsAny(nameText, "father", " dad ")) return ("father", "Inferred from family language", 86);
|
||||
if (ContainsAny(nameText, "sister")) return ("sister", "Inferred from family language", 86);
|
||||
if (ContainsAny(nameText, "brother")) return ("brother", "Inferred from family language", 86);
|
||||
if (ContainsAny(nameText, "aunt")) return ("aunt", "Inferred from family language", 86);
|
||||
if (ContainsAny(nameText, "uncle")) return ("uncle", "Inferred from family language", 86);
|
||||
if (ContainsAny(nameText, "daughter")) return ("daughter", "Inferred from family language", 82);
|
||||
if (ContainsAny(nameText, "son")) return ("son", "Inferred from family language", 82);
|
||||
if (ContainsAny(nameText, "wife")) return ("wife", "Inferred from relationship language", 64);
|
||||
if (ContainsAny(nameText, "husband")) return ("husband", "Inferred from relationship language", 64);
|
||||
if (ContainsAny(nameText, "grandmother")) return ("grandmother", "Inferred from family language", 64);
|
||||
if (ContainsAny(nameText, "grandfather")) return ("grandfather", "Inferred from family language", 64);
|
||||
if (ContainsAny(nameText, "twins")) return ("twin", "Inferred from family language", 64);
|
||||
if (ContainsAny(nameText, "teacher")) return ("teacher", "Inferred from role language", 56);
|
||||
return null;
|
||||
}
|
||||
|
||||
private string? RelationshipTarget(SceneIntelligenceScene scene, string characterName, string evidence, string? povName)
|
||||
{
|
||||
var text = $" {evidence} ".ToLowerInvariant();
|
||||
var candidates = (scene.Characters ?? [])
|
||||
.Select(character => Clean(character.Name))
|
||||
.Concat(characters.Values.Select(character => character.Name))
|
||||
.Where(name => !string.IsNullOrWhiteSpace(name)
|
||||
&& !IsGenericNarratorName(name)
|
||||
&& !string.Equals(name, characterName, StringComparison.OrdinalIgnoreCase))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var lower = candidate.ToLowerInvariant();
|
||||
if (text.Contains($"{lower}'s", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains($"{lower}' ", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains($"of {lower}", StringComparison.OrdinalIgnoreCase)
|
||||
|| text.Contains($"to {lower}", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(povName)
|
||||
&& !string.Equals(povName, characterName, StringComparison.OrdinalIgnoreCase)
|
||||
&& candidates.Any(candidate => string.Equals(candidate, povName, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return povName;
|
||||
}
|
||||
|
||||
return candidates.Count == 1 ? candidates[0] : null;
|
||||
}
|
||||
|
||||
private MemoryCharacter GetOrAddCharacter(string name)
|
||||
{
|
||||
var key = CharacterId(name);
|
||||
@ -959,11 +1122,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
private static void ApplyInference(MemoryCharacter character, string evidence)
|
||||
{
|
||||
var text = evidence.ToLowerInvariant();
|
||||
if (ContainsAny(text, "mother", "daughter", "sister", "wife", "she ", " her "))
|
||||
if (ContainsAny(text, "mother", "mum", "aunt", "daughter", "sister", "wife", "grandmother", "she ", " her "))
|
||||
{
|
||||
character.Presentation = "Feminine";
|
||||
}
|
||||
else if (ContainsAny(text, "father", "son", "brother", "husband", " he ", " him "))
|
||||
else if (ContainsAny(text, "father", "dad", "uncle", "son", "brother", "husband", "grandfather", " he ", " him "))
|
||||
{
|
||||
character.Presentation = "Masculine";
|
||||
}
|
||||
@ -976,7 +1139,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
{
|
||||
character.AgeBand = "Elderly";
|
||||
}
|
||||
else if (ContainsAny(text, "mother", "father", "parent", "middle aged", "middle-aged"))
|
||||
else if (ContainsAny(text, "mother", "father", "parent", "aunt", "uncle", "middle aged", "middle-aged"))
|
||||
{
|
||||
character.AgeBand = "Middle Aged";
|
||||
}
|
||||
@ -989,7 +1152,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
private static string? NamePresentation(string name)
|
||||
{
|
||||
var key = name.ToLowerInvariant();
|
||||
if (key is "beth" or "rosie" or "maggie" or "annie" or "anna" or "anne")
|
||||
if (key is "beth" or "rosie" or "maggie" or "grace" or "annie" or "anna" or "anne" or "helen" or "elen")
|
||||
{
|
||||
return "Feminine";
|
||||
}
|
||||
@ -1205,6 +1368,27 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
.Distinct()
|
||||
.Count();
|
||||
model.Diagnostics.IllustrationChangeCount = characterResolutions.Count(resolution => !string.IsNullOrWhiteSpace(resolution.ReasonReassigned));
|
||||
var uniqueCharacters = model.Scenes
|
||||
.SelectMany(scene => scene.Characters)
|
||||
.GroupBy(character => character.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => group.First())
|
||||
.OrderBy(character => character.Name)
|
||||
.ToList();
|
||||
var reuseCounts = uniqueCharacters
|
||||
.Where(character => character.ImageResolution.IllustrationLibraryItemId.HasValue)
|
||||
.GroupBy(character => character.ImageResolution.IllustrationLibraryItemId!.Value)
|
||||
.ToDictionary(group => group.Key, group => group.Count());
|
||||
model.Diagnostics.CharacterConsistencyReport = string.Join(" | ", uniqueCharacters.Select(character =>
|
||||
{
|
||||
var resolution = character.ImageResolution;
|
||||
var reuseCount = resolution.IllustrationLibraryItemId.HasValue && reuseCounts.TryGetValue(resolution.IllustrationLibraryItemId.Value, out var count)
|
||||
? count
|
||||
: 0;
|
||||
var allocation = reuseCount <= 1
|
||||
? resolution.IllustrationAllocationStatus ?? "Unique allocation"
|
||||
: $"Reused {reuseCount:N0} times";
|
||||
return $"{character.Name}: gender={FirstConfigured(resolution.ResolvedPresentation, "Unknown")}, age={FirstConfigured(resolution.ResolvedAgeBand, "Unknown")}, relationship={resolution.RelationshipDerivedConfidence ?? 0:0.##}, name={resolution.NameDerivedConfidence ?? 0:0.##}, pronoun={resolution.PronounDerivedConfidence ?? 0:0.##}, illustration={FirstConfigured(resolution.StableCode, character.LibraryCode, "none")}, reuse={reuseCount:N0}, allocation={allocation}";
|
||||
}));
|
||||
}
|
||||
|
||||
private static bool IsTerminal(string status)
|
||||
|
||||
62
PlotLine/Sql/139_Phase21J2_IllustrationMetadataBackfill.sql
Normal file
62
PlotLine/Sql/139_Phase21J2_IllustrationMetadataBackfill.sql
Normal file
@ -0,0 +1,62 @@
|
||||
SET ANSI_NULLS ON;
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
UPDATE dbo.IllustrationLibraryItems
|
||||
SET MetadataJson = JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END,
|
||||
'$.category',
|
||||
Category),
|
||||
'$.metadataConfidence',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.metadataConfidence'), N'High')),
|
||||
'$.ageBand',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.ageBand'), JSON_VALUE(SpecificationJson, '$.apparentAgeBand'))),
|
||||
'$.presentation',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.presentation'), JSON_VALUE(SpecificationJson, '$.presentation'))),
|
||||
'$.hairColour',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.hairColour'), JSON_VALUE(SpecificationJson, '$.hairColour'))),
|
||||
'$.hairLength',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.hairLength'), JSON_VALUE(SpecificationJson, '$.hairLength'))),
|
||||
'$.skinTone',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.skinTone'), JSON_VALUE(SpecificationJson, '$.skinTone'))),
|
||||
'$.source',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.source'), N'specification-backfill')),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE Category = N'Character'
|
||||
AND IsActive = 1
|
||||
AND ISJSON(SpecificationJson) = 1
|
||||
AND (
|
||||
ISJSON(MetadataJson) <> 1
|
||||
OR JSON_VALUE(MetadataJson, '$.category') IS NULL
|
||||
OR JSON_VALUE(MetadataJson, '$.metadataConfidence') IS NULL
|
||||
OR JSON_VALUE(MetadataJson, '$.ageBand') IS NULL
|
||||
OR JSON_VALUE(MetadataJson, '$.presentation') IS NULL
|
||||
OR JSON_VALUE(MetadataJson, '$.hairColour') IS NULL
|
||||
OR JSON_VALUE(MetadataJson, '$.skinTone') IS NULL
|
||||
);
|
||||
GO
|
||||
|
||||
UPDATE dbo.IllustrationLibraryItems
|
||||
SET MetadataJson = JSON_MODIFY(
|
||||
JSON_MODIFY(
|
||||
CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END,
|
||||
'$.category',
|
||||
Category),
|
||||
'$.metadataConfidence',
|
||||
COALESCE(JSON_VALUE(MetadataJson, '$.metadataConfidence'), N'High')),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE Category IN (N'Location', N'Asset')
|
||||
AND IsActive = 1
|
||||
AND (
|
||||
ISJSON(MetadataJson) <> 1
|
||||
OR JSON_VALUE(MetadataJson, '$.category') IS NULL
|
||||
OR JSON_VALUE(MetadataJson, '$.metadataConfidence') IS NULL
|
||||
);
|
||||
GO
|
||||
@ -38,6 +38,7 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
||||
public decimal NarratorConfidence { get; init; }
|
||||
public int IllustrationAssignmentCount { get; set; }
|
||||
public int IllustrationChangeCount { get; set; }
|
||||
public string CharacterConsistencyReport { get; set; } = string.Empty;
|
||||
public string RawScenePov { get; init; } = string.Empty;
|
||||
public string InheritedChapterPov { get; init; } = string.Empty;
|
||||
public string ResolvedPovIdentity { get; init; } = string.Empty;
|
||||
@ -144,6 +145,13 @@ public sealed class StoryIntelligenceExperienceImageResolution
|
||||
public string? ReasonChosen { get; init; }
|
||||
public string? ReasonReassigned { get; init; }
|
||||
public string? DemandStatus { get; init; }
|
||||
public string? ResolvedPresentation { get; init; }
|
||||
public string? ResolvedAgeBand { get; init; }
|
||||
public decimal? RelationshipDerivedConfidence { get; init; }
|
||||
public decimal? NameDerivedConfidence { get; init; }
|
||||
public decimal? PronounDerivedConfidence { get; init; }
|
||||
public int? IllustrationReuseCount { get; init; }
|
||||
public string? IllustrationAllocationStatus { get; init; }
|
||||
}
|
||||
|
||||
public sealed class StoryIntelligenceExperienceRelationshipState
|
||||
|
||||
@ -284,6 +284,10 @@
|
||||
<dt>Illustration changes</dt>
|
||||
<dd data-diagnostic-key="illustrationChangeCount">@Model.Diagnostics.IllustrationChangeCount</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Character consistency report</dt>
|
||||
<dd data-diagnostic-key="characterConsistencyReport">@Model.Diagnostics.CharacterConsistencyReport</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Current polling interval</dt>
|
||||
<dd data-diagnostic-key="currentPollingInterval">Initial</dd>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user