165 lines
6.3 KiB
C#
165 lines
6.3 KiB
C#
using System.Text.Json;
|
|
using PlotLine.Models;
|
|
using PlotLine.Models.StoryIntelligence;
|
|
|
|
namespace PlotLine.Services;
|
|
|
|
public static class ChapterStructureBoundaryNormaliser
|
|
{
|
|
public static ChapterStructureBoundaryNormalisationResult Normalise(ChapterStructureModel? chapterStructure, int paragraphCount)
|
|
{
|
|
if (chapterStructure?.SceneBoundaries is null || chapterStructure.SceneBoundaries.Count < 2 || paragraphCount <= 0)
|
|
{
|
|
return new ChapterStructureBoundaryNormalisationResult(chapterStructure, []);
|
|
}
|
|
|
|
var boundaries = chapterStructure.SceneBoundaries;
|
|
var finalIndex = boundaries.Count - 1;
|
|
var final = boundaries[finalIndex];
|
|
var previous = boundaries[finalIndex - 1];
|
|
var phantomParagraph = paragraphCount + 1;
|
|
|
|
if (IsTrailingEmptyBoundary(final, previous, paragraphCount, phantomParagraph)
|
|
|| IsTrailingInvalidPlaceholder(final, previous, paragraphCount, phantomParagraph))
|
|
{
|
|
var normalisedBoundaries = boundaries
|
|
.Take(finalIndex)
|
|
.Select((boundary, index) => CopyBoundary(boundary, index + 1))
|
|
.ToList();
|
|
|
|
var normalised = new ChapterStructureModel
|
|
{
|
|
SchemaVersion = chapterStructure.SchemaVersion,
|
|
ChapterSummary = chapterStructure.ChapterSummary,
|
|
SceneBoundaries = normalisedBoundaries,
|
|
ExtensionData = chapterStructure.ExtensionData
|
|
};
|
|
|
|
var issue = new ValidationIssue
|
|
{
|
|
Severity = "Warning",
|
|
Path = $"sceneBoundaries[{finalIndex}]",
|
|
Message = BuildNormalisationMessage(final, finalIndex, paragraphCount, phantomParagraph),
|
|
SuggestedFix = "Do not return a final scene boundary unless it references an actual supplied paragraph."
|
|
};
|
|
|
|
return new ChapterStructureBoundaryNormalisationResult(normalised, [issue]);
|
|
}
|
|
|
|
return new ChapterStructureBoundaryNormalisationResult(chapterStructure, []);
|
|
}
|
|
|
|
public static void AddIssuesTo(ValidationResult validation, ChapterStructureBoundaryNormalisationResult normalisation)
|
|
{
|
|
foreach (var issue in normalisation.Issues)
|
|
{
|
|
validation.Warnings.Add(issue);
|
|
}
|
|
}
|
|
|
|
public static string SerialiseParsedModel(ChapterStructureModel? chapterStructure, ChapterStructureBoundaryNormalisationResult normalisation, JsonSerializerOptions jsonOptions)
|
|
{
|
|
if (chapterStructure is null)
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
if (normalisation.Issues.Count == 0)
|
|
{
|
|
return JsonSerializer.Serialize(chapterStructure, jsonOptions);
|
|
}
|
|
|
|
return JsonSerializer.Serialize(
|
|
new
|
|
{
|
|
chapterStructure.SchemaVersion,
|
|
chapterStructure.ChapterSummary,
|
|
chapterStructure.SceneBoundaries,
|
|
Normalisation = normalisation.Issues.Select(issue => new
|
|
{
|
|
issue.Severity,
|
|
issue.Path,
|
|
issue.Message,
|
|
issue.SuggestedFix
|
|
})
|
|
},
|
|
jsonOptions);
|
|
}
|
|
|
|
private static ChapterSceneBoundary CopyBoundary(ChapterSceneBoundary boundary, int sceneNumber)
|
|
=> new()
|
|
{
|
|
SceneNumber = sceneNumber,
|
|
StartParagraph = boundary.StartParagraph,
|
|
EndParagraph = boundary.EndParagraph,
|
|
Confidence = boundary.Confidence,
|
|
Reason = boundary.Reason,
|
|
ExtensionData = boundary.ExtensionData
|
|
};
|
|
|
|
private static bool IsTrailingEmptyBoundary(
|
|
ChapterSceneBoundary final,
|
|
ChapterSceneBoundary previous,
|
|
int paragraphCount,
|
|
int phantomParagraph)
|
|
=> final.StartParagraph == phantomParagraph
|
|
&& final.EndParagraph == phantomParagraph
|
|
&& previous.EndParagraph == paragraphCount
|
|
&& final.StartParagraph > paragraphCount
|
|
&& final.EndParagraph > paragraphCount;
|
|
|
|
private static bool IsTrailingInvalidPlaceholder(
|
|
ChapterSceneBoundary final,
|
|
ChapterSceneBoundary previous,
|
|
int paragraphCount,
|
|
int phantomParagraph)
|
|
{
|
|
if (!final.StartParagraph.HasValue || !final.EndParagraph.HasValue || previous.EndParagraph != paragraphCount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var invalidTrailingRange = final.StartParagraph.Value > paragraphCount
|
|
&& (final.EndParagraph.Value <= paragraphCount
|
|
|| final.EndParagraph.Value == phantomParagraph
|
|
|| final.EndParagraph.Value < final.StartParagraph.Value);
|
|
|
|
return invalidTrailingRange && HasPlaceholderSignal(final);
|
|
}
|
|
|
|
private static bool HasPlaceholderSignal(ChapterSceneBoundary boundary)
|
|
{
|
|
if (boundary.Confidence <= 0.05m)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var reason = boundary.Reason ?? string.Empty;
|
|
return reason.Contains("invalid", StringComparison.OrdinalIgnoreCase)
|
|
|| reason.Contains("placeholder", StringComparison.OrdinalIgnoreCase)
|
|
|| reason.Contains("ensure coverage", StringComparison.OrdinalIgnoreCase)
|
|
|| reason.Contains("coverage", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static string BuildNormalisationMessage(
|
|
ChapterSceneBoundary final,
|
|
int finalIndex,
|
|
int paragraphCount,
|
|
int phantomParagraph)
|
|
{
|
|
var start = final.StartParagraph ?? 0;
|
|
var end = final.EndParagraph ?? 0;
|
|
if (start == phantomParagraph && end == phantomParagraph)
|
|
{
|
|
return $"Discarded trailing empty scene boundary {phantomParagraph}-{phantomParagraph} because submitted chapter has {paragraphCount} paragraphs and previous scene already ended at {paragraphCount}.";
|
|
}
|
|
|
|
var sceneNumber = final.SceneNumber ?? finalIndex + 1;
|
|
return $"Discarded trailing invalid placeholder scene {sceneNumber} with range {start}-{end} because previous scene already ended at paragraph {paragraphCount}.";
|
|
}
|
|
}
|
|
|
|
public sealed record ChapterStructureBoundaryNormalisationResult(
|
|
ChapterStructureModel? ChapterStructure,
|
|
IReadOnlyList<ValidationIssue> Issues);
|