Hide discarded scan character candidates

This commit is contained in:
Nick Beckley 2026-08-17 19:47:56 +00:00
parent 09f003b0cd
commit 1ead298fa8
3 changed files with 30 additions and 6 deletions

View File

@ -67,6 +67,7 @@ var tests = new (string Name, Action Test)[]
("Character sex hints are conservative and reviewable", CharacterSexHintsAreConservativeAndReviewable),
("Character discovery does not truncate scan review candidates", CharacterDiscoveryDoesNotTruncateScanReviewCandidates),
("Character discovery rejects sentence-start words", CharacterDiscoveryRejectsSentenceStartWords),
("Manuscript scan preview keeps only auto-selected character candidates", ManuscriptScanPreviewKeepsOnlyAutoSelectedCharacterCandidates),
("Scan review aliases feed Story Intelligence known character context", ScanReviewAliasesFeedStoryIntelligenceKnownCharacterContext),
("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
@ -1315,6 +1316,23 @@ static void CharacterDiscoveryRejectsSentenceStartWords()
Assert(!actionCandidates.Any(candidate => candidate.Text is "Door" or "Glass" or "Window"), "Repeated sentence-start action nouns must not become character candidates.");
}
static void ManuscriptScanPreviewKeepsOnlyAutoSelectedCharacterCandidates()
{
var preview = new ManuscriptScanPreview
{
CharacterCandidates =
[
new() { TemporaryCharacterKey = "c-colin", Name = "Colin", MentionCount = 4, QualityScore = 7, Category = "ProbableCharacter" },
new() { TemporaryCharacterKey = "c-door", Name = "Door", MentionCount = 4, QualityScore = 2, Category = "PossibleCharacter" },
new() { TemporaryCharacterKey = "c-road", Name = "Road", MentionCount = 8, QualityScore = 1, Category = "Excluded" }
]
};
var candidates = WordCompanionService.DiscoverManuscriptScanCharacterCandidates(preview);
Assert(candidates.Count == 1, $"Scan preview should keep only auto-selected character candidates; got {candidates.Count}.");
Assert(candidates[0].Name == "Colin", "Scan preview should keep probable character candidates.");
}
static void StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel()
{
var view = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Views/Development/StoryIntelligenceExperience.cshtml"));

View File

@ -658,6 +658,7 @@ public sealed class WordCompanionService(
}
return DiscoverCharacterCandidates(manuscriptText)
.Where(IsAutoSelectedScanCharacterCandidate)
.Select((candidate, index) => new ManuscriptScanCharacterCandidatePreview
{
TemporaryCharacterKey = $"c-{StableCandidateKey(candidate.Text, index)}",
@ -995,7 +996,7 @@ public sealed class WordCompanionService(
IReadOnlyList<ManuscriptScanCharacterCandidatePreview> candidates)
=> candidates
.Where(candidate => !string.IsNullOrWhiteSpace(candidate.Name))
.Where(candidate => !string.Equals(candidate.Category, "Excluded", StringComparison.OrdinalIgnoreCase))
.Where(IsAutoSelectedScanCharacterCandidate)
.Where(candidate =>
candidate.Name.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length > 1
|| candidate.QualityScore >= 4
@ -1008,6 +1009,12 @@ public sealed class WordCompanionService(
.ThenBy(candidate => candidate.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
private static bool IsAutoSelectedScanCharacterCandidate(WordCompanionCharacterCandidateDto candidate)
=> string.Equals(candidate.Category, "ProbableCharacter", StringComparison.OrdinalIgnoreCase);
private static bool IsAutoSelectedScanCharacterCandidate(ManuscriptScanCharacterCandidatePreview candidate)
=> string.Equals(candidate.Category, "ProbableCharacter", StringComparison.OrdinalIgnoreCase);
private static string StableCandidateKey(string name, int index)
{
var slug = Regex.Replace(name.ToLowerInvariant(), @"[^a-z0-9]+", "-").Trim('-');

View File

@ -2349,12 +2349,11 @@
const discovery = await postJson("/api/word-companion/manuscript/discover-characters", {
projectId: preview.projectID,
documentText: documentTextForDiscovery,
includeExcluded: true
includeExcluded: false
});
preview.characterCandidates = mapOnboardingCharacterCandidates(discovery?.candidates);
preview.characterCandidateCount = preview.characterCandidates
.filter((candidate) => String(candidate.category || "").toLowerCase() !== "excluded")
.length;
preview.characterCandidates = mapOnboardingCharacterCandidates(discovery?.candidates)
.filter((candidate) => String(candidate.category || "").toLowerCase() === "probablecharacter");
preview.characterCandidateCount = preview.characterCandidates.length;
} catch (discoveryError) {
console.warn("Unable to refine onboarding character candidates.", discoveryError);
preview.characterCandidates = [];