Phase 21J - Story Memory Visualisation Refinement
This commit is contained in:
parent
b78a9bea9b
commit
4ebbfdab92
@ -115,7 +115,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
var current = PickCurrentScene(run, completed);
|
var current = PickCurrentScene(run, completed);
|
||||||
var scenes = BuildSceneStates(run, parsed, completed, current).ToList();
|
var storyMemory = new StoryMemory();
|
||||||
|
var scenes = BuildSceneStates(run, parsed, completed, current, storyMemory: storyMemory).ToList();
|
||||||
if (scenes.Count == 0)
|
if (scenes.Count == 0)
|
||||||
{
|
{
|
||||||
scenes.Add(BuildEmptyScene(run));
|
scenes.Add(BuildEmptyScene(run));
|
||||||
@ -130,7 +131,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ChangeToken = ChangeToken(run, results),
|
ChangeToken = ChangeToken(run, results),
|
||||||
IsTerminal = IsTerminal(run.Status),
|
IsTerminal = IsTerminal(run.Status),
|
||||||
SnapshotMessage = SnapshotMessage(run, completed.Count),
|
SnapshotMessage = SnapshotMessage(run, completed.Count),
|
||||||
Diagnostics = BuildDiagnostics(run, results, parsed, completed, current),
|
Diagnostics = BuildDiagnostics(run, results, parsed, completed, current, storyMemory),
|
||||||
ProjectName = DisplayTitle(run.ProjectTitle, "Untitled project"),
|
ProjectName = DisplayTitle(run.ProjectTitle, "Untitled project"),
|
||||||
BookTitle = DisplayTitle(run.BookTitle, "Untitled book"),
|
BookTitle = DisplayTitle(run.BookTitle, "Untitled book"),
|
||||||
Scenes = scenes
|
Scenes = scenes
|
||||||
@ -141,6 +142,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
if (run.ProjectID.HasValue)
|
if (run.ProjectID.HasValue)
|
||||||
{
|
{
|
||||||
await illustrationMatcher.ApplyCharacterIllustrationsAsync(run.ProjectID.Value, model);
|
await illustrationMatcher.ApplyCharacterIllustrationsAsync(run.ProjectID.Value, model);
|
||||||
|
ApplyIllustrationDiagnostics(model);
|
||||||
}
|
}
|
||||||
logger.LogInformation("Built Story Intelligence visualisation snapshot for run {RunId} in {ElapsedMs}ms.", runId, stopwatch.ElapsedMilliseconds);
|
logger.LogInformation("Built Story Intelligence visualisation snapshot for run {RunId} in {ElapsedMs}ms.", runId, stopwatch.ElapsedMilliseconds);
|
||||||
return model;
|
return model;
|
||||||
@ -198,6 +200,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
||||||
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
||||||
var nextSceneNumber = 1;
|
var nextSceneNumber = 1;
|
||||||
|
var storyMemory = new StoryMemory();
|
||||||
foreach (var snapshot in runSnapshots)
|
foreach (var snapshot in runSnapshots)
|
||||||
{
|
{
|
||||||
var runScenes = BuildSceneStates(
|
var runScenes = BuildSceneStates(
|
||||||
@ -215,7 +218,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
overallElapsed: overallElapsed,
|
overallElapsed: overallElapsed,
|
||||||
overallRemaining: overallRemaining,
|
overallRemaining: overallRemaining,
|
||||||
continuousSceneNumbering: true,
|
continuousSceneNumbering: true,
|
||||||
overallTerminal: isTerminal)
|
overallTerminal: isTerminal,
|
||||||
|
storyMemory: storyMemory)
|
||||||
.ToList();
|
.ToList();
|
||||||
scenes.AddRange(runScenes);
|
scenes.AddRange(runScenes);
|
||||||
nextSceneNumber += snapshot.Completed.Count;
|
nextSceneNumber += snapshot.Completed.Count;
|
||||||
@ -236,7 +240,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
ChangeToken = SessionChangeToken(session, runSnapshots),
|
ChangeToken = SessionChangeToken(session, runSnapshots),
|
||||||
IsTerminal = isTerminal,
|
IsTerminal = isTerminal,
|
||||||
SnapshotMessage = SessionSnapshotMessage(session, runSnapshots),
|
SnapshotMessage = SessionSnapshotMessage(session, runSnapshots),
|
||||||
Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, totalScenes, overallProgressPercent),
|
Diagnostics = BuildSessionDiagnostics(session, runSnapshots, activeSnapshot, current, parsedScenes, totalPersistedResults, totalScenes, overallProgressPercent, storyMemory),
|
||||||
ProjectName = DisplayTitle(session.ProjectName, "Untitled project"),
|
ProjectName = DisplayTitle(session.ProjectName, "Untitled project"),
|
||||||
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
BookTitle = DisplayTitle(session.BookDisplayTitle, "Untitled book"),
|
||||||
Scenes = scenes
|
Scenes = scenes
|
||||||
@ -245,6 +249,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
StabiliseCharacterIllustrations(model);
|
StabiliseCharacterIllustrations(model);
|
||||||
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
await illustrationLibrary.ApplyApprovedIllustrationsAsync(model);
|
||||||
await illustrationMatcher.ApplyCharacterIllustrationsAsync(session.ProjectID, model);
|
await illustrationMatcher.ApplyCharacterIllustrationsAsync(session.ProjectID, model);
|
||||||
|
ApplyIllustrationDiagnostics(model);
|
||||||
logger.LogInformation("Built Story Intelligence import session visualisation snapshot for session {ImportSessionId} in {ElapsedMs}ms.", importSessionId, stopwatch.ElapsedMilliseconds);
|
logger.LogInformation("Built Story Intelligence import session visualisation snapshot for session {ImportSessionId} in {ElapsedMs}ms.", importSessionId, stopwatch.ElapsedMilliseconds);
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
@ -264,14 +269,17 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
string? overallElapsed = null,
|
string? overallElapsed = null,
|
||||||
string? overallRemaining = null,
|
string? overallRemaining = null,
|
||||||
bool continuousSceneNumbering = false,
|
bool continuousSceneNumbering = false,
|
||||||
bool overallTerminal = false)
|
bool overallTerminal = false,
|
||||||
|
StoryMemory? storyMemory = null)
|
||||||
{
|
{
|
||||||
|
storyMemory ??= new StoryMemory();
|
||||||
var totalScenes = Math.Max(run.TotalDetectedScenes ?? 0, allResults.Count);
|
var totalScenes = Math.Max(run.TotalDetectedScenes ?? 0, allResults.Count);
|
||||||
var completedScenes = run.CompletedScenes ?? completed.Count;
|
var completedScenes = run.CompletedScenes ?? completed.Count;
|
||||||
var displayIndex = 0;
|
var displayIndex = 0;
|
||||||
foreach (var item in completed)
|
foreach (var item in completed)
|
||||||
{
|
{
|
||||||
var scene = item.Scene!;
|
var scene = item.Scene!;
|
||||||
|
storyMemory.Update(scene);
|
||||||
var sourceSceneNumber = SceneNumber(item);
|
var sourceSceneNumber = SceneNumber(item);
|
||||||
var sceneNumber = continuousSceneNumbering ? continuousSceneNumberStart + displayIndex++ : sourceSceneNumber;
|
var sceneNumber = continuousSceneNumbering ? continuousSceneNumberStart + displayIndex++ : sourceSceneNumber;
|
||||||
var isCurrent = current?.Result.SceneResultID == item.Result.SceneResultID;
|
var isCurrent = current?.Result.SceneResultID == item.Result.SceneResultID;
|
||||||
@ -295,11 +303,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes),
|
SceneTotal = Math.Max(1, overallTotalScenes ?? totalScenes),
|
||||||
ElapsedTime = overallElapsed ?? EstimateElapsed(run),
|
ElapsedTime = overallElapsed ?? EstimateElapsed(run),
|
||||||
EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes),
|
EstimatedRemaining = overallRemaining ?? EstimateRemaining(run, completedScenes, totalScenes),
|
||||||
PovCharacterId = CharacterId(ResolveNarratorName(scene) ?? scene.PointOfView?.CharacterName),
|
PovCharacterId = CharacterId(storyMemory.ResolvePointOfView(scene)),
|
||||||
Location = BuildLocation(scene),
|
Location = BuildLocation(scene),
|
||||||
Characters = BuildCharacters(scene),
|
Characters = BuildCharacters(scene, storyMemory),
|
||||||
Assets = BuildAssets(scene),
|
Assets = BuildAssets(scene),
|
||||||
Relationships = BuildRelationships(scene),
|
Relationships = BuildRelationships(scene, storyMemory),
|
||||||
KnowledgeThreads = BuildKnowledge(scene),
|
KnowledgeThreads = BuildKnowledge(scene),
|
||||||
Observations = BuildObservations(scene),
|
Observations = BuildObservations(scene),
|
||||||
LatestDiscoveries = isCurrent ? BuildDiscoveries(scene).ToList() : [],
|
LatestDiscoveries = isCurrent ? BuildDiscoveries(scene).ToList() : [],
|
||||||
@ -341,12 +349,13 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
return completed.Last();
|
return completed.Last();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<StoryIntelligenceExperienceCharacterState> BuildCharacters(SceneIntelligenceScene scene)
|
private static IReadOnlyList<StoryIntelligenceExperienceCharacterState> BuildCharacters(SceneIntelligenceScene scene, StoryMemory? storyMemory = null)
|
||||||
{
|
{
|
||||||
var narratorResolution = ResolveNarratorName(scene);
|
var narratorResolution = storyMemory?.ResolvePointOfView(scene) ?? ResolveNarratorName(scene);
|
||||||
var povName = Clean(narratorResolution ?? scene.PointOfView?.CharacterName);
|
var povName = Clean(narratorResolution ?? scene.PointOfView?.CharacterName);
|
||||||
var states = new List<StoryIntelligenceExperienceCharacterState>();
|
var states = new List<StoryIntelligenceExperienceCharacterState>();
|
||||||
var reservedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var reservedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var sceneCharacterNames = new HashSet<string>((scene.Characters ?? []).Select(character => Clean(character.Name)), StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var group in (scene.Characters ?? [])
|
foreach (var group in (scene.Characters ?? [])
|
||||||
.Where(character => !string.IsNullOrWhiteSpace(character.Name))
|
.Where(character => !string.IsNullOrWhiteSpace(character.Name))
|
||||||
.GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase)
|
.GroupBy(character => Clean(character.Name), StringComparer.OrdinalIgnoreCase)
|
||||||
@ -388,6 +397,28 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (storyMemory is not null)
|
||||||
|
{
|
||||||
|
foreach (var memoryCharacter in storyMemory.Characters
|
||||||
|
.Where(character => !sceneCharacterNames.Contains(character.Name))
|
||||||
|
.OrderByDescending(character => character.Weight)
|
||||||
|
.Take(Math.Max(0, 5 - states.Count)))
|
||||||
|
{
|
||||||
|
var libraryCode = CharacterLibraryCode(memoryCharacter.Name, null, scene, reservedCodes);
|
||||||
|
reservedCodes.Add(libraryCode);
|
||||||
|
states.Add(new StoryIntelligenceExperienceCharacterState
|
||||||
|
{
|
||||||
|
Id = CharacterId(memoryCharacter.Name),
|
||||||
|
LibraryCode = libraryCode,
|
||||||
|
Name = memoryCharacter.Name,
|
||||||
|
Role = memoryCharacter.Role,
|
||||||
|
Relevance = memoryCharacter.Relevance,
|
||||||
|
Weight = Math.Max(38, memoryCharacter.Weight - 18),
|
||||||
|
ImagePath = CharacterFallback(memoryCharacter.Name)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return states.OrderByDescending(character => character.Weight).ToList();
|
return states.OrderByDescending(character => character.Weight).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -493,8 +524,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
.Take(5)
|
.Take(5)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
private static IReadOnlyList<StoryIntelligenceExperienceRelationshipState> BuildRelationships(SceneIntelligenceScene scene)
|
private static IReadOnlyList<StoryIntelligenceExperienceRelationshipState> BuildRelationships(SceneIntelligenceScene scene, StoryMemory? storyMemory = null)
|
||||||
=> (scene.Relationships ?? [])
|
{
|
||||||
|
var current = (scene.Relationships ?? [])
|
||||||
.Where(item => !string.IsNullOrWhiteSpace(item.CharacterA) && !string.IsNullOrWhiteSpace(item.CharacterB))
|
.Where(item => !string.IsNullOrWhiteSpace(item.CharacterA) && !string.IsNullOrWhiteSpace(item.CharacterB))
|
||||||
.Select(item => new StoryIntelligenceExperienceRelationshipState
|
.Select(item => new StoryIntelligenceExperienceRelationshipState
|
||||||
{
|
{
|
||||||
@ -505,9 +537,26 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
State = Trim(FirstConfigured(item.Evidence, item.RelationshipSignal, "Emerging"), 58),
|
State = Trim(FirstConfigured(item.Evidence, item.RelationshipSignal, "Emerging"), 58),
|
||||||
Weight = ConfidenceWeight(item.Confidence, 70)
|
Weight = ConfidenceWeight(item.Confidence, 70)
|
||||||
})
|
})
|
||||||
|
.OrderByDescending(item => item.Weight)
|
||||||
|
.ToList();
|
||||||
|
var seen = new HashSet<string>(current.Select(item => $"{item.SourceId}|{item.TargetId}"), StringComparer.OrdinalIgnoreCase);
|
||||||
|
var memoryRelationships = (storyMemory?.Relationships ?? [])
|
||||||
|
.Where(item => seen.Add($"{item.SourceId}|{item.TargetId}"))
|
||||||
|
.Select(item => new StoryIntelligenceExperienceRelationshipState
|
||||||
|
{
|
||||||
|
Id = item.Id,
|
||||||
|
SourceId = item.SourceId,
|
||||||
|
TargetId = item.TargetId,
|
||||||
|
Label = item.Label,
|
||||||
|
State = item.State,
|
||||||
|
Weight = Math.Max(44, item.Weight - 16)
|
||||||
|
});
|
||||||
|
|
||||||
|
return current.Concat(memoryRelationships)
|
||||||
.OrderByDescending(item => item.Weight)
|
.OrderByDescending(item => item.Weight)
|
||||||
.Take(4)
|
.Take(4)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> BuildKnowledge(SceneIntelligenceScene scene)
|
private static IReadOnlyList<StoryIntelligenceExperienceTextItem> BuildKnowledge(SceneIntelligenceScene scene)
|
||||||
{
|
{
|
||||||
@ -733,6 +782,179 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
: "All known chapter runs are complete. Awaiting the next book import stage.";
|
: "All known chapter runs are complete. Awaiting the next book import stage.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class StoryMemory
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, MemoryCharacter> characters = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<string, StoryIntelligenceExperienceRelationshipState> relationships = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private string? narratorResolvedName;
|
||||||
|
|
||||||
|
public IReadOnlyCollection<MemoryCharacter> Characters => characters.Values;
|
||||||
|
public IReadOnlyCollection<StoryIntelligenceExperienceRelationshipState> Relationships => relationships.Values;
|
||||||
|
public decimal NarratorConfidence { get; private set; }
|
||||||
|
public int ResolvedCharacterCount => characters.Values.Count(character => character.IsResolved);
|
||||||
|
public int UnresolvedCharacterCount => characters.Values.Count(character => !character.IsResolved);
|
||||||
|
|
||||||
|
public void Update(SceneIntelligenceScene scene)
|
||||||
|
{
|
||||||
|
var resolvedNarrator = ResolveNarratorName(scene);
|
||||||
|
if (!string.IsNullOrWhiteSpace(resolvedNarrator))
|
||||||
|
{
|
||||||
|
narratorResolvedName = resolvedNarrator;
|
||||||
|
NarratorConfidence = Math.Max(NarratorConfidence, 0.82m);
|
||||||
|
}
|
||||||
|
else if (IsGenericNarratorName(scene.PointOfView?.CharacterName))
|
||||||
|
{
|
||||||
|
NarratorConfidence = Math.Max(NarratorConfidence, 0.35m);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var character in scene.Characters ?? [])
|
||||||
|
{
|
||||||
|
var name = Clean(character.Name);
|
||||||
|
if (string.IsNullOrWhiteSpace(name) || IsGenericNarratorName(name))
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(narratorResolvedName))
|
||||||
|
{
|
||||||
|
name = narratorResolvedName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
name = "Narrator";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var memory = GetOrAddCharacter(name);
|
||||||
|
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 povName = ResolvePointOfView(scene);
|
||||||
|
if (!string.IsNullOrWhiteSpace(povName))
|
||||||
|
{
|
||||||
|
var pov = GetOrAddCharacter(povName);
|
||||||
|
pov.PovCount += 1;
|
||||||
|
pov.Weight = Math.Max(pov.Weight, 100);
|
||||||
|
pov.Role = "POV character";
|
||||||
|
pov.Relevance = pov.PovCount > 1 ? "Recurring point of view" : "Point of view";
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var relationship in scene.Relationships ?? [])
|
||||||
|
{
|
||||||
|
var a = ResolveCharacterName(relationship.CharacterA);
|
||||||
|
var b = ResolveCharacterName(relationship.CharacterB);
|
||||||
|
if (string.IsNullOrWhiteSpace(a) || string.IsNullOrWhiteSpace(b))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyInference(GetOrAddCharacter(a), $"{relationship.RelationshipSignal} {relationship.Evidence}");
|
||||||
|
ApplyInference(GetOrAddCharacter(b), $"{relationship.RelationshipSignal} {relationship.Evidence}");
|
||||||
|
|
||||||
|
var key = $"{CharacterId(a)}|{CharacterId(b)}";
|
||||||
|
relationships[key] = new StoryIntelligenceExperienceRelationshipState
|
||||||
|
{
|
||||||
|
Id = $"memory-relationship-{StableKey(a)}-{StableKey(b)}",
|
||||||
|
SourceId = CharacterId(a),
|
||||||
|
TargetId = CharacterId(b),
|
||||||
|
Label = Trim(FirstConfigured(relationship.RelationshipSignal, "Relationship remembered"), 44),
|
||||||
|
State = Trim(FirstConfigured(relationship.Evidence, relationship.RelationshipSignal, "Carried forward from earlier scenes"), 58),
|
||||||
|
Weight = ConfidenceWeight(relationship.Confidence, 64)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? ResolvePointOfView(SceneIntelligenceScene scene)
|
||||||
|
{
|
||||||
|
var raw = Clean(scene.PointOfView?.CharacterName);
|
||||||
|
if (IsGenericNarratorName(raw))
|
||||||
|
{
|
||||||
|
return narratorResolvedName ?? raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(raw) ? narratorResolvedName : raw;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveCharacterName(string? raw)
|
||||||
|
{
|
||||||
|
var name = Clean(raw);
|
||||||
|
return IsGenericNarratorName(name) && !string.IsNullOrWhiteSpace(narratorResolvedName) ? narratorResolvedName! : name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemoryCharacter GetOrAddCharacter(string name)
|
||||||
|
{
|
||||||
|
var key = CharacterId(name);
|
||||||
|
if (!characters.TryGetValue(key, out var character))
|
||||||
|
{
|
||||||
|
character = new MemoryCharacter(name);
|
||||||
|
characters[key] = character;
|
||||||
|
}
|
||||||
|
|
||||||
|
return character;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyInference(MemoryCharacter character, string evidence)
|
||||||
|
{
|
||||||
|
var text = evidence.ToLowerInvariant();
|
||||||
|
if (ContainsAny(text, "mother", "daughter", "sister", "wife", "she ", " her "))
|
||||||
|
{
|
||||||
|
character.Presentation = "Feminine";
|
||||||
|
}
|
||||||
|
else if (ContainsAny(text, "father", "son", "brother", "husband", " he ", " him "))
|
||||||
|
{
|
||||||
|
character.Presentation = "Masculine";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
character.Presentation ??= NamePresentation(character.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ContainsAny(text, "grandfather", "grandmother", "elderly"))
|
||||||
|
{
|
||||||
|
character.AgeBand = "Elderly";
|
||||||
|
}
|
||||||
|
else if (ContainsAny(text, "mother", "father", "parent", "middle aged", "middle-aged"))
|
||||||
|
{
|
||||||
|
character.AgeBand = "Middle Aged";
|
||||||
|
}
|
||||||
|
else if (ContainsAny(text, "teenage", "teenager", "fifteen", "sixteen", "seventeen"))
|
||||||
|
{
|
||||||
|
character.AgeBand = "Older Teen";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NamePresentation(string name)
|
||||||
|
{
|
||||||
|
var key = name.ToLowerInvariant();
|
||||||
|
if (key is "beth" or "rosie" or "maggie" or "annie" or "anna" or "anne")
|
||||||
|
{
|
||||||
|
return "Feminine";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key is "graham" or "simon" or "kevin" or "colin" or "john" or "david")
|
||||||
|
{
|
||||||
|
return "Masculine";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsAny(string text, params string[] needles)
|
||||||
|
=> needles.Any(needle => text.Contains(needle, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class MemoryCharacter(string name)
|
||||||
|
{
|
||||||
|
public string Name { get; } = name;
|
||||||
|
public string Role { get; set; } = "Remembered character";
|
||||||
|
public string Relevance { get; set; } = "Known from earlier scenes";
|
||||||
|
public int Weight { get; set; } = 48;
|
||||||
|
public int PovCount { get; set; }
|
||||||
|
public string? Presentation { get; set; }
|
||||||
|
public string? AgeBand { get; set; }
|
||||||
|
public bool IsResolved => !IsGenericNarratorName(Name);
|
||||||
|
}
|
||||||
|
|
||||||
private static string CurrentSceneText(RunSnapshot snapshot)
|
private static string CurrentSceneText(RunSnapshot snapshot)
|
||||||
{
|
{
|
||||||
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
if (snapshot.Run.TotalDetectedScenes.HasValue)
|
||||||
@ -773,7 +995,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
int parsedScenes,
|
int parsedScenes,
|
||||||
int totalPersistedResults,
|
int totalPersistedResults,
|
||||||
int totalScenes,
|
int totalScenes,
|
||||||
int overallProgressPercent)
|
int overallProgressPercent,
|
||||||
|
StoryMemory storyMemory)
|
||||||
{
|
{
|
||||||
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
var failed = snapshots.Sum(snapshot => snapshot.Parsed.Count(item => item.Scene is null));
|
||||||
var warning = failed == 0 ? null : $"{failed:N0} scene result(s) skipped because they could not be parsed.";
|
var warning = failed == 0 ? null : $"{failed:N0} scene result(s) skipped because they could not be parsed.";
|
||||||
@ -792,6 +1015,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
TotalScenes = totalScenes,
|
TotalScenes = totalScenes,
|
||||||
ProgressSource = "ImportSession",
|
ProgressSource = "ImportSession",
|
||||||
CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run),
|
CurrentBackendStage = BackendStageDisplay(activeSnapshot.Run),
|
||||||
|
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
||||||
|
ResolvedCharacterCount = storyMemory.ResolvedCharacterCount,
|
||||||
|
UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount,
|
||||||
|
NarratorConfidence = storyMemory.NarratorConfidence,
|
||||||
SnapshotSource = "Real",
|
SnapshotSource = "Real",
|
||||||
RunStatus = SessionStatus(session, snapshots),
|
RunStatus = SessionStatus(session, snapshots),
|
||||||
PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count),
|
PersistedCompletedSceneCount = snapshots.Sum(snapshot => snapshot.Run.CompletedScenes ?? snapshot.Completed.Count),
|
||||||
@ -813,7 +1040,8 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
IReadOnlyList<StoryIntelligenceSavedSceneResult> results,
|
IReadOnlyList<StoryIntelligenceSavedSceneResult> results,
|
||||||
IReadOnlyList<ParsedSceneResult> parsed,
|
IReadOnlyList<ParsedSceneResult> parsed,
|
||||||
IReadOnlyList<ParsedSceneResult> completed,
|
IReadOnlyList<ParsedSceneResult> completed,
|
||||||
ParsedSceneResult? current)
|
ParsedSceneResult? current,
|
||||||
|
StoryMemory storyMemory)
|
||||||
{
|
{
|
||||||
var failed = parsed.Where(item => item.Scene is null).ToList();
|
var failed = parsed.Where(item => item.Scene is null).ToList();
|
||||||
var warning = failed.Count == 0
|
var warning = failed.Count == 0
|
||||||
@ -834,6 +1062,10 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
TotalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count),
|
TotalScenes = Math.Max(run.TotalDetectedScenes ?? 0, results.Count),
|
||||||
ProgressSource = "ChapterRun",
|
ProgressSource = "ChapterRun",
|
||||||
CurrentBackendStage = BackendStageDisplay(run),
|
CurrentBackendStage = BackendStageDisplay(run),
|
||||||
|
StoryMemoryCharacterCount = storyMemory.Characters.Count,
|
||||||
|
ResolvedCharacterCount = storyMemory.ResolvedCharacterCount,
|
||||||
|
UnresolvedCharacterCount = storyMemory.UnresolvedCharacterCount,
|
||||||
|
NarratorConfidence = storyMemory.NarratorConfidence,
|
||||||
SnapshotSource = "Real",
|
SnapshotSource = "Real",
|
||||||
RunStatus = run.Status,
|
RunStatus = run.Status,
|
||||||
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
PersistedCompletedSceneCount = run.CompletedScenes ?? 0,
|
||||||
@ -884,6 +1116,20 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplyIllustrationDiagnostics(StoryIntelligenceExperiencePrototypeViewModel model)
|
||||||
|
{
|
||||||
|
var characterResolutions = model.Scenes
|
||||||
|
.SelectMany(scene => scene.Characters)
|
||||||
|
.Select(character => character.ImageResolution)
|
||||||
|
.Where(resolution => resolution.IllustrationLibraryItemId.HasValue)
|
||||||
|
.ToList();
|
||||||
|
model.Diagnostics.IllustrationAssignmentCount = characterResolutions
|
||||||
|
.Select(resolution => resolution.IllustrationLibraryItemId!.Value)
|
||||||
|
.Distinct()
|
||||||
|
.Count();
|
||||||
|
model.Diagnostics.IllustrationChangeCount = characterResolutions.Count(resolution => !string.IsNullOrWhiteSpace(resolution.ReasonReassigned));
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsTerminal(string status)
|
private static bool IsTerminal(string status)
|
||||||
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings or StoryIntelligenceRunStatuses.Failed or StoryIntelligenceRunStatuses.Cancelled;
|
=> status is StoryIntelligenceRunStatuses.Completed or StoryIntelligenceRunStatuses.CompletedWithWarnings or StoryIntelligenceRunStatuses.Failed or StoryIntelligenceRunStatuses.Cancelled;
|
||||||
|
|
||||||
@ -1102,6 +1348,9 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
|||||||
private static string LocationLibraryCode(string name, SceneIntelligenceLocation? location, SceneIntelligenceSetting? setting)
|
private static string LocationLibraryCode(string name, SceneIntelligenceLocation? location, SceneIntelligenceSetting? setting)
|
||||||
{
|
{
|
||||||
var value = $"{name} {location?.LocationType} {location?.GenericRoomType} {setting?.LocationType}".ToLowerInvariant();
|
var value = $"{name} {location?.LocationType} {location?.GenericRoomType} {setting?.LocationType}".ToLowerInvariant();
|
||||||
|
if (value.Contains("bathroom") || value.Contains("toilet") || value.Contains("washroom")) return "loc-laundry-utility-room";
|
||||||
|
if (value.Contains("kitchen") || value.Contains("desk") || value.Contains("office") || value.Contains("study") || value.Contains("library")) return "loc-domestic-desk";
|
||||||
|
if (value.Contains("waiting room") || value.Contains("bedroom") || value.Contains("sitting room") || value.Contains("living room")) return "loc-small-flat-interior";
|
||||||
if (value.Contains("laundry") || value.Contains("utility")) return "loc-laundry-utility-room";
|
if (value.Contains("laundry") || value.Contains("utility")) return "loc-laundry-utility-room";
|
||||||
if (value.Contains("stair")) return "loc-narrow-stairwell";
|
if (value.Contains("stair")) return "loc-narrow-stairwell";
|
||||||
if (value.Contains("car park") || value.Contains("parking")) return "loc-wet-car-park";
|
if (value.Contains("car park") || value.Contains("parking")) return "loc-wet-car-park";
|
||||||
|
|||||||
@ -32,6 +32,12 @@ public sealed class StoryIntelligenceExperienceSnapshotDiagnostics
|
|||||||
public int TotalScenes { get; init; }
|
public int TotalScenes { get; init; }
|
||||||
public string ProgressSource { get; init; } = string.Empty;
|
public string ProgressSource { get; init; } = string.Empty;
|
||||||
public string CurrentBackendStage { get; init; } = string.Empty;
|
public string CurrentBackendStage { get; init; } = string.Empty;
|
||||||
|
public int StoryMemoryCharacterCount { get; init; }
|
||||||
|
public int ResolvedCharacterCount { get; init; }
|
||||||
|
public int UnresolvedCharacterCount { get; init; }
|
||||||
|
public decimal NarratorConfidence { get; init; }
|
||||||
|
public int IllustrationAssignmentCount { get; set; }
|
||||||
|
public int IllustrationChangeCount { get; set; }
|
||||||
public string SnapshotSource { get; init; } = string.Empty;
|
public string SnapshotSource { get; init; } = string.Empty;
|
||||||
public string RunStatus { get; init; } = string.Empty;
|
public string RunStatus { get; init; } = string.Empty;
|
||||||
public int PersistedCompletedSceneCount { get; init; }
|
public int PersistedCompletedSceneCount { get; init; }
|
||||||
|
|||||||
@ -237,6 +237,30 @@
|
|||||||
<dt>Current backend stage</dt>
|
<dt>Current backend stage</dt>
|
||||||
<dd data-diagnostic-key="currentBackendStage">@Model.Diagnostics.CurrentBackendStage</dd>
|
<dd data-diagnostic-key="currentBackendStage">@Model.Diagnostics.CurrentBackendStage</dd>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Story Memory character count</dt>
|
||||||
|
<dd data-diagnostic-key="storyMemoryCharacterCount">@Model.Diagnostics.StoryMemoryCharacterCount</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Resolved characters</dt>
|
||||||
|
<dd data-diagnostic-key="resolvedCharacterCount">@Model.Diagnostics.ResolvedCharacterCount</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Unresolved characters</dt>
|
||||||
|
<dd data-diagnostic-key="unresolvedCharacterCount">@Model.Diagnostics.UnresolvedCharacterCount</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Narrator confidence</dt>
|
||||||
|
<dd data-diagnostic-key="narratorConfidence">@Model.Diagnostics.NarratorConfidence</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Illustration assignments</dt>
|
||||||
|
<dd data-diagnostic-key="illustrationAssignmentCount">@Model.Diagnostics.IllustrationAssignmentCount</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Illustration changes</dt>
|
||||||
|
<dd data-diagnostic-key="illustrationChangeCount">@Model.Diagnostics.IllustrationChangeCount</dd>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt>Current polling interval</dt>
|
<dt>Current polling interval</dt>
|
||||||
<dd data-diagnostic-key="currentPollingInterval">Initial</dd>
|
<dd data-diagnostic-key="currentPollingInterval">Initial</dd>
|
||||||
|
|||||||
@ -781,7 +781,7 @@ input:focus-visible {
|
|||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
justify-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
width: min(176px, calc(var(--node-size, 150px) + 44px));
|
width: min(188px, calc(var(--node-size, 150px) + 58px));
|
||||||
max-width: calc(100vw - 32px);
|
max-width: calc(100vw - 32px);
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
border: 1px solid rgba(151, 185, 218, 0.1);
|
border: 1px solid rgba(151, 185, 218, 0.1);
|
||||||
@ -831,23 +831,27 @@ input:focus-visible {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.story-character__text strong {
|
.story-character__text strong {
|
||||||
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
font-size: clamp(0.82rem, 0.9vw, 0.98rem);
|
font-size: clamp(0.82rem, 0.9vw, 0.98rem);
|
||||||
line-height: 1.05;
|
line-height: 1.05;
|
||||||
text-overflow: ellipsis;
|
overflow-wrap: anywhere;
|
||||||
white-space: nowrap;
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-character__text span {
|
.story-character__text span {
|
||||||
|
display: -webkit-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
color: var(--story-muted);
|
color: var(--story-muted);
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
text-overflow: ellipsis;
|
overflow-wrap: anywhere;
|
||||||
white-space: nowrap;
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.story-character.is-context .story-character__text {
|
.story-character.is-context .story-character__text {
|
||||||
|
|||||||
@ -193,6 +193,7 @@
|
|||||||
function stageIndex(scene) {
|
function stageIndex(scene) {
|
||||||
const stage = String(scene.stage || "").toLowerCase();
|
const stage = String(scene.stage || "").toLowerCase();
|
||||||
if (scene.progressPercent >= 100 || stage.includes("review")) return analysisStages.length - 1;
|
if (scene.progressPercent >= 100 || stage.includes("review")) return analysisStages.length - 1;
|
||||||
|
if (stage.includes("final")) return 10;
|
||||||
if (stage.includes("continuity")) return 9;
|
if (stage.includes("continuity")) return 9;
|
||||||
if (stage.includes("knowledge")) return 8;
|
if (stage.includes("knowledge")) return 8;
|
||||||
if (stage.includes("relationship")) return 7;
|
if (stage.includes("relationship")) return 7;
|
||||||
@ -202,6 +203,7 @@
|
|||||||
if (stage.includes("content") || stage.includes("inferring")) return 3;
|
if (stage.includes("content") || stage.includes("inferring")) return 3;
|
||||||
if (stage.includes("scene")) return 2;
|
if (stage.includes("scene")) return 2;
|
||||||
if (stage.includes("chapter")) return 1;
|
if (stage.includes("chapter")) return 1;
|
||||||
|
if (stage.includes("reading") || stage.includes("read ")) return 0;
|
||||||
return Math.max(0, Math.min(analysisStages.length - 1, Math.floor(scene.progressPercent / 10)));
|
return Math.max(0, Math.min(analysisStages.length - 1, Math.floor(scene.progressPercent / 10)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user