Phase 20AG – Complete Scene Intelligence Import
This commit is contained in:
parent
771ac9eb91
commit
8afc0a3b40
@ -0,0 +1,269 @@
|
|||||||
|
# PlotDirector Story Intelligence Visualisation
|
||||||
|
|
||||||
|
## Design Vision (Future Phase)
|
||||||
|
|
||||||
|
**Version:** Draft 1\
|
||||||
|
**Status:** Future Feature\
|
||||||
|
**Priority:** High
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Replace the traditional progress page with a live visualisation of
|
||||||
|
PlotDirector understanding the user's story.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
The current Story Intelligence progress page successfully reports
|
||||||
|
progress, but it behaves like an engineering dashboard.
|
||||||
|
|
||||||
|
It shows percentages, counters and diagnostics. Although technically
|
||||||
|
informative, it does not demonstrate the value of PlotDirector nor
|
||||||
|
encourage the user to remain engaged during what may be a lengthy
|
||||||
|
analysis.
|
||||||
|
|
||||||
|
Instead, Story Intelligence should become an experience.
|
||||||
|
|
||||||
|
The objective is to allow users to literally watch PlotDirector build an
|
||||||
|
understanding of their story.
|
||||||
|
|
||||||
|
A living knowledge map rather than a loading screen.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Overall User Flow
|
||||||
|
|
||||||
|
Welcome → Project → Book → Word Companion → Scan manuscript → Review
|
||||||
|
chapters → Start Story Intelligence
|
||||||
|
|
||||||
|
The wizard ends here.
|
||||||
|
|
||||||
|
The user sees:
|
||||||
|
|
||||||
|
- Story Intelligence has begun.
|
||||||
|
- Analysis continues even if Word or the browser is closed.
|
||||||
|
- Typical novels may take between 30 minutes and several hours.
|
||||||
|
- We'll email you when analysis is complete.
|
||||||
|
|
||||||
|
Buttons:
|
||||||
|
|
||||||
|
- Continue in Background
|
||||||
|
- Watch Story Intelligence
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Experience 1 -- Background Processing
|
||||||
|
|
||||||
|
Ideal for users who simply wish to import.
|
||||||
|
|
||||||
|
Analysis continues entirely server-side.
|
||||||
|
|
||||||
|
Completion provides:
|
||||||
|
|
||||||
|
- Email notification
|
||||||
|
- Notification inside PlotDirector
|
||||||
|
- Resume button from the Projects page
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Experience 2 -- Watch Story Intelligence
|
||||||
|
|
||||||
|
A dedicated full-screen experience.
|
||||||
|
|
||||||
|
Not part of the wizard.
|
||||||
|
|
||||||
|
Minimal chrome.
|
||||||
|
|
||||||
|
Mission Control for authors.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Visual Style
|
||||||
|
|
||||||
|
Dark theme.
|
||||||
|
|
||||||
|
Deep blues.
|
||||||
|
|
||||||
|
Subtle animated particles.
|
||||||
|
|
||||||
|
Glowing connection lines.
|
||||||
|
|
||||||
|
Calm motion.
|
||||||
|
|
||||||
|
Inspired by:
|
||||||
|
|
||||||
|
- NASA Mission Control
|
||||||
|
- Obsidian Graph
|
||||||
|
- Neo4j Browser
|
||||||
|
- Apple-quality animation
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Screen Layout
|
||||||
|
|
||||||
|
Top: - PlotDirector - Story Intelligence - Current Book - Analysis
|
||||||
|
Status - Continue in Background
|
||||||
|
|
||||||
|
Centre: - Living knowledge graph
|
||||||
|
|
||||||
|
Left: - Progress - Latest discoveries - Live activity feed
|
||||||
|
|
||||||
|
Right: - Story observations - Knowledge threads - Interesting
|
||||||
|
discoveries
|
||||||
|
|
||||||
|
Bottom: - Current scene - Timeline - Recent scenes
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Living Knowledge Graph
|
||||||
|
|
||||||
|
Initially almost empty.
|
||||||
|
|
||||||
|
As Story Intelligence progresses:
|
||||||
|
|
||||||
|
- Characters appear.
|
||||||
|
- Locations appear.
|
||||||
|
- Assets appear.
|
||||||
|
- Relationships appear.
|
||||||
|
- Timeline events appear.
|
||||||
|
- Knowledge threads appear.
|
||||||
|
|
||||||
|
Connections animate into existence.
|
||||||
|
|
||||||
|
The graph constantly grows.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Character Portraits
|
||||||
|
|
||||||
|
Initially use a built-in library of approximately 300--500 illustrated
|
||||||
|
portraits.
|
||||||
|
|
||||||
|
These are generic visual identifiers, not canonical artwork.
|
||||||
|
|
||||||
|
Categorised by:
|
||||||
|
|
||||||
|
- Gender
|
||||||
|
- Age
|
||||||
|
- Hair colour
|
||||||
|
- General appearance
|
||||||
|
|
||||||
|
Authors may later replace them.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Live Discovery Feed
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Character discovered: Rosie MacDonald
|
||||||
|
- Location discovered: The Doweries
|
||||||
|
- Relationship discovered: Beth ↔ Rosie
|
||||||
|
- Asset discovered: Red TR6
|
||||||
|
- Timeline event: 24 December 1982
|
||||||
|
- Mystery thread: Who left the note?
|
||||||
|
|
||||||
|
Older discoveries gently fade away.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Story Observations
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- Beth now appears in seven scenes.
|
||||||
|
- Rosie has become central to the story.
|
||||||
|
- Three names may refer to the same person.
|
||||||
|
- A mystery thread has emerged.
|
||||||
|
|
||||||
|
These should feel insightful rather than like warnings.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Current Scene
|
||||||
|
|
||||||
|
Display:
|
||||||
|
|
||||||
|
- Scene number
|
||||||
|
- Short summary
|
||||||
|
- Progress
|
||||||
|
|
||||||
|
Each completed scene joins the graph.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Timeline
|
||||||
|
|
||||||
|
A growing ribbon along the bottom.
|
||||||
|
|
||||||
|
Dates, scenes and events appear naturally.
|
||||||
|
|
||||||
|
Later this becomes the PlotDirector timeline.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Animation Style
|
||||||
|
|
||||||
|
Nodes gently appear.
|
||||||
|
|
||||||
|
Connections draw themselves.
|
||||||
|
|
||||||
|
The graph subtly rebalances.
|
||||||
|
|
||||||
|
The camera slowly pans.
|
||||||
|
|
||||||
|
Everything should feel elegant and calm.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# SignalR
|
||||||
|
|
||||||
|
All updates are pushed live using SignalR.
|
||||||
|
|
||||||
|
No polling.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Completion
|
||||||
|
|
||||||
|
When analysis finishes:
|
||||||
|
|
||||||
|
Story Intelligence Complete
|
||||||
|
|
||||||
|
Your manuscript is ready for review.
|
||||||
|
|
||||||
|
Button:
|
||||||
|
|
||||||
|
Review Imported Scenes
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Long-Term Vision
|
||||||
|
|
||||||
|
The same graph engine becomes:
|
||||||
|
|
||||||
|
- Relationship Explorer
|
||||||
|
- Character Explorer
|
||||||
|
- Knowledge Explorer
|
||||||
|
- Continuity Explorer
|
||||||
|
- Timeline Explorer
|
||||||
|
|
||||||
|
During Story Intelligence it displays temporary AI discoveries.
|
||||||
|
|
||||||
|
After import it displays real PlotDirector data.
|
||||||
|
|
||||||
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Design Goal
|
||||||
|
|
||||||
|
The user should feel:
|
||||||
|
|
||||||
|
'I can actually see PlotDirector understanding my novel.'
|
||||||
|
|
||||||
|
not
|
||||||
|
|
||||||
|
'I'm waiting for a progress bar.'
|
||||||
|
|
||||||
|
The visualisation should become one of PlotDirector's defining features
|
||||||
|
and something authors genuinely enjoy watching.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
35
PlotLine/Docs/StoryIntelligence_Phase20AG_SceneImport.md
Normal file
35
PlotLine/Docs/StoryIntelligence_Phase20AG_SceneImport.md
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
# Phase 20AG - Scene Intelligence Import
|
||||||
|
|
||||||
|
Story Intelligence scene import now uses the stored Scene Intelligence JSON to populate the existing PlotDirector scene fields. This phase does not add new OpenAI requests or new analysis passes.
|
||||||
|
|
||||||
|
## Imported Scene Fields
|
||||||
|
|
||||||
|
- Scene title: generated from the imported scene number and short summary.
|
||||||
|
- Short summary: saved to the same scene summary field used by manually authored scenes.
|
||||||
|
- Detailed summary: saved to scene outcome notes.
|
||||||
|
- Scene purpose: `observedFunction` is saved to scene purpose notes.
|
||||||
|
- Purpose labels: matched to existing PlotDirector scene purpose labels when the observed purpose text or returned purpose labels match existing labels. New purpose labels are not created.
|
||||||
|
- Point of view: linked only when the returned POV name resolves to an existing project character or alias. Unresolved POV is left blank and noted.
|
||||||
|
- Metrics: supported project metric values are imported for Action, Emotion, Tension, Conflict, Mystery, Romance, Humour, Revelation and Pacing when matching metric types exist.
|
||||||
|
- Time reference: the returned setting time is saved as the scene relative time text.
|
||||||
|
- Timeline clues: factual timeline clues are saved as scene notes. Timeline events are not created.
|
||||||
|
- Scene notes: useful non-duplicative atmosphere, ambiguity and descriptive observation notes are saved as Story Intelligence import notes.
|
||||||
|
- Source tracking: each scene records the Story Intelligence run and source paragraph range.
|
||||||
|
|
||||||
|
## Confidence
|
||||||
|
|
||||||
|
Confidence remains stored in Story Intelligence audit data and metric notes. Author-facing onboarding review displays confidence as High, Medium or Low rather than numeric values.
|
||||||
|
|
||||||
|
## Not Imported Yet
|
||||||
|
|
||||||
|
The following Story Intelligence areas remain future phases:
|
||||||
|
|
||||||
|
- Characters
|
||||||
|
- Locations
|
||||||
|
- Assets
|
||||||
|
- Relationships
|
||||||
|
- Knowledge changes
|
||||||
|
- Continuity warnings
|
||||||
|
- Real timeline events
|
||||||
|
|
||||||
|
These are intentionally not created during scene import so that this phase only completes the scene-level import surface.
|
||||||
@ -415,7 +415,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
|||||||
{
|
{
|
||||||
SceneNumber = scene.TemporarySceneNumber,
|
SceneNumber = scene.TemporarySceneNumber,
|
||||||
Summary = Trim(FirstConfigured(scene.ParsedScene.Summary?.Short, scene.ParsedScene.Summary?.Detailed, $"Scene {scene.TemporarySceneNumber:N0}")!, 180),
|
Summary = Trim(FirstConfigured(scene.ParsedScene.Summary?.Short, scene.ParsedScene.Summary?.Detailed, $"Scene {scene.TemporarySceneNumber:N0}")!, 180),
|
||||||
Pov = FirstConfigured(scene.ParsedScene.PointOfView?.CharacterName, scene.ParsedScene.PointOfView?.NarrativeMode),
|
Pov = scene.PovCharacterID.HasValue ? FirstConfigured(scene.ParsedScene.PointOfView?.CharacterName, scene.ParsedScene.PointOfView?.NarrativeMode) : null,
|
||||||
Setting = BuildSetting(scene.ParsedScene.Setting),
|
Setting = BuildSetting(scene.ParsedScene.Setting),
|
||||||
Confidence = scene.ParsedScene.Summary?.Confidence ?? scene.ParsedScene.Setting?.Confidence,
|
Confidence = scene.ParsedScene.Summary?.Confidence ?? scene.ParsedScene.Setting?.Confidence,
|
||||||
HasWarnings = false
|
HasWarnings = false
|
||||||
|
|||||||
@ -37,8 +37,10 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
["mystery"] = ["Mystery"],
|
["mystery"] = ["Mystery"],
|
||||||
["romance"] = ["Romance", "Emotional Intimacy"],
|
["romance"] = ["Romance", "Emotional Intimacy"],
|
||||||
["humour"] = ["Humour", "Humor", "Comedy", "Comic Relief"],
|
["humour"] = ["Humour", "Humor", "Comedy", "Comic Relief"],
|
||||||
|
["humor"] = ["Humour", "Humor", "Comedy", "Comic Relief"],
|
||||||
["revelation"] = ["Revelation", "Reveal"],
|
["revelation"] = ["Revelation", "Reveal"],
|
||||||
["pacingIntensity"] = ["PacingIntensity", "Pacing Intensity", "Overall Intensity"]
|
["pacing"] = ["Pacing", "PacingIntensity", "Pacing Intensity", "Overall Intensity"],
|
||||||
|
["pacingIntensity"] = ["Pacing", "PacingIntensity", "Pacing Intensity", "Overall Intensity"]
|
||||||
};
|
};
|
||||||
|
|
||||||
public async Task<StoryIntelligenceImportConfirmationViewModel?> BuildConfirmationAsync(int runId)
|
public async Task<StoryIntelligenceImportConfirmationViewModel?> BuildConfirmationAsync(int runId)
|
||||||
@ -152,7 +154,7 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
var timelineNoteTypeId = noteTypes.FirstOrDefault(type => string.Equals(type.TypeName, "Timeline Note", StringComparison.OrdinalIgnoreCase))?.SceneNoteTypeID;
|
var timelineNoteTypeId = noteTypes.FirstOrDefault(type => string.Equals(type.TypeName, "Timeline Note", StringComparison.OrdinalIgnoreCase))?.SceneNoteTypeID;
|
||||||
if (timelineNoteTypeId is null)
|
if (timelineNoteTypeId is null)
|
||||||
{
|
{
|
||||||
warnings.Add("Timeline clues will not be imported because the Timeline Note type is not available.");
|
warnings.Add("Timeline clues and Story Intelligence import notes will not be imported because the Timeline Note type is not available.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var purposeMap = lookupData.ScenePurposeTypes.ToDictionary(type => Clean(type.PurposeName), type => type.ScenePurposeTypeID, StringComparer.OrdinalIgnoreCase);
|
var purposeMap = lookupData.ScenePurposeTypes.ToDictionary(type => Clean(type.PurposeName), type => type.ScenePurposeTypeID, StringComparer.OrdinalIgnoreCase);
|
||||||
@ -242,12 +244,12 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
SourceLabel = sceneResult.SourceLabel,
|
SourceLabel = sceneResult.SourceLabel,
|
||||||
ParsedScene = parsed,
|
ParsedScene = parsed,
|
||||||
PovCharacterID = povCharacterId,
|
PovCharacterID = povCharacterId,
|
||||||
PurposeTypeIDs = MatchPurposes(parsed.ScenePurpose?.ObservedFunction, purposeMap),
|
PurposeTypeIDs = MatchPurposes(parsed, purposeMap),
|
||||||
Metrics = BuildMetrics(parsed, metricMap),
|
Metrics = BuildMetrics(parsed, metricMap),
|
||||||
SceneTitle = sceneTitle,
|
SceneTitle = sceneTitle,
|
||||||
Summary = string.IsNullOrWhiteSpace(summary) ? null : summary,
|
Summary = string.IsNullOrWhiteSpace(summary) ? null : summary,
|
||||||
PurposeNotes = BuildPurposeNotes(parsed, sceneResult, povName, povCharacterId),
|
PurposeNotes = BuildPurposeNotes(parsed, sceneResult, povName, povCharacterId),
|
||||||
OutcomeNotes = string.IsNullOrWhiteSpace(parsed.Summary?.Detailed) ? null : $"Detailed summary: {parsed.Summary.Detailed}",
|
OutcomeNotes = string.IsNullOrWhiteSpace(parsed.Summary?.Detailed) ? null : Clean(parsed.Summary.Detailed),
|
||||||
ImportNoteText = BuildImportNote(parsed, povName, povCharacterId)
|
ImportNoteText = BuildImportNote(parsed, povName, povCharacterId)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -356,6 +358,7 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var values = new List<SceneMetricValue>();
|
var values = new List<SceneMetricValue>();
|
||||||
|
var importedMetricTypeIds = new HashSet<int>();
|
||||||
foreach (var (name, metric) in parsed.Metrics)
|
foreach (var (name, metric) in parsed.Metrics)
|
||||||
{
|
{
|
||||||
if (!metric.Score.HasValue || !metricMap.TryGetValue(name, out var metricType))
|
if (!metric.Score.HasValue || !metricMap.TryGetValue(name, out var metricType))
|
||||||
@ -363,6 +366,11 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!importedMetricTypeIds.Add(metricType.MetricTypeID))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
values.Add(new SceneMetricValue
|
values.Add(new SceneMetricValue
|
||||||
{
|
{
|
||||||
MetricTypeID = metricType.MetricTypeID,
|
MetricTypeID = metricType.MetricTypeID,
|
||||||
@ -375,16 +383,16 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<int> MatchPurposes(string? observedFunction, IReadOnlyDictionary<string, int> purposeMap)
|
private static IReadOnlyList<int> MatchPurposes(SceneIntelligenceScene parsed, IReadOnlyDictionary<string, int> purposeMap)
|
||||||
{
|
{
|
||||||
var value = Clean(observedFunction);
|
var values = ReadPurposeTexts(parsed).Select(Clean).Where(value => !string.IsNullOrWhiteSpace(value)).Distinct(StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
if (string.IsNullOrWhiteSpace(value))
|
if (values.Count == 0)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var matches = purposeMap
|
var matches = purposeMap
|
||||||
.Where(pair => value.Contains(pair.Key, StringComparison.OrdinalIgnoreCase) || pair.Key.Contains(value, StringComparison.OrdinalIgnoreCase))
|
.Where(pair => values.Any(value => value.Contains(pair.Key, StringComparison.OrdinalIgnoreCase) || pair.Key.Contains(value, StringComparison.OrdinalIgnoreCase)))
|
||||||
.Select(pair => pair.Value)
|
.Select(pair => pair.Value)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToList();
|
.ToList();
|
||||||
@ -403,6 +411,12 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
lines.Add($"Observed function: {parsed.ScenePurpose.ObservedFunction}");
|
lines.Add($"Observed function: {parsed.ScenePurpose.ObservedFunction}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var purposeLabels = ReadPurposeLabels(parsed.ScenePurpose).ToList();
|
||||||
|
if (purposeLabels.Count > 0)
|
||||||
|
{
|
||||||
|
lines.Add($"Purpose labels observed: {string.Join(", ", purposeLabels)}");
|
||||||
|
}
|
||||||
|
|
||||||
if (parsed.Setting is not null)
|
if (parsed.Setting is not null)
|
||||||
{
|
{
|
||||||
var setting = string.Join("; ", new[]
|
var setting = string.Join("; ", new[]
|
||||||
@ -430,10 +444,23 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
private static string? BuildImportNote(SceneIntelligenceScene parsed, string povName, int? povCharacterId)
|
private static string? BuildImportNote(SceneIntelligenceScene parsed, string povName, int? povCharacterId)
|
||||||
{
|
{
|
||||||
var lines = new List<string>();
|
var lines = new List<string>();
|
||||||
if (parsed.TimelineClues?.Count > 0)
|
var timelineClues = ReadFactualTimelineClues(parsed).ToList();
|
||||||
|
if (timelineClues.Count > 0)
|
||||||
{
|
{
|
||||||
lines.Add("Timeline clues:");
|
lines.Add("Timeline clues:");
|
||||||
lines.AddRange(parsed.TimelineClues.Select(clue => $"- {Clean(clue.AbsoluteDate ?? clue.RelativeOrder ?? clue.Clue)}"));
|
lines.AddRange(timelineClues.Select(clue => $"- {clue}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
var sceneNotes = ReadUsefulSceneNotes(parsed).ToList();
|
||||||
|
if (sceneNotes.Count > 0)
|
||||||
|
{
|
||||||
|
if (lines.Count > 0)
|
||||||
|
{
|
||||||
|
lines.Add(string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.Add("Scene notes:");
|
||||||
|
lines.AddRange(sceneNotes.Select(note => $"- {note}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(povName) && !povCharacterId.HasValue)
|
if (!string.IsNullOrWhiteSpace(povName) && !povCharacterId.HasValue)
|
||||||
@ -444,6 +471,178 @@ public sealed class StoryIntelligenceImportCommitService(
|
|||||||
return lines.Count == 0 ? null : string.Join(Environment.NewLine, lines);
|
return lines.Count == 0 ? null : string.Join(Environment.NewLine, lines);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ReadPurposeTexts(SceneIntelligenceScene parsed)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(parsed.ScenePurpose?.ObservedFunction))
|
||||||
|
{
|
||||||
|
yield return parsed.ScenePurpose.ObservedFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var label in ReadPurposeLabels(parsed.ScenePurpose))
|
||||||
|
{
|
||||||
|
yield return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ReadPurposeLabels(SceneIntelligenceScenePurpose? purpose)
|
||||||
|
{
|
||||||
|
if (purpose?.ExtensionData is null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var propertyName in new[] { "purposeLabels", "labels", "label" })
|
||||||
|
{
|
||||||
|
if (!purpose.ExtensionData.TryGetValue(propertyName, out var element))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var label = Clean(element.GetString());
|
||||||
|
if (!string.IsNullOrWhiteSpace(label))
|
||||||
|
{
|
||||||
|
yield return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (element.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
foreach (var item in element.EnumerateArray())
|
||||||
|
{
|
||||||
|
if (item.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var label = Clean(item.GetString());
|
||||||
|
if (!string.IsNullOrWhiteSpace(label))
|
||||||
|
{
|
||||||
|
yield return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ReadFactualTimelineClues(SceneIntelligenceScene parsed)
|
||||||
|
{
|
||||||
|
if (parsed.TimelineClues is null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var clue in parsed.TimelineClues)
|
||||||
|
{
|
||||||
|
var value = Clean(clue.AbsoluteDate ?? clue.RelativeOrder ?? clue.Clue);
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var source = $"{clue.Clue} {clue.RelativeOrder} {clue.AbsoluteDate} {clue.Evidence}";
|
||||||
|
if (source.Contains("speculat", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| source.Contains("uncertain", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| source.Contains("unknown", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ReadUsefulSceneNotes(SceneIntelligenceScene parsed)
|
||||||
|
{
|
||||||
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var note in ReadSettingNotes(parsed.Setting))
|
||||||
|
{
|
||||||
|
if (seen.Add(note))
|
||||||
|
{
|
||||||
|
yield return note;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.SourceLimits?.AmbiguityNotes is not null)
|
||||||
|
{
|
||||||
|
foreach (var ambiguity in parsed.SourceLimits.AmbiguityNotes.Select(Clean).Where(value => !string.IsNullOrWhiteSpace(value)))
|
||||||
|
{
|
||||||
|
var note = $"Ambiguity: {ambiguity}";
|
||||||
|
if (seen.Add(note))
|
||||||
|
{
|
||||||
|
yield return note;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.Observations is null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var observation in parsed.Observations)
|
||||||
|
{
|
||||||
|
var note = Clean(observation.Description);
|
||||||
|
if (string.IsNullOrWhiteSpace(note) || !IsUsefulSceneObservation(observation))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.Add(note))
|
||||||
|
{
|
||||||
|
yield return note;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ReadSettingNotes(SceneIntelligenceSetting? setting)
|
||||||
|
{
|
||||||
|
if (setting is null)
|
||||||
|
{
|
||||||
|
yield break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var atmosphere = ReadStringExtension(setting.ExtensionData, "atmosphere", "mood", "tone");
|
||||||
|
if (!string.IsNullOrWhiteSpace(atmosphere))
|
||||||
|
{
|
||||||
|
yield return $"Atmosphere: {atmosphere}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsUsefulSceneObservation(SceneIntelligenceObservation observation)
|
||||||
|
{
|
||||||
|
var type = Clean(observation.ObservationType);
|
||||||
|
var predicate = Clean(observation.Predicate);
|
||||||
|
return type.Contains("setting", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| type.Contains("atmosphere", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| type.Contains("scene", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| predicate.Contains("observes", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| predicate.Contains("reveals", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| predicate.Contains("confirms", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| predicate.Contains("discovers", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ReadStringExtension(Dictionary<string, JsonElement>? data, params string[] names)
|
||||||
|
{
|
||||||
|
if (data is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var name in names)
|
||||||
|
{
|
||||||
|
if (data.TryGetValue(name, out var element) && element.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var value = Clean(element.GetString());
|
||||||
|
if (!string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static SceneIntelligenceScene? TryReadScene(StoryIntelligenceSavedSceneResult result)
|
private static SceneIntelligenceScene? TryReadScene(StoryIntelligenceSavedSceneResult result)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(result.ParsedJson))
|
if (string.IsNullOrWhiteSpace(result.ParsedJson))
|
||||||
|
|||||||
@ -285,7 +285,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatConfidence(decimal? confidence)
|
private static string FormatConfidence(decimal? confidence)
|
||||||
=> confidence.HasValue ? $"{Math.Clamp(confidence.Value, 0m, 1m):P0}" : "Not available";
|
{
|
||||||
|
if (!confidence.HasValue)
|
||||||
|
{
|
||||||
|
return "Not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = Math.Clamp(confidence.Value, 0m, 1m);
|
||||||
|
return value >= 0.75m
|
||||||
|
? "High"
|
||||||
|
: value >= 0.45m
|
||||||
|
? "Medium"
|
||||||
|
: "Low";
|
||||||
|
}
|
||||||
|
|
||||||
private static string FormatCost(decimal? cost)
|
private static string FormatCost(decimal? cost)
|
||||||
=> cost.HasValue ? $"About ${cost.Value:0.00}" : "Not available";
|
=> cost.HasValue ? $"About ${cost.Value:0.00}" : "Not available";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user