Phase 21O - Story Intelligence Usability and Continuity Refinement
This commit is contained in:
parent
f80734a80c
commit
63f427066d
@ -52,6 +52,7 @@ var tests = new (string Name, Action Test)[]
|
||||
("Illustration demand archetypes stay broad and use active template", IllustrationDemandArchetypesStayBroadAndUseActiveTemplate),
|
||||
("Story Intelligence evidence extraction prevents observed attribute leakage", StoryIntelligenceEvidenceExtractionPreventsObservedAttributeLeakage),
|
||||
("Illustration assignment repair enforces explicit evidence groups and strict subtypes", IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes),
|
||||
("Phase 21O preserves adult defaults and real-mode continuity", Phase21OPreservesAdultDefaultsAndRealModeContinuity),
|
||||
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
|
||||
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
|
||||
("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics),
|
||||
@ -581,7 +582,7 @@ static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictS
|
||||
Assert(femaleInstructor.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"female instructor should be Feminine, got {femaleInstructor.Presentation}.");
|
||||
Assert(maleExaminers.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"male examiners should be Masculine, got {maleExaminers.Presentation}.");
|
||||
Assert(lad.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"lad should be Masculine, got {lad.Presentation}.");
|
||||
Assert(lad.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"lad should infer a young age band, got {lad.AgeBand}.");
|
||||
Assert(lad.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown, $"lad should not default to Child without explicit child-age evidence, got {lad.AgeBand}.");
|
||||
Assert(girl.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"girl should be Feminine, got {girl.Presentation}.");
|
||||
Assert(girl.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"schoolgirl should infer Child, got {girl.AgeBand}.");
|
||||
Assert(kevin.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Masculine, $"Kevin should not inherit feminine presentation from his instructor, got {kevin.Presentation}.");
|
||||
@ -597,7 +598,7 @@ static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictS
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Bathroom", "LaundryUtilityRoom"), "Bathroom must not match laundry room.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("Road", "CarPark"), "Road must not match car park.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("WaitingRoom", "FlatInterior"), "Waiting room must not match flat interior.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Ford Fiesta") == "Car", "Ford Fiesta should classify as Car.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Ford Fiesta") == "Hatchback", "Ford Fiesta should classify as Hatchback.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("Car") == "Car", "Car should classify as Car.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("supercharger") == "CarPart", "Supercharger should not classify as full car.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.AssetType("biscuit tin") == "Tin", "Biscuit tin should not classify as Photograph.");
|
||||
@ -612,6 +613,28 @@ static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictS
|
||||
Assert(Rejected(beth, adult).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Existing adult assignment must be discarded when Beth evidence says YoungTeen.");
|
||||
}
|
||||
|
||||
static void Phase21OPreservesAdultDefaultsAndRealModeContinuity()
|
||||
{
|
||||
var unknownNamed = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Sarah", ["Sarah watches."], []);
|
||||
var explicitGirl = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Mia", ["schoolgirl"], []);
|
||||
var teacher = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Mrs Patterson", ["Mrs Patterson is a teacher."], []);
|
||||
var examiner = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("driving examiner", ["female driving examiner"], []);
|
||||
|
||||
Assert(unknownNamed.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown, $"Unknown age should remain Unknown before illustration fallback, got {unknownNamed.AgeBand}.");
|
||||
Assert(explicitGirl.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Child, $"Schoolgirl should remain explicit child evidence, got {explicitGirl.AgeBand}.");
|
||||
Assert(teacher.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Teacher title/role should infer Adult, got {teacher.AgeBand}.");
|
||||
Assert(examiner.Presentation == StoryIntelligenceIllustrationCompatibility.Presentations.Feminine, $"Female examiner should infer Feminine, got {examiner.Presentation}.");
|
||||
Assert(examiner.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Driving examiner should infer Adult, got {examiner.AgeBand}.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Bristol Road") == "Road", "Road names should classify as Road.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("Cock Hill Lane") == "Road", "Lane names should classify as Road.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.AssetCompatible("Hatchback", "Car"), "Hatchbacks must not borrow generic car artwork.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.AssetCompatible("SportsCar", "Hatchback"), "Vehicle subtypes should remain strict.");
|
||||
|
||||
var view = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "PlotLine", "Views", "Development", "StoryIntelligenceExperience.cshtml"));
|
||||
Assert(view.Contains("Return to Import", StringComparison.Ordinal), "Real visualisation should expose a return-to-import link.");
|
||||
Assert(view.Contains("Model.Mode == \"simulation\"", StringComparison.Ordinal), "Prototype controls should be gated to simulation mode.");
|
||||
}
|
||||
|
||||
static IllustrationCharacterMetadata Metadata(string code, string ageBand, string presentation, bool isUnknownFigure = false)
|
||||
=> new(
|
||||
ageBand,
|
||||
|
||||
@ -77,9 +77,13 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
presentation = guardedPresentation.Value;
|
||||
presentationConfidence = guardedPresentation.Confidence;
|
||||
var explicitAge = ExplicitAgeBand(scopedText);
|
||||
if (explicitAge.Value == AgeBands.Unknown)
|
||||
{
|
||||
explicitAge = EntityIdentityAgeBand(name, identityText);
|
||||
}
|
||||
var relationshipAge = RelationshipAgeBand(name, scopedText);
|
||||
var namedAge = NameAgeBand(name);
|
||||
var age = FirstKnown(relationshipAge.Value, explicitAge.Value, namedAge.Value, AgeBands.Unknown);
|
||||
var age = FirstKnown(explicitAge.Value, relationshipAge.Value, namedAge.Value, AgeBands.Unknown);
|
||||
var ageConfidence = new[] { explicitAge.Confidence, relationshipAge.Confidence, namedAge.Confidence }.Max();
|
||||
var attributeText = EntityAttributeText(entityText, name);
|
||||
var hairColour = HairColour(attributeText);
|
||||
@ -171,7 +175,9 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
rejected.Add($"presentation hard mismatch: wanted {evidence.Presentation}, candidate {candidate.Presentation}");
|
||||
}
|
||||
|
||||
if (ExplicitKnown(evidence.HairColour, evidence.AgeConfidence) && KnownMismatch(evidence.HairColour, candidate.HairColour))
|
||||
if (!string.Equals(evidence.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(candidate.HairColour, HairColours.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||
&& KnownMismatch(evidence.HairColour, candidate.HairColour))
|
||||
{
|
||||
rejected.Add($"hair colour hard mismatch: wanted {evidence.HairColour}, candidate {candidate.HairColour}");
|
||||
}
|
||||
@ -189,7 +195,7 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
var text = (value ?? string.Empty).Replace("-", string.Empty, StringComparison.OrdinalIgnoreCase).Replace(" ", string.Empty, StringComparison.OrdinalIgnoreCase).ToLowerInvariant();
|
||||
if (text.Contains("youngteen") || text.Contains("13") || text.Contains("14") || text.Contains("15") || text.Contains("fifteen")) return AgeBands.YoungTeen;
|
||||
if (text.Contains("olderteen") || text.Contains("teen") || text.Contains("16") || text.Contains("17") || text.Contains("18") || text.Contains("19")) return AgeBands.OlderTeen;
|
||||
if (text.Contains("child") || text.Contains("boy") || text.Contains("girl")) return AgeBands.Child;
|
||||
if (text.Contains("child")) return AgeBands.Child;
|
||||
if (text.Contains("youngadult")) return AgeBands.YoungAdult;
|
||||
if (text.Contains("middleaged") || text.Contains("matureadult") || text.Contains("parent") || text.Contains("mother") || text.Contains("father") || text.Contains("aunt") || text.Contains("uncle")) return AgeBands.MatureAdult;
|
||||
if (text.Contains("adult")) return AgeBands.Adult;
|
||||
@ -251,7 +257,7 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
if (ContainsAny(text, "office", "desk", "study", "library")) return "Office";
|
||||
if (text.Contains("school", StringComparison.OrdinalIgnoreCase)) return "School";
|
||||
if (text.Contains("waiting room", StringComparison.OrdinalIgnoreCase)) return "WaitingRoom";
|
||||
if (ContainsAny(text, "road", "street", "lane", "drive", "motorway", "bridge")) return "Road";
|
||||
if (ContainsAny(text, "road", "street", "lane", "drive", "close", "avenue", "boulevard", "motorway", "bridge", "roundabout", "footpath", "track")) return "Road";
|
||||
if (ContainsAny(text, "car park", "parking")) return "CarPark";
|
||||
if (text.Contains("shop", StringComparison.OrdinalIgnoreCase)) return "Shop";
|
||||
if (text.Contains("hospital", StringComparison.OrdinalIgnoreCase)) return "Hospital";
|
||||
@ -272,7 +278,11 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
if (ContainsAny(text, "driving licence", "driver licence", "provisional licence", "license", "licence", "pass certificate", "certificate", "paperwork")) return "Document";
|
||||
if (ContainsAny(text, "ring", "necklace", "jewellery", "jewelry")) return "Jewellery";
|
||||
if (ContainsAny(text, "gun", "pistol", "rifle", "weapon")) return "Weapon";
|
||||
if (ContainsAny(padded, " ford fiesta ", " fiesta ", " motor car ", " car ", " vehicle ")) return "Car";
|
||||
if (ContainsAny(padded, " hatchback ", " vw golf ", " volkswagen golf ", " golf gti ", " ford fiesta ", " fiesta ")) return "Hatchback";
|
||||
if (ContainsAny(padded, " family car ", " estate car ", " saloon ")) return "FamilyCar";
|
||||
if (ContainsAny(padded, " sports car ", " tr6 ", " roadster ")) return "SportsCar";
|
||||
if (ContainsAny(padded, " van ", " transit ")) return "Van";
|
||||
if (ContainsAny(padded, " motor car ", " car ", " vehicle ")) return "Car";
|
||||
if (ContainsAny(text, "supercharger", "engine part")) return "CarPart";
|
||||
if (ContainsAny(text, "biscuit tin", "tin")) return "Tin";
|
||||
if (ContainsAny(text, "parcel", "package")) return "Parcel";
|
||||
@ -326,22 +336,40 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
if (ContainsAny(text, "13", "thirteen", "14", "fourteen")) return new(AgeBands.YoungTeen, 0.98m);
|
||||
if (ContainsAny(text, "16", "sixteen", "17", "seventeen", "18", "eighteen", "19", "nineteen")) return new(AgeBands.OlderTeen, 0.98m);
|
||||
if (ContainsAny(text, "toddler", "baby", "infant", "newborn")) return new(AgeBands.Child, 0.98m);
|
||||
if (ContainsAny(text, "child", "young girl", "young boy", "schoolboy", "schoolgirl", " boy ", " girl ", " lad ")) return new(AgeBands.Child, 0.86m);
|
||||
if (ContainsAny(text, "child", "young child", "schoolboy", "schoolgirl", "ten-year-old", "ten year old", "aged 10", "aged 11", "aged 12")) return new(AgeBands.Child, 0.9m);
|
||||
if (ContainsAny(text, "teenager", "teenage")) return new(AgeBands.OlderTeen, 0.86m);
|
||||
if (ContainsAny(text, "young adult", "young woman", "young man")) return new(AgeBands.YoungAdult, 0.82m);
|
||||
if (ContainsAny(text, "middle-aged", "middle aged", "mature adult")) return new(AgeBands.MatureAdult, 0.86m);
|
||||
if (ContainsAny(text, " mr ", " mr. ", " mrs ", " mrs. ", " miss ", " miss. ", " ms ", " ms. ", " doctor", " dr ", " dr. ", " professor", " driving examiner", " teacher", " detective", " police officer", " nurse", " receptionist", " solicitor", " landlord", " manager")) return new(AgeBands.Adult, 0.88m);
|
||||
if (ContainsAny(text, "elderly", "senior", "old woman", "old man")) return new(AgeBands.Senior, 0.9m);
|
||||
return EvidenceSignal.Unknown;
|
||||
}
|
||||
|
||||
private static EvidenceSignal EntityIdentityAgeBand(string name, string identityText)
|
||||
{
|
||||
var cleanName = (name ?? string.Empty).Trim().ToLowerInvariant();
|
||||
if (ContainsAny(cleanName, "mother", " mum", "father", " dad", "parent", "aunt", "uncle", "grandmother", "grandfather"))
|
||||
{
|
||||
return EvidenceSignal.Unknown;
|
||||
}
|
||||
|
||||
return ExplicitAgeBand(identityText);
|
||||
}
|
||||
|
||||
private static EvidenceSignal RelationshipAgeBand(string name, string text)
|
||||
{
|
||||
if (ContainsAny(text, "toddler", "baby", "newborn")) return new(AgeBands.Child, 0.96m);
|
||||
var identity = $"{name} {text}";
|
||||
if (ContainsAny(identity, "mother", " mum", "father", " dad", "parent", "aunt", "uncle", "teacher")
|
||||
if (ContainsAny(identity, "mother", " mum", "father", " dad", "parent", "aunt", "uncle")
|
||||
&& !PossessiveOfOtherIdentity(name, text))
|
||||
{
|
||||
return new(AgeBands.MatureAdult, 0.74m);
|
||||
return new(AgeBands.MatureAdult, 0.82m);
|
||||
}
|
||||
|
||||
if (ContainsAny(identity, "teacher", "driving examiner", "detective", "police officer", "nurse", "receptionist", "solicitor", "landlord", "manager", "professor", "doctor")
|
||||
&& !PossessiveOfOtherIdentity(name, text))
|
||||
{
|
||||
return new(AgeBands.Adult, 0.82m);
|
||||
}
|
||||
|
||||
if (ContainsAny(text, "grandmother", "grandfather")) return new(AgeBands.Senior, 0.82m);
|
||||
@ -351,8 +379,7 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
|
||||
private static EvidenceSignal NameAgeBand(string name)
|
||||
{
|
||||
var clean = name.Trim().ToLowerInvariant();
|
||||
return clean is "beth" ? new(AgeBands.YoungTeen, 0.86m) : EvidenceSignal.Unknown;
|
||||
return EvidenceSignal.Unknown;
|
||||
}
|
||||
|
||||
private static EvidenceSignal TitlePresentation(string text)
|
||||
@ -378,8 +405,8 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
|
||||
private static EvidenceSignal NamePresentation(string text)
|
||||
{
|
||||
if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " annie ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m);
|
||||
if (ContainsAny(text, " graham ", " george ", " john ", " david ", " simon ", " kevin ", " colin ")) return new(Presentations.Masculine, 0.82m);
|
||||
if (ContainsAny(text, " beth ", " maggie ", " rosie ", " grace ", " rebecca ", " annie ", " mary ", " susan ", " helen ", " elen ")) return new(Presentations.Feminine, 0.82m);
|
||||
if (ContainsAny(text, " adam ", " colin ", " david ", " kevin ", " simon ", " graham ", " gareth ", " john ", " michael ", " peter ", " george ")) return new(Presentations.Masculine, 0.82m);
|
||||
return EvidenceSignal.Unknown;
|
||||
}
|
||||
|
||||
@ -415,13 +442,13 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
}
|
||||
|
||||
if (StartsWithAny(clean, "mr", "father", "dad", "uncle", "brother", "son", "boy", "man", "male")
|
||||
|| clean is "graham" or "george" or "john" or "david" or "simon" or "kevin" or "colin")
|
||||
|| clean is "adam" or "colin" or "david" or "kevin" or "simon" or "graham" or "gareth" or "john" or "michael" or "peter" or "george")
|
||||
{
|
||||
return new(Presentations.Masculine, 0.98m);
|
||||
}
|
||||
|
||||
if (StartsWithAny(clean, "mrs", "miss", "ms", "mother", "mum", "mom", "aunt", "sister", "daughter", "girl", "woman", "female")
|
||||
|| clean is "beth" or "maggie" or "rosie" or "grace" or "annie" or "helen" or "elen")
|
||||
|| clean is "beth" or "maggie" or "grace" or "rosie" or "rebecca" or "annie" or "mary" or "susan" or "helen" or "elen")
|
||||
{
|
||||
return new(Presentations.Feminine, 0.98m);
|
||||
}
|
||||
|
||||
@ -443,6 +443,14 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
candidate.Metadata.Profile,
|
||||
isNamedCharacter: !IsUnresolvedName(observation.Name),
|
||||
alreadyAssignedToAnotherSignificantCharacter: alreadyAssigned && observation.IsSignificant));
|
||||
if (observation.Evidence.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown
|
||||
&& candidate.Metadata.AgeBand is StoryIntelligenceIllustrationCompatibility.AgeBands.Child
|
||||
or StoryIntelligenceIllustrationCompatibility.AgeBands.YoungTeen
|
||||
or StoryIntelligenceIllustrationCompatibility.AgeBands.OlderTeen)
|
||||
{
|
||||
rejected.Add("unknown age should use an adult-compatible illustration until explicit child or teen evidence appears");
|
||||
}
|
||||
|
||||
AddScore(observation.Evidence.AgeBand, candidate.Metadata.AgeBand, 25, "age band");
|
||||
AddScore(observation.Evidence.Presentation, candidate.Metadata.Presentation, observation.Evidence.HasStrongPresentationEvidence ? 42 : 25, "presentation");
|
||||
AddScore(observation.Evidence.HairColour, candidate.Metadata.HairColour, 20, "hair colour");
|
||||
|
||||
@ -130,6 +130,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
{
|
||||
Mode = "real",
|
||||
RunId = run.StoryIntelligenceRunID,
|
||||
BookId = run.BookID,
|
||||
RunStatus = run.Status,
|
||||
GeneratedUtc = DateTime.UtcNow,
|
||||
ChangeToken = ChangeToken(run, results),
|
||||
@ -208,11 +209,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
? "All known chapter runs completed successfully"
|
||||
: "Weighted chapter-run progress";
|
||||
var overallProgressPercent = authoritativeImportProgress;
|
||||
var overallElapsed = SessionElapsed(runSnapshots);
|
||||
var overallRemaining = isSuccessfullyComplete ? "Ready to review" : isTerminal ? "Ready to review" : EstimateSessionRemaining(runSnapshots, parsedScenes, totalScenes);
|
||||
var overallElapsedMs = SessionElapsedMilliseconds(runSnapshots, isTerminal);
|
||||
var overallElapsed = FormatDuration(overallElapsedMs);
|
||||
var overallRemaining = isSuccessfullyComplete ? "Ready to review" : isTerminal ? "Ready to review" : EstimateSessionRemaining(overallElapsedMs, parsedScenes, totalScenes);
|
||||
var overallStage = isSuccessfullyComplete ? "Analysis Complete" : isTerminal ? "Analysis stopped" : BackendStageDisplay(activeSnapshot.Run);
|
||||
var scenes = new List<StoryIntelligenceExperienceSceneState>();
|
||||
var nextSceneNumber = 1;
|
||||
var storyMemory = new StoryMemory();
|
||||
var allCompleted = runSnapshots.SelectMany(snapshot => snapshot.Completed).ToList();
|
||||
var visibleSceneIds = VisibleSceneResultIds(allCompleted, current, SceneWindowPrevious, SceneWindowFollowing);
|
||||
@ -223,7 +224,6 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
snapshot.Parsed,
|
||||
snapshot.Completed,
|
||||
current,
|
||||
continuousSceneNumberStart: nextSceneNumber,
|
||||
overallCompletedScenes: parsedScenes,
|
||||
overallTotalScenes: totalScenes,
|
||||
overallChaptersAnalysed: completedChapterRuns,
|
||||
@ -232,13 +232,11 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
overallStage: overallStage,
|
||||
overallElapsed: overallElapsed,
|
||||
overallRemaining: overallRemaining,
|
||||
continuousSceneNumbering: true,
|
||||
overallTerminal: isTerminal,
|
||||
storyMemory: storyMemory,
|
||||
includedSceneResultIds: visibleSceneIds)
|
||||
.ToList();
|
||||
scenes.AddRange(runScenes);
|
||||
nextSceneNumber += snapshot.Completed.Count;
|
||||
}
|
||||
|
||||
if (scenes.Count == 0)
|
||||
@ -251,6 +249,7 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
Mode = "real",
|
||||
ImportSessionId = session.StoryIntelligenceBookPipelineID,
|
||||
RunId = activeSnapshot.Run.StoryIntelligenceRunID,
|
||||
BookId = session.BookID,
|
||||
RunStatus = SessionStatus(session, runSnapshots),
|
||||
GeneratedUtc = DateTime.UtcNow,
|
||||
ChangeToken = SessionChangeToken(session, runSnapshots),
|
||||
@ -837,19 +836,18 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
};
|
||||
}
|
||||
|
||||
private static string EstimateSessionRemaining(IReadOnlyList<RunSnapshot> snapshots, int completedScenes, int totalScenes)
|
||||
private static string EstimateSessionRemaining(long elapsedMs, int completedScenes, int totalScenes)
|
||||
{
|
||||
if (totalScenes <= 0 || completedScenes <= 0)
|
||||
{
|
||||
return snapshots.All(snapshot => IsTerminal(snapshot.Run.Status)) ? "Ready to review" : "Calculating";
|
||||
return "Calculating";
|
||||
}
|
||||
|
||||
if (completedScenes >= totalScenes && snapshots.All(snapshot => IsTerminal(snapshot.Run.Status)))
|
||||
if (completedScenes >= totalScenes)
|
||||
{
|
||||
return "Ready to review";
|
||||
}
|
||||
|
||||
var elapsedMs = snapshots.Sum(snapshot => snapshot.Run.TotalDurationMs ?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - snapshot.Run.StartedUtc).TotalMilliseconds)));
|
||||
var averageMs = elapsedMs / Math.Max(1, completedScenes);
|
||||
return FormatDuration(averageMs * Math.Max(0, totalScenes - completedScenes));
|
||||
}
|
||||
@ -1798,11 +1796,21 @@ public sealed class StoryIntelligenceVisualisationSnapshotService(
|
||||
return FormatDuration(elapsedMs);
|
||||
}
|
||||
|
||||
private static string SessionElapsed(IReadOnlyList<RunSnapshot> snapshots)
|
||||
private static long SessionElapsedMilliseconds(IReadOnlyList<RunSnapshot> snapshots, bool isTerminal)
|
||||
{
|
||||
var elapsedMs = snapshots.Sum(snapshot => snapshot.Run.TotalDurationMs
|
||||
?? Math.Max(0, Convert.ToInt64((DateTime.UtcNow - snapshot.Run.StartedUtc).TotalMilliseconds)));
|
||||
return FormatDuration(elapsedMs);
|
||||
if (snapshots.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var started = snapshots.Min(snapshot => snapshot.Run.StartedUtc);
|
||||
var ended = isTerminal
|
||||
? snapshots
|
||||
.Select(snapshot => snapshot.Run.CompletedUtc ?? snapshot.Run.UpdatedUtc)
|
||||
.DefaultIfEmpty(DateTime.UtcNow)
|
||||
.Max()
|
||||
: DateTime.UtcNow;
|
||||
return Math.Max(0, Convert.ToInt64((ended - started).TotalMilliseconds));
|
||||
}
|
||||
|
||||
private static string FormatDuration(long milliseconds)
|
||||
|
||||
@ -5,6 +5,7 @@ public sealed class StoryIntelligenceExperiencePrototypeViewModel
|
||||
public string Mode { get; init; } = "simulation";
|
||||
public int? RunId { get; init; }
|
||||
public int? ImportSessionId { get; init; }
|
||||
public int? BookId { get; init; }
|
||||
public string? RunStatus { get; init; }
|
||||
public string? ChangeToken { get; init; }
|
||||
public DateTime GeneratedUtc { get; init; } = DateTime.UtcNow;
|
||||
|
||||
@ -24,6 +24,7 @@
|
||||
Mode = Model.Mode,
|
||||
RunId = Model.RunId,
|
||||
ImportSessionId = Model.ImportSessionId,
|
||||
BookId = Model.BookId,
|
||||
RunStatus = Model.RunStatus,
|
||||
GeneratedUtc = Model.GeneratedUtc,
|
||||
IsTerminal = false,
|
||||
@ -50,6 +51,7 @@
|
||||
data-image-diagnostics="true"
|
||||
data-mode="@Model.Mode"
|
||||
data-run-id="@Model.RunId"
|
||||
data-book-id="@Model.BookId"
|
||||
data-import-session-id="@Model.ImportSessionId"
|
||||
data-snapshot-url="@(Model.ImportSessionId.HasValue ? $"/api/story-intelligence/import-sessions/{Model.ImportSessionId.Value}/visualisation-snapshot" : Model.RunId.HasValue ? $"/api/story-intelligence/runs/{Model.RunId.Value}/visualisation-snapshot" : string.Empty)"
|
||||
data-change-token="@(Model.Mode == "real" ? string.Empty : Model.ChangeToken)"
|
||||
@ -66,7 +68,14 @@
|
||||
<span data-stage-label>Preparing</span>
|
||||
<span>Scene <strong data-current-scene>1</strong></span>
|
||||
</div>
|
||||
<a class="story-exp-background" href="/Development/StoryIntelligenceExperience?mode=simulation">Simulation</a>
|
||||
@if (Model.Mode == "real" && Model.BookId.HasValue)
|
||||
{
|
||||
<a class="story-exp-background" href="/onboarding/story-intelligence/book/@Model.BookId.Value/continue">Return to Import</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a class="story-exp-background" href="/Development/StoryIntelligenceExperience?mode=simulation">Simulation</a>
|
||||
}
|
||||
</header>
|
||||
|
||||
@if (Model.AvailableRuns.Count > 0)
|
||||
@ -181,20 +190,23 @@
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<aside class="story-exp-controls" aria-label="Prototype controls">
|
||||
<div>
|
||||
<span>Prototype controls</span>
|
||||
<small>@sourceLabel</small>
|
||||
</div>
|
||||
<button type="button" data-control="restart">Restart</button>
|
||||
<button type="button" data-control="previous">Previous</button>
|
||||
<button type="button" data-control="play">Pause</button>
|
||||
<button type="button" data-control="next">Next</button>
|
||||
<label>
|
||||
Speed
|
||||
<input type="range" min="3500" max="10000" step="500" value="6200" data-speed-control />
|
||||
</label>
|
||||
</aside>
|
||||
@if (Model.Mode == "simulation")
|
||||
{
|
||||
<aside class="story-exp-controls" aria-label="Prototype controls">
|
||||
<div>
|
||||
<span>Prototype controls</span>
|
||||
<small>@sourceLabel</small>
|
||||
</div>
|
||||
<button type="button" data-control="restart">Restart</button>
|
||||
<button type="button" data-control="previous">Previous</button>
|
||||
<button type="button" data-control="play">Pause</button>
|
||||
<button type="button" data-control="next">Next</button>
|
||||
<label>
|
||||
Speed
|
||||
<input type="range" min="3500" max="10000" step="500" value="6200" data-speed-control />
|
||||
</label>
|
||||
</aside>
|
||||
}
|
||||
|
||||
<details class="story-exp-diagnostics" data-diagnostics>
|
||||
<summary>Diagnostics</summary>
|
||||
|
||||
@ -288,10 +288,10 @@ input:focus-visible {
|
||||
|
||||
.story-exp-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(206px, 0.54fr) minmax(760px, 2.44fr) minmax(214px, 0.56fr);
|
||||
gap: 16px;
|
||||
grid-template-columns: minmax(180px, 0.46fr) minmax(820px, 2.72fr) minmax(188px, 0.48fr);
|
||||
gap: 12px;
|
||||
min-height: 0;
|
||||
padding: 14px 22px 74px;
|
||||
padding: 12px 18px 58px;
|
||||
}
|
||||
|
||||
.story-exp-left,
|
||||
@ -565,7 +565,7 @@ input:focus-visible {
|
||||
.story-exp-connections {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
z-index: 60;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
@ -641,7 +641,7 @@ input:focus-visible {
|
||||
.story-exp-zone {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
z-index: 20;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
pointer-events: none;
|
||||
@ -683,13 +683,13 @@ input:focus-visible {
|
||||
height: var(--node-size, 150px);
|
||||
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
||||
opacity: var(--opacity, 1);
|
||||
z-index: var(--z, 3);
|
||||
z-index: calc(var(--z, 3) + 30);
|
||||
pointer-events: auto;
|
||||
transition: transform 1050ms cubic-bezier(.16, .84, .28, 1), opacity 780ms ease;
|
||||
}
|
||||
|
||||
.story-character.is-pov {
|
||||
z-index: 7;
|
||||
z-index: 48;
|
||||
}
|
||||
|
||||
.story-character.is-context .story-character__image {
|
||||
@ -699,6 +699,8 @@ input:focus-visible {
|
||||
.story-character__image,
|
||||
.story-location__image,
|
||||
.story-asset__image {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(169, 205, 240, 0.36);
|
||||
@ -727,6 +729,7 @@ input:focus-visible {
|
||||
|
||||
.story-character__text {
|
||||
position: absolute;
|
||||
z-index: 80;
|
||||
top: calc(var(--node-size, 150px) + 9px);
|
||||
left: 50%;
|
||||
display: grid;
|
||||
@ -817,7 +820,7 @@ input:focus-visible {
|
||||
gap: 10px;
|
||||
width: var(--node-size, 190px);
|
||||
transform: translate(var(--x, 0), var(--y, 0));
|
||||
z-index: 4;
|
||||
z-index: 42;
|
||||
pointer-events: auto;
|
||||
transition: opacity 560ms ease, transform 850ms cubic-bezier(.16, .84, .28, 1);
|
||||
}
|
||||
@ -841,6 +844,8 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-location__text {
|
||||
position: relative;
|
||||
z-index: 80;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 3px;
|
||||
@ -887,7 +892,7 @@ input:focus-visible {
|
||||
width: max(var(--node-size, 78px), 104px);
|
||||
opacity: var(--opacity, 1);
|
||||
transform: translate(var(--x, 0), var(--y, 0)) scale(var(--scale, 1));
|
||||
z-index: var(--z, 3);
|
||||
z-index: calc(var(--z, 3) + 30);
|
||||
pointer-events: auto;
|
||||
transition: transform 860ms cubic-bezier(.16, .84, .28, 1), opacity 620ms ease;
|
||||
}
|
||||
@ -901,6 +906,8 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.story-asset__text {
|
||||
position: relative;
|
||||
z-index: 80;
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 2px;
|
||||
|
||||
@ -1920,6 +1920,21 @@
|
||||
let scene = null;
|
||||
let totalWordCount = 0;
|
||||
let detectedHeadingChapters = 0;
|
||||
let manuscriptStarted = false;
|
||||
|
||||
const isFrontMatterHeading = (value) => {
|
||||
const clean = normalizeTitleForResolve(value || "", "").toLocaleLowerCase();
|
||||
return [
|
||||
"title page",
|
||||
"copyright",
|
||||
"dedication",
|
||||
"contents",
|
||||
"table of contents",
|
||||
"acknowledgements",
|
||||
"acknowledgments",
|
||||
"about the author"
|
||||
].includes(clean);
|
||||
};
|
||||
|
||||
const finishScene = () => {
|
||||
scene = null;
|
||||
@ -1982,10 +1997,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
documentText.push(text);
|
||||
const words = countWords(text);
|
||||
|
||||
if (isBuiltInHeading(paragraph, 1)) {
|
||||
if (!manuscriptStarted && isFrontMatterHeading(text)) {
|
||||
return;
|
||||
}
|
||||
|
||||
manuscriptStarted = true;
|
||||
documentText.push(text);
|
||||
detectedHeadingChapters += 1;
|
||||
startChapter(paragraph, text);
|
||||
totalWordCount += words;
|
||||
@ -1995,9 +2015,15 @@
|
||||
}
|
||||
|
||||
if (!chapter) {
|
||||
if (!manuscriptStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
startChapter(paragraph, "Opening pages", true);
|
||||
}
|
||||
|
||||
documentText.push(text);
|
||||
|
||||
if (isBuiltInHeading(paragraph, 2)) {
|
||||
startScene(paragraph, text);
|
||||
totalWordCount += words;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user