Add name intelligence and generation safeguards
This commit is contained in:
parent
77369a86fd
commit
25a8ce65fa
@ -31,9 +31,15 @@ var tests = new (string Name, Action Test)[]
|
||||
("Knowledge signals map to existing knowledge states", KnowledgeSignalsMapToExistingStates),
|
||||
("Knowledge duplicate statements share canonical keys", KnowledgeDuplicateStatementsShareCanonicalKeys),
|
||||
("Illustration prompt builder separates spec and prompt", IllustrationPromptBuilderSeparatesSpecAndPrompt),
|
||||
("Illustration prompt builder uses realistic image direction", IllustrationPromptBuilderUsesRealisticImageDirection),
|
||||
("Illustration specification validation catches missing typed fields", IllustrationSpecificationValidationCatchesMissingTypedFields),
|
||||
("Illustration category validation rejects unknown values", IllustrationCategoryValidationRejectsUnknownValues),
|
||||
("Illustration status transitions protect approval flow", IllustrationStatusTransitionsProtectApprovalFlow),
|
||||
("Illustration status transitions support archive flow", IllustrationStatusTransitionsSupportArchiveFlow),
|
||||
("Illustration metadata flags fallback location labels", IllustrationMetadataFlagsFallbackLocationLabels),
|
||||
("Illustration location matching rejects vehicle living room reuse", IllustrationLocationMatchingRejectsVehicleLivingRoomReuse),
|
||||
("Illustration automatic generation policy blocks vague fallbacks", IllustrationAutomaticGenerationPolicyBlocksVagueFallbacks),
|
||||
("Story Memory import image generation defaults are safe", StoryMemoryImportImageGenerationDefaultsAreSafe),
|
||||
("Illustration storage codes are normalised safely", IllustrationStorageCodesAreNormalisedSafely),
|
||||
("Illustration starter batch has required category counts", IllustrationStarterBatchHasRequiredCategoryCounts),
|
||||
("Illustration starter generation plan skips successful items", IllustrationStarterGenerationPlanSkipsSuccessfulItems),
|
||||
@ -71,6 +77,10 @@ var tests = new (string Name, Action Test)[]
|
||||
("Manuscript scan preview keeps only auto-selected character candidates", ManuscriptScanPreviewKeepsOnlyAutoSelectedCharacterCandidates),
|
||||
("Onboarding wizard keeps character pages inside one main step", OnboardingWizardKeepsCharacterPagesInsideOneMainStep),
|
||||
("Scan review aliases feed Story Intelligence known character context", ScanReviewAliasesFeedStoryIntelligenceKnownCharacterContext),
|
||||
("Name Intelligence package contract is present", NameIntelligencePackageContractIsPresent),
|
||||
("Name Intelligence reference data is copied on publish", NameIntelligenceReferenceDataIsCopiedOnPublish),
|
||||
("Name Intelligence repository uses stored procedures", NameIntelligenceRepositoryUsesStoredProcedures),
|
||||
("Name Intelligence review warning is wired", NameIntelligenceReviewWarningIsWired),
|
||||
("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),
|
||||
@ -789,12 +799,12 @@ static void IllustrationPromptBuilderSeparatesSpecAndPrompt()
|
||||
var result = builder.Build(spec);
|
||||
|
||||
Assert(result.TemplateVersion == IllustrationPromptBuilder.CurrentTemplateVersion, "Template version was not recorded.");
|
||||
Assert(result.TemplateVersion == "21J.3", "Illustration prompt template version should reflect the current approved art direction.");
|
||||
Assert(result.TemplateVersion == "22C.1", "Illustration prompt template version should reflect the current realistic art direction.");
|
||||
Assert(result.Prompt.Contains("no visible text", StringComparison.OrdinalIgnoreCase), "Prompt did not include visual safety guidance.");
|
||||
Assert(result.Prompt.Contains("Category composition rules", StringComparison.Ordinal), "Prompt did not include category composition rules.");
|
||||
Assert(result.Prompt.Contains("Structured details", StringComparison.Ordinal), "Prompt did not include structured details.");
|
||||
Assert(result.Prompt.Contains("modern cinematic digital illustration", StringComparison.OrdinalIgnoreCase), "Prompt did not include the new cinematic digital art direction.");
|
||||
Assert(result.Prompt.Contains("premium narrative game concept art", StringComparison.OrdinalIgnoreCase), "Prompt did not include premium game concept-art guidance.");
|
||||
Assert(result.Prompt.Contains("realistic cinematic still", StringComparison.OrdinalIgnoreCase), "Prompt did not include the realistic cinematic direction.");
|
||||
Assert(result.Prompt.Contains("production photography", StringComparison.OrdinalIgnoreCase), "Prompt did not include the new grounded realism guidance.");
|
||||
Assert(result.Prompt.Contains("balanced cinematic lighting", StringComparison.OrdinalIgnoreCase), "Prompt did not include current lighting guidance.");
|
||||
Assert(result.Prompt.Contains("avoid muddy olive and brown grading", StringComparison.OrdinalIgnoreCase), "Prompt did not explicitly avoid the previous muddy grading failure mode.");
|
||||
Assert(result.Prompt.Contains(spec.Code, StringComparison.Ordinal), "Prompt did not include stable code.");
|
||||
@ -802,6 +812,16 @@ static void IllustrationPromptBuilderSeparatesSpecAndPrompt()
|
||||
Assert(result.MetadataJson.Contains("generic-no-manuscript-data", StringComparison.Ordinal), "Metadata JSON did not preserve privacy metadata.");
|
||||
}
|
||||
|
||||
static void IllustrationPromptBuilderUsesRealisticImageDirection()
|
||||
{
|
||||
var spec = IllustrationStarterBatchDefinition.All.Single(x => x.Code == "char-young-adult-red-haired-witness");
|
||||
var result = new IllustrationPromptBuilder().Build(spec);
|
||||
|
||||
Assert(result.Prompt.Contains("realistic", StringComparison.OrdinalIgnoreCase), "Generated image prompts should move toward realism.");
|
||||
Assert(result.Prompt.Contains("no anime, cartoon", StringComparison.OrdinalIgnoreCase), "Prompt should reject stylised/cartoon output.");
|
||||
Assert(result.Prompt.Contains("oil painting", StringComparison.OrdinalIgnoreCase), "Prompt should reject painterly output.");
|
||||
}
|
||||
|
||||
static void IllustrationSpecificationValidationCatchesMissingTypedFields()
|
||||
{
|
||||
var invalid = new IllustrationGenerationSpecification
|
||||
@ -835,6 +855,58 @@ static void IllustrationStatusTransitionsProtectApprovalFlow()
|
||||
Assert(!IllustrationLibraryStatuses.CanTransition(IllustrationLibraryStatuses.Queued, IllustrationLibraryStatuses.Approved), "Queued should not approve directly.");
|
||||
}
|
||||
|
||||
static void IllustrationStatusTransitionsSupportArchiveFlow()
|
||||
{
|
||||
Assert(IllustrationLibraryStatuses.IsValid(IllustrationLibraryStatuses.Archived), "Archived should be a valid library state.");
|
||||
Assert(IllustrationLibraryStatuses.CanTransition(IllustrationLibraryStatuses.Generated, IllustrationLibraryStatuses.Archived), "Generated items should archive.");
|
||||
Assert(IllustrationLibraryStatuses.CanTransition(IllustrationLibraryStatuses.Approved, IllustrationLibraryStatuses.Archived), "Approved items should archive.");
|
||||
}
|
||||
|
||||
static void IllustrationMetadataFlagsFallbackLocationLabels()
|
||||
{
|
||||
var item = new IllustrationLibraryItem
|
||||
{
|
||||
Category = IllustrationLibraryCategories.Location,
|
||||
StableCode = "demand-location-unknowninterior",
|
||||
DisplayTitle = "Unknown Interior location",
|
||||
Status = IllustrationLibraryStatuses.Generated,
|
||||
IsActive = true,
|
||||
MetadataJson = """{"baseType":"UnknownInterior","reuseScope":"Global","isFallbackClassification":"true"}"""
|
||||
};
|
||||
|
||||
var metadata = StoryIntelligenceIllustrationCompatibility.LocationMetadataFrom(item);
|
||||
Assert(metadata.IsFallbackClassification, "Unknown Interior must be marked as fallback classification.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateLocation(metadata.BaseType), "Unknown Interior must not auto-generate.");
|
||||
}
|
||||
|
||||
static void IllustrationLocationMatchingRejectsVehicleLivingRoomReuse()
|
||||
{
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("driver seat inside the car") == "DriverSeat", "Driver seat context should remain vehicle-specific.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.LocationType("living room party") == "LivingRoom", "Living room context should remain domestic.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("DriverSeat", "LivingRoom"), "Vehicle interiors must not match living rooms.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.LocationCompatible("VehicleInterior", "LivingRoom"), "Car interiors must not match living rooms.");
|
||||
}
|
||||
|
||||
static void IllustrationAutomaticGenerationPolicyBlocksVagueFallbacks()
|
||||
{
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateLocation("UnknownInterior"), "Unknown Interior should be review-only.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateLocation("FlatInterior"), "Generic flat interior should be review-only.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateLocation("Garage"), "Specific location types may be generated when caps/config allow.");
|
||||
Assert(!StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateAsset("UnknownObject"), "Unknown objects should be review-only.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateAsset("DrivingLicence"), "Specific assets may be generated when caps/config allow.");
|
||||
}
|
||||
|
||||
static void StoryMemoryImportImageGenerationDefaultsAreSafe()
|
||||
{
|
||||
var defaults = new StoryIntelligenceOptions();
|
||||
Assert(!defaults.AutomaticImageGenerationDuringImportEnabled, "Automatic image generation during import should default off.");
|
||||
Assert(defaults.RequireImageGenerationApproval, "Image generation should require approval by default.");
|
||||
Assert(defaults.MaxImagesPerImport > 0 && defaults.MaxImagesPerImport <= 12, "Default import image cap should be low.");
|
||||
Assert(defaults.ImageGenerationModel == "gpt-image-1.5", "Default image model should use the requested newer model.");
|
||||
Assert(defaults.ImageGenerationQuality == "medium", "Default image quality should be medium.");
|
||||
Assert(defaults.ImageGenerationOutputFormat == "webp", "Default output format should be WebP.");
|
||||
}
|
||||
|
||||
static void IllustrationStorageCodesAreNormalisedSafely()
|
||||
{
|
||||
Assert(IllustrationLibraryStorageService.SafeSegment("Asset Letter_01.png") == "asset-letter-01-png", "Storage code was not normalised.");
|
||||
@ -1001,7 +1073,7 @@ static void IllustrationDemandArchetypesStayBroadAndUseActiveTemplate()
|
||||
Assert(key == "character|youngteen|feminine|red|light", $"Demand key was too broad or too specific: {key}");
|
||||
Assert(!key.Contains("beth", StringComparison.OrdinalIgnoreCase), "Demand key must not include character name.");
|
||||
Assert(!key.Contains("eye", StringComparison.OrdinalIgnoreCase), "Demand key must not include eye colour.");
|
||||
Assert(IllustrationPromptBuilder.CurrentTemplateVersion == "21J.3", $"Demand generation should use the active shared template, got {IllustrationPromptBuilder.CurrentTemplateVersion}.");
|
||||
Assert(IllustrationPromptBuilder.CurrentTemplateVersion == "22C.1", $"Demand generation should use the active shared template, got {IllustrationPromptBuilder.CurrentTemplateVersion}.");
|
||||
}
|
||||
|
||||
static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes()
|
||||
@ -1203,7 +1275,7 @@ static void IllustrationProviderReportsMissingImageModel()
|
||||
{
|
||||
var provider = new OpenAIIllustrationImageProvider(
|
||||
new HttpClient(),
|
||||
Options.Create(new StoryIntelligenceOptions { ApiKey = "configured-for-test" }),
|
||||
Options.Create(new StoryIntelligenceOptions { ApiKey = "configured-for-test", ImageGenerationModel = "" }),
|
||||
NullLogger<OpenAIIllustrationImageProvider>.Instance);
|
||||
|
||||
Assert(!provider.IsConfigured, "Provider should not be configured without an image model.");
|
||||
@ -1697,14 +1769,14 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
||||
Assert(storyMemory.Contains("SkinToneInstruction(skinTone)", StringComparison.Ordinal), "Demand generation must use hard skin-tone wording.");
|
||||
Assert(storyMemory.Contains("HairColours.Red", StringComparison.Ordinal) && storyMemory.Contains("SkinTones.Light", StringComparison.Ordinal), "Red-hair demand evidence must force light skin when skin is otherwise unknown.");
|
||||
Assert(storyMemory.Contains("ApplyPrimaryAgeContext", StringComparison.Ordinal), "Primary age-group context must guide generic adult/unknown character ages.");
|
||||
Assert(persistedRunner.Contains("QueueDemand = true", StringComparison.Ordinal), "Live imports must queue missing illustration demand, not only record it.");
|
||||
Assert(persistedRunner.Contains("MaxDemandToQueue = 24", StringComparison.Ordinal), "Live imports must cap automatic demand generation.");
|
||||
Assert(persistedRunner.Contains("AutomaticImageGenerationDuringImportEnabled", StringComparison.Ordinal), "Live imports must gate automatic image generation behind configuration.");
|
||||
Assert(persistedRunner.Contains("RequireImageGenerationApproval", StringComparison.Ordinal), "Live imports must not queue generation when approval is required.");
|
||||
Assert(persistedRunner.Contains("MaxImagesPerImport", StringComparison.Ordinal), "Live imports must cap automatic demand generation.");
|
||||
Assert(storyMemory.Contains("UpsertPlannedAsync(specification", StringComparison.Ordinal), "Durable Story Memory demand must create reusable illustration library items.");
|
||||
Assert(storyMemory.Contains("QueueAsync(planned.IllustrationLibraryItemID", StringComparison.Ordinal), "Durable Story Memory demand must queue generated library items for the worker.");
|
||||
Assert(storyMemory.Contains("demand.IllustrationLibraryItemId", StringComparison.Ordinal), "Demand fallback assignments must retain queued item IDs so generated images can attach later.");
|
||||
Assert(storyMemory.Contains("var categoryCap = maxDemandToQueue;", StringComparison.Ordinal), "Automatic demand generation should cap in-flight work per category, not starve locations and assets behind a divided cap.");
|
||||
Assert(storyMemory.Contains("or IllustrationLibraryStatuses.Generating);", StringComparison.Ordinal), "Demand generation cap must count only in-flight jobs, not already generated preserved images.");
|
||||
Assert(!storyMemory.Contains("or IllustrationLibraryStatuses.Generated\r\n or IllustrationLibraryStatuses.Approved", StringComparison.Ordinal), "Generated or approved preserved images must not permanently fill the demand queue cap.");
|
||||
Assert(storyMemory.Contains("CategoryCap(options, specification.Category)", StringComparison.Ordinal), "Automatic demand generation should use category-specific caps.");
|
||||
Assert(storyMemory.Contains("QueuedIllustrationLibraryItemID.HasValue || item.GeneratedIllustrationLibraryItemID.HasValue", StringComparison.Ordinal), "Demand generation cap must count already queued or generated automatic demand for the import.");
|
||||
Assert(storyMemory.Contains("\"Flyover\"", StringComparison.Ordinal) && storyMemory.Contains("concrete overpass", StringComparison.Ordinal), "Location prompts must distinguish road-under-flyover scenes from generic roads.");
|
||||
Assert(storyMemory.Contains("semanticType is \"Road\" or \"Lane\" or \"Street\"", StringComparison.Ordinal), "Road-family locations must be treated as exterior scenes.");
|
||||
Assert(matchingService.Contains("type is \"Road\" or \"Lane\" or \"Street\"", StringComparison.Ordinal), "Live location demand generation must treat lane/street/road as exterior transport scenes.");
|
||||
@ -1851,6 +1923,65 @@ static void ScanReviewAliasesFeedStoryIntelligenceKnownCharacterContext()
|
||||
Assert(!context.Any(item => item.CanonicalName == "Road"), "Excluded scan candidates should not guide Story Intelligence.");
|
||||
}
|
||||
|
||||
static void NameIntelligencePackageContractIsPresent()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/Data/Reference/Names/v1.0.0");
|
||||
var manifestPath = Path.Combine(root, "manifest.json");
|
||||
Assert(File.Exists(manifestPath), "Name Intelligence manifest is missing.");
|
||||
foreach (var required in new[] { "README.md", "DATA_CONTRACT.md", "METHODOLOGY.md", "VALIDATION.md", "validation_report.json", "name_locales.csv", "given_names.csv", "name_relationships.csv", "name_usages.csv" })
|
||||
{
|
||||
Assert(File.Exists(Path.Combine(root, required)), $"Name Intelligence package file is missing: {required}.");
|
||||
}
|
||||
|
||||
using var manifest = JsonDocument.Parse(File.ReadAllText(manifestPath));
|
||||
var document = manifest.RootElement;
|
||||
Assert(document.GetProperty("packageId").GetString() == NameIntelligenceReferencePackage.PackageId, "Unexpected Name Intelligence package id.");
|
||||
Assert(document.GetProperty("version").GetString() == NameIntelligenceReferencePackage.Version, "Unexpected Name Intelligence package version.");
|
||||
Assert(CountCsvRows(Path.Combine(root, "name_locales.csv")) == document.GetProperty("files").GetProperty("name_locales").GetProperty("rows").GetInt32(), "Locale row count does not match manifest.");
|
||||
Assert(CountCsvRows(Path.Combine(root, "given_names.csv")) == document.GetProperty("files").GetProperty("given_names").GetProperty("rows").GetInt32(), "Given-name row count does not match manifest.");
|
||||
Assert(CountCsvRows(Path.Combine(root, "name_relationships.csv")) == document.GetProperty("files").GetProperty("name_relationships").GetProperty("rows").GetInt32(), "Relationship row count does not match manifest.");
|
||||
Assert(CountCsvRows(Path.Combine(root, "name_usages.csv")) == document.GetProperty("files").GetProperty("name_usages").GetProperty("rows").GetInt32(), "Usage row count does not match manifest.");
|
||||
|
||||
using var validation = JsonDocument.Parse(File.ReadAllText(Path.Combine(root, "validation_report.json")));
|
||||
Assert(validation.RootElement.GetProperty("ok").GetBoolean(), "Bundled Name Intelligence validation report must be green.");
|
||||
}
|
||||
|
||||
static void NameIntelligenceReferenceDataIsCopiedOnPublish()
|
||||
{
|
||||
var project = File.ReadAllText(Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine/PlotLine.csproj"));
|
||||
Assert(project.Contains(@"Content Include=""Data\Reference\Names\**\*.csv;Data\Reference\Names\**\*.md""", StringComparison.Ordinal), "Name CSV and documentation files must be configured as publish content.");
|
||||
Assert(project.Contains(@"Content Update=""Data\Reference\Names\**\*.json""", StringComparison.Ordinal), "Name JSON files must be configured as publish content.");
|
||||
Assert(project.Contains("<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>", StringComparison.Ordinal), "Name reference package must copy to publish output.");
|
||||
}
|
||||
|
||||
static void NameIntelligenceRepositoryUsesStoredProcedures()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||
var repository = File.ReadAllText(Path.Combine(root, "Data/NameIntelligenceRepository.cs"));
|
||||
Assert(repository.Contains("CommandType.StoredProcedure", StringComparison.Ordinal), "Name Intelligence repository must call stored procedures.");
|
||||
Assert(!repository.Contains("SELECT ", StringComparison.OrdinalIgnoreCase), "Name Intelligence repository should not contain inline SELECT SQL.");
|
||||
Assert(!repository.Contains("INSERT ", StringComparison.OrdinalIgnoreCase), "Name Intelligence repository should not contain inline INSERT SQL.");
|
||||
Assert(!repository.Contains("UPDATE ", StringComparison.OrdinalIgnoreCase), "Name Intelligence repository should not contain inline UPDATE SQL.");
|
||||
Assert(File.Exists(Path.Combine(root, "Sql/148_Phase23_NameIntelligence.sql")), "Name Intelligence SQL migration is missing.");
|
||||
}
|
||||
|
||||
static void NameIntelligenceReviewWarningIsWired()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "../../../../PlotLine");
|
||||
var service = File.ReadAllText(Path.Combine(root, "Services/StoryIntelligenceCharacterImportService.cs"));
|
||||
var model = File.ReadAllText(Path.Combine(root, "ViewModels/OnboardingViewModels.cs"));
|
||||
var view = File.ReadAllText(Path.Combine(root, "Views/Onboarding/StoryIntelligenceCharacters.cshtml"));
|
||||
|
||||
Assert(service.Contains("BuildNameIntelligenceWarningsAsync", StringComparison.Ordinal), "Story Intelligence character review should build Name Intelligence warnings.");
|
||||
Assert(service.Contains("RelationshipType, \"SpellingVariant\"", StringComparison.Ordinal), "Name warning should require spelling-variant evidence.");
|
||||
Assert(service.Contains("nameIntelligence.RecogniseAsync", StringComparison.Ordinal), "Name warning should use the reusable Name Intelligence service.");
|
||||
Assert(model.Contains("NameIntelligenceWarning", StringComparison.Ordinal), "Character review model should carry the name warning.");
|
||||
Assert(view.Contains("Name warning", StringComparison.Ordinal), "Character review view should render the name warning.");
|
||||
}
|
||||
|
||||
static int CountCsvRows(string path)
|
||||
=> File.ReadLines(path).Skip(1).Count(line => !string.IsNullOrWhiteSpace(line));
|
||||
|
||||
static JsonSerializerOptions JsonOptions()
|
||||
=> new()
|
||||
{
|
||||
|
||||
@ -21,6 +21,7 @@ public sealed class AdminController(
|
||||
IStoryIntelligenceExistingChapterQueueService existingChapterQueue,
|
||||
IStoryIntelligenceFullChapterTestService fullChapterTest,
|
||||
IStoryIntelligenceImportCommitService storyIntelligenceImportCommit,
|
||||
INameIntelligenceService nameIntelligence,
|
||||
IIllustrationLibraryService illustrationLibrary) : Controller
|
||||
{
|
||||
[HttpGet("")]
|
||||
@ -39,6 +40,34 @@ public sealed class AdminController(
|
||||
return View();
|
||||
}
|
||||
|
||||
[HttpPost("name-intelligence/validate")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ValidateNameIntelligence(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await nameIntelligence.ValidatePackageAsync(cancellationToken);
|
||||
TempData[result.Succeeded ? "AdminMessage" : "AdminError"] = $"{result.Message} Locales={result.LocaleRows:N0}, names={result.GivenNameRows:N0}, relationships={result.RelationshipRows:N0}, usages={result.UsageRows:N0}.";
|
||||
if (result.Errors.Count > 0)
|
||||
{
|
||||
TempData["AdminError"] = string.Join(" ", result.Errors.Take(5));
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpPost("name-intelligence/import")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> ImportNameIntelligence(bool force, CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await nameIntelligence.EnsureReferenceDataImportedAsync(force, cancellationToken);
|
||||
TempData[result.Succeeded ? "AdminMessage" : "AdminError"] = $"{result.Message} Package {result.PackageId} {result.Version}: locales={result.LocaleRows:N0}, names={result.GivenNameRows:N0}, relationships={result.RelationshipRows:N0}, usages={result.UsageRows:N0}.";
|
||||
if (result.Errors.Count > 0)
|
||||
{
|
||||
TempData["AdminError"] = string.Join(" ", result.Errors.Take(5));
|
||||
}
|
||||
|
||||
return RedirectToAction(nameof(Index));
|
||||
}
|
||||
|
||||
[HttpGet("story-intelligence/illustration-library")]
|
||||
public async Task<IActionResult> StoryIntelligenceIllustrationLibrary([FromQuery] IllustrationLibraryFilter filter)
|
||||
{
|
||||
|
||||
21
PlotLine/Controllers/NamesController.cs
Normal file
21
PlotLine/Controllers/NamesController.cs
Normal file
@ -0,0 +1,21 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.Services;
|
||||
|
||||
namespace PlotLine.Controllers;
|
||||
|
||||
[Authorize]
|
||||
public sealed class NamesController(INameIntelligenceService names) : Controller
|
||||
{
|
||||
public async Task<IActionResult> Index([FromQuery] NameSearchFilter filter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (filter.ProjectID <= 0)
|
||||
{
|
||||
return BadRequest("ProjectID is required.");
|
||||
}
|
||||
|
||||
var model = await names.BuildLibraryAsync(filter, cancellationToken);
|
||||
return model is null ? NotFound() : View(model);
|
||||
}
|
||||
}
|
||||
@ -107,6 +107,9 @@ public sealed class IllustrationLibraryRepository(ISqlConnectionFactory connecti
|
||||
image.MimeType,
|
||||
Provider = providerResult.Provider,
|
||||
Model = providerResult.Model,
|
||||
providerResult.Quality,
|
||||
providerResult.Size,
|
||||
providerResult.OutputFormat,
|
||||
PromptText = prompt,
|
||||
PromptTemplateVersion = templateVersion,
|
||||
ProviderRequestID = providerResult.ProviderRequestID
|
||||
|
||||
161
PlotLine/Data/NameIntelligenceRepository.cs
Normal file
161
PlotLine/Data/NameIntelligenceRepository.cs
Normal file
@ -0,0 +1,161 @@
|
||||
using System.Data;
|
||||
using Dapper;
|
||||
using Microsoft.Data.SqlClient;
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.Data;
|
||||
|
||||
public interface INameIntelligenceRepository
|
||||
{
|
||||
Task<NameReferenceImportStatus?> GetLatestImportAsync(string packageId, string version, CancellationToken cancellationToken = default);
|
||||
Task<int> StartImportAsync(string packageId, string version, string checksum, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
|
||||
Task CompleteImportAsync(int importId, bool succeeded, int localeRows, int givenNameRows, int relationshipRows, int usageRows, string? errorSummary, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
|
||||
Task UpsertLocalesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
|
||||
Task UpsertGivenNamesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
|
||||
Task UpsertRelationshipsAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
|
||||
Task UpsertUsagesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NameLocale>> ListLocalesAsync(CancellationToken cancellationToken = default);
|
||||
Task<NameRecognitionResult> RecogniseAsync(string normalisedName, string? localeCode, int? birthYear, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NameRelationshipReference>> ListRelationshipsAsync(int givenNameId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NameSearchResult>> SearchAsync(int userId, NameSearchFilter filter, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NameUsageDecoration>> ListUsedDecorationsAsync(int userId, int projectId, int? bookId, string normalisedName, string usedScope, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class NameIntelligenceRepository(ISqlConnectionFactory connectionFactory) : INameIntelligenceRepository
|
||||
{
|
||||
public async Task<NameReferenceImportStatus?> GetLatestImportAsync(string packageId, string version, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<NameReferenceImportStatus>(Command(
|
||||
"dbo.NameReferenceImport_GetLatest",
|
||||
new { PackageID = packageId, SemanticVersion = version },
|
||||
cancellationToken: cancellationToken));
|
||||
}
|
||||
|
||||
public async Task<int> StartImportAsync(string packageId, string version, string checksum, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
|
||||
=> await connection.QuerySingleAsync<int>(Command(
|
||||
"dbo.NameReferenceImport_Start",
|
||||
new { PackageID = packageId, SemanticVersion = version, PackageChecksum = checksum },
|
||||
transaction,
|
||||
cancellationToken));
|
||||
|
||||
public Task CompleteImportAsync(int importId, bool succeeded, int localeRows, int givenNameRows, int relationshipRows, int usageRows, string? errorSummary, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
|
||||
=> connection.ExecuteAsync(Command(
|
||||
"dbo.NameReferenceImport_Complete",
|
||||
new
|
||||
{
|
||||
NameReferenceImportID = importId,
|
||||
Succeeded = succeeded,
|
||||
LocaleRows = localeRows,
|
||||
GivenNameRows = givenNameRows,
|
||||
RelationshipRows = relationshipRows,
|
||||
UsageRows = usageRows,
|
||||
ErrorSummary = errorSummary
|
||||
},
|
||||
transaction,
|
||||
cancellationToken));
|
||||
|
||||
public Task UpsertLocalesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
|
||||
=> ExecuteTableAsync("dbo.NameLocale_BulkUpsert", "Rows", "dbo.NameLocaleImportTable", rows, connection, transaction, cancellationToken);
|
||||
|
||||
public Task UpsertGivenNamesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
|
||||
=> ExecuteTableAsync("dbo.GivenName_BulkUpsert", "Rows", "dbo.GivenNameImportTable", rows, connection, transaction, cancellationToken);
|
||||
|
||||
public Task UpsertRelationshipsAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
|
||||
=> ExecuteTableAsync("dbo.NameRelationship_BulkUpsert", "Rows", "dbo.NameRelationshipImportTable", rows, connection, transaction, cancellationToken);
|
||||
|
||||
public Task UpsertUsagesAsync(DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken = default)
|
||||
=> ExecuteTableAsync("dbo.NameUsage_BulkUpsert", "Rows", "dbo.NameUsageImportTable", rows, connection, transaction, cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<NameLocale>> ListLocalesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<NameLocale>(Command("dbo.NameLocale_List", cancellationToken: cancellationToken));
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<NameRecognitionResult> RecogniseAsync(string normalisedName, string? localeCode, int? birthYear, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
using var grid = await connection.QueryMultipleAsync(Command(
|
||||
"dbo.GivenName_Recognise",
|
||||
new { NormalisedName = normalisedName, LocaleCode = localeCode, BirthYear = birthYear },
|
||||
cancellationToken: cancellationToken));
|
||||
var name = await grid.ReadSingleOrDefaultAsync<GivenNameReference>();
|
||||
if (name is null)
|
||||
{
|
||||
return new NameRecognitionResult();
|
||||
}
|
||||
|
||||
var usage = await grid.ReadSingleOrDefaultAsync<NameUsageProfile>();
|
||||
var related = (await grid.ReadAsync<NameRelationshipReference>()).ToList();
|
||||
return new NameRecognitionResult
|
||||
{
|
||||
Recognised = true,
|
||||
Name = name,
|
||||
Usage = usage,
|
||||
RelatedForms = related,
|
||||
Message = "Recognised given-name form."
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NameRelationshipReference>> ListRelationshipsAsync(int givenNameId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<NameRelationshipReference>(Command(
|
||||
"dbo.GivenName_Relationships",
|
||||
new { GivenNameID = givenNameId },
|
||||
cancellationToken: cancellationToken));
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NameSearchResult>> SearchAsync(int userId, NameSearchFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<NameSearchResult>(Command(
|
||||
"dbo.GivenName_Search",
|
||||
new
|
||||
{
|
||||
UserID = userId,
|
||||
filter.ProjectID,
|
||||
filter.BookID,
|
||||
Search = NullIfWhiteSpace(filter.Search),
|
||||
LocaleCode = NullIfWhiteSpace(filter.LocaleCode),
|
||||
filter.BirthYear,
|
||||
GenderPreference = NullIfWhiteSpace(filter.GenderPreference),
|
||||
filter.GenderThreshold,
|
||||
NameType = NullIfWhiteSpace(filter.NameType),
|
||||
filter.MinimumUsage,
|
||||
filter.UsedScope,
|
||||
filter.UsedOnly,
|
||||
filter.Page,
|
||||
filter.PageSize
|
||||
},
|
||||
cancellationToken: cancellationToken));
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NameUsageDecoration>> ListUsedDecorationsAsync(int userId, int projectId, int? bookId, string normalisedName, string usedScope, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
var rows = await connection.QueryAsync<NameUsageDecoration>(Command(
|
||||
"dbo.GivenName_UsedDecorations",
|
||||
new { UserID = userId, ProjectID = projectId, BookID = bookId, NormalisedName = normalisedName, UsedScope = usedScope },
|
||||
cancellationToken: cancellationToken));
|
||||
return rows.ToList();
|
||||
}
|
||||
|
||||
public IDbConnection CreateConnection() => connectionFactory.CreateConnection();
|
||||
|
||||
private static async Task ExecuteTableAsync(string procedure, string parameterName, string typeName, DataTable rows, IDbConnection connection, IDbTransaction transaction, CancellationToken cancellationToken)
|
||||
{
|
||||
var parameters = new DynamicParameters();
|
||||
parameters.Add(parameterName, rows.AsTableValuedParameter(typeName));
|
||||
await connection.ExecuteAsync(Command(procedure, parameters, transaction, cancellationToken));
|
||||
}
|
||||
|
||||
private static CommandDefinition Command(string commandText, object? parameters = null, IDbTransaction? transaction = null, CancellationToken cancellationToken = default)
|
||||
=> new(commandText, parameters, transaction, commandType: CommandType.StoredProcedure, cancellationToken: cancellationToken);
|
||||
|
||||
private static string? NullIfWhiteSpace(string? value) => string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
32
PlotLine/Data/Reference/Names/v1.0.0/DATA_CONTRACT.md
Normal file
32
PlotLine/Data/Reference/Names/v1.0.0/DATA_CONTRACT.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Data contract
|
||||
|
||||
All four datasets are shared/global and contain no user, project, series, book, character or alias ownership.
|
||||
|
||||
## GivenNames
|
||||
|
||||
`given_name_key` is the immutable import identity (`gn_elizabeth`); database implementations may additionally use an integer surrogate PK. `display_name` preserves spelling. `normalised_name` is the lookup value. `name_type` is one of `Canonical`, `Independent`, `Diminutive`, `Nickname`, or `Variant`. `is_word_ambiguous` asks parsers to require stronger context. `is_active` supports non-destructive correction.
|
||||
|
||||
## NameRelationships
|
||||
|
||||
`relationship_key` is immutable import identity. `from_given_name_key` and `to_given_name_key` reference GivenNames. `relationship_type` is one of `ShortForm`, `Diminutive`, `Nickname`, `SpellingVariant`, `AlternativeForm`, or `AnglicisedForm`. `strength` describes confidence in the linguistic relationship, not confidence that two manuscript mentions identify one character. `is_bidirectional` controls graph traversal. `curation_source` preserves provenance.
|
||||
|
||||
## NameUsages
|
||||
|
||||
Each row covers one recognised form, one locale and one inclusive birth-year range. `male_weight` + `female_weight` equals 1.0 within tolerance. These are gender priors. `usage_weight` is relative recognisability within a locale/era and is suitable for ranking, not as an absolute population probability. `confidence` describes evidential confidence. `source_type` separates direct statistical derivation from blended estimates.
|
||||
|
||||
Lookup precedence: known birth year → estimated birth year → story year fallback → nearest band. Use exact locale first. Do not silently treat US evidence as another locale’s direct statistic.
|
||||
|
||||
## NameLocales
|
||||
|
||||
Supported codes: `en-GB`, `en-IE`, `en-US`, `en-CA`, `en-AU`, `en-NZ`. `evidence_basis` and `default_confidence` make limitations visible.
|
||||
|
||||
## Recommended constraints
|
||||
|
||||
- Unique stable keys in every table.
|
||||
- Unique Unicode-aware `GivenNames.NormalisedName` under an explicitly chosen collation.
|
||||
- Unique active `(FromNameId, ToNameId, RelationshipType)`.
|
||||
- Unique active `(GivenNameId, LocaleId, BirthYearFrom, BirthYearTo)`.
|
||||
- All weights/confidence in `[0,1]`; gender weights sum to 1 within `0.0002`.
|
||||
- Ordered, non-overlapping usage bands.
|
||||
- Reference tables read-only to ordinary users.
|
||||
|
||||
30
PlotLine/Data/Reference/Names/v1.0.0/METHODOLOGY.md
Normal file
30
PlotLine/Data/Reference/Names/v1.0.0/METHODOLOGY.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Methodology and limitations
|
||||
|
||||
## Inclusion
|
||||
|
||||
The seed selects names with sustained or count-based evidence in US or UK birth-registration series from 1900 onward, then retains every required hand-curated canonical, diminutive, nickname and variant from the prototype. The result is intentionally broad enough to include established names of many linguistic origins used in English-speaking societies.
|
||||
|
||||
## Usage profiles
|
||||
|
||||
- `en-US`: derived from US Social Security Administration annual national name counts.
|
||||
- `en-GB`: derived from the unified ONS, National Records of Scotland and NISRA series. Historical ranked rows lacking counts receive a decreasing rank-based proxy so the period remains usable without implying exact births.
|
||||
- `en-IE`: Northern Ireland evidence with conservative UK/US smoothing; labelled as an estimate rather than Republic of Ireland direct statistics.
|
||||
- `en-CA`: a documented US/UK evidence blend.
|
||||
- `en-AU` and `en-NZ`: documented UK/US evidence blends.
|
||||
|
||||
Within each locale and band, gender weights use observed counts with light smoothing. Usage weights use a logarithmic scale relative to the strongest name in the same evidence pool, preventing a few extremely common names from flattening the remainder. Zero-count bands retain a low usage weight and a broader all-era gender prior with reduced confidence.
|
||||
|
||||
## Sources
|
||||
|
||||
- SSA Popular Baby Names: https://www.ssa.gov/oact/babynames/limits.html
|
||||
- SSA qualifications: https://www.ssa.gov/OACT/babynames/background.html
|
||||
- ONS baby names: https://www.ons.gov.uk/peoplepopulationandcommunity/birthsdeathsandmarriages/livebirths/datasets/babynamesinenglandandwalesfrom1996/1996tocurrent
|
||||
- UK unified processing and upstream source links: https://github.com/rossbowen/babynames
|
||||
- SSA mirror and processing documentation: https://github.com/hackerb9/ssa-baby-names
|
||||
- Curated nickname mappings: https://github.com/dxdc/babynames
|
||||
- Irish CSO reference for future direct enrichment: https://www.cso.ie/en/statistics/birthsdeathsandmarriages/irishbabiesnames/
|
||||
|
||||
## Limitations
|
||||
|
||||
Registration spelling does not prove etymological canonicity, and official privacy thresholds omit very rare names. Locale estimates are deliberately labelled. This package is a deterministic knowledge layer, not an onomastic authority or demographic classifier. Future releases may replace blended profiles with direct official observations without changing stable name keys or consumer semantics.
|
||||
|
||||
28
PlotLine/Data/Reference/Names/v1.0.0/README.md
Normal file
28
PlotLine/Data/Reference/Names/v1.0.0/README.md
Normal file
@ -0,0 +1,28 @@
|
||||
# PlotDirector Name Intelligence reference data v1.0.0
|
||||
|
||||
Production-candidate global reference package for deterministic name recognition, ranking and relationship evidence in PlotDirector.
|
||||
|
||||
## Scale
|
||||
|
||||
- 3,067 recognised given-name forms.
|
||||
- 525 curated short-form, diminutive, nickname, spelling-variant and alternative-form relationships.
|
||||
- 92,010 usage profiles: every name × six locales × five inclusive birth-era bands.
|
||||
- Direct registration-derived profiles for the United States and United Kingdom; explicitly labelled blended estimates for Ireland, Canada, Australia and New Zealand.
|
||||
|
||||
## Non-negotiable semantics
|
||||
|
||||
This library is **positive evidence, not a whitelist**. Missing names remain eligible for character detection through manuscript evidence. Gender values are priors, not character facts. Relationships generate candidates and continuity warnings; they never auto-merge characters. Existing project-specific `CharacterAliases` remain untouched and authoritative for that project.
|
||||
|
||||
## Import order
|
||||
|
||||
1. `name_locales.csv`
|
||||
2. `given_names.csv`
|
||||
3. `name_relationships.csv`
|
||||
4. `name_usages.csv`
|
||||
|
||||
Upsert using the stable string keys, optionally resolving them to application surrogate keys. Never use CSV row order as identity.
|
||||
|
||||
## Review and validation
|
||||
|
||||
Start with `REVIEW.md` and the three `review_sample_*.csv` files. `validation_report.json` records the full structural validation result. See `DATA_CONTRACT.md`, `METHODOLOGY.md`, and `VALIDATION.md` before implementation.
|
||||
|
||||
21
PlotLine/Data/Reference/Names/v1.0.0/VALIDATION.md
Normal file
21
PlotLine/Data/Reference/Names/v1.0.0/VALIDATION.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Validation rules
|
||||
|
||||
Reject an import atomically if any error rule fails:
|
||||
|
||||
1. Exact documented headers; UTF-8; unique, non-empty stable keys.
|
||||
2. Unique normalised names; trimmed display values; controlled enum/boolean values.
|
||||
3. Every relationship endpoint and every usage foreign key resolves.
|
||||
4. No self-relationships or duplicate `(from, to, type)` relationships.
|
||||
5. All numeric weights are finite and in `[0,1]`.
|
||||
6. Male and female weights total 1 within `0.0002`.
|
||||
7. Every name/locale has exactly five ordered, contiguous, non-overlapping bands covering 1900–2026.
|
||||
8. No ownership columns or project-specific aliases enter the reference package.
|
||||
9. Package version and import outcome are recorded transactionally.
|
||||
|
||||
Behavioural invariants:
|
||||
|
||||
- Unknown name → no dictionary evidence, never “not a person”.
|
||||
- Relationship → candidate/explanation/warning, never automatic identity merge.
|
||||
- Gender prior → ranking hint, always subordinate to manuscript evidence.
|
||||
- Used-name status → computed from project/book/series character data at query time.
|
||||
|
||||
3068
PlotLine/Data/Reference/Names/v1.0.0/given_names.csv
Normal file
3068
PlotLine/Data/Reference/Names/v1.0.0/given_names.csv
Normal file
File diff suppressed because it is too large
Load Diff
71
PlotLine/Data/Reference/Names/v1.0.0/manifest.json
Normal file
71
PlotLine/Data/Reference/Names/v1.0.0/manifest.json
Normal file
@ -0,0 +1,71 @@
|
||||
{
|
||||
"packageId": "plotdirector-name-intelligence",
|
||||
"version": "1.0.0",
|
||||
"status": "production-candidate",
|
||||
"generatedAt": "2026-08-24",
|
||||
"positiveEvidenceNotWhitelist": true,
|
||||
"coverage": {
|
||||
"birthYears": "1900-2026",
|
||||
"locales": [
|
||||
"en-GB",
|
||||
"en-IE",
|
||||
"en-US",
|
||||
"en-CA",
|
||||
"en-AU",
|
||||
"en-NZ"
|
||||
],
|
||||
"selection": "Established by sustained/count-based US or UK registration evidence, plus required curated forms",
|
||||
"maximumRecognisedForms": 3000
|
||||
},
|
||||
"files": {
|
||||
"name_locales": {
|
||||
"file": "name_locales.csv",
|
||||
"rows": 6
|
||||
},
|
||||
"given_names": {
|
||||
"file": "given_names.csv",
|
||||
"rows": 3067
|
||||
},
|
||||
"name_relationships": {
|
||||
"file": "name_relationships.csv",
|
||||
"rows": 525
|
||||
},
|
||||
"name_usages": {
|
||||
"file": "name_usages.csv",
|
||||
"rows": 92010
|
||||
}
|
||||
},
|
||||
"valueSets": {
|
||||
"nameType": [
|
||||
"Canonical",
|
||||
"Independent",
|
||||
"Diminutive",
|
||||
"Nickname",
|
||||
"Variant"
|
||||
],
|
||||
"relationshipType": [
|
||||
"ShortForm",
|
||||
"Diminutive",
|
||||
"Nickname",
|
||||
"SpellingVariant",
|
||||
"AlternativeForm",
|
||||
"AnglicisedForm"
|
||||
],
|
||||
"sourceType": [
|
||||
"OfficialStatisticalDerived",
|
||||
"OfficialSourceBlendedEstimate"
|
||||
]
|
||||
},
|
||||
"weights": {
|
||||
"minimum": 0,
|
||||
"maximum": 1,
|
||||
"genderSumTolerance": 0.0002
|
||||
},
|
||||
"importOrder": [
|
||||
"name_locales.csv",
|
||||
"given_names.csv",
|
||||
"name_relationships.csv",
|
||||
"name_usages.csv"
|
||||
],
|
||||
"encoding": "UTF-8 with CRLF CSV"
|
||||
}
|
||||
7
PlotLine/Data/Reference/Names/v1.0.0/name_locales.csv
Normal file
7
PlotLine/Data/Reference/Names/v1.0.0/name_locales.csv
Normal file
@ -0,0 +1,7 @@
|
||||
locale_code,display_name,iso_country_code,evidence_basis,default_confidence,sort_order,is_active
|
||||
en-GB,United Kingdom,GBR,Direct UK registration series,0.88,1,true
|
||||
en-IE,Ireland,IRL,Northern Ireland evidence plus UK/US smoothing,0.67,2,true
|
||||
en-US,United States,USA,Direct SSA registration series,0.92,3,true
|
||||
en-CA,Canada,CAN,US/UK evidence blend,0.64,4,true
|
||||
en-AU,Australia,AUS,UK/US evidence blend,0.62,5,true
|
||||
en-NZ,New Zealand,NZL,UK/US evidence blend,0.62,6,true
|
||||
|
526
PlotLine/Data/Reference/Names/v1.0.0/name_relationships.csv
Normal file
526
PlotLine/Data/Reference/Names/v1.0.0/name_relationships.csv
Normal file
@ -0,0 +1,526 @@
|
||||
relationship_key,from_given_name_key,to_given_name_key,relationship_type,strength,is_bidirectional,is_active,curation_source,notes
|
||||
nr_000001,gn_abbey,gn_abigail,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000002,gn_abbie,gn_abigail,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000003,gn_abby,gn_abigail,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000004,gn_abby,gn_abigail,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000005,gn_abe,gn_abraham,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000006,gn_addie,gn_adelaide,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000007,gn_addie,gn_adeline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000008,gn_al,gn_alan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000009,gn_al,gn_albert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000010,gn_alec,gn_alexander,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000011,gn_alex,gn_alexander,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000012,gn_alex,gn_alexander,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000013,gn_alex,gn_alexandra,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000014,gn_alf,gn_alfred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000015,gn_alfie,gn_alfred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000016,gn_alfie,gn_alfred,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000017,gn_ali,gn_alison,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000018,gn_allie,gn_alison,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000019,gn_allison,gn_alison,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000020,gn_ally,gn_alexandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000021,gn_andy,gn_andrew,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000022,gn_andy,gn_andrew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000023,gn_angie,gn_angela,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000024,gn_angie,gn_angela,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000025,gn_angie,gn_angelina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000026,gn_anne,gn_ann,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000027,gn_annie,gn_ann,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000028,gn_annie,gn_ann,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000029,gn_annie,gn_anna,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000030,gn_annie,gn_anna,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000031,gn_ant,gn_anthony,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000032,gn_archie,gn_archibald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000033,gn_arnie,gn_arnold,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000034,gn_art,gn_arthur,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000035,gn_art,gn_arthur,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000036,gn_artie,gn_arthur,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000037,gn_artie,gn_arthur,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000038,gn_babs,gn_barbara,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000039,gn_barb,gn_barbara,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000040,gn_barb,gn_barbara,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000041,gn_barbie,gn_barbara,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000042,gn_barbie,gn_barbara,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000043,gn_barney,gn_bernard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000044,gn_bastian,gn_sebastian,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000045,gn_bea,gn_beatrice,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000046,gn_becca,gn_rebecca,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000047,gn_becca,gn_rebecca,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000048,gn_becky,gn_rebecca,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000049,gn_becky,gn_rebecca,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000050,gn_bella,gn_isabel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000051,gn_bella,gn_isabella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000052,gn_belle,gn_isabella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000053,gn_ben,gn_benjamin,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000054,gn_ben,gn_benjamin,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000055,gn_benny,gn_benjamin,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000056,gn_benny,gn_benjamin,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000057,gn_bernie,gn_bernard,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000058,gn_bernie,gn_bernard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000059,gn_bert,gn_albert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000060,gn_bert,gn_herbert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000061,gn_bert,gn_robert,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000062,gn_bertie,gn_albert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000063,gn_bess,gn_elizabeth,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000064,gn_bessie,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000065,gn_beth,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000066,gn_beth,gn_elizabeth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000067,gn_betsy,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000068,gn_betty,gn_elizabeth,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000069,gn_bex,gn_rebecca,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000070,gn_bill,gn_william,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000071,gn_billie,gn_wilhelmina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000072,gn_billy,gn_william,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000073,gn_billy,gn_william,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000074,gn_bob,gn_robert,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000075,gn_bobbie,gn_roberta,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000076,gn_bobby,gn_robert,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000077,gn_bobby,gn_robert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000078,gn_bridie,gn_bridget,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000079,gn_cam,gn_cameron,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000080,gn_carol,gn_caroline,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000081,gn_carrie,gn_carol,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000082,gn_carrie,gn_caroline,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000083,gn_carrie,gn_caroline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000084,gn_cass,gn_cassandra,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000085,gn_cass,gn_cassandra,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000086,gn_cassie,gn_cassandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000087,gn_cat,gn_catherine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000088,gn_cathy,gn_catherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000089,gn_cathy,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000090,gn_charlie,gn_charles,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000091,gn_charlie,gn_charles,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000092,gn_charlie,gn_charlotte,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000093,gn_charlie,gn_charlotte,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000094,gn_chas,gn_charles,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000095,gn_chris,gn_christina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000096,gn_chris,gn_christina,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000097,gn_chris,gn_christine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000098,gn_chris,gn_christine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000099,gn_chris,gn_christopher,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000100,gn_chris,gn_christopher,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000101,gn_chrissie,gn_christine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000102,gn_chrissy,gn_christina,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000103,gn_chuck,gn_charles,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000104,gn_cindy,gn_cynthia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000105,gn_cindy,gn_lucinda,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000106,gn_clare,gn_claire,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000107,gn_clint,gn_clinton,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000108,gn_connie,gn_constance,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000109,gn_dan,gn_daniel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000110,gn_dan,gn_daniel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000111,gn_dani,gn_danielle,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000112,gn_danny,gn_daniel,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000113,gn_danny,gn_daniel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000114,gn_danny,gn_danielle,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000115,gn_dave,gn_david,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000116,gn_dave,gn_david,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000117,gn_davey,gn_david,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000118,gn_davy,gn_david,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000119,gn_davy,gn_david,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000120,gn_deb,gn_deborah,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000121,gn_deb,gn_deborah,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000122,gn_debbie,gn_deborah,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000123,gn_debbie,gn_deborah,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000124,gn_debby,gn_deborah,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000125,gn_della,gn_adelaide,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000126,gn_dick,gn_richard,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000127,gn_dolly,gn_dorothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000128,gn_dom,gn_dominic,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000129,gn_don,gn_donald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000130,gn_donnie,gn_donald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000131,gn_donny,gn_donald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000132,gn_dora,gn_theodora,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000133,gn_dot,gn_dorothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000134,gn_dot,gn_dorothy,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000135,gn_dottie,gn_dorothy,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000136,gn_dottie,gn_dorothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000137,gn_drew,gn_andrew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000138,gn_drew,gn_andrew,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000139,gn_ed,gn_edmund,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000140,gn_ed,gn_edward,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000141,gn_ed,gn_edward,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000142,gn_eddie,gn_edmund,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000143,gn_eddie,gn_edward,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000144,gn_eddie,gn_edward,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000145,gn_edie,gn_edith,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000146,gn_eli,gn_elias,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000147,gn_eli,gn_elijah,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000148,gn_eliza,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000149,gn_eliza,gn_elizabeth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000150,gn_ella,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000151,gn_ellie,gn_eleanor,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000152,gn_ellie,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000153,gn_ellie,gn_ellen,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000154,gn_elliot,gn_elliott,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000155,gn_elsie,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000156,gn_emmy,gn_emily,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000157,gn_ernie,gn_ernest,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000158,gn_etta,gn_henrietta,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000159,gn_fanny,gn_frances,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000160,gn_flo,gn_florence,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000161,gn_flo,gn_florence,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000162,gn_florrie,gn_florence,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000163,gn_fran,gn_frances,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000164,gn_fran,gn_frances,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000165,gn_frank,gn_francis,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000166,gn_frankie,gn_frances,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000167,gn_frankie,gn_francis,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000168,gn_frankie,gn_francis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000169,gn_frankie,gn_frank,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000170,gn_fred,gn_alfred,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000171,gn_fred,gn_frederick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000172,gn_fred,gn_frederick,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000173,gn_freddie,gn_alfred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000174,gn_freddie,gn_frederick,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000175,gn_freddie,gn_frederick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000176,gn_freddy,gn_frederick,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000177,gn_freddy,gn_frederick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000178,gn_gabby,gn_gabrielle,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000179,gn_gabe,gn_gabriel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000180,gn_gabe,gn_gabriel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000181,gn_gabi,gn_gabrielle,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000182,gn_gail,gn_abigail,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000183,gn_gail,gn_abigail,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000184,gn_gene,gn_eugene,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000185,gn_geoff,gn_geoffrey,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000186,gn_georgie,gn_george,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000187,gn_georgie,gn_georgia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000188,gn_gerry,gn_gerald,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000189,gn_gerry,gn_gerald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000190,gn_gerry,gn_geraldine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000191,gn_gigi,gn_virginia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000192,gn_ginger,gn_virginia,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000193,gn_ginny,gn_virginia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000194,gn_ginny,gn_virginia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000195,gn_greg,gn_gregory,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000196,gn_greg,gn_gregory,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000197,gn_gus,gn_augustus,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000198,gn_gus,gn_gustavo,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000199,gn_gwen,gn_gwendoline,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000200,gn_gwen,gn_gwendolyn,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000201,gn_hal,gn_harold,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000202,gn_hal,gn_henry,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000203,gn_hank,gn_henry,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000204,gn_harry,gn_harold,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000205,gn_harry,gn_henry,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000206,gn_harry,gn_henry,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000207,gn_hattie,gn_harriet,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000208,gn_hattie,gn_harriet,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000209,gn_isabella,gn_isabel,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000210,gn_isabelle,gn_isabel,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000211,gn_izzy,gn_isabel,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000212,gn_izzy,gn_isabella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000213,gn_jack,gn_john,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000214,gn_jackie,gn_jacqueline,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000215,gn_jackie,gn_jacqueline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000216,gn_jake,gn_jacob,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000217,gn_jamie,gn_james,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000218,gn_janey,gn_jane,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000219,gn_jay,gn_james,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000220,gn_jay,gn_jason,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000221,gn_jeff,gn_jeffrey,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000222,gn_jeffrey,gn_geoffrey,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000223,gn_jen,gn_jennifer,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000224,gn_jen,gn_jennifer,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000225,gn_jenny,gn_jennifer,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000226,gn_jenny,gn_jennifer,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000227,gn_jerry,gn_gerald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000228,gn_jerry,gn_gerald,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000229,gn_jerry,gn_jeremy,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000230,gn_jess,gn_jessica,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000231,gn_jess,gn_jessica,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000232,gn_jessie,gn_jessica,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000233,gn_jessie,gn_jessica,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000234,gn_jez,gn_jeremy,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000235,gn_jim,gn_james,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000236,gn_jimmy,gn_james,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000237,gn_jimmy,gn_james,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000238,gn_jo,gn_joanne,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000239,gn_jo,gn_josephine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000240,gn_jo,gn_josephine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000241,gn_joe,gn_joseph,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000242,gn_joe,gn_joseph,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000243,gn_joey,gn_joseph,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000244,gn_joey,gn_joseph,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000245,gn_johnny,gn_john,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000246,gn_johnny,gn_john,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000247,gn_johnny,gn_jonathan,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000248,gn_jon,gn_jonathan,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000249,gn_jonny,gn_jonathan,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000250,gn_josh,gn_joshua,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000251,gn_josh,gn_joshua,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000252,gn_josie,gn_josephine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000253,gn_josie,gn_josephine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000254,gn_joy,gn_joyce,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000255,gn_judy,gn_judith,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000256,gn_judy,gn_judith,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000257,gn_julie,gn_julia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000258,gn_julie,gn_julia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000259,gn_kaitlin,gn_caitlin,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000260,gn_kate,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000261,gn_kate,gn_catherine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000262,gn_kate,gn_katherine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000263,gn_katelyn,gn_caitlin,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000264,gn_katherine,gn_catherine,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000265,gn_kathryn,gn_catherine,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000266,gn_kathy,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000267,gn_kathy,gn_katherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000268,gn_katie,gn_catherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000269,gn_katie,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000270,gn_katie,gn_katherine,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000271,gn_kay,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000272,gn_ken,gn_kenneth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000273,gn_ken,gn_kenneth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000274,gn_kenny,gn_kenneth,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000275,gn_kenny,gn_kenneth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000276,gn_kim,gn_kimberly,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000277,gn_kit,gn_christopher,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000278,gn_kitty,gn_catherine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000279,gn_kris,gn_christina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000280,gn_larry,gn_laurence,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000281,gn_larry,gn_lawrence,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000282,gn_laurie,gn_laura,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000283,gn_laurie,gn_laura,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000284,gn_laurie,gn_lauren,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000285,gn_laurie,gn_laurence,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000286,gn_lawrence,gn_laurence,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000287,gn_len,gn_leonard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000288,gn_lena,gn_helena,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000289,gn_lenny,gn_leonard,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000290,gn_lenny,gn_leonard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000291,gn_leo,gn_leonard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000292,gn_leo,gn_leonard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000293,gn_leo,gn_leonardo,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000294,gn_lettie,gn_charlotte,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000295,gn_lettie,gn_letitia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000296,gn_lewis,gn_louis,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000297,gn_lex,gn_alexander,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000298,gn_lexi,gn_alexandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000299,gn_lexi,gn_alexandra,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000300,gn_lexi,gn_alexis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000301,gn_lexie,gn_alexandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000302,gn_liam,gn_william,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000303,gn_libby,gn_elizabeth,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000304,gn_lillie,gn_lillian,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000305,gn_lily,gn_lillian,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000306,gn_lisa,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000307,gn_liv,gn_olivia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000308,gn_liv,gn_olivia,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000309,gn_livvy,gn_olivia,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000310,gn_liz,gn_elizabeth,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000311,gn_liz,gn_elizabeth,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000312,gn_lizzie,gn_elizabeth,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000313,gn_lizzy,gn_elizabeth,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000314,gn_lori,gn_lorraine,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000315,gn_lottie,gn_charlotte,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000316,gn_lottie,gn_charlotte,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000317,gn_lou,gn_louis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000318,gn_lou,gn_louis,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000319,gn_lou,gn_louise,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000320,gn_louie,gn_louis,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000321,gn_lucy,gn_lucille,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000322,gn_lucy,gn_lucinda,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000323,gn_lulu,gn_louise,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000324,gn_lulu,gn_lucille,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000325,gn_maddie,gn_madeline,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000326,gn_mae,gn_mary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000327,gn_maggie,gn_margaret,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000328,gn_maggie,gn_margaret,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000329,gn_maisie,gn_margaret,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000330,gn_mamie,gn_mary,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000331,gn_mandy,gn_amanda,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000332,gn_mandy,gn_amanda,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000333,gn_margo,gn_margaret,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000334,gn_mari,gn_marianne,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000335,gn_marie,gn_maria,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000336,gn_marty,gn_martin,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000337,gn_mary,gn_maria,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000338,gn_matt,gn_matthew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000339,gn_matt,gn_matthew,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000340,gn_mattie,gn_matilda,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000341,gn_matty,gn_matthew,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000342,gn_matty,gn_matthew,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000343,gn_max,gn_maximilian,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000344,gn_max,gn_maxwell,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000345,gn_meg,gn_margaret,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000346,gn_meg,gn_margaret,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000347,gn_mel,gn_melissa,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000348,gn_mick,gn_michael,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000349,gn_mickey,gn_michael,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000350,gn_mike,gn_michael,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000351,gn_mike,gn_michael,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000352,gn_mikey,gn_michael,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000353,gn_millie,gn_mildred,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000354,gn_millie,gn_millicent,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000355,gn_mimi,gn_miriam,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000356,gn_mindy,gn_melinda,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000357,gn_minnie,gn_minerva,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000358,gn_missy,gn_melissa,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000359,gn_mitch,gn_michelle,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000360,gn_mitch,gn_mitchell,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000361,gn_molly,gn_mary,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000362,gn_monty,gn_montgomery,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000363,gn_nat,gn_natalie,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000364,gn_nate,gn_nathan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000365,gn_nate,gn_nathaniel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000366,gn_nate,gn_nathaniel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000367,gn_nathan,gn_nathaniel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000368,gn_ned,gn_edmund,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000369,gn_ned,gn_edward,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000370,gn_nell,gn_eleanor,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000371,gn_nell,gn_ellen,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000372,gn_nell,gn_helen,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000373,gn_nell,gn_penelope,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000374,gn_nellie,gn_eleanor,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000375,gn_nellie,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000376,gn_nellie,gn_helen,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000377,gn_nellie,gn_helen,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000378,gn_nelly,gn_helen,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000379,gn_nic,gn_nicole,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000380,gn_nick,gn_dominic,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000381,gn_nick,gn_nicholas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000382,gn_nick,gn_nicholas,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000383,gn_nicky,gn_nicholas,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000384,gn_nicky,gn_nicholas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000385,gn_nicky,gn_nicole,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000386,gn_nico,gn_nicholas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000387,gn_nikki,gn_nicole,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000388,gn_nora,gn_eleanor,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000389,gn_paddy,gn_patrick,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000390,gn_pat,gn_patricia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000391,gn_pat,gn_patricia,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000392,gn_pat,gn_patrick,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000393,gn_pat,gn_patrick,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000394,gn_patsy,gn_patricia,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000395,gn_peggy,gn_margaret,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000396,gn_penny,gn_penelope,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000397,gn_penny,gn_penelope,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000398,gn_pete,gn_peter,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000399,gn_pete,gn_peter,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000400,gn_phil,gn_philip,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000401,gn_phil,gn_philip,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000402,gn_phillip,gn_philip,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000403,gn_polly,gn_mary,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000404,gn_ray,gn_raymond,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000405,gn_ray,gn_raymond,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000406,gn_reg,gn_reginald,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000407,gn_reggie,gn_reginald,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000408,gn_reggie,gn_reginald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000409,gn_rich,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000410,gn_rich,gn_richard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000411,gn_richie,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000412,gn_rick,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000413,gn_rick,gn_richard,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000414,gn_ricky,gn_richard,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000415,gn_ricky,gn_richard,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000416,gn_rob,gn_robert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000417,gn_rob,gn_robert,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000418,gn_robbie,gn_robert,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000419,gn_robbie,gn_robert,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000420,gn_rodger,gn_roger,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000421,gn_ron,gn_ronald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000422,gn_ron,gn_ronald,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000423,gn_ronnie,gn_ronald,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000424,gn_ronnie,gn_ronald,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000425,gn_rose,gn_rosemary,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000426,gn_rosie,gn_rose,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000427,gn_rosie,gn_rosemary,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000428,gn_rosie,gn_rosemary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000429,gn_roxie,gn_roxanne,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000430,gn_roxy,gn_roxanne,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000431,gn_sadie,gn_sarah,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000432,gn_sally,gn_sarah,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000433,gn_sam,gn_samantha,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000434,gn_sam,gn_samantha,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000435,gn_sam,gn_samuel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000436,gn_sam,gn_samuel,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000437,gn_sammie,gn_samantha,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000438,gn_sammy,gn_samantha,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000439,gn_sammy,gn_samuel,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000440,gn_sammy,gn_samuel,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000441,gn_sandra,gn_cassandra,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000442,gn_sandy,gn_alexander,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000443,gn_sandy,gn_sandra,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000444,gn_sandy,gn_sandra,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000445,gn_sara,gn_sarah,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000446,gn_sasha,gn_alexandra,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000447,gn_seb,gn_sebastian,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000448,gn_shelly,gn_michelle,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000449,gn_sofia,gn_sophia,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000450,gn_sophie,gn_sophia,AlternativeForm,0.8800,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000451,gn_stella,gn_estella,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000452,gn_steph,gn_stephanie,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000453,gn_steve,gn_stephen,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000454,gn_steve,gn_steven,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000455,gn_steven,gn_stephen,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000456,gn_stevie,gn_stephanie,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000457,gn_stevie,gn_stephen,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000458,gn_stevie,gn_steven,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000459,gn_sue,gn_susan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000460,gn_sue,gn_susan,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000461,gn_sue,gn_suzanne,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000462,gn_susie,gn_susan,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000463,gn_susie,gn_susan,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000464,gn_susy,gn_susan,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000465,gn_suzie,gn_susan,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000466,gn_suzie,gn_suzanne,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000467,gn_suzy,gn_susan,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000468,gn_suzy,gn_suzanne,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000469,gn_tammy,gn_tamara,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000470,gn_ted,gn_edward,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000471,gn_ted,gn_theodore,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000472,gn_teddy,gn_edward,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000473,gn_teddy,gn_edward,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000474,gn_teddy,gn_theodore,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000475,gn_teddy,gn_theodore,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000476,gn_terri,gn_teresa,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000477,gn_tess,gn_teresa,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000478,gn_tessa,gn_teresa,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000479,gn_tessa,gn_teresa,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000480,gn_theo,gn_theodore,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000481,gn_theo,gn_theodore,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000482,gn_theresa,gn_teresa,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000483,gn_tilly,gn_matilda,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000484,gn_tim,gn_timothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000485,gn_tim,gn_timothy,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000486,gn_timmy,gn_timothy,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000487,gn_timmy,gn_timothy,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000488,gn_tina,gn_christina,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000489,gn_tina,gn_christina,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000490,gn_tina,gn_christine,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000491,gn_tom,gn_thomas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000492,gn_tom,gn_thomas,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000493,gn_tommy,gn_thomas,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000494,gn_tommy,gn_thomas,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000495,gn_toni,gn_antoinette,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000496,gn_toni,gn_antonia,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000497,gn_tony,gn_anthony,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000498,gn_tony,gn_anthony,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000499,gn_topher,gn_christopher,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000500,gn_tori,gn_victoria,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000501,gn_tori,gn_victoria,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000502,gn_tricia,gn_patricia,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000503,gn_trixie,gn_beatrice,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000504,gn_trudy,gn_gertrude,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000505,gn_vicki,gn_victoria,SpellingVariant,0.9700,true,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000506,gn_vicky,gn_victoria,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000507,gn_vicky,gn_victoria,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000508,gn_vince,gn_vincent,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000509,gn_vinny,gn_vincent,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000510,gn_wally,gn_walter,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000511,gn_wally,gn_walter,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000512,gn_walt,gn_walter,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000513,gn_walt,gn_walter,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000514,gn_wendy,gn_gwendoline,Nickname,0.7200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000515,gn_wendy,gn_gwendolyn,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000516,gn_will,gn_william,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000517,gn_will,gn_william,ShortForm,0.9200,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000518,gn_willie,gn_william,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000519,gn_willie,gn_william,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000520,gn_winnie,gn_winifred,Diminutive,0.9000,false,true,PrototypeCurated,Potential relationship only; never auto-merge characters
|
||||
nr_000521,gn_winnie,gn_winifred,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000522,gn_woody,gn_woodrow,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000523,gn_xander,gn_alexander,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000524,gn_zach,gn_zachary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
nr_000525,gn_zack,gn_zachary,Nickname,0.7800,false,true,DxdcCuratedNicknameMap,Potential relationship only; never auto-merge characters
|
||||
|
92011
PlotLine/Data/Reference/Names/v1.0.0/name_usages.csv
Normal file
92011
PlotLine/Data/Reference/Names/v1.0.0/name_usages.csv
Normal file
File diff suppressed because it is too large
Load Diff
15
PlotLine/Data/Reference/Names/v1.0.0/validation_report.json
Normal file
15
PlotLine/Data/Reference/Names/v1.0.0/validation_report.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"ok": true,
|
||||
"counts": {
|
||||
"locales": 6,
|
||||
"givenNames": 3067,
|
||||
"relationships": 525,
|
||||
"usages": 92010
|
||||
},
|
||||
"coverage": {
|
||||
"usageGroups": 18402,
|
||||
"expectedUsageGroups": 18402
|
||||
},
|
||||
"errors": [],
|
||||
"warnings": []
|
||||
}
|
||||
@ -23,6 +23,7 @@ public interface IStoryMemoryRepository
|
||||
Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request);
|
||||
Task InvalidateAssignmentAsync(int assignmentId, string reason);
|
||||
Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request);
|
||||
Task<IReadOnlyList<StoryMemoryIllustrationDemand>> ListDemandsAsync(int importSessionId);
|
||||
Task<string> GetAppearancePreferenceAsync(int importSessionId);
|
||||
Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId);
|
||||
Task SetAppearancePreferenceAsync(int importSessionId, string preference);
|
||||
@ -363,6 +364,18 @@ public sealed class StoryMemoryRepository(ISqlConnectionFactory connectionFactor
|
||||
request);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<StoryMemoryIllustrationDemand>> ListDemandsAsync(int importSessionId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return (await connection.QueryAsync<StoryMemoryIllustrationDemand>(
|
||||
"""
|
||||
SELECT *
|
||||
FROM dbo.StoryMemoryIllustrationDemands
|
||||
WHERE ImportSessionID = @ImportSessionID;
|
||||
""",
|
||||
new { ImportSessionID = importSessionId })).ToList();
|
||||
}
|
||||
|
||||
public async Task<string> GetAppearancePreferenceAsync(int importSessionId)
|
||||
=> (await GetImportContextAsync(importSessionId)).AppearancePreference;
|
||||
|
||||
|
||||
36
PlotLine/Docs/Features/NameIntelligence.md
Normal file
36
PlotLine/Docs/Features/NameIntelligence.md
Normal file
@ -0,0 +1,36 @@
|
||||
# Name Intelligence
|
||||
|
||||
Name Intelligence is PlotDirector's shared reference layer for recognised given-name forms, locale/era usage priors and relationship evidence between name forms. The reference tables are global application data: they do not contain user, project, book, character, alias or `IsUsed` state.
|
||||
|
||||
## Reference Package
|
||||
|
||||
The bundled package is `plotdirector-name-intelligence` version `1.0.0` and lives in:
|
||||
|
||||
`Data/Reference/Names/v1.0.0/`
|
||||
|
||||
The app publishes the CSVs, manifest, validation report and data-contract documentation as content. Do not embed the 92,010 usage rows in migration SQL or C# constants.
|
||||
|
||||
## Import
|
||||
|
||||
Administrators can validate or import the package from the Admin dashboard. The importer reads `manifest.json`, validates exact headers, stable-key uniqueness, controlled values, foreign keys, numeric ranges, gender-weight totals and contiguous usage bands before writing data. Imports run in manifest order:
|
||||
|
||||
1. `name_locales.csv`
|
||||
2. `given_names.csv`
|
||||
3. `name_relationships.csv`
|
||||
4. `name_usages.csv`
|
||||
|
||||
Rows are upserted by stable keys through stored procedures and table-valued parameters. Re-running the same package is idempotent when the checksum matches; use the force re-import action after correcting a local package or applying a future package. Import history is stored in `dbo.NameReferenceImports`.
|
||||
|
||||
## Consumer Semantics
|
||||
|
||||
Recognition is positive evidence, not a whitelist. Unknown names still pass through the existing deterministic, contextual and AI detection paths. Gender weights are ranking priors only; explicit manuscript evidence wins. Relationships are evidence that two forms can be related and must not auto-merge characters or aliases.
|
||||
|
||||
Birth-era lookup should use character birth year when known, then an estimated birth year, then story year, then the nearest/general profile. Locale estimates labelled as blended estimates should be shown as approximate and not described as demographic certainty.
|
||||
|
||||
## Project Boundary
|
||||
|
||||
Used-name state is computed dynamically from existing `Characters` and `CharacterAliases` for an authorised scope. The global `GivenNames` rows never store ownership or usage. The Names Library page decorates results with current project/book/library usage without mutating reference rows.
|
||||
|
||||
## Future Packages
|
||||
|
||||
A future package should be placed in a new versioned folder and the expected version in `NameIntelligenceReferencePackage` should be updated. Keep stable keys immutable; add new rows or update non-key fields instead of recycling identities. Validate and import through the admin path after applying the corresponding schema/procedure migration.
|
||||
@ -34,6 +34,7 @@ public static class IllustrationLibraryStatuses
|
||||
public const string Failed = "Failed";
|
||||
public const string Superseded = "Superseded";
|
||||
public const string Disabled = "Disabled";
|
||||
public const string Archived = "Archived";
|
||||
|
||||
public static readonly IReadOnlySet<string> All = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
@ -45,7 +46,8 @@ public static class IllustrationLibraryStatuses
|
||||
Rejected,
|
||||
Failed,
|
||||
Superseded,
|
||||
Disabled
|
||||
Disabled,
|
||||
Archived
|
||||
};
|
||||
|
||||
public static bool IsValid(string? status)
|
||||
@ -60,10 +62,13 @@ public static class IllustrationLibraryStatuses
|
||||
(Generating, Failed) => true,
|
||||
(Generated, Approved) => true,
|
||||
(Generated, Rejected) => true,
|
||||
(Generated, Archived) => true,
|
||||
(Approved, Superseded) => true,
|
||||
(Approved, Archived) => true,
|
||||
(Rejected, Queued) => true,
|
||||
(Failed, Queued) => true,
|
||||
(Approved, Disabled) => true,
|
||||
(Disabled, Archived) => true,
|
||||
(Disabled, Queued) => true,
|
||||
_ when string.Equals(from, to, StringComparison.OrdinalIgnoreCase) => true,
|
||||
_ => false
|
||||
@ -95,6 +100,18 @@ public sealed class IllustrationGenerationSpecification
|
||||
public IReadOnlyList<string> Palette { get; init; } = [];
|
||||
public IReadOnlyList<string> MetadataTags { get; init; } = [];
|
||||
public Dictionary<string, string> Metadata { get; init; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public string? ImageKind { get; init; }
|
||||
public string? CanonicalName { get; init; }
|
||||
public string? BaseType { get; init; }
|
||||
public string? SpecificityLevel { get; init; }
|
||||
public bool IsFallbackClassification { get; init; }
|
||||
public string? ReuseScope { get; init; }
|
||||
public int? SourceBookId { get; init; }
|
||||
public int? SourceChapterId { get; init; }
|
||||
public int? SourceSceneId { get; init; }
|
||||
public string? LinkedEntityType { get; init; }
|
||||
public int? LinkedEntityId { get; init; }
|
||||
public string? GenerationReason { get; init; }
|
||||
|
||||
public IReadOnlyList<string> Validate()
|
||||
{
|
||||
@ -173,6 +190,28 @@ public sealed class IllustrationLibraryItem
|
||||
public string? Prompt { get; set; }
|
||||
public string? PromptTemplateVersion { get; set; }
|
||||
public string? ProviderRequestID { get; set; }
|
||||
public string? Quality { get; set; }
|
||||
public string? Size { get; set; }
|
||||
public string? OutputFormat { get; set; }
|
||||
public string? ImageKind { get; set; }
|
||||
public string? CanonicalName { get; set; }
|
||||
public string? BaseType { get; set; }
|
||||
public string? SpecificityLevel { get; set; }
|
||||
public bool IsFallbackClassification { get; set; }
|
||||
public string? ReuseScope { get; set; }
|
||||
public int? SourceProjectID { get; set; }
|
||||
public int? SourceBookID { get; set; }
|
||||
public int? SourceChapterID { get; set; }
|
||||
public int? SourceSceneID { get; set; }
|
||||
public string? LinkedEntityType { get; set; }
|
||||
public int? LinkedEntityID { get; set; }
|
||||
public string? GenerationReason { get; set; }
|
||||
public string? GenerationStatus { get; set; }
|
||||
public bool IsArchived { get; set; }
|
||||
public DateTime? ArchivedAt { get; set; }
|
||||
public string? ArchiveReason { get; set; }
|
||||
public int? SupersededByImageID { get; set; }
|
||||
public int? ReuseOfImageID { get; set; }
|
||||
public int AttemptCount { get; set; }
|
||||
public int GenerationAttemptCount { get; set; }
|
||||
public int? ParentItemID { get; set; }
|
||||
@ -200,6 +239,7 @@ public sealed class IllustrationLibrarySummary
|
||||
public int FailedCount { get; set; }
|
||||
public int SupersededCount { get; set; }
|
||||
public int DisabledCount { get; set; }
|
||||
public int ArchivedCount { get; set; }
|
||||
}
|
||||
|
||||
public sealed class IllustrationLibraryFilter
|
||||
@ -229,6 +269,9 @@ public sealed record IllustrationImageGenerationResult(
|
||||
string? Provider = null,
|
||||
string? Model = null,
|
||||
string? ProviderRequestID = null,
|
||||
string? Quality = null,
|
||||
string? Size = null,
|
||||
string? OutputFormat = null,
|
||||
string? ErrorMessage = null)
|
||||
{
|
||||
[JsonIgnore]
|
||||
|
||||
161
PlotLine/Models/NameIntelligenceModels.cs
Normal file
161
PlotLine/Models/NameIntelligenceModels.cs
Normal file
@ -0,0 +1,161 @@
|
||||
namespace PlotLine.Models;
|
||||
|
||||
public static class NameIntelligenceReferencePackage
|
||||
{
|
||||
public const string PackageId = "plotdirector-name-intelligence";
|
||||
public const string Version = "1.0.0";
|
||||
public const string RelativePath = "Data/Reference/Names/v1.0.0";
|
||||
}
|
||||
|
||||
public sealed class NameLocale
|
||||
{
|
||||
public int NameLocaleID { get; set; }
|
||||
public string LocaleCode { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string IsoCountryCode { get; set; } = string.Empty;
|
||||
public string EvidenceBasis { get; set; } = string.Empty;
|
||||
public decimal DefaultConfidence { get; set; }
|
||||
public int SortOrder { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
}
|
||||
|
||||
public sealed class GivenNameReference
|
||||
{
|
||||
public int GivenNameID { get; set; }
|
||||
public string GivenNameKey { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string NormalisedName { get; set; } = string.Empty;
|
||||
public string NameType { get; set; } = string.Empty;
|
||||
public bool IsWordAmbiguous { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class NameUsageProfile
|
||||
{
|
||||
public int NameUsageID { get; set; }
|
||||
public string UsageKey { get; set; } = string.Empty;
|
||||
public int GivenNameID { get; set; }
|
||||
public int NameLocaleID { get; set; }
|
||||
public string LocaleCode { get; set; } = string.Empty;
|
||||
public string LocaleDisplayName { get; set; } = string.Empty;
|
||||
public int BirthYearFrom { get; set; }
|
||||
public int BirthYearTo { get; set; }
|
||||
public decimal MaleWeight { get; set; }
|
||||
public decimal FemaleWeight { get; set; }
|
||||
public decimal UsageWeight { get; set; }
|
||||
public decimal Confidence { get; set; }
|
||||
public string SourceType { get; set; } = string.Empty;
|
||||
public string? SourceReference { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
public string YearMatchKind { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class NameRelationshipReference
|
||||
{
|
||||
public int NameRelationshipID { get; set; }
|
||||
public string RelationshipKey { get; set; } = string.Empty;
|
||||
public int FromGivenNameID { get; set; }
|
||||
public int ToGivenNameID { get; set; }
|
||||
public string FromDisplayName { get; set; } = string.Empty;
|
||||
public string ToDisplayName { get; set; } = string.Empty;
|
||||
public string RelationshipType { get; set; } = string.Empty;
|
||||
public decimal Strength { get; set; }
|
||||
public bool IsBidirectional { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public string? CurationSource { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
}
|
||||
|
||||
public sealed class NameUsageDecoration
|
||||
{
|
||||
public string MatchedName { get; set; } = string.Empty;
|
||||
public string MatchType { get; set; } = string.Empty;
|
||||
public int CharacterID { get; set; }
|
||||
public string CharacterName { get; set; } = string.Empty;
|
||||
public int ProjectID { get; set; }
|
||||
public string ProjectName { get; set; } = string.Empty;
|
||||
public int? BookID { get; set; }
|
||||
public string? BookTitle { get; set; }
|
||||
public string? BookSubtitle { get; set; }
|
||||
public string Scope { get; set; } = string.Empty;
|
||||
public string BookDisplayTitle => BookTitleFormatter.DisplayTitle(BookTitle, BookSubtitle);
|
||||
}
|
||||
|
||||
public sealed class NameRecognitionResult
|
||||
{
|
||||
public bool Recognised { get; init; }
|
||||
public GivenNameReference? Name { get; init; }
|
||||
public NameUsageProfile? Usage { get; init; }
|
||||
public IReadOnlyList<NameRelationshipReference> RelatedForms { get; init; } = [];
|
||||
public string Message { get; init; } = "No reference evidence.";
|
||||
}
|
||||
|
||||
public sealed class NameSearchFilter
|
||||
{
|
||||
public int ProjectID { get; set; }
|
||||
public int? BookID { get; set; }
|
||||
public string? Search { get; set; }
|
||||
public string? LocaleCode { get; set; } = "en-US";
|
||||
public int? BirthYear { get; set; }
|
||||
public string? GenderPreference { get; set; }
|
||||
public decimal? GenderThreshold { get; set; }
|
||||
public string? NameType { get; set; }
|
||||
public decimal? MinimumUsage { get; set; }
|
||||
public string UsedScope { get; set; } = "Project";
|
||||
public bool? UsedOnly { get; set; }
|
||||
public int Page { get; set; } = 1;
|
||||
public int PageSize { get; set; } = 25;
|
||||
}
|
||||
|
||||
public sealed class NameSearchResult
|
||||
{
|
||||
public int GivenNameID { get; set; }
|
||||
public string GivenNameKey { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string NormalisedName { get; set; } = string.Empty;
|
||||
public string NameType { get; set; } = string.Empty;
|
||||
public bool IsWordAmbiguous { get; set; }
|
||||
public decimal? MaleWeight { get; set; }
|
||||
public decimal? FemaleWeight { get; set; }
|
||||
public decimal? UsageWeight { get; set; }
|
||||
public decimal? Confidence { get; set; }
|
||||
public int? BirthYearFrom { get; set; }
|
||||
public int? BirthYearTo { get; set; }
|
||||
public string? LocaleCode { get; set; }
|
||||
public string? LocaleDisplayName { get; set; }
|
||||
public string? SourceType { get; set; }
|
||||
public string? RelatedForms { get; set; }
|
||||
public string? UsedSummary { get; set; }
|
||||
public int UsedCount { get; set; }
|
||||
public int TotalRows { get; set; }
|
||||
}
|
||||
|
||||
public sealed class NameReferenceImportStatus
|
||||
{
|
||||
public int? NameReferenceImportID { get; set; }
|
||||
public string PackageID { get; set; } = string.Empty;
|
||||
public string SemanticVersion { get; set; } = string.Empty;
|
||||
public string? PackageChecksum { get; set; }
|
||||
public DateTime? StartedUtc { get; set; }
|
||||
public DateTime? CompletedUtc { get; set; }
|
||||
public bool Succeeded { get; set; }
|
||||
public int LocaleRows { get; set; }
|
||||
public int GivenNameRows { get; set; }
|
||||
public int RelationshipRows { get; set; }
|
||||
public int UsageRows { get; set; }
|
||||
public string? ErrorSummary { get; set; }
|
||||
}
|
||||
|
||||
public sealed record NameReferenceImportResult(
|
||||
bool Succeeded,
|
||||
bool AlreadyCurrent,
|
||||
string Message,
|
||||
string PackageId,
|
||||
string Version,
|
||||
int LocaleRows,
|
||||
int GivenNameRows,
|
||||
int RelationshipRows,
|
||||
int UsageRows,
|
||||
IReadOnlyList<string> Errors);
|
||||
@ -99,8 +99,16 @@ public sealed class StoryIntelligenceOptions
|
||||
public string Model { get; init; } = string.Empty;
|
||||
public string ChapterStructureModel { get; init; } = string.Empty;
|
||||
public string SceneIntelligenceModel { get; init; } = string.Empty;
|
||||
public string ImageGenerationModel { get; init; } = string.Empty;
|
||||
public string ImageGenerationModel { get; init; } = "gpt-image-1.5";
|
||||
public string ImageGenerationSize { get; init; } = "1024x1024";
|
||||
public string ImageGenerationQuality { get; init; } = "medium";
|
||||
public string ImageGenerationOutputFormat { get; init; } = "webp";
|
||||
public bool AutomaticImageGenerationDuringImportEnabled { get; init; }
|
||||
public bool RequireImageGenerationApproval { get; init; } = true;
|
||||
public int MaxImagesPerImport { get; init; } = 12;
|
||||
public int MaxCharacterImagesPerImport { get; init; } = 4;
|
||||
public int MaxLocationImagesPerImport { get; init; } = 6;
|
||||
public int MaxAssetImagesPerImport { get; init; } = 2;
|
||||
public int MaxOutputTokens { get; init; } = 4000;
|
||||
public int? SceneIntelligenceMaxOutputTokens { get; init; }
|
||||
public int TimeoutSeconds { get; init; } = 120;
|
||||
|
||||
@ -220,6 +220,9 @@ public sealed record StoryMemoryRebuildOptions
|
||||
public bool RebuildAll { get; init; }
|
||||
public bool QueueDemand { get; init; }
|
||||
public int? MaxDemandToQueue { get; init; }
|
||||
public int? MaxCharacterDemandToQueue { get; init; }
|
||||
public int? MaxLocationDemandToQueue { get; init; }
|
||||
public int? MaxAssetDemandToQueue { get; init; }
|
||||
public string AppearancePreference { get; init; } = StoryMemoryAppearancePreferences.Balanced;
|
||||
public string PrimaryAgeGroup { get; init; } = string.Empty;
|
||||
public string StoryEra { get; init; } = string.Empty;
|
||||
@ -230,6 +233,9 @@ public sealed record StoryMemoryResolveOptions
|
||||
{
|
||||
public bool QueueDemand { get; init; }
|
||||
public int? MaxDemandToQueue { get; init; }
|
||||
public int? MaxCharacterDemandToQueue { get; init; }
|
||||
public int? MaxLocationDemandToQueue { get; init; }
|
||||
public int? MaxAssetDemandToQueue { get; init; }
|
||||
public string AppearancePreference { get; init; } = StoryMemoryAppearancePreferences.Balanced;
|
||||
public string PrimaryAgeGroup { get; init; } = string.Empty;
|
||||
public string StoryEra { get; init; } = string.Empty;
|
||||
|
||||
@ -25,6 +25,14 @@
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Include="Data\Reference\Names\**\*.csv;Data\Reference\Names\**\*.md">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
<Content Update="Data\Reference\Names\**\*.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@ -151,6 +151,7 @@ public class Program
|
||||
builder.Services.AddScoped<IStoryIntelligenceIllustrationMatchingRepository, StoryIntelligenceIllustrationMatchingRepository>();
|
||||
builder.Services.AddScoped<IStoryMemoryRepository, StoryMemoryRepository>();
|
||||
builder.Services.AddScoped<ICharacterIntelligenceRepository, CharacterIntelligenceRepository>();
|
||||
builder.Services.AddScoped<INameIntelligenceRepository, NameIntelligenceRepository>();
|
||||
builder.Services.AddScoped<IIllustrationLibraryRepository, IllustrationLibraryRepository>();
|
||||
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
|
||||
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
|
||||
@ -227,6 +228,7 @@ public class Program
|
||||
builder.Services.AddScoped<IStoryIntelligenceVisualisationSnapshotService, StoryIntelligenceVisualisationSnapshotService>();
|
||||
builder.Services.AddScoped<IStoryMemoryService, StoryMemoryService>();
|
||||
builder.Services.AddScoped<ICharacterIntelligenceService, CharacterIntelligenceService>();
|
||||
builder.Services.AddScoped<INameIntelligenceService, NameIntelligenceService>();
|
||||
builder.Services.AddScoped<IStoryIntelligencePipelineStateService, StoryIntelligencePipelineStateService>();
|
||||
builder.Services.AddScoped<IStoryIntelligenceProgressNotifier, StoryIntelligenceProgressNotifier>();
|
||||
builder.Services.AddScoped<ISceneImportResolver, SceneImportResolver>();
|
||||
|
||||
@ -16,7 +16,7 @@ public interface IIllustrationPromptBuilder
|
||||
|
||||
public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
{
|
||||
public const string CurrentTemplateVersion = "21J.3";
|
||||
public const string CurrentTemplateVersion = "22C.1";
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
@ -57,7 +57,7 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
var categoryRules = CategoryRules(specification.Category);
|
||||
|
||||
var prompt = $"""
|
||||
Create a high-quality editorial illustration for PlotDirector's reusable Story Intelligence illustration library.
|
||||
Create a realistic cinematic still for PlotDirector's reusable Story Intelligence illustration library.
|
||||
Category: {specification.Category}
|
||||
Library code: {specification.Code}
|
||||
Title: {specification.Title}
|
||||
@ -71,16 +71,16 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
Category composition rules: {categoryRules}
|
||||
|
||||
Requirements:
|
||||
- representative visual identifier only, not a canonical depiction of any manuscript
|
||||
- modern cinematic digital illustration with premium narrative game concept art quality
|
||||
- subdued atmospheric background treatment with contemporary polished rendering, realistic anatomy and believable proportions
|
||||
- realistic grounded visual identifier only, not a canonical depiction of any manuscript
|
||||
- naturalistic cinematic still, close to believable production photography while remaining safe for a reusable UI library
|
||||
- subtle 1980s UK period detail where requested, grounded wardrobe and hair, not costume-party exaggeration
|
||||
- neutral background treatment suitable for PlotDirector UI, with believable anatomy and proportions
|
||||
- balanced cinematic lighting, clear facial illumination where faces are present, and strong local contrast
|
||||
- crisp subject definition with strong foreground-background separation and clean readable silhouettes
|
||||
- natural controlled colour with rich but restrained accents and warm skin tones where appropriate
|
||||
- polished promotional artwork for a premium story-driven adventure game, not a literal photograph
|
||||
- subtle depth of field, clean modern rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
|
||||
- subtle depth of field, clean realistic rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
|
||||
- no visible text, captions, logos, watermarks, UI, signatures, or typography
|
||||
- no anime, cartoon, Pixar-like, caricature, celebrity likeness, or photoreal identity claim
|
||||
- no anime, cartoon, Pixar-like, caricature, celebrity likeness, oil painting, brushwork, painterly texture, fantasy gloss, fashion glamour portrait, or over-stylised concept art
|
||||
- for character images, the specified hair colour, hair length, apparent age, era, location context and skin tone are hard requirements; make hair colour plainly visible at thumbnail size and do not reinterpret or diversify away from it
|
||||
- for character images, vary face shape, nose, eyes, mouth, jaw, expression, hairstyle silhouette, and posture so reusable portraits do not look like siblings unless the specification explicitly asks for family resemblance
|
||||
- no bright flat studio backdrop, no plain colour-block background, no stock-photo lighting, and no cartoon simplicity
|
||||
@ -103,8 +103,8 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
private static string CategoryRules(string category)
|
||||
=> category switch
|
||||
{
|
||||
IllustrationLibraryCategories.Character => "Square image; head-and-shoulders or upper body; one person only; face-forward enough to read at small size; naturally expressive; cleanly lit face; distinct from a subtle atmospheric story background; suitable for circular cropping; avoid bright flat studio backgrounds, plain colour blocks, dark collars vanishing into black backgrounds, and photographic headshot styling.",
|
||||
IllustrationLibraryCategories.Location => "One clear focal environment; vivid and readable; clearly lit and visually inviting even when dramatic; rich colour and depth; strong composition for circular and landscape crops; no prominent people; no text or readable signage; do not default every location to nighttime, rain-soaked, blue-black, ominous, or haunted-looking.",
|
||||
IllustrationLibraryCategories.Character => "Square image; realistic head-and-shoulders or upper body; one person only; face-forward enough to read at small size; naturally expressive; cleanly lit face; distinct from a subtle grounded story background; suitable for circular cropping; avoid bright flat studio backgrounds, plain colour blocks, dark collars vanishing into black backgrounds, and fashion headshot styling.",
|
||||
IllustrationLibraryCategories.Location => "One clear focal environment; specific readable place type; realistic light and materials; strong composition for circular and landscape crops; no prominent people; no text or readable signage; do not default every location to nighttime, rain-soaked, blue-black, ominous, haunted-looking, or generic domestic interior.",
|
||||
IllustrationLibraryCategories.Asset => "Square image; one dominant centred object; bright clean high-contrast presentation; crisp silhouette; clear separation from background; immediately recognisable at small size; entire object visible where practical.",
|
||||
_ => string.Empty
|
||||
};
|
||||
@ -333,6 +333,9 @@ public interface IIllustrationImageProvider
|
||||
{
|
||||
string ProviderName { get; }
|
||||
string EffectiveModel { get; }
|
||||
string EffectiveQuality { get; }
|
||||
string EffectiveSize { get; }
|
||||
string EffectiveOutputFormat { get; }
|
||||
bool IsConfigured { get; }
|
||||
IReadOnlyList<string> MissingConfiguration { get; }
|
||||
Task<IllustrationImageGenerationResult> GenerateAsync(IllustrationImageGenerationRequest request, CancellationToken cancellationToken);
|
||||
@ -347,6 +350,9 @@ public sealed class OpenAIIllustrationImageProvider(
|
||||
|
||||
public string ProviderName => "OpenAI";
|
||||
public string EffectiveModel => string.IsNullOrWhiteSpace(_options.ImageGenerationModel) ? "not configured" : _options.ImageGenerationModel;
|
||||
public string EffectiveQuality => string.IsNullOrWhiteSpace(_options.ImageGenerationQuality) ? "auto" : _options.ImageGenerationQuality;
|
||||
public string EffectiveSize => string.IsNullOrWhiteSpace(_options.ImageGenerationSize) ? "1024x1024" : _options.ImageGenerationSize;
|
||||
public string EffectiveOutputFormat => string.IsNullOrWhiteSpace(_options.ImageGenerationOutputFormat) ? "default" : _options.ImageGenerationOutputFormat;
|
||||
public bool IsConfigured => MissingConfiguration.Count == 0;
|
||||
public IReadOnlyList<string> MissingConfiguration
|
||||
{
|
||||
@ -374,21 +380,35 @@ public sealed class OpenAIIllustrationImageProvider(
|
||||
return new(false, Provider: ProviderName, Model: EffectiveModel, ErrorMessage: $"OpenAI image generation is not configured. Missing: {string.Join(", ", MissingConfiguration)}.");
|
||||
}
|
||||
|
||||
var size = string.IsNullOrWhiteSpace(_options.ImageGenerationSize) ? "1024x1024" : _options.ImageGenerationSize;
|
||||
var quality = string.IsNullOrWhiteSpace(_options.ImageGenerationQuality) ? null : _options.ImageGenerationQuality;
|
||||
var outputFormat = string.IsNullOrWhiteSpace(_options.ImageGenerationOutputFormat) ? null : _options.ImageGenerationOutputFormat;
|
||||
var body = new Dictionary<string, object?>
|
||||
{
|
||||
["model"] = _options.ImageGenerationModel,
|
||||
["prompt"] = request.Prompt,
|
||||
["size"] = size,
|
||||
["n"] = 1
|
||||
};
|
||||
if (!string.IsNullOrWhiteSpace(quality))
|
||||
{
|
||||
body["quality"] = quality;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(outputFormat))
|
||||
{
|
||||
body["output_format"] = outputFormat;
|
||||
}
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/images/generations");
|
||||
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiKey);
|
||||
httpRequest.Content = new StringContent(JsonSerializer.Serialize(new
|
||||
{
|
||||
model = _options.ImageGenerationModel,
|
||||
prompt = request.Prompt,
|
||||
size = string.IsNullOrWhiteSpace(_options.ImageGenerationSize) ? "1024x1024" : _options.ImageGenerationSize,
|
||||
n = 1
|
||||
}), Encoding.UTF8, "application/json");
|
||||
httpRequest.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
|
||||
|
||||
using var response = await httpClient.SendAsync(httpRequest, cancellationToken);
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var error = OpenAIImageError.FromResponseBody(body);
|
||||
var error = OpenAIImageError.FromResponseBody(responseBody);
|
||||
logger.LogWarning(
|
||||
"OpenAI image generation failed for illustration {ItemID} with HTTP {StatusCode}, error code {ErrorCode}: {ErrorMessage}",
|
||||
request.IllustrationLibraryItemID,
|
||||
@ -398,7 +418,7 @@ public sealed class OpenAIIllustrationImageProvider(
|
||||
return new(false, Provider: ProviderName, Model: EffectiveModel, ErrorMessage: $"Provider returned {(int)response.StatusCode}: {error.ForStorage()}");
|
||||
}
|
||||
|
||||
using var document = JsonDocument.Parse(body);
|
||||
using var document = JsonDocument.Parse(responseBody);
|
||||
var requestId = response.Headers.TryGetValues("x-request-id", out var values) ? values.FirstOrDefault() : null;
|
||||
var b64 = document.RootElement.GetProperty("data")[0].GetProperty("b64_json").GetString();
|
||||
if (string.IsNullOrWhiteSpace(b64))
|
||||
@ -406,7 +426,7 @@ public sealed class OpenAIIllustrationImageProvider(
|
||||
return new(false, Provider: ProviderName, Model: EffectiveModel, ProviderRequestID: requestId, ErrorMessage: "Provider response did not include image data.");
|
||||
}
|
||||
|
||||
return new(true, Convert.FromBase64String(b64), "image/png", ProviderName, EffectiveModel, requestId);
|
||||
return new(true, Convert.FromBase64String(b64), "image/png", ProviderName, EffectiveModel, requestId, quality, size, outputFormat);
|
||||
}
|
||||
|
||||
private static string TrimForStorage(string value)
|
||||
@ -560,6 +580,16 @@ public sealed class IllustrationGenerationRunner(
|
||||
|
||||
var prompt = promptBuilder.Build(specification);
|
||||
var request = new IllustrationImageGenerationRequest(item.IllustrationLibraryItemID, item.StableCode, item.Category, prompt.Prompt, prompt.TemplateVersion);
|
||||
logger.LogInformation(
|
||||
"Illustration generation decision: generating queued item. IllustrationLibraryItemID={IllustrationLibraryItemID} Category={Category} StableCode={StableCode} Model={Model} Quality={Quality} Size={Size} OutputFormat={OutputFormat} PromptTemplateVersion={PromptTemplateVersion}",
|
||||
item.IllustrationLibraryItemID,
|
||||
item.Category,
|
||||
item.StableCode,
|
||||
provider.EffectiveModel,
|
||||
provider.EffectiveQuality,
|
||||
provider.EffectiveSize,
|
||||
provider.EffectiveOutputFormat,
|
||||
prompt.TemplateVersion);
|
||||
var result = await provider.GenerateAsync(request, cancellationToken);
|
||||
if (!result.Succeeded || !result.HasImage)
|
||||
{
|
||||
@ -928,6 +958,8 @@ public sealed class IllustrationLibraryService(
|
||||
var items = await repository.ListAsync(new IllustrationLibraryFilter());
|
||||
var lookup = items
|
||||
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated)
|
||||
.Where(item => item.IsActive && !item.IsArchived)
|
||||
.Where(item => !item.IsFallbackClassification)
|
||||
.OrderBy(item => item.Status == IllustrationLibraryStatuses.Approved ? 0 : 1)
|
||||
.ThenByDescending(item => item.UpdatedUtc)
|
||||
.GroupBy(item => item.StableCode, StringComparer.OrdinalIgnoreCase)
|
||||
|
||||
569
PlotLine/Services/NameIntelligenceService.cs
Normal file
569
PlotLine/Services/NameIntelligenceService.cs
Normal file
@ -0,0 +1,569 @@
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Dapper;
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.ViewModels;
|
||||
|
||||
namespace PlotLine.Services;
|
||||
|
||||
public interface INameIntelligenceService
|
||||
{
|
||||
Task<NameReferenceImportResult> EnsureReferenceDataImportedAsync(bool force = false, CancellationToken cancellationToken = default);
|
||||
Task<NameReferenceImportResult> ValidatePackageAsync(CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NameLocale>> ListLocalesAsync(CancellationToken cancellationToken = default);
|
||||
Task<NameRecognitionResult> RecogniseAsync(string rawName, string? localeCode = null, int? birthYear = null, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<NameSearchResult>> SearchAsync(NameSearchFilter filter, CancellationToken cancellationToken = default);
|
||||
Task<NamesLibraryViewModel?> BuildLibraryAsync(NameSearchFilter filter, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public sealed class NameIntelligenceService(
|
||||
INameIntelligenceRepository repository,
|
||||
ISqlConnectionFactory connectionFactory,
|
||||
IProjectRepository projects,
|
||||
IBookRepository books,
|
||||
ICurrentUserService currentUser,
|
||||
IWebHostEnvironment environment,
|
||||
ILogger<NameIntelligenceService> logger) : INameIntelligenceService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
public async Task<NameReferenceImportResult> EnsureReferenceDataImportedAsync(bool force = false, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var package = await ReadAndValidatePackageAsync(cancellationToken);
|
||||
if (package.Errors.Count > 0)
|
||||
{
|
||||
return package.ToResult(false, false, "Name reference package validation failed.");
|
||||
}
|
||||
|
||||
var latest = await repository.GetLatestImportAsync(package.Manifest.PackageId, package.Manifest.Version, cancellationToken);
|
||||
if (!force && latest is not null && string.Equals(latest.PackageChecksum, package.Checksum, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return package.ToResult(true, true, "Name reference package is already imported.");
|
||||
}
|
||||
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
connection.Open();
|
||||
using var transaction = connection.BeginTransaction();
|
||||
var importId = 0;
|
||||
try
|
||||
{
|
||||
importId = await repository.StartImportAsync(package.Manifest.PackageId, package.Manifest.Version, package.Checksum, connection, transaction, cancellationToken);
|
||||
await repository.UpsertLocalesAsync(package.LocaleTable, connection, transaction, cancellationToken);
|
||||
await repository.UpsertGivenNamesAsync(package.GivenNameTable, connection, transaction, cancellationToken);
|
||||
await repository.UpsertRelationshipsAsync(package.RelationshipTable, connection, transaction, cancellationToken);
|
||||
foreach (var batch in package.UsageTables)
|
||||
{
|
||||
await repository.UpsertUsagesAsync(batch, connection, transaction, cancellationToken);
|
||||
}
|
||||
|
||||
await repository.CompleteImportAsync(importId, true, package.LocaleRows, package.GivenNameRows, package.RelationshipRows, package.UsageRows, null, connection, transaction, cancellationToken);
|
||||
transaction.Commit();
|
||||
return package.ToResult(true, false, "Name reference package imported.");
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||||
{
|
||||
logger.LogError(ex, "Name reference import failed for package {PackageId} {Version}.", package.Manifest.PackageId, package.Manifest.Version);
|
||||
try
|
||||
{
|
||||
if (importId > 0)
|
||||
{
|
||||
await repository.CompleteImportAsync(importId, false, 0, 0, 0, 0, ex.Message, connection, transaction, cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
transaction.Rollback();
|
||||
}
|
||||
|
||||
return package.ToResult(false, false, "Name reference package import failed.", [ex.Message]);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<NameReferenceImportResult> ValidatePackageAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var package = await ReadAndValidatePackageAsync(cancellationToken);
|
||||
return package.ToResult(package.Errors.Count == 0, false, package.Errors.Count == 0 ? "Name reference package validation succeeded." : "Name reference package validation failed.");
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<NameLocale>> ListLocalesAsync(CancellationToken cancellationToken = default)
|
||||
=> repository.ListLocalesAsync(cancellationToken);
|
||||
|
||||
public Task<NameRecognitionResult> RecogniseAsync(string rawName, string? localeCode = null, int? birthYear = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalised = NormaliseName(rawName);
|
||||
return string.IsNullOrWhiteSpace(normalised)
|
||||
? Task.FromResult(new NameRecognitionResult())
|
||||
: repository.RecogniseAsync(normalised, localeCode, birthYear, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<NameSearchResult>> SearchAsync(NameSearchFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (currentUser.UserId is not int userId || filter.ProjectID <= 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
filter.Page = Math.Max(1, filter.Page);
|
||||
filter.PageSize = Math.Clamp(filter.PageSize, 1, 100);
|
||||
filter.UsedScope = string.IsNullOrWhiteSpace(filter.UsedScope) ? "Project" : filter.UsedScope.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(filter.GenderPreference) && !filter.GenderThreshold.HasValue)
|
||||
{
|
||||
filter.GenderThreshold = string.Equals(filter.GenderPreference, "Mixed", StringComparison.OrdinalIgnoreCase)
|
||||
? 0.65m
|
||||
: 0.5m;
|
||||
}
|
||||
|
||||
return await repository.SearchAsync(userId, filter, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<NamesLibraryViewModel?> BuildLibraryAsync(NameSearchFilter filter, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (currentUser.UserId is not int userId || filter.ProjectID <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var project = await projects.GetForUserAsync(filter.ProjectID, userId);
|
||||
if (project is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var projectBooks = await books.ListByProjectAsync(filter.ProjectID);
|
||||
var rows = await SearchAsync(filter, cancellationToken);
|
||||
return new NamesLibraryViewModel
|
||||
{
|
||||
Project = project,
|
||||
Filter = filter,
|
||||
Locales = await ListLocalesAsync(cancellationToken),
|
||||
Books = projectBooks,
|
||||
Results = rows
|
||||
};
|
||||
}
|
||||
|
||||
public static string NormaliseName(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value)
|
||||
? string.Empty
|
||||
: value.Trim().Normalize(NormalizationForm.FormKC).ToUpperInvariant();
|
||||
|
||||
private async Task<ValidatedNamePackage> ReadAndValidatePackageAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var root = Path.Combine(environment.ContentRootPath, NameIntelligenceReferencePackage.RelativePath);
|
||||
var errors = new List<string>();
|
||||
var manifestPath = Path.Combine(root, "manifest.json");
|
||||
if (!File.Exists(manifestPath))
|
||||
{
|
||||
return ValidatedNamePackage.Empty(errors.Append($"Missing manifest: {manifestPath}").ToList());
|
||||
}
|
||||
|
||||
var manifest = JsonSerializer.Deserialize<NamePackageManifest>(await File.ReadAllTextAsync(manifestPath, cancellationToken), JsonOptions)
|
||||
?? new NamePackageManifest();
|
||||
if (!string.Equals(manifest.PackageId, NameIntelligenceReferencePackage.PackageId, StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add($"Unexpected package id '{manifest.PackageId}'.");
|
||||
}
|
||||
|
||||
if (!string.Equals(manifest.Version, NameIntelligenceReferencePackage.Version, StringComparison.Ordinal))
|
||||
{
|
||||
errors.Add($"Unexpected package version '{manifest.Version}'.");
|
||||
}
|
||||
|
||||
var checksum = ComputePackageChecksum(root, manifest);
|
||||
var locales = ReadCsv(Path.Combine(root, "name_locales.csv"), ["locale_code", "display_name", "iso_country_code", "evidence_basis", "default_confidence", "sort_order", "is_active"], errors);
|
||||
var names = ReadCsv(Path.Combine(root, "given_names.csv"), ["given_name_key", "display_name", "normalised_name", "name_type", "is_word_ambiguous", "is_active", "notes"], errors);
|
||||
var relationships = ReadCsv(Path.Combine(root, "name_relationships.csv"), ["relationship_key", "from_given_name_key", "to_given_name_key", "relationship_type", "strength", "is_bidirectional", "is_active", "curation_source", "notes"], errors);
|
||||
var usages = ReadCsv(Path.Combine(root, "name_usages.csv"), ["usage_key", "given_name_key", "locale_code", "birth_year_from", "birth_year_to", "male_weight", "female_weight", "usage_weight", "confidence", "source_type", "source_reference", "is_active", "notes"], errors);
|
||||
|
||||
ValidateCounts(manifest, locales.Count, names.Count, relationships.Count, usages.Count, errors);
|
||||
ValidatePackageRows(manifest, locales, names, relationships, usages, errors);
|
||||
|
||||
return new ValidatedNamePackage(
|
||||
manifest,
|
||||
checksum,
|
||||
BuildLocaleTable(locales),
|
||||
BuildGivenNameTable(names),
|
||||
BuildRelationshipTable(relationships),
|
||||
BuildUsageTables(usages),
|
||||
locales.Count,
|
||||
names.Count,
|
||||
relationships.Count,
|
||||
usages.Count,
|
||||
errors);
|
||||
}
|
||||
|
||||
private static void ValidateCounts(NamePackageManifest manifest, int localeRows, int nameRows, int relationshipRows, int usageRows, List<string> errors)
|
||||
{
|
||||
if (manifest.Files.TryGetValue("name_locales", out var locales) && locales.Rows != localeRows) errors.Add($"Locale row count mismatch: manifest {locales.Rows}, file {localeRows}.");
|
||||
if (manifest.Files.TryGetValue("given_names", out var names) && names.Rows != nameRows) errors.Add($"Given name row count mismatch: manifest {names.Rows}, file {nameRows}.");
|
||||
if (manifest.Files.TryGetValue("name_relationships", out var relationships) && relationships.Rows != relationshipRows) errors.Add($"Relationship row count mismatch: manifest {relationships.Rows}, file {relationshipRows}.");
|
||||
if (manifest.Files.TryGetValue("name_usages", out var usages) && usages.Rows != usageRows) errors.Add($"Usage row count mismatch: manifest {usages.Rows}, file {usageRows}.");
|
||||
}
|
||||
|
||||
private static void ValidatePackageRows(NamePackageManifest manifest, List<Dictionary<string, string>> locales, List<Dictionary<string, string>> names, List<Dictionary<string, string>> relationships, List<Dictionary<string, string>> usages, List<string> errors)
|
||||
{
|
||||
var localeCodes = new HashSet<string>(StringComparer.Ordinal);
|
||||
var nameKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
var normalisedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var relationshipKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
var usageKeys = new HashSet<string>(StringComparer.Ordinal);
|
||||
var usageBands = new Dictionary<string, List<(int From, int To)>>(StringComparer.Ordinal);
|
||||
var nameTypes = manifest.ValueSets.GetValueOrDefault("nameType") ?? [];
|
||||
var relationshipTypes = manifest.ValueSets.GetValueOrDefault("relationshipType") ?? [];
|
||||
var sourceTypes = manifest.ValueSets.GetValueOrDefault("sourceType") ?? [];
|
||||
|
||||
foreach (var row in locales)
|
||||
{
|
||||
RequireUnique(row["locale_code"], localeCodes, "locale_code", errors);
|
||||
ValidateDecimal(row["default_confidence"], "default_confidence", errors);
|
||||
ValidateBool(row["is_active"], "locale is_active", errors);
|
||||
}
|
||||
|
||||
foreach (var row in names)
|
||||
{
|
||||
RequireUnique(row["given_name_key"], nameKeys, "given_name_key", errors);
|
||||
RequireUnique(row["normalised_name"], normalisedNames, "normalised_name", errors);
|
||||
RequireControlled(row["name_type"], nameTypes, "name_type", errors);
|
||||
ValidateBool(row["is_word_ambiguous"], "is_word_ambiguous", errors);
|
||||
ValidateBool(row["is_active"], "given name is_active", errors);
|
||||
}
|
||||
|
||||
foreach (var row in relationships)
|
||||
{
|
||||
RequireUnique(row["relationship_key"], relationshipKeys, "relationship_key", errors);
|
||||
RequireForeign(row["from_given_name_key"], nameKeys, "relationship from_given_name_key", errors);
|
||||
RequireForeign(row["to_given_name_key"], nameKeys, "relationship to_given_name_key", errors);
|
||||
if (row["from_given_name_key"] == row["to_given_name_key"]) errors.Add($"Self relationship '{row["relationship_key"]}' is not allowed.");
|
||||
RequireControlled(row["relationship_type"], relationshipTypes, "relationship_type", errors);
|
||||
ValidateDecimal(row["strength"], "strength", errors);
|
||||
ValidateBool(row["is_bidirectional"], "is_bidirectional", errors);
|
||||
ValidateBool(row["is_active"], "relationship is_active", errors);
|
||||
}
|
||||
|
||||
foreach (var row in usages)
|
||||
{
|
||||
RequireUnique(row["usage_key"], usageKeys, "usage_key", errors);
|
||||
RequireForeign(row["given_name_key"], nameKeys, "usage given_name_key", errors);
|
||||
RequireForeign(row["locale_code"], localeCodes, "usage locale_code", errors);
|
||||
var from = ValidateInt(row["birth_year_from"], "birth_year_from", errors);
|
||||
var to = ValidateInt(row["birth_year_to"], "birth_year_to", errors);
|
||||
if (from > to) errors.Add($"Usage '{row["usage_key"]}' has invalid year range.");
|
||||
var male = ValidateDecimal(row["male_weight"], "male_weight", errors);
|
||||
var female = ValidateDecimal(row["female_weight"], "female_weight", errors);
|
||||
ValidateDecimal(row["usage_weight"], "usage_weight", errors);
|
||||
ValidateDecimal(row["confidence"], "confidence", errors);
|
||||
if (Math.Abs((male + female) - 1m) > 0.0002m) errors.Add($"Usage '{row["usage_key"]}' gender weights do not total 1.");
|
||||
RequireControlled(row["source_type"], sourceTypes, "source_type", errors);
|
||||
ValidateBool(row["is_active"], "usage is_active", errors);
|
||||
var group = row["given_name_key"] + "|" + row["locale_code"];
|
||||
if (!usageBands.TryGetValue(group, out var bands))
|
||||
{
|
||||
bands = [];
|
||||
usageBands[group] = bands;
|
||||
}
|
||||
bands.Add((from, to));
|
||||
}
|
||||
|
||||
foreach (var pair in usageBands)
|
||||
{
|
||||
var bands = pair.Value.OrderBy(item => item.From).ToList();
|
||||
if (bands.Count != 5 || bands[0].From != 1900 || bands[^1].To != 2026)
|
||||
{
|
||||
errors.Add($"Usage bands for {pair.Key} must contain five bands covering 1900-2026.");
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = 1; i < bands.Count; i++)
|
||||
{
|
||||
if (bands[i].From != bands[i - 1].To + 1)
|
||||
{
|
||||
errors.Add($"Usage bands for {pair.Key} are not contiguous.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Dictionary<string, string>> ReadCsv(string path, IReadOnlyList<string> expectedHeaders, List<string> errors)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
errors.Add($"Missing CSV file: {path}");
|
||||
return [];
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(path, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
|
||||
var headerLine = reader.ReadLine();
|
||||
var headers = ParseCsvLine(headerLine ?? string.Empty);
|
||||
if (!headers.SequenceEqual(expectedHeaders, StringComparer.Ordinal))
|
||||
{
|
||||
errors.Add($"Unexpected headers in {Path.GetFileName(path)}.");
|
||||
return [];
|
||||
}
|
||||
|
||||
var rows = new List<Dictionary<string, string>>();
|
||||
string? line;
|
||||
while ((line = reader.ReadLine()) is not null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var values = ParseCsvLine(line);
|
||||
if (values.Count != headers.Count)
|
||||
{
|
||||
errors.Add($"Invalid CSV column count in {Path.GetFileName(path)} at row {rows.Count + 2}.");
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.Add(headers.Zip(values, (key, value) => new { key, value }).ToDictionary(item => item.key, item => item.value, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static List<string> ParseCsvLine(string line)
|
||||
{
|
||||
var values = new List<string>();
|
||||
var builder = new StringBuilder();
|
||||
var quoted = false;
|
||||
for (var i = 0; i < line.Length; i++)
|
||||
{
|
||||
var ch = line[i];
|
||||
if (quoted)
|
||||
{
|
||||
if (ch == '"' && i + 1 < line.Length && line[i + 1] == '"')
|
||||
{
|
||||
builder.Append('"');
|
||||
i++;
|
||||
}
|
||||
else if (ch == '"')
|
||||
{
|
||||
quoted = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(ch);
|
||||
}
|
||||
}
|
||||
else if (ch == ',')
|
||||
{
|
||||
values.Add(builder.ToString());
|
||||
builder.Clear();
|
||||
}
|
||||
else if (ch == '"')
|
||||
{
|
||||
quoted = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
builder.Append(ch);
|
||||
}
|
||||
}
|
||||
|
||||
values.Add(builder.ToString());
|
||||
return values;
|
||||
}
|
||||
|
||||
private static DataTable BuildLocaleTable(IEnumerable<Dictionary<string, string>> rows)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("LocaleCode", typeof(string));
|
||||
table.Columns.Add("DisplayName", typeof(string));
|
||||
table.Columns.Add("IsoCountryCode", typeof(string));
|
||||
table.Columns.Add("EvidenceBasis", typeof(string));
|
||||
table.Columns.Add("DefaultConfidence", typeof(decimal));
|
||||
table.Columns.Add("SortOrder", typeof(int));
|
||||
table.Columns.Add("IsActive", typeof(bool));
|
||||
foreach (var row in rows)
|
||||
{
|
||||
table.Rows.Add(row["locale_code"], row["display_name"], row["iso_country_code"], row["evidence_basis"], Decimal(row["default_confidence"]), Int(row["sort_order"]), Bool(row["is_active"]));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private static DataTable BuildGivenNameTable(IEnumerable<Dictionary<string, string>> rows)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("GivenNameKey", typeof(string));
|
||||
table.Columns.Add("DisplayName", typeof(string));
|
||||
table.Columns.Add("NormalisedName", typeof(string));
|
||||
table.Columns.Add("NameType", typeof(string));
|
||||
table.Columns.Add("IsWordAmbiguous", typeof(bool));
|
||||
table.Columns.Add("IsActive", typeof(bool));
|
||||
table.Columns.Add("Notes", typeof(string));
|
||||
foreach (var row in rows)
|
||||
{
|
||||
table.Rows.Add(row["given_name_key"], row["display_name"], row["normalised_name"], row["name_type"], Bool(row["is_word_ambiguous"]), Bool(row["is_active"]), DbNull(row["notes"]));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private static DataTable BuildRelationshipTable(IEnumerable<Dictionary<string, string>> rows)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("RelationshipKey", typeof(string));
|
||||
table.Columns.Add("FromGivenNameKey", typeof(string));
|
||||
table.Columns.Add("ToGivenNameKey", typeof(string));
|
||||
table.Columns.Add("RelationshipType", typeof(string));
|
||||
table.Columns.Add("Strength", typeof(decimal));
|
||||
table.Columns.Add("IsBidirectional", typeof(bool));
|
||||
table.Columns.Add("IsActive", typeof(bool));
|
||||
table.Columns.Add("CurationSource", typeof(string));
|
||||
table.Columns.Add("Notes", typeof(string));
|
||||
foreach (var row in rows)
|
||||
{
|
||||
table.Rows.Add(row["relationship_key"], row["from_given_name_key"], row["to_given_name_key"], row["relationship_type"], Decimal(row["strength"]), Bool(row["is_bidirectional"]), Bool(row["is_active"]), DbNull(row["curation_source"]), DbNull(row["notes"]));
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<DataTable> BuildUsageTables(IEnumerable<Dictionary<string, string>> rows)
|
||||
{
|
||||
var tables = new List<DataTable>();
|
||||
var table = NewUsageTable();
|
||||
foreach (var row in rows)
|
||||
{
|
||||
if (table.Rows.Count >= 5000)
|
||||
{
|
||||
tables.Add(table);
|
||||
table = NewUsageTable();
|
||||
}
|
||||
|
||||
table.Rows.Add(row["usage_key"], row["given_name_key"], row["locale_code"], Int(row["birth_year_from"]), Int(row["birth_year_to"]), Decimal(row["male_weight"]), Decimal(row["female_weight"]), Decimal(row["usage_weight"]), Decimal(row["confidence"]), row["source_type"], DbNull(row["source_reference"]), Bool(row["is_active"]), DbNull(row["notes"]));
|
||||
}
|
||||
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
tables.Add(table);
|
||||
}
|
||||
|
||||
return tables;
|
||||
}
|
||||
|
||||
private static DataTable NewUsageTable()
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("UsageKey", typeof(string));
|
||||
table.Columns.Add("GivenNameKey", typeof(string));
|
||||
table.Columns.Add("LocaleCode", typeof(string));
|
||||
table.Columns.Add("BirthYearFrom", typeof(int));
|
||||
table.Columns.Add("BirthYearTo", typeof(int));
|
||||
table.Columns.Add("MaleWeight", typeof(decimal));
|
||||
table.Columns.Add("FemaleWeight", typeof(decimal));
|
||||
table.Columns.Add("UsageWeight", typeof(decimal));
|
||||
table.Columns.Add("Confidence", typeof(decimal));
|
||||
table.Columns.Add("SourceType", typeof(string));
|
||||
table.Columns.Add("SourceReference", typeof(string));
|
||||
table.Columns.Add("IsActive", typeof(bool));
|
||||
table.Columns.Add("Notes", typeof(string));
|
||||
return table;
|
||||
}
|
||||
|
||||
private static string ComputePackageChecksum(string root, NamePackageManifest manifest)
|
||||
{
|
||||
using var hash = SHA256.Create();
|
||||
foreach (var fileName in manifest.ImportOrder)
|
||||
{
|
||||
var bytes = File.ReadAllBytes(Path.Combine(root, fileName));
|
||||
hash.TransformBlock(bytes, 0, bytes.Length, null, 0);
|
||||
}
|
||||
hash.TransformFinalBlock([], 0, 0);
|
||||
return Convert.ToHexString(hash.Hash ?? []);
|
||||
}
|
||||
|
||||
private static void RequireUnique(string value, HashSet<string> values, string label, List<string> errors)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || !values.Add(value))
|
||||
{
|
||||
errors.Add($"Duplicate or empty {label}: '{value}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequireForeign(string value, HashSet<string> values, string label, List<string> errors)
|
||||
{
|
||||
if (!values.Contains(value))
|
||||
{
|
||||
errors.Add($"Unknown {label}: '{value}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void RequireControlled(string value, IReadOnlyCollection<string> allowed, string label, List<string> errors)
|
||||
{
|
||||
if (!allowed.Contains(value, StringComparer.Ordinal))
|
||||
{
|
||||
errors.Add($"Invalid {label}: '{value}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static int ValidateInt(string value, string label, List<string> errors)
|
||||
{
|
||||
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
errors.Add($"Invalid integer {label}: '{value}'.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static decimal ValidateDecimal(string value, string label, List<string> errors)
|
||||
{
|
||||
if (decimal.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out var result) && result >= 0 && result <= 1)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
errors.Add($"Invalid decimal {label}: '{value}'.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static void ValidateBool(string value, string label, List<string> errors)
|
||||
{
|
||||
if (!bool.TryParse(value, out _))
|
||||
{
|
||||
errors.Add($"Invalid boolean {label}: '{value}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static int Int(string value) => int.Parse(value, CultureInfo.InvariantCulture);
|
||||
private static decimal Decimal(string value) => decimal.Parse(value, CultureInfo.InvariantCulture);
|
||||
private static bool Bool(string value) => bool.Parse(value);
|
||||
private static object DbNull(string value) => string.IsNullOrWhiteSpace(value) ? DBNull.Value : value;
|
||||
|
||||
private sealed record ValidatedNamePackage(
|
||||
NamePackageManifest Manifest,
|
||||
string Checksum,
|
||||
DataTable LocaleTable,
|
||||
DataTable GivenNameTable,
|
||||
DataTable RelationshipTable,
|
||||
IReadOnlyList<DataTable> UsageTables,
|
||||
int LocaleRows,
|
||||
int GivenNameRows,
|
||||
int RelationshipRows,
|
||||
int UsageRows,
|
||||
IReadOnlyList<string> Errors)
|
||||
{
|
||||
public static ValidatedNamePackage Empty(IReadOnlyList<string> errors) => new(new(), string.Empty, new(), new(), new(), [], 0, 0, 0, 0, errors);
|
||||
|
||||
public NameReferenceImportResult ToResult(bool succeeded, bool alreadyCurrent, string message, IReadOnlyList<string>? extraErrors = null)
|
||||
=> new(succeeded, alreadyCurrent, message, Manifest.PackageId, Manifest.Version, LocaleRows, GivenNameRows, RelationshipRows, UsageRows, Errors.Concat(extraErrors ?? []).ToList());
|
||||
}
|
||||
|
||||
private sealed class NamePackageManifest
|
||||
{
|
||||
public string PackageId { get; set; } = string.Empty;
|
||||
public string Version { get; set; } = string.Empty;
|
||||
public Dictionary<string, NamePackageFile> Files { get; set; } = [];
|
||||
public Dictionary<string, string[]> ValueSets { get; set; } = [];
|
||||
public string[] ImportOrder { get; set; } = [];
|
||||
}
|
||||
|
||||
private sealed class NamePackageFile
|
||||
{
|
||||
public string File { get; set; } = string.Empty;
|
||||
public int Rows { get; set; }
|
||||
}
|
||||
}
|
||||
@ -344,11 +344,21 @@ public sealed class PersistedStoryIntelligenceRunner(
|
||||
{
|
||||
try
|
||||
{
|
||||
var queueImageDemand = settings.AutomaticImageGenerationDuringImportEnabled
|
||||
&& !settings.RequireImageGenerationApproval
|
||||
&& settings.MaxImagesPerImport > 0;
|
||||
await storyMemory.ProcessSceneResultAsync(
|
||||
importSessionId.Value,
|
||||
run.UserID,
|
||||
sceneResultId,
|
||||
new StoryMemoryResolveOptions { QueueDemand = true, MaxDemandToQueue = 24 },
|
||||
new StoryMemoryResolveOptions
|
||||
{
|
||||
QueueDemand = queueImageDemand,
|
||||
MaxDemandToQueue = settings.MaxImagesPerImport,
|
||||
MaxCharacterDemandToQueue = settings.MaxCharacterImagesPerImport,
|
||||
MaxLocationDemandToQueue = settings.MaxLocationImagesPerImport,
|
||||
MaxAssetDemandToQueue = settings.MaxAssetImagesPerImport
|
||||
},
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception ex) when (ex is not OperationCanceledException && !IsFatal(ex))
|
||||
|
||||
@ -17,6 +17,7 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
ICharacterRepository characters,
|
||||
ISceneRepository scenes,
|
||||
IStoryIntelligencePipelineStateService pipelineState,
|
||||
INameIntelligenceService nameIntelligence,
|
||||
ILogger<StoryIntelligenceCharacterImportService> logger) : IStoryIntelligenceCharacterImportService
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
@ -70,6 +71,8 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
var decidedKeys = batch.CharacterDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList();
|
||||
var existingIndex = await BuildCharacterIndexAsync(batch.ProjectID);
|
||||
var nameWarnings = await BuildNameIntelligenceWarningsAsync(visibleCandidates, existingIndex);
|
||||
return new StoryIntelligenceCharacterReviewViewModel
|
||||
{
|
||||
HasCommittedScenes = data.HasCommittedScenes,
|
||||
@ -86,11 +89,74 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
PossibleAliases = candidate.Aliases.OrderBy(value => value).ToList(),
|
||||
ExampleFirstAppearance = candidate.FirstAppearanceNote,
|
||||
ExistingCharacterID = candidate.ExistingCharacterID,
|
||||
ExistingCharacterName = candidate.ExistingCharacterName
|
||||
ExistingCharacterName = candidate.ExistingCharacterName,
|
||||
NameIntelligenceWarning = nameWarnings.GetValueOrDefault(candidate.Key)
|
||||
}).ToList()
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyDictionary<string, string>> BuildNameIntelligenceWarningsAsync(
|
||||
IReadOnlyList<CharacterCandidate> candidates,
|
||||
CharacterIndex existingIndex)
|
||||
{
|
||||
if (candidates.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, string>();
|
||||
}
|
||||
|
||||
var warnings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
var existingForms = existingIndex.Rows
|
||||
.Select(row => new { row.CharacterName, row.MatchName, FirstName = FirstNameWithoutTitle(row.MatchName) ?? string.Empty })
|
||||
.Where(row => !string.IsNullOrWhiteSpace(row.FirstName))
|
||||
.ToList();
|
||||
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var candidateFirst = FirstNameWithoutTitle(candidate.DisplayName);
|
||||
if (string.IsNullOrWhiteSpace(candidateFirst))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var candidateEvidence = await nameIntelligence.RecogniseAsync(candidateFirst, "en-US");
|
||||
if (!candidateEvidence.Recognised)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var existing in existingForms)
|
||||
{
|
||||
if (string.Equals(existing.FirstName, candidateFirst, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var match = candidateEvidence.RelatedForms.FirstOrDefault(relationship =>
|
||||
string.Equals(relationship.RelationshipType, "SpellingVariant", StringComparison.OrdinalIgnoreCase)
|
||||
&& relationship.Strength >= 0.9m
|
||||
&& (string.Equals(relationship.FromDisplayName, existing.FirstName, StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(relationship.ToDisplayName, existing.FirstName, StringComparison.OrdinalIgnoreCase)));
|
||||
if (match is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var occurrences = candidate.PresentAppearances.Concat(candidate.MentionedOnlyAppearances)
|
||||
.Select(appearance => appearance.SceneNumber)
|
||||
.Distinct()
|
||||
.OrderBy(value => value)
|
||||
.Take(5)
|
||||
.Select(value => value.ToString("0.##"))
|
||||
.ToList();
|
||||
var sceneLabel = occurrences.Count == 0 ? "the imported manuscript" : $"scene {string.Join(", ", occurrences)}";
|
||||
warnings[candidate.Key] = $"Possible inconsistent name: manuscript form '{candidate.DisplayName}' is a strong spelling variant of existing {existing.MatchName} for {existing.CharacterName}; seen in {sceneLabel}.";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return warnings;
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceImportCommitResult> ImportAsync(OnboardingStoryIntelligenceBatch batch, StoryIntelligenceCharacterImportForm form)
|
||||
{
|
||||
var data = await BuildCandidateDataAsync(batch);
|
||||
@ -877,6 +943,8 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
private sealed class CharacterIndex
|
||||
{
|
||||
private readonly Dictionary<string, CharacterMatch> byName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly List<CharacterIndexRow> rows = [];
|
||||
public IReadOnlyList<CharacterIndexRow> Rows => rows;
|
||||
|
||||
public void Add(string? name, int characterId, string characterName)
|
||||
{
|
||||
@ -884,6 +952,7 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
byName.TryAdd(key, new CharacterMatch(characterId, characterName));
|
||||
rows.Add(new CharacterIndexRow(name!, characterName));
|
||||
}
|
||||
}
|
||||
|
||||
@ -892,6 +961,7 @@ public sealed class StoryIntelligenceCharacterImportService(
|
||||
}
|
||||
|
||||
private sealed record CharacterMatch(int CharacterID, string CharacterName);
|
||||
private sealed record CharacterIndexRow(string MatchName, string CharacterName);
|
||||
|
||||
private sealed class CharacterCandidate(string key, string displayName, int? existingCharacterId, string? existingCharacterName)
|
||||
{
|
||||
|
||||
@ -154,6 +154,37 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
FirstMetadata(metadata, "generationSource", "source") ?? string.Empty);
|
||||
}
|
||||
|
||||
public static IllustrationLocationMetadata LocationMetadataFrom(IllustrationLibraryItem item)
|
||||
{
|
||||
Dictionary<string, string>? metadata = null;
|
||||
if (!string.IsNullOrWhiteSpace(item.MetadataJson))
|
||||
{
|
||||
try
|
||||
{
|
||||
metadata = JsonSerializer.Deserialize<Dictionary<string, string>>(item.MetadataJson, new JsonSerializerOptions { PropertyNameCaseInsensitive = true });
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
var baseType = FirstConfigured(
|
||||
item.BaseType,
|
||||
FirstMetadata(metadata, "baseType", "locationType"),
|
||||
item.SpecificationJson,
|
||||
item.StableCode,
|
||||
item.DisplayTitle,
|
||||
item.ShortDescription);
|
||||
var type = LocationType(baseType);
|
||||
var isFallback = item.IsFallbackClassification
|
||||
|| BoolMetadata(metadata, "isFallbackClassification")
|
||||
|| IsFallbackLocationType(type)
|
||||
|| item.StableCode.Contains("unknowninterior", StringComparison.OrdinalIgnoreCase)
|
||||
|| item.DisplayTitle.Contains("Unknown Interior", StringComparison.OrdinalIgnoreCase);
|
||||
var reuseScope = FirstConfigured(item.ReuseScope, FirstMetadata(metadata, "reuseScope"), "Project");
|
||||
return new(type, isFallback, reuseScope);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<string> CharacterHardRejections(CharacterEvidenceProfile evidence, IllustrationCharacterMetadata candidate, bool isNamedCharacter, bool alreadyAssignedToAnotherSignificantCharacter)
|
||||
{
|
||||
var rejected = new List<string>();
|
||||
@ -445,6 +476,19 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
|| (requestedType == "Note" && candidateType is "Letter" or "Document")
|
||||
|| (requestedType == "Rucksack" && candidateType == "Rucksack");
|
||||
|
||||
public static bool IsFallbackLocationType(string? locationType)
|
||||
=> string.Equals(locationType, "UnknownInterior", StringComparison.OrdinalIgnoreCase)
|
||||
|| string.Equals(locationType, "FlatInterior", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool IsFallbackAssetType(string? assetType)
|
||||
=> string.Equals(assetType, "UnknownObject", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public static bool CanAutomaticallyGenerateLocation(string? locationType)
|
||||
=> !IsFallbackLocationType(locationType);
|
||||
|
||||
public static bool CanAutomaticallyGenerateAsset(string? assetType)
|
||||
=> !IsFallbackAssetType(assetType);
|
||||
|
||||
public static bool IsGroupEntity(string? value)
|
||||
{
|
||||
var clean = (value ?? string.Empty).Trim().ToLowerInvariant();
|
||||
@ -654,6 +698,9 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
private static string? FirstMetadata(Dictionary<string, string>? metadata, params string[] keys)
|
||||
=> keys.Select(key => metadata?.GetValueOrDefault(key)).FirstOrDefault(value => !string.IsNullOrWhiteSpace(value));
|
||||
|
||||
private static string FirstConfigured(params string?[] values)
|
||||
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;
|
||||
|
||||
private static bool BoolMetadata(Dictionary<string, string>? metadata, string key)
|
||||
=> bool.TryParse(metadata?.GetValueOrDefault(key), out var value) && value;
|
||||
|
||||
@ -821,3 +868,8 @@ public sealed record IllustrationCharacterMetadata(
|
||||
bool IsSuitableForNamedCharacter,
|
||||
string? PromptTemplateVersion,
|
||||
string GenerationSource);
|
||||
|
||||
public sealed record IllustrationLocationMetadata(
|
||||
string BaseType,
|
||||
bool IsFallbackClassification,
|
||||
string ReuseScope);
|
||||
|
||||
@ -2,6 +2,7 @@ using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Options;
|
||||
using PlotLine.Data;
|
||||
using PlotLine.Models;
|
||||
using PlotLine.ViewModels;
|
||||
@ -20,6 +21,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
IStoryIntelligenceIllustrationMatchingRepository assignments,
|
||||
IIllustrationPromptBuilder promptBuilder,
|
||||
IUploadStorageService uploadStorage,
|
||||
IOptions<StoryIntelligenceOptions> options,
|
||||
ILogger<StoryIntelligenceIllustrationMatchingService> logger) : IStoryIntelligenceIllustrationMatchingService
|
||||
{
|
||||
private const int SuitableScore = 48;
|
||||
@ -30,6 +32,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = false
|
||||
};
|
||||
private readonly StoryIntelligenceOptions settings = options.Value;
|
||||
|
||||
public async Task ApplyCharacterIllustrationsAsync(int projectId, StoryIntelligenceExperiencePrototypeViewModel prototype)
|
||||
{
|
||||
@ -40,6 +43,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
|
||||
var catalogue = (await library.ListAsync(new IllustrationLibraryFilter { Category = IllustrationLibraryCategories.Character }))
|
||||
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated)
|
||||
.Where(item => item.IsActive && !item.IsArchived)
|
||||
.Where(item => !item.IsFallbackClassification)
|
||||
.Select(CharacterCandidate.From)
|
||||
.Where(candidate => candidate is not null)
|
||||
.Cast<CharacterCandidate>()
|
||||
@ -171,6 +176,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
|
||||
var catalogue = (await library.ListAsync(new IllustrationLibraryFilter { Category = IllustrationLibraryCategories.Character }))
|
||||
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated)
|
||||
.Where(item => item.IsActive && !item.IsArchived)
|
||||
.Where(item => !item.IsFallbackClassification)
|
||||
.Where(item => !string.IsNullOrWhiteSpace(item.StableCode))
|
||||
.GroupBy(item => item.IllustrationLibraryItemID)
|
||||
.ToDictionary(group => group.Key, group => group.First());
|
||||
@ -225,6 +232,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
var items = await library.ListAsync(new IllustrationLibraryFilter());
|
||||
var lookup = items
|
||||
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated)
|
||||
.Where(item => item.IsActive && !item.IsArchived)
|
||||
.Where(item => !item.IsFallbackClassification)
|
||||
.GroupBy(item => item.StableCode, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@ -282,7 +291,9 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
"Unknown",
|
||||
"Unknown",
|
||||
"Unknown"));
|
||||
var queued = await QueueDemandSpecAsync(LocationDemandSpecification(code, demandKey, type));
|
||||
var queued = ShouldQueueAutomaticDemand()
|
||||
&& StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateLocation(type)
|
||||
&& await QueueDemandSpecAsync(LocationDemandSpecification(code, demandKey, type));
|
||||
location.ImageResolution = WithDemandDiagnostics(
|
||||
location.ImageResolution,
|
||||
demandKey,
|
||||
@ -339,7 +350,9 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
"Unknown",
|
||||
"Unknown",
|
||||
"Unknown"));
|
||||
var queued = await QueueDemandSpecAsync(AssetDemandSpecification(code, demandKey, type, colour));
|
||||
var queued = ShouldQueueAutomaticDemand()
|
||||
&& StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateAsset(type)
|
||||
&& await QueueDemandSpecAsync(AssetDemandSpecification(code, demandKey, type, colour));
|
||||
asset.ImageResolution = WithDemandDiagnostics(
|
||||
asset.ImageResolution,
|
||||
demandKey,
|
||||
@ -434,6 +447,11 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
return await library.QueueAsync(item.IllustrationLibraryItemID, null) > 0;
|
||||
}
|
||||
|
||||
private bool ShouldQueueAutomaticDemand()
|
||||
=> settings.AutomaticImageGenerationDuringImportEnabled
|
||||
&& !settings.RequireImageGenerationApproval
|
||||
&& settings.MaxImagesPerImport > 0;
|
||||
|
||||
private MatchScore? ChooseAssignment(
|
||||
CharacterObservation observation,
|
||||
StoryIntelligenceCharacterIllustrationAssignment? existing,
|
||||
|
||||
@ -80,6 +80,9 @@ public sealed class StoryMemoryService(
|
||||
{
|
||||
QueueDemand = options.QueueDemand,
|
||||
MaxDemandToQueue = options.MaxDemandToQueue,
|
||||
MaxCharacterDemandToQueue = options.MaxCharacterDemandToQueue,
|
||||
MaxLocationDemandToQueue = options.MaxLocationDemandToQueue,
|
||||
MaxAssetDemandToQueue = options.MaxAssetDemandToQueue,
|
||||
AppearancePreference = options.AppearancePreference,
|
||||
PrimaryAgeGroup = options.PrimaryAgeGroup,
|
||||
StoryEra = options.StoryEra,
|
||||
@ -149,6 +152,8 @@ public sealed class StoryMemoryService(
|
||||
var assignments = await memory.ListAssignmentsAsync(importSessionId);
|
||||
var catalogue = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter()))
|
||||
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated)
|
||||
.Where(item => item.IsActive && !item.IsArchived)
|
||||
.Where(item => !item.IsFallbackClassification)
|
||||
.Where(item => ResolveUploadUrl(item) is not null)
|
||||
.ToList();
|
||||
|
||||
@ -201,7 +206,7 @@ public sealed class StoryMemoryService(
|
||||
continue;
|
||||
}
|
||||
|
||||
var demand = await RecordDemandAsync(importSessionId, character.ProjectID, StoryMemoryEntityTypes.Character, DemandKey(character.CanonicalIdentityKey, evidence.AgeBand, evidence.Presentation, evidence.HairColour, evidence.HairLength, evidence.SkinTone), options, CharacterDemandSpecification(character, evidence, options), cancellationToken);
|
||||
var demand = await RecordDemandAsync(importSessionId, character.ProjectID, StoryMemoryEntityTypes.Character, DemandKey(character.CanonicalIdentityKey, evidence.AgeBand, evidence.Presentation, evidence.HairColour, evidence.HairLength, evidence.SkinTone), options, CharacterDemandSpecification(importSessionId, character, evidence, options), cancellationToken);
|
||||
await memory.UpsertAssignmentAsync(new StoryMemoryIllustrationAssignmentSave(importSessionId, StoryMemoryEntityTypes.Character, character.StoryMemoryCharacterID, demand.IllustrationLibraryItemId, demand.StableCode, demand.AssignmentStatus, true, 0, 0, demand.AssignmentReason, ProcessorVersion));
|
||||
}
|
||||
|
||||
@ -211,18 +216,26 @@ public sealed class StoryMemoryService(
|
||||
var requestedType = StoryIntelligenceIllustrationCompatibility.LocationType($"{location.DisplayName} {location.SemanticType} {location.Subtype}");
|
||||
var chosen = catalogue
|
||||
.Where(item => item.Category == IllustrationLibraryCategories.Location)
|
||||
.Select(item => (Item: item, Type: StoryIntelligenceIllustrationCompatibility.LocationType($"{item.StableCode} {item.DisplayTitle} {item.ShortDescription} {item.MetadataJson} {item.SpecificationJson}")))
|
||||
.Where(item => StoryIntelligenceIllustrationCompatibility.LocationCompatible(requestedType, item.Type))
|
||||
.OrderByDescending(item => string.Equals(item.Type, requestedType, StringComparison.OrdinalIgnoreCase) ? 100 : 50)
|
||||
.Select(item => (Item: item, Metadata: StoryIntelligenceIllustrationCompatibility.LocationMetadataFrom(item)))
|
||||
.Where(item => !item.Metadata.IsFallbackClassification)
|
||||
.Where(item => !string.Equals(item.Metadata.ReuseScope, "None", StringComparison.OrdinalIgnoreCase))
|
||||
.Where(item => StoryIntelligenceIllustrationCompatibility.LocationCompatible(requestedType, item.Metadata.BaseType))
|
||||
.OrderByDescending(item => string.Equals(item.Metadata.BaseType, requestedType, StringComparison.OrdinalIgnoreCase) ? 100 : 50)
|
||||
.ThenBy(item => item.Item.StableCode)
|
||||
.FirstOrDefault();
|
||||
if (chosen.Item is not null)
|
||||
{
|
||||
await memory.UpsertAssignmentAsync(new StoryMemoryIllustrationAssignmentSave(importSessionId, StoryMemoryEntityTypes.Location, location.StoryMemoryLocationID, chosen.Item.IllustrationLibraryItemID, chosen.Item.StableCode, "Assigned", false, 100, 0.9m, $"Assigned {chosen.Item.StableCode}; location type {requestedType} matched {chosen.Type}.", ProcessorVersion));
|
||||
await memory.UpsertAssignmentAsync(new StoryMemoryIllustrationAssignmentSave(importSessionId, StoryMemoryEntityTypes.Location, location.StoryMemoryLocationID, chosen.Item.IllustrationLibraryItemID, chosen.Item.StableCode, "Assigned", false, 100, 0.9m, $"Assigned {chosen.Item.StableCode}; location type {requestedType} matched {chosen.Metadata.BaseType}.", ProcessorVersion));
|
||||
}
|
||||
else
|
||||
{
|
||||
var demand = await RecordDemandAsync(importSessionId, location.ProjectID, StoryMemoryEntityTypes.Location, DemandKey(location.CanonicalIdentityKey, requestedType), options, LocationDemandSpecification(location, requestedType, options), cancellationToken);
|
||||
if (!StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateLocation(requestedType))
|
||||
{
|
||||
await memory.UpsertAssignmentAsync(new StoryMemoryIllustrationAssignmentSave(importSessionId, StoryMemoryEntityTypes.Location, location.StoryMemoryLocationID, null, null, "DemandRecorded", true, 0, 0, $"Location type {requestedType} is too broad for automatic generation; demand recorded for review.", ProcessorVersion));
|
||||
continue;
|
||||
}
|
||||
|
||||
var demand = await RecordDemandAsync(importSessionId, location.ProjectID, StoryMemoryEntityTypes.Location, DemandKey(location.CanonicalIdentityKey, requestedType), options, LocationDemandSpecification(importSessionId, location, requestedType, options), cancellationToken);
|
||||
await memory.UpsertAssignmentAsync(new StoryMemoryIllustrationAssignmentSave(importSessionId, StoryMemoryEntityTypes.Location, location.StoryMemoryLocationID, demand.IllustrationLibraryItemId, demand.StableCode, demand.AssignmentStatus, true, 0, 0, demand.AssignmentReason, ProcessorVersion));
|
||||
}
|
||||
}
|
||||
@ -247,7 +260,13 @@ public sealed class StoryMemoryService(
|
||||
}
|
||||
else
|
||||
{
|
||||
var demand = await RecordDemandAsync(importSessionId, asset.ProjectID, StoryMemoryEntityTypes.Asset, DemandKey(asset.CanonicalIdentityKey, requestedType, requestedColour), options, AssetDemandSpecification(asset, requestedType, requestedColour, options), cancellationToken);
|
||||
if (!StoryIntelligenceIllustrationCompatibility.CanAutomaticallyGenerateAsset(requestedType))
|
||||
{
|
||||
await memory.UpsertAssignmentAsync(new StoryMemoryIllustrationAssignmentSave(importSessionId, StoryMemoryEntityTypes.Asset, asset.StoryMemoryAssetID, null, null, "DemandRecorded", true, 0, 0, $"Asset type {requestedType} is too broad for automatic generation; demand recorded for review.", ProcessorVersion));
|
||||
continue;
|
||||
}
|
||||
|
||||
var demand = await RecordDemandAsync(importSessionId, asset.ProjectID, StoryMemoryEntityTypes.Asset, DemandKey(asset.CanonicalIdentityKey, requestedType, requestedColour), options, AssetDemandSpecification(importSessionId, asset, requestedType, requestedColour, options), cancellationToken);
|
||||
await memory.UpsertAssignmentAsync(new StoryMemoryIllustrationAssignmentSave(importSessionId, StoryMemoryEntityTypes.Asset, asset.StoryMemoryAssetID, demand.IllustrationLibraryItemId, demand.StableCode, demand.AssignmentStatus, true, 0, 0, demand.AssignmentReason, ProcessorVersion));
|
||||
}
|
||||
}
|
||||
@ -926,6 +945,13 @@ public sealed class StoryMemoryService(
|
||||
var recorded = await memory.UpsertDemandAsync(new StoryMemoryIllustrationDemandSave(importSessionId, projectId, entityType, archetypeKey, 1, "Recorded", null, null));
|
||||
if (!options.QueueDemand)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Story Memory illustration decision: recorded demand without queueing. ImportSessionID={ImportSessionID} EntityType={EntityType} ArchetypeKey={ArchetypeKey} Category={Category} Reason={Reason}",
|
||||
importSessionId,
|
||||
entityType,
|
||||
archetypeKey,
|
||||
specification.Category,
|
||||
"Automatic image generation is disabled or requires approval.");
|
||||
return DemandResolution.Recorded();
|
||||
}
|
||||
|
||||
@ -936,6 +962,7 @@ public sealed class StoryMemoryService(
|
||||
.FirstOrDefault(item => item.IllustrationLibraryItemID == existingId);
|
||||
if (existingItem is not null
|
||||
&& existingItem.IsActive
|
||||
&& !existingItem.IsArchived
|
||||
&& string.Equals(existingItem.StableCode, specification.Code, StringComparison.OrdinalIgnoreCase)
|
||||
&& existingItem.Status is IllustrationLibraryStatuses.Planned
|
||||
or IllustrationLibraryStatuses.Queued
|
||||
@ -943,23 +970,70 @@ public sealed class StoryMemoryService(
|
||||
or IllustrationLibraryStatuses.Generated
|
||||
or IllustrationLibraryStatuses.Approved)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Story Memory illustration decision: reused existing queued/generated demand item. ImportSessionID={ImportSessionID} EntityType={EntityType} EntityID={LinkedEntityID} Category={Category} IllustrationLibraryItemID={IllustrationLibraryItemID} StableCode={StableCode}",
|
||||
importSessionId,
|
||||
entityType,
|
||||
specification.LinkedEntityId,
|
||||
specification.Category,
|
||||
existingId,
|
||||
specification.Code);
|
||||
return new(existingId, specification.Code, "DemandQueued", $"Demand already has queued illustration item {existingId:N0}.");
|
||||
}
|
||||
}
|
||||
|
||||
var importCap = options.MaxDemandToQueue.GetValueOrDefault();
|
||||
var categoryCap = CategoryCap(options, specification.Category);
|
||||
if (importCap > 0 || categoryCap > 0)
|
||||
{
|
||||
var queuedOrGenerated = (await memory.ListDemandsAsync(importSessionId))
|
||||
.Where(item => item.QueuedIllustrationLibraryItemID.HasValue || item.GeneratedIllustrationLibraryItemID.HasValue)
|
||||
.ToList();
|
||||
if (importCap > 0 && queuedOrGenerated.Count >= importCap)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Story Memory illustration decision: skipped queue because import cap was reached. ImportSessionID={ImportSessionID} EntityType={EntityType} EntityID={LinkedEntityID} Category={Category} Cap={Cap}",
|
||||
importSessionId,
|
||||
entityType,
|
||||
specification.LinkedEntityId,
|
||||
specification.Category,
|
||||
importCap);
|
||||
return new(null, null, "DemandRecorded", $"No compatible illustration exists; demand recorded but import automatic generation cap {importCap:N0} has been reached.");
|
||||
}
|
||||
|
||||
if (categoryCap > 0 && queuedOrGenerated.Count(item => string.Equals(item.EntityType, entityType, StringComparison.OrdinalIgnoreCase)) >= categoryCap)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Story Memory illustration decision: skipped queue because category cap was reached. ImportSessionID={ImportSessionID} EntityType={EntityType} EntityID={LinkedEntityID} Category={Category} Cap={Cap}",
|
||||
importSessionId,
|
||||
entityType,
|
||||
specification.LinkedEntityId,
|
||||
specification.Category,
|
||||
categoryCap);
|
||||
return new(null, null, "DemandRecorded", $"No compatible illustration exists; demand recorded but {specification.Category.ToLowerInvariant()} automatic generation cap {categoryCap:N0} has been reached.");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.MaxDemandToQueue is int maxDemandToQueue && maxDemandToQueue > 0)
|
||||
{
|
||||
var categoryCap = maxDemandToQueue;
|
||||
var queuedDemandItems = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter()))
|
||||
.Count(item => string.Equals(item.Category, specification.Category, StringComparison.OrdinalIgnoreCase)
|
||||
&& item.IsActive
|
||||
&& !item.IsArchived
|
||||
&& item.StableCode.StartsWith("demand-", StringComparison.OrdinalIgnoreCase)
|
||||
&& item.Status is IllustrationLibraryStatuses.Planned
|
||||
or IllustrationLibraryStatuses.Queued
|
||||
or IllustrationLibraryStatuses.Generating);
|
||||
if (queuedDemandItems >= categoryCap)
|
||||
if (queuedDemandItems >= maxDemandToQueue)
|
||||
{
|
||||
return new(null, null, "DemandRecorded", $"No compatible illustration exists; demand recorded but {specification.Category.ToLowerInvariant()} in-flight generation cap {categoryCap:N0} has been reached.");
|
||||
logger.LogInformation(
|
||||
"Story Memory illustration decision: skipped queue because in-flight cap was reached. ImportSessionID={ImportSessionID} EntityType={EntityType} EntityID={LinkedEntityID} Category={Category} Cap={Cap}",
|
||||
importSessionId,
|
||||
entityType,
|
||||
specification.LinkedEntityId,
|
||||
specification.Category,
|
||||
maxDemandToQueue);
|
||||
return new(null, null, "DemandRecorded", $"No compatible illustration exists; demand recorded but import in-flight generation cap {maxDemandToQueue:N0} has been reached.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -975,10 +1049,29 @@ public sealed class StoryMemoryService(
|
||||
"Queued",
|
||||
planned.IllustrationLibraryItemID,
|
||||
null));
|
||||
logger.LogInformation(
|
||||
"Story Memory illustration decision: queued generated library item. ImportSessionID={ImportSessionID} EntityType={EntityType} EntityID={LinkedEntityID} Category={Category} IllustrationLibraryItemID={IllustrationLibraryItemID} StableCode={StableCode} ReuseScope={ReuseScope} BaseType={BaseType}",
|
||||
importSessionId,
|
||||
entityType,
|
||||
specification.LinkedEntityId,
|
||||
specification.Category,
|
||||
queued.QueuedIllustrationLibraryItemID ?? planned.IllustrationLibraryItemID,
|
||||
planned.StableCode,
|
||||
specification.ReuseScope,
|
||||
specification.BaseType);
|
||||
return new(queued.QueuedIllustrationLibraryItemID ?? planned.IllustrationLibraryItemID, planned.StableCode, "DemandQueued", $"No compatible illustration exists; queued demand-generated library item {planned.StableCode}.");
|
||||
}
|
||||
|
||||
private static IllustrationGenerationSpecification CharacterDemandSpecification(StoryMemoryCharacter character, CharacterEvidenceProfile evidence, StoryMemoryResolveOptions options)
|
||||
private static int CategoryCap(StoryMemoryResolveOptions options, string category)
|
||||
=> category switch
|
||||
{
|
||||
IllustrationLibraryCategories.Character => options.MaxCharacterDemandToQueue.GetValueOrDefault(),
|
||||
IllustrationLibraryCategories.Location => options.MaxLocationDemandToQueue.GetValueOrDefault(),
|
||||
IllustrationLibraryCategories.Asset => options.MaxAssetDemandToQueue.GetValueOrDefault(),
|
||||
_ => 0
|
||||
};
|
||||
|
||||
private static IllustrationGenerationSpecification CharacterDemandSpecification(int importSessionId, StoryMemoryCharacter character, CharacterEvidenceProfile evidence, StoryMemoryResolveOptions options)
|
||||
{
|
||||
var code = DemandStableCode(StoryMemoryEntityTypes.Character, character.CanonicalIdentityKey, evidence.AgeBand, evidence.Presentation, evidence.HairColour, evidence.HairLength, evidence.SkinTone);
|
||||
var age = UnknownToDefault(evidence.AgeBand, NormaliseAgeContext(options.PrimaryAgeGroup));
|
||||
@ -1008,11 +1101,22 @@ public sealed class StoryMemoryService(
|
||||
ClothingStyle = $"{era} {location} everyday naturalistic",
|
||||
Palette = ["clear blue", "warm amber", "natural skin tones"],
|
||||
MetadataTags = ["character", "story-memory-demand"],
|
||||
Metadata = DemandMetadata("Character", character.CanonicalIdentityKey, options)
|
||||
ImageKind = "Character",
|
||||
CanonicalName = character.DisplayName,
|
||||
BaseType = presentation,
|
||||
SpecificityLevel = character.IsNamed ? "NamedEntity" : "Archetype",
|
||||
IsFallbackClassification = false,
|
||||
ReuseScope = character.IsNamed ? "Book" : "None",
|
||||
SourceBookId = character.BookID,
|
||||
SourceSceneId = character.LastUpdatedSceneResultID,
|
||||
LinkedEntityType = StoryMemoryEntityTypes.Character,
|
||||
LinkedEntityId = character.StoryMemoryCharacterID,
|
||||
GenerationReason = "Story Memory character demand",
|
||||
Metadata = DemandMetadata(importSessionId, "Character", character.CanonicalIdentityKey, options, character.ProjectID, character.BookID, character.LastUpdatedSceneResultID, StoryMemoryEntityTypes.Character, character.StoryMemoryCharacterID, character.DisplayName, presentation, character.IsNamed ? "NamedEntity" : "Archetype", character.IsNamed ? "Book" : "None", false)
|
||||
};
|
||||
}
|
||||
|
||||
private static IllustrationGenerationSpecification LocationDemandSpecification(StoryMemoryLocation location, string requestedType, StoryMemoryResolveOptions options)
|
||||
private static IllustrationGenerationSpecification LocationDemandSpecification(int importSessionId, StoryMemoryLocation location, string requestedType, StoryMemoryResolveOptions options)
|
||||
{
|
||||
var code = DemandStableCode(StoryMemoryEntityTypes.Location, location.CanonicalIdentityKey, requestedType);
|
||||
var locationType = UnknownToDefault(requestedType, "UnknownInterior");
|
||||
@ -1033,11 +1137,22 @@ public sealed class StoryMemoryService(
|
||||
Setting = location.InteriorExterior ?? InferInteriorExterior(locationType),
|
||||
Palette = ["clear navy", "warm light", "balanced natural colour"],
|
||||
MetadataTags = ["location", "story-memory-demand"],
|
||||
Metadata = DemandMetadata("Location", location.CanonicalIdentityKey, options)
|
||||
ImageKind = "Location",
|
||||
CanonicalName = location.DisplayName,
|
||||
BaseType = locationType,
|
||||
SpecificityLevel = LocationSpecificity(location, locationType),
|
||||
IsFallbackClassification = StoryIntelligenceIllustrationCompatibility.IsFallbackLocationType(locationType),
|
||||
ReuseScope = LocationReuseScope(location, locationType),
|
||||
SourceBookId = location.BookID,
|
||||
SourceSceneId = location.LastSeenSceneResultID,
|
||||
LinkedEntityType = StoryMemoryEntityTypes.Location,
|
||||
LinkedEntityId = location.StoryMemoryLocationID,
|
||||
GenerationReason = "Story Memory location demand",
|
||||
Metadata = DemandMetadata(importSessionId, "Location", location.CanonicalIdentityKey, options, location.ProjectID, location.BookID, location.LastSeenSceneResultID, StoryMemoryEntityTypes.Location, location.StoryMemoryLocationID, location.DisplayName, locationType, LocationSpecificity(location, locationType), LocationReuseScope(location, locationType), StoryIntelligenceIllustrationCompatibility.IsFallbackLocationType(locationType))
|
||||
};
|
||||
}
|
||||
|
||||
private static IllustrationGenerationSpecification AssetDemandSpecification(StoryMemoryAsset asset, string requestedType, string requestedColour, StoryMemoryResolveOptions options)
|
||||
private static IllustrationGenerationSpecification AssetDemandSpecification(int importSessionId, StoryMemoryAsset asset, string requestedType, string requestedColour, StoryMemoryResolveOptions options)
|
||||
{
|
||||
var code = DemandStableCode(StoryMemoryEntityTypes.Asset, asset.CanonicalIdentityKey, requestedType, requestedColour);
|
||||
var objectType = UnknownToDefault(requestedType, "UnknownObject");
|
||||
@ -1056,20 +1171,62 @@ public sealed class StoryMemoryService(
|
||||
Era = asset.Era ?? "1980s",
|
||||
Palette = ["clean blue", "warm light", "natural object colour"],
|
||||
MetadataTags = ["asset", "story-memory-demand"],
|
||||
Metadata = DemandMetadata("Asset", asset.CanonicalIdentityKey, options)
|
||||
ImageKind = requestedType.Contains("Car", StringComparison.OrdinalIgnoreCase) || requestedType is "Hatchback" or "SportsCar" or "Van" ? "Vehicle" : "Object",
|
||||
CanonicalName = asset.DisplayName,
|
||||
BaseType = objectType,
|
||||
SpecificityLevel = StoryIntelligenceIllustrationCompatibility.IsFallbackAssetType(objectType) ? "Generic" : "Archetype",
|
||||
IsFallbackClassification = StoryIntelligenceIllustrationCompatibility.IsFallbackAssetType(objectType),
|
||||
ReuseScope = StoryIntelligenceIllustrationCompatibility.IsFallbackAssetType(objectType) ? "None" : "Book",
|
||||
SourceBookId = asset.BookID,
|
||||
SourceSceneId = asset.LastSeenSceneResultID,
|
||||
LinkedEntityType = StoryMemoryEntityTypes.Asset,
|
||||
LinkedEntityId = asset.StoryMemoryAssetID,
|
||||
GenerationReason = "Story Memory asset demand",
|
||||
Metadata = DemandMetadata(importSessionId, "Asset", asset.CanonicalIdentityKey, options, asset.ProjectID, asset.BookID, asset.LastSeenSceneResultID, StoryMemoryEntityTypes.Asset, asset.StoryMemoryAssetID, asset.DisplayName, objectType, StoryIntelligenceIllustrationCompatibility.IsFallbackAssetType(objectType) ? "Generic" : "Archetype", StoryIntelligenceIllustrationCompatibility.IsFallbackAssetType(objectType) ? "None" : "Book", StoryIntelligenceIllustrationCompatibility.IsFallbackAssetType(objectType))
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> DemandMetadata(string category, string entityKey, StoryMemoryResolveOptions options)
|
||||
private static Dictionary<string, string> DemandMetadata(
|
||||
int importSessionId,
|
||||
string category,
|
||||
string entityKey,
|
||||
StoryMemoryResolveOptions options,
|
||||
int projectId,
|
||||
int bookId,
|
||||
int? sceneId,
|
||||
string linkedEntityType,
|
||||
int linkedEntityId,
|
||||
string canonicalName,
|
||||
string baseType,
|
||||
string specificityLevel,
|
||||
string reuseScope,
|
||||
bool isFallbackClassification)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["category"] = category,
|
||||
["imageKind"] = category,
|
||||
["canonicalName"] = canonicalName,
|
||||
["baseType"] = baseType,
|
||||
["specificityLevel"] = specificityLevel,
|
||||
["isFallbackClassification"] = isFallbackClassification ? "true" : "false",
|
||||
["reuseScope"] = reuseScope,
|
||||
["sourceImportSessionId"] = importSessionId.ToString("N0"),
|
||||
["sourceProjectId"] = projectId.ToString("N0"),
|
||||
["sourceBookId"] = bookId.ToString("N0"),
|
||||
["linkedEntityType"] = linkedEntityType,
|
||||
["linkedEntityId"] = linkedEntityId.ToString("N0"),
|
||||
["generationReason"] = $"Story Memory {category.ToLowerInvariant()} demand",
|
||||
["generationSource"] = "Story Memory demand",
|
||||
["libraryPurpose"] = "Live import demand fulfilment",
|
||||
["privacy"] = "generic-no-manuscript-data",
|
||||
["storyMemoryEntityHash"] = ShortHash(entityKey)
|
||||
};
|
||||
if (sceneId.HasValue)
|
||||
{
|
||||
metadata["sourceSceneId"] = sceneId.Value.ToString("N0");
|
||||
}
|
||||
|
||||
Add("primaryAgeGroup", options.PrimaryAgeGroup);
|
||||
Add("storyEra", options.StoryEra);
|
||||
Add("storyLocation", options.StoryLocation);
|
||||
@ -1313,6 +1470,29 @@ public sealed class StoryMemoryService(
|
||||
? "one readable public washroom environment, cubicles or sink area obvious at thumbnail size, no bath, no domestic home decor"
|
||||
: "one readable focal environment, no prominent people, cinematic but bright enough for a small interface thumbnail";
|
||||
|
||||
private static string LocationSpecificity(StoryMemoryLocation location, string locationType)
|
||||
{
|
||||
if (StoryIntelligenceIllustrationCompatibility.IsFallbackLocationType(locationType))
|
||||
{
|
||||
return "Generic";
|
||||
}
|
||||
|
||||
return !string.IsNullOrWhiteSpace(location.DisplayName)
|
||||
&& !string.Equals(location.DisplayName, SplitWords(locationType), StringComparison.OrdinalIgnoreCase)
|
||||
? "NamedLocation"
|
||||
: "Archetype";
|
||||
}
|
||||
|
||||
private static string LocationReuseScope(StoryMemoryLocation location, string locationType)
|
||||
{
|
||||
if (StoryIntelligenceIllustrationCompatibility.IsFallbackLocationType(locationType))
|
||||
{
|
||||
return "None";
|
||||
}
|
||||
|
||||
return LocationSpecificity(location, locationType) == "NamedLocation" ? "Book" : "Project";
|
||||
}
|
||||
|
||||
private static string InferInteriorExterior(string semanticType)
|
||||
=> semanticType is "Road" or "Lane" or "Street" or "Avenue" or "Drive" or "Close" or "Motorway" or "Bridge" or "Flyover" or "Underpass" or "CarPark" or "Beach" or "Canal" or "HouseExterior" or "StreetFurniture" ? "Exterior" : "Interior";
|
||||
|
||||
|
||||
@ -40,7 +40,8 @@ public interface IWordCompanionService
|
||||
public sealed class WordCompanionService(
|
||||
IWordCompanionRepository repository,
|
||||
IManuscriptDocumentRepository manuscriptDocuments,
|
||||
ICurrentUserService currentUser) : IWordCompanionService
|
||||
ICurrentUserService currentUser,
|
||||
INameIntelligenceService nameIntelligence) : IWordCompanionService
|
||||
{
|
||||
private const int CharacterDiscoveryMinimumMentions = 2;
|
||||
private const int CharacterCreationSelectionLimit = 250;
|
||||
@ -539,7 +540,8 @@ public sealed class WordCompanionService(
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidates = DiscoverCharacterCandidates(request.DocumentText ?? string.Empty, request.IncludeExcluded);
|
||||
var candidates = await DecorateNameIntelligenceEvidenceAsync(
|
||||
DiscoverCharacterCandidates(request.DocumentText ?? string.Empty, request.IncludeExcluded));
|
||||
return new WordCompanionDiscoverCharactersResponse
|
||||
{
|
||||
ProjectId = request.ProjectId,
|
||||
@ -547,6 +549,50 @@ public sealed class WordCompanionService(
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<WordCompanionCharacterCandidateDto>> DecorateNameIntelligenceEvidenceAsync(IReadOnlyList<WordCompanionCharacterCandidateDto> candidates)
|
||||
{
|
||||
var decorated = new List<WordCompanionCharacterCandidateDto>(candidates.Count);
|
||||
foreach (var candidate in candidates)
|
||||
{
|
||||
var firstName = FirstName(candidate.Text);
|
||||
var recognition = await nameIntelligence.RecogniseAsync(firstName, "en-US");
|
||||
if (!recognition.Recognised || recognition.Name is null)
|
||||
{
|
||||
decorated.Add(candidate);
|
||||
continue;
|
||||
}
|
||||
|
||||
var ambiguous = recognition.Name.IsWordAmbiguous;
|
||||
var hasContext = candidate.QualityScore >= 4
|
||||
|| string.Equals(candidate.Category, "ProbableCharacter", StringComparison.OrdinalIgnoreCase)
|
||||
|| candidate.Reason.Contains("Dialogue", StringComparison.OrdinalIgnoreCase)
|
||||
|| candidate.Reason.Contains("honorific", StringComparison.OrdinalIgnoreCase);
|
||||
decorated.Add(new WordCompanionCharacterCandidateDto
|
||||
{
|
||||
Text = candidate.Text,
|
||||
MentionCount = candidate.MentionCount,
|
||||
Confidence = candidate.Confidence,
|
||||
QualityScore = ambiguous && !hasContext ? candidate.QualityScore : candidate.QualityScore + 1,
|
||||
Category = candidate.Category,
|
||||
Reason = ambiguous
|
||||
? $"{candidate.Reason}; recognised ambiguous given-name form requiring context"
|
||||
: $"{candidate.Reason}; recognised given-name form",
|
||||
IsExistingCharacterMatch = candidate.IsExistingCharacterMatch
|
||||
});
|
||||
}
|
||||
|
||||
return decorated
|
||||
.OrderByDescending(item => CategorySortValue(item.Category))
|
||||
.ThenByDescending(item => item.QualityScore)
|
||||
.ThenByDescending(item => ConfidenceSortValue(item.Confidence))
|
||||
.ThenByDescending(item => item.MentionCount)
|
||||
.ThenBy(item => item.Text, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static string FirstName(string? value)
|
||||
=> (value ?? string.Empty).Split(' ', StringSplitOptions.RemoveEmptyEntries).FirstOrDefault() ?? string.Empty;
|
||||
|
||||
public Task<WordCompanionCreateDiscoveredCharactersResponse?> CreateDiscoveredCharactersAsync(WordCompanionCreateDiscoveredCharactersRequest request)
|
||||
{
|
||||
if (request.ProjectId <= 0 || request.CandidateNames.Count == 0)
|
||||
|
||||
416
PlotLine/Sql/147_Phase22C_SafeIllustrationGenerationPolicy.sql
Normal file
416
PlotLine/Sql/147_Phase22C_SafeIllustrationGenerationPolicy.sql
Normal file
@ -0,0 +1,416 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'Quality') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD Quality nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'Size') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD Size nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'OutputFormat') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD OutputFormat nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ImageKind') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD ImageKind nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'CanonicalName') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD CanonicalName nvarchar(200) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'BaseType') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD BaseType nvarchar(80) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SpecificityLevel') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD SpecificityLevel nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'IsFallbackClassification') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD IsFallbackClassification bit NOT NULL CONSTRAINT DF_IllustrationLibraryItems_IsFallbackClassification DEFAULT 0;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ReuseScope') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD ReuseScope nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceProjectID') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD SourceProjectID int NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceBookID') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD SourceBookID int NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceChapterID') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD SourceChapterID int NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SourceSceneID') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD SourceSceneID int NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'LinkedEntityType') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD LinkedEntityType nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'LinkedEntityID') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD LinkedEntityID int NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'GenerationReason') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD GenerationReason nvarchar(200) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'GenerationStatus') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD GenerationStatus nvarchar(40) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'IsArchived') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD IsArchived bit NOT NULL CONSTRAINT DF_IllustrationLibraryItems_IsArchived DEFAULT 0;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ArchivedAt') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD ArchivedAt datetime2(0) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ArchiveReason') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD ArchiveReason nvarchar(500) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'SupersededByImageID') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD SupersededByImageID int NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.IllustrationLibraryItems', N'ReuseOfImageID') IS NULL
|
||||
ALTER TABLE dbo.IllustrationLibraryItems ADD ReuseOfImageID int NULL;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_IllustrationLibraryItems_ArchiveReuse' AND object_id = OBJECT_ID(N'dbo.IllustrationLibraryItems'))
|
||||
CREATE INDEX IX_IllustrationLibraryItems_ArchiveReuse ON dbo.IllustrationLibraryItems(IsArchived, IsActive, Category, Status, BaseType, ReuseScope);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_IllustrationLibraryItems_SourceContext' AND object_id = OBJECT_ID(N'dbo.IllustrationLibraryItems'))
|
||||
CREATE INDEX IX_IllustrationLibraryItems_SourceContext ON dbo.IllustrationLibraryItems(SourceProjectID, SourceBookID, SourceChapterID, SourceSceneID, LinkedEntityType, LinkedEntityID);
|
||||
GO
|
||||
|
||||
UPDATE dbo.IllustrationLibraryItems
|
||||
SET ImageKind = COALESCE(ImageKind, Category),
|
||||
CanonicalName = COALESCE(CanonicalName, JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.canonicalName'), DisplayTitle),
|
||||
BaseType = COALESCE(BaseType, JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.baseType'), JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.locationType'), JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.locationType'), JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.objectType')),
|
||||
SpecificityLevel = COALESCE(SpecificityLevel, JSON_VALUE(CASE WHEN ISJSON(MetadataJson) = 1 THEN MetadataJson ELSE N'{}' END, '$.specificityLevel'), CASE WHEN StableCode LIKE N'demand-%' THEN N'Archetype' ELSE N'Generic' END),
|
||||
IsFallbackClassification = CASE
|
||||
WHEN IsFallbackClassification = 1 THEN 1
|
||||
WHEN Category = N'Location' AND (DisplayTitle LIKE N'%Unknown Interior%' OR StableCode LIKE N'%unknowninterior%' OR JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.locationType') = N'UnknownInterior') THEN 1
|
||||
WHEN Category = N'Asset' AND (DisplayTitle LIKE N'%Unknown Object%' OR StableCode LIKE N'%unknownobject%' OR JSON_VALUE(CASE WHEN ISJSON(SpecificationJson) = 1 THEN SpecificationJson ELSE N'{}' END, '$.objectType') = N'UnknownObject') THEN 1
|
||||
ELSE 0
|
||||
END,
|
||||
ReuseScope = COALESCE(ReuseScope, CASE
|
||||
WHEN StableCode LIKE N'demand-%' THEN N'Book'
|
||||
WHEN StableCode LIKE N'loc-%' OR StableCode LIKE N'char-%' OR StableCode LIKE N'asset-%' THEN N'Global'
|
||||
ELSE N'Project'
|
||||
END),
|
||||
GenerationStatus = COALESCE(GenerationStatus, Status),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE IsActive = 1
|
||||
AND (
|
||||
ImageKind IS NULL
|
||||
OR CanonicalName IS NULL
|
||||
OR SpecificityLevel IS NULL
|
||||
OR ReuseScope IS NULL
|
||||
OR GenerationStatus IS NULL
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_List
|
||||
@Category nvarchar(40) = NULL,
|
||||
@Status nvarchar(40) = NULL,
|
||||
@Search nvarchar(120) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT *
|
||||
FROM dbo.IllustrationLibraryItems
|
||||
WHERE IsArchived = 0
|
||||
AND (@Category IS NULL OR Category = @Category)
|
||||
AND (@Status IS NULL OR Status = @Status)
|
||||
AND (
|
||||
@Search IS NULL
|
||||
OR StableCode LIKE N'%' + @Search + N'%'
|
||||
OR DisplayTitle LIKE N'%' + @Search + N'%'
|
||||
OR ShortDescription LIKE N'%' + @Search + N'%'
|
||||
OR CanonicalName LIKE N'%' + @Search + N'%'
|
||||
OR BaseType LIKE N'%' + @Search + N'%'
|
||||
)
|
||||
ORDER BY
|
||||
CASE Status
|
||||
WHEN N'Generating' THEN 1
|
||||
WHEN N'Queued' THEN 2
|
||||
WHEN N'Generated' THEN 3
|
||||
WHEN N'Planned' THEN 4
|
||||
WHEN N'Failed' THEN 5
|
||||
WHEN N'Rejected' THEN 6
|
||||
WHEN N'Approved' THEN 7
|
||||
ELSE 8
|
||||
END,
|
||||
UpdatedUtc DESC,
|
||||
IllustrationLibraryItemID DESC;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_Summary
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
COUNT(1) AS TotalCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Planned' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS PlannedCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Queued' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS QueuedCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Generating' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS GeneratingCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Generated' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS GeneratedCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Approved' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS ApprovedCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Rejected' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS RejectedCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Failed' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS FailedCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Superseded' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS SupersededCount,
|
||||
ISNULL(SUM(CASE WHEN Status = N'Disabled' AND IsArchived = 0 THEN 1 ELSE 0 END), 0) AS DisabledCount,
|
||||
ISNULL(SUM(CASE WHEN IsArchived = 1 THEN 1 ELSE 0 END), 0) AS ArchivedCount
|
||||
FROM dbo.IllustrationLibraryItems;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_UpsertPlanned
|
||||
@Category nvarchar(40),
|
||||
@StableCode nvarchar(120),
|
||||
@DisplayTitle nvarchar(200),
|
||||
@ShortDescription nvarchar(500) = NULL,
|
||||
@MetadataJson nvarchar(max) = NULL,
|
||||
@SpecificationJson nvarchar(max) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM dbo.IllustrationLibraryItems
|
||||
WHERE StableCode = @StableCode
|
||||
AND ParentItemID IS NULL
|
||||
AND IsActive = 1
|
||||
AND IsArchived = 0
|
||||
AND Status IN (N'Planned', N'Queued', N'Generating', N'Generated', N'Approved')
|
||||
)
|
||||
BEGIN
|
||||
SELECT * FROM dbo.IllustrationLibraryItems
|
||||
WHERE StableCode = @StableCode
|
||||
AND ParentItemID IS NULL
|
||||
AND IsActive = 1
|
||||
AND IsArchived = 0
|
||||
AND Status IN (N'Planned', N'Queued', N'Generating', N'Generated', N'Approved');
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
INSERT dbo.IllustrationLibraryItems
|
||||
(Category, StableCode, DisplayTitle, ShortDescription, MetadataJson, SpecificationJson, Status, ImageKind, CanonicalName, BaseType, SpecificityLevel, IsFallbackClassification, ReuseScope, SourceProjectID, SourceBookID, SourceChapterID, SourceSceneID, LinkedEntityType, LinkedEntityID, GenerationReason, GenerationStatus)
|
||||
VALUES
|
||||
(
|
||||
@Category,
|
||||
@StableCode,
|
||||
@DisplayTitle,
|
||||
@ShortDescription,
|
||||
@MetadataJson,
|
||||
@SpecificationJson,
|
||||
N'Planned',
|
||||
COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.imageKind'), @Category),
|
||||
COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.canonicalName'), @DisplayTitle),
|
||||
COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.baseType'), JSON_VALUE(CASE WHEN ISJSON(@SpecificationJson) = 1 THEN @SpecificationJson ELSE N'{}' END, CASE WHEN @Category = N'Location' THEN '$.locationType' WHEN @Category = N'Asset' THEN '$.objectType' ELSE '$.presentation' END)),
|
||||
COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.specificityLevel'), N'Archetype'),
|
||||
CASE WHEN JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.isFallbackClassification') IN (N'true', N'True', N'1') THEN 1 ELSE 0 END,
|
||||
COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.reuseScope'), N'Book'),
|
||||
TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceProjectId')),
|
||||
TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceBookId')),
|
||||
TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceChapterId')),
|
||||
TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.sourceSceneId')),
|
||||
JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.linkedEntityType'),
|
||||
TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.linkedEntityId')),
|
||||
COALESCE(JSON_VALUE(CASE WHEN ISJSON(@MetadataJson) = 1 THEN @MetadataJson ELSE N'{}' END, '$.generationReason'), N'Demand generation'),
|
||||
N'Planned'
|
||||
);
|
||||
|
||||
SELECT * FROM dbo.IllustrationLibraryItems WHERE IllustrationLibraryItemID = SCOPE_IDENTITY();
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_Queue
|
||||
@IllustrationLibraryItemID int = NULL,
|
||||
@Category nvarchar(40) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.IllustrationLibraryItems
|
||||
SET Status = N'Queued',
|
||||
GenerationStatus = N'Queued',
|
||||
QueuedUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME(),
|
||||
LastError = NULL
|
||||
WHERE (@IllustrationLibraryItemID IS NULL OR IllustrationLibraryItemID = @IllustrationLibraryItemID)
|
||||
AND (@Category IS NULL OR Category = @Category)
|
||||
AND Status IN (N'Planned', N'Failed', N'Rejected')
|
||||
AND IsActive = 1
|
||||
AND IsArchived = 0;
|
||||
|
||||
SELECT @@ROWCOUNT AS AffectedRows;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_ClaimNext
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
SET XACT_ABORT ON;
|
||||
|
||||
BEGIN TRANSACTION;
|
||||
|
||||
DECLARE @Claimed TABLE (IllustrationLibraryItemID int NOT NULL);
|
||||
|
||||
;WITH nextItem AS
|
||||
(
|
||||
SELECT TOP (1) *
|
||||
FROM dbo.IllustrationLibraryItems WITH (UPDLOCK, READPAST, ROWLOCK)
|
||||
WHERE Status = N'Queued' AND IsActive = 1 AND IsArchived = 0
|
||||
ORDER BY QueuedUtc, IllustrationLibraryItemID
|
||||
)
|
||||
UPDATE nextItem
|
||||
SET Status = N'Generating',
|
||||
GenerationStatus = N'Generating',
|
||||
AttemptCount = AttemptCount + 1,
|
||||
StartedUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
OUTPUT inserted.IllustrationLibraryItemID INTO @Claimed;
|
||||
|
||||
SELECT item.*
|
||||
FROM dbo.IllustrationLibraryItems item
|
||||
INNER JOIN @Claimed claimed ON claimed.IllustrationLibraryItemID = item.IllustrationLibraryItemID;
|
||||
|
||||
COMMIT TRANSACTION;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_MarkGenerated
|
||||
@IllustrationLibraryItemID int,
|
||||
@FilePath nvarchar(500),
|
||||
@ThumbnailPath nvarchar(500),
|
||||
@Width int,
|
||||
@Height int,
|
||||
@FileSizeBytes bigint,
|
||||
@MimeType nvarchar(100),
|
||||
@Provider nvarchar(80),
|
||||
@Model nvarchar(120),
|
||||
@PromptText nvarchar(max),
|
||||
@PromptTemplateVersion nvarchar(40),
|
||||
@ProviderRequestID nvarchar(160) = NULL,
|
||||
@Quality nvarchar(40) = NULL,
|
||||
@Size nvarchar(40) = NULL,
|
||||
@OutputFormat nvarchar(40) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.IllustrationLibraryItems
|
||||
SET Status = N'Generated',
|
||||
GenerationStatus = N'Generated',
|
||||
FilePath = @FilePath,
|
||||
ThumbnailPath = @ThumbnailPath,
|
||||
Width = @Width,
|
||||
Height = @Height,
|
||||
FileSizeBytes = @FileSizeBytes,
|
||||
MimeType = @MimeType,
|
||||
Provider = @Provider,
|
||||
Model = @Model,
|
||||
Quality = @Quality,
|
||||
Size = @Size,
|
||||
OutputFormat = @OutputFormat,
|
||||
PromptText = @PromptText,
|
||||
PromptTemplateVersion = @PromptTemplateVersion,
|
||||
ProviderRequestID = @ProviderRequestID,
|
||||
CompletedUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME(),
|
||||
LastError = NULL
|
||||
WHERE IllustrationLibraryItemID = @IllustrationLibraryItemID
|
||||
AND Status = N'Generating'
|
||||
AND IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_MarkFailed
|
||||
@IllustrationLibraryItemID int,
|
||||
@LastError nvarchar(1000)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.IllustrationLibraryItems
|
||||
SET Status = N'Failed',
|
||||
GenerationStatus = N'Failed',
|
||||
LastError = @LastError,
|
||||
CompletedUtc = SYSUTCDATETIME(),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHERE IllustrationLibraryItemID = @IllustrationLibraryItemID
|
||||
AND Status = N'Generating'
|
||||
AND IsArchived = 0;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_GetApproved
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT *
|
||||
FROM dbo.IllustrationLibraryItems
|
||||
WHERE Status = N'Approved'
|
||||
AND IsApproved = 1
|
||||
AND IsActive = 1
|
||||
AND IsArchived = 0
|
||||
AND FilePath IS NOT NULL
|
||||
ORDER BY StableCode, ApprovedUtc DESC;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.IllustrationLibraryItem_ArchiveGeneratedSet
|
||||
@ProjectID int = NULL,
|
||||
@BookID int = NULL,
|
||||
@ImportSessionID int = NULL,
|
||||
@CreatedBeforeUtc datetime2(0) = NULL,
|
||||
@StableCodePrefix nvarchar(120) = NULL,
|
||||
@Reason nvarchar(500) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE item
|
||||
SET IsArchived = 1,
|
||||
IsActive = 0,
|
||||
ArchivedAt = COALESCE(ArchivedAt, SYSUTCDATETIME()),
|
||||
ArchiveReason = COALESCE(@Reason, N'Archived generated illustration set out of active reuse pool.'),
|
||||
GenerationStatus = COALESCE(GenerationStatus, Status),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
FROM dbo.IllustrationLibraryItems item
|
||||
WHERE item.IsArchived = 0
|
||||
AND item.Status IN (N'Generated', N'Approved')
|
||||
AND item.Provider = N'OpenAI'
|
||||
AND (@ProjectID IS NULL OR item.SourceProjectID = @ProjectID)
|
||||
AND (@BookID IS NULL OR item.SourceBookID = @BookID)
|
||||
AND (@ImportSessionID IS NULL OR TRY_CONVERT(int, JSON_VALUE(CASE WHEN ISJSON(item.MetadataJson) = 1 THEN item.MetadataJson ELSE N'{}' END, '$.sourceImportSessionId')) = @ImportSessionID)
|
||||
AND (@CreatedBeforeUtc IS NULL OR item.CreatedUtc < @CreatedBeforeUtc)
|
||||
AND (@StableCodePrefix IS NULL OR item.StableCode LIKE @StableCodePrefix + N'%');
|
||||
|
||||
SELECT @@ROWCOUNT AS ArchivedCount;
|
||||
END;
|
||||
GO
|
||||
629
PlotLine/Sql/148_Phase23_NameIntelligence.sql
Normal file
629
PlotLine/Sql/148_Phase23_NameIntelligence.sql
Normal file
@ -0,0 +1,629 @@
|
||||
SET ANSI_NULLS ON;
|
||||
GO
|
||||
SET QUOTED_IDENTIFIER ON;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.NameLocales', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.NameLocales
|
||||
(
|
||||
NameLocaleID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameLocales PRIMARY KEY,
|
||||
LocaleCode nvarchar(16) NOT NULL,
|
||||
DisplayName nvarchar(100) NOT NULL,
|
||||
IsoCountryCode nvarchar(3) NOT NULL,
|
||||
EvidenceBasis nvarchar(300) NOT NULL,
|
||||
DefaultConfidence decimal(9,4) NOT NULL,
|
||||
SortOrder int NOT NULL,
|
||||
IsActive bit NOT NULL CONSTRAINT DF_NameLocales_IsActive DEFAULT 1,
|
||||
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameLocales_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameLocales_UpdatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT UQ_NameLocales_LocaleCode UNIQUE (LocaleCode),
|
||||
CONSTRAINT CK_NameLocales_DefaultConfidence CHECK (DefaultConfidence >= 0 AND DefaultConfidence <= 1)
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.GivenNames', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.GivenNames
|
||||
(
|
||||
GivenNameID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_GivenNames PRIMARY KEY,
|
||||
GivenNameKey nvarchar(80) NOT NULL,
|
||||
DisplayName nvarchar(120) NOT NULL,
|
||||
NormalisedName nvarchar(120) COLLATE Latin1_General_100_CI_AI_SC NOT NULL,
|
||||
NameType nvarchar(40) NOT NULL,
|
||||
IsWordAmbiguous bit NOT NULL,
|
||||
IsActive bit NOT NULL CONSTRAINT DF_GivenNames_IsActive DEFAULT 1,
|
||||
Notes nvarchar(500) NULL,
|
||||
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_GivenNames_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_GivenNames_UpdatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT UQ_GivenNames_GivenNameKey UNIQUE (GivenNameKey),
|
||||
CONSTRAINT UQ_GivenNames_NormalisedName UNIQUE (NormalisedName),
|
||||
CONSTRAINT CK_GivenNames_NameType CHECK (NameType IN (N'Canonical', N'Independent', N'Diminutive', N'Nickname', N'Variant'))
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.NameRelationships', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.NameRelationships
|
||||
(
|
||||
NameRelationshipID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameRelationships PRIMARY KEY,
|
||||
RelationshipKey nvarchar(80) NOT NULL,
|
||||
FromGivenNameID int NOT NULL,
|
||||
ToGivenNameID int NOT NULL,
|
||||
RelationshipType nvarchar(40) NOT NULL,
|
||||
Strength decimal(9,4) NOT NULL,
|
||||
IsBidirectional bit NOT NULL,
|
||||
IsActive bit NOT NULL CONSTRAINT DF_NameRelationships_IsActive DEFAULT 1,
|
||||
CurationSource nvarchar(120) NULL,
|
||||
Notes nvarchar(500) NULL,
|
||||
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameRelationships_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameRelationships_UpdatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT UQ_NameRelationships_RelationshipKey UNIQUE (RelationshipKey),
|
||||
CONSTRAINT FK_NameRelationships_FromGivenName FOREIGN KEY (FromGivenNameID) REFERENCES dbo.GivenNames(GivenNameID),
|
||||
CONSTRAINT FK_NameRelationships_ToGivenName FOREIGN KEY (ToGivenNameID) REFERENCES dbo.GivenNames(GivenNameID),
|
||||
CONSTRAINT CK_NameRelationships_NotSelf CHECK (FromGivenNameID <> ToGivenNameID),
|
||||
CONSTRAINT CK_NameRelationships_Strength CHECK (Strength >= 0 AND Strength <= 1),
|
||||
CONSTRAINT CK_NameRelationships_Type CHECK (RelationshipType IN (N'ShortForm', N'Diminutive', N'Nickname', N'SpellingVariant', N'AlternativeForm', N'AnglicisedForm'))
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'UX_NameRelationships_ActivePairType' AND object_id = OBJECT_ID(N'dbo.NameRelationships'))
|
||||
CREATE UNIQUE INDEX UX_NameRelationships_ActivePairType ON dbo.NameRelationships(FromGivenNameID, ToGivenNameID, RelationshipType) WHERE IsActive = 1;
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.NameUsages', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.NameUsages
|
||||
(
|
||||
NameUsageID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameUsages PRIMARY KEY,
|
||||
UsageKey nvarchar(80) NOT NULL,
|
||||
GivenNameID int NOT NULL,
|
||||
NameLocaleID int NOT NULL,
|
||||
BirthYearFrom int NOT NULL,
|
||||
BirthYearTo int NOT NULL,
|
||||
MaleWeight decimal(9,4) NOT NULL,
|
||||
FemaleWeight decimal(9,4) NOT NULL,
|
||||
UsageWeight decimal(9,4) NOT NULL,
|
||||
Confidence decimal(9,4) NOT NULL,
|
||||
SourceType nvarchar(60) NOT NULL,
|
||||
SourceReference nvarchar(300) NULL,
|
||||
IsActive bit NOT NULL CONSTRAINT DF_NameUsages_IsActive DEFAULT 1,
|
||||
Notes nvarchar(500) NULL,
|
||||
CreatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameUsages_CreatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
UpdatedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameUsages_UpdatedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CONSTRAINT UQ_NameUsages_UsageKey UNIQUE (UsageKey),
|
||||
CONSTRAINT UQ_NameUsages_NameLocaleYears UNIQUE (GivenNameID, NameLocaleID, BirthYearFrom, BirthYearTo),
|
||||
CONSTRAINT FK_NameUsages_GivenName FOREIGN KEY (GivenNameID) REFERENCES dbo.GivenNames(GivenNameID),
|
||||
CONSTRAINT FK_NameUsages_NameLocale FOREIGN KEY (NameLocaleID) REFERENCES dbo.NameLocales(NameLocaleID),
|
||||
CONSTRAINT CK_NameUsages_YearOrder CHECK (BirthYearFrom <= BirthYearTo),
|
||||
CONSTRAINT CK_NameUsages_Weights CHECK (MaleWeight >= 0 AND MaleWeight <= 1 AND FemaleWeight >= 0 AND FemaleWeight <= 1 AND UsageWeight >= 0 AND UsageWeight <= 1 AND Confidence >= 0 AND Confidence <= 1),
|
||||
CONSTRAINT CK_NameUsages_GenderTotal CHECK (ABS((MaleWeight + FemaleWeight) - 1.0000) <= 0.0002),
|
||||
CONSTRAINT CK_NameUsages_SourceType CHECK (SourceType IN (N'OfficialStatisticalDerived', N'OfficialSourceBlendedEstimate'))
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_NameUsages_QueryPath' AND object_id = OBJECT_ID(N'dbo.NameUsages'))
|
||||
CREATE INDEX IX_NameUsages_QueryPath ON dbo.NameUsages(GivenNameID, NameLocaleID, IsActive, BirthYearFrom, BirthYearTo) INCLUDE (MaleWeight, FemaleWeight, UsageWeight, Confidence, SourceType);
|
||||
GO
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE name = N'IX_GivenNames_Search' AND object_id = OBJECT_ID(N'dbo.GivenNames'))
|
||||
CREATE INDEX IX_GivenNames_Search ON dbo.GivenNames(IsActive, NormalisedName, DisplayName) INCLUDE (NameType, IsWordAmbiguous);
|
||||
GO
|
||||
|
||||
IF OBJECT_ID(N'dbo.NameReferenceImports', N'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE dbo.NameReferenceImports
|
||||
(
|
||||
NameReferenceImportID int IDENTITY(1,1) NOT NULL CONSTRAINT PK_NameReferenceImports PRIMARY KEY,
|
||||
PackageID nvarchar(120) NOT NULL,
|
||||
SemanticVersion nvarchar(40) NOT NULL,
|
||||
PackageChecksum nvarchar(128) NULL,
|
||||
StartedUtc datetime2(0) NOT NULL CONSTRAINT DF_NameReferenceImports_StartedUtc DEFAULT SYSUTCDATETIME(),
|
||||
CompletedUtc datetime2(0) NULL,
|
||||
Succeeded bit NOT NULL CONSTRAINT DF_NameReferenceImports_Succeeded DEFAULT 0,
|
||||
LocaleRows int NOT NULL CONSTRAINT DF_NameReferenceImports_LocaleRows DEFAULT 0,
|
||||
GivenNameRows int NOT NULL CONSTRAINT DF_NameReferenceImports_GivenNameRows DEFAULT 0,
|
||||
RelationshipRows int NOT NULL CONSTRAINT DF_NameReferenceImports_RelationshipRows DEFAULT 0,
|
||||
UsageRows int NOT NULL CONSTRAINT DF_NameReferenceImports_UsageRows DEFAULT 0,
|
||||
ErrorSummary nvarchar(max) NULL
|
||||
);
|
||||
END;
|
||||
GO
|
||||
|
||||
IF TYPE_ID(N'dbo.NameLocaleImportTable') IS NULL
|
||||
CREATE TYPE dbo.NameLocaleImportTable AS TABLE
|
||||
(
|
||||
LocaleCode nvarchar(16) NOT NULL,
|
||||
DisplayName nvarchar(100) NOT NULL,
|
||||
IsoCountryCode nvarchar(3) NOT NULL,
|
||||
EvidenceBasis nvarchar(300) NOT NULL,
|
||||
DefaultConfidence decimal(9,4) NOT NULL,
|
||||
SortOrder int NOT NULL,
|
||||
IsActive bit NOT NULL
|
||||
);
|
||||
GO
|
||||
|
||||
IF TYPE_ID(N'dbo.GivenNameImportTable') IS NULL
|
||||
CREATE TYPE dbo.GivenNameImportTable AS TABLE
|
||||
(
|
||||
GivenNameKey nvarchar(80) NOT NULL,
|
||||
DisplayName nvarchar(120) NOT NULL,
|
||||
NormalisedName nvarchar(120) NOT NULL,
|
||||
NameType nvarchar(40) NOT NULL,
|
||||
IsWordAmbiguous bit NOT NULL,
|
||||
IsActive bit NOT NULL,
|
||||
Notes nvarchar(500) NULL
|
||||
);
|
||||
GO
|
||||
|
||||
IF TYPE_ID(N'dbo.NameRelationshipImportTable') IS NULL
|
||||
CREATE TYPE dbo.NameRelationshipImportTable AS TABLE
|
||||
(
|
||||
RelationshipKey nvarchar(80) NOT NULL,
|
||||
FromGivenNameKey nvarchar(80) NOT NULL,
|
||||
ToGivenNameKey nvarchar(80) NOT NULL,
|
||||
RelationshipType nvarchar(40) NOT NULL,
|
||||
Strength decimal(9,4) NOT NULL,
|
||||
IsBidirectional bit NOT NULL,
|
||||
IsActive bit NOT NULL,
|
||||
CurationSource nvarchar(120) NULL,
|
||||
Notes nvarchar(500) NULL
|
||||
);
|
||||
GO
|
||||
|
||||
IF TYPE_ID(N'dbo.NameUsageImportTable') IS NULL
|
||||
CREATE TYPE dbo.NameUsageImportTable AS TABLE
|
||||
(
|
||||
UsageKey nvarchar(80) NOT NULL,
|
||||
GivenNameKey nvarchar(80) NOT NULL,
|
||||
LocaleCode nvarchar(16) NOT NULL,
|
||||
BirthYearFrom int NOT NULL,
|
||||
BirthYearTo int NOT NULL,
|
||||
MaleWeight decimal(9,4) NOT NULL,
|
||||
FemaleWeight decimal(9,4) NOT NULL,
|
||||
UsageWeight decimal(9,4) NOT NULL,
|
||||
Confidence decimal(9,4) NOT NULL,
|
||||
SourceType nvarchar(60) NOT NULL,
|
||||
SourceReference nvarchar(300) NULL,
|
||||
IsActive bit NOT NULL,
|
||||
Notes nvarchar(500) NULL
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.NameReferenceImport_GetLatest
|
||||
@PackageID nvarchar(120),
|
||||
@SemanticVersion nvarchar(40)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT TOP (1) *
|
||||
FROM dbo.NameReferenceImports
|
||||
WHERE PackageID = @PackageID
|
||||
AND SemanticVersion = @SemanticVersion
|
||||
AND Succeeded = 1
|
||||
ORDER BY CompletedUtc DESC, NameReferenceImportID DESC;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.NameReferenceImport_Start
|
||||
@PackageID nvarchar(120),
|
||||
@SemanticVersion nvarchar(40),
|
||||
@PackageChecksum nvarchar(128)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
INSERT dbo.NameReferenceImports (PackageID, SemanticVersion, PackageChecksum)
|
||||
VALUES (@PackageID, @SemanticVersion, @PackageChecksum);
|
||||
|
||||
SELECT CAST(SCOPE_IDENTITY() AS int) AS NameReferenceImportID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.NameReferenceImport_Complete
|
||||
@NameReferenceImportID int,
|
||||
@Succeeded bit,
|
||||
@LocaleRows int,
|
||||
@GivenNameRows int,
|
||||
@RelationshipRows int,
|
||||
@UsageRows int,
|
||||
@ErrorSummary nvarchar(max) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
UPDATE dbo.NameReferenceImports
|
||||
SET CompletedUtc = SYSUTCDATETIME(),
|
||||
Succeeded = @Succeeded,
|
||||
LocaleRows = @LocaleRows,
|
||||
GivenNameRows = @GivenNameRows,
|
||||
RelationshipRows = @RelationshipRows,
|
||||
UsageRows = @UsageRows,
|
||||
ErrorSummary = @ErrorSummary
|
||||
WHERE NameReferenceImportID = @NameReferenceImportID;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.NameLocale_BulkUpsert
|
||||
@Rows dbo.NameLocaleImportTable READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
MERGE dbo.NameLocales WITH (HOLDLOCK) AS target
|
||||
USING @Rows AS source
|
||||
ON target.LocaleCode = source.LocaleCode
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET DisplayName = source.DisplayName,
|
||||
IsoCountryCode = source.IsoCountryCode,
|
||||
EvidenceBasis = source.EvidenceBasis,
|
||||
DefaultConfidence = source.DefaultConfidence,
|
||||
SortOrder = source.SortOrder,
|
||||
IsActive = source.IsActive,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (LocaleCode, DisplayName, IsoCountryCode, EvidenceBasis, DefaultConfidence, SortOrder, IsActive)
|
||||
VALUES (source.LocaleCode, source.DisplayName, source.IsoCountryCode, source.EvidenceBasis, source.DefaultConfidence, source.SortOrder, source.IsActive);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.GivenName_BulkUpsert
|
||||
@Rows dbo.GivenNameImportTable READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
MERGE dbo.GivenNames WITH (HOLDLOCK) AS target
|
||||
USING @Rows AS source
|
||||
ON target.GivenNameKey = source.GivenNameKey
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET DisplayName = source.DisplayName,
|
||||
NormalisedName = source.NormalisedName,
|
||||
NameType = source.NameType,
|
||||
IsWordAmbiguous = source.IsWordAmbiguous,
|
||||
IsActive = source.IsActive,
|
||||
Notes = source.Notes,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (GivenNameKey, DisplayName, NormalisedName, NameType, IsWordAmbiguous, IsActive, Notes)
|
||||
VALUES (source.GivenNameKey, source.DisplayName, source.NormalisedName, source.NameType, source.IsWordAmbiguous, source.IsActive, source.Notes);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.NameRelationship_BulkUpsert
|
||||
@Rows dbo.NameRelationshipImportTable READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
MERGE dbo.NameRelationships WITH (HOLDLOCK) AS target
|
||||
USING
|
||||
(
|
||||
SELECT r.*, f.GivenNameID AS FromGivenNameID, t.GivenNameID AS ToGivenNameID
|
||||
FROM @Rows r
|
||||
INNER JOIN dbo.GivenNames f ON f.GivenNameKey = r.FromGivenNameKey
|
||||
INNER JOIN dbo.GivenNames t ON t.GivenNameKey = r.ToGivenNameKey
|
||||
) AS source
|
||||
ON target.RelationshipKey = source.RelationshipKey
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET FromGivenNameID = source.FromGivenNameID,
|
||||
ToGivenNameID = source.ToGivenNameID,
|
||||
RelationshipType = source.RelationshipType,
|
||||
Strength = source.Strength,
|
||||
IsBidirectional = source.IsBidirectional,
|
||||
IsActive = source.IsActive,
|
||||
CurationSource = source.CurationSource,
|
||||
Notes = source.Notes,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (RelationshipKey, FromGivenNameID, ToGivenNameID, RelationshipType, Strength, IsBidirectional, IsActive, CurationSource, Notes)
|
||||
VALUES (source.RelationshipKey, source.FromGivenNameID, source.ToGivenNameID, source.RelationshipType, source.Strength, source.IsBidirectional, source.IsActive, source.CurationSource, source.Notes);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.NameUsage_BulkUpsert
|
||||
@Rows dbo.NameUsageImportTable READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
MERGE dbo.NameUsages WITH (HOLDLOCK) AS target
|
||||
USING
|
||||
(
|
||||
SELECT r.*, g.GivenNameID, l.NameLocaleID
|
||||
FROM @Rows r
|
||||
INNER JOIN dbo.GivenNames g ON g.GivenNameKey = r.GivenNameKey
|
||||
INNER JOIN dbo.NameLocales l ON l.LocaleCode = r.LocaleCode
|
||||
) AS source
|
||||
ON target.UsageKey = source.UsageKey
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET GivenNameID = source.GivenNameID,
|
||||
NameLocaleID = source.NameLocaleID,
|
||||
BirthYearFrom = source.BirthYearFrom,
|
||||
BirthYearTo = source.BirthYearTo,
|
||||
MaleWeight = source.MaleWeight,
|
||||
FemaleWeight = source.FemaleWeight,
|
||||
UsageWeight = source.UsageWeight,
|
||||
Confidence = source.Confidence,
|
||||
SourceType = source.SourceType,
|
||||
SourceReference = source.SourceReference,
|
||||
IsActive = source.IsActive,
|
||||
Notes = source.Notes,
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (UsageKey, GivenNameID, NameLocaleID, BirthYearFrom, BirthYearTo, MaleWeight, FemaleWeight, UsageWeight, Confidence, SourceType, SourceReference, IsActive, Notes)
|
||||
VALUES (source.UsageKey, source.GivenNameID, source.NameLocaleID, source.BirthYearFrom, source.BirthYearTo, source.MaleWeight, source.FemaleWeight, source.UsageWeight, source.Confidence, source.SourceType, source.SourceReference, source.IsActive, source.Notes);
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.NameLocale_List
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT *
|
||||
FROM dbo.NameLocales
|
||||
WHERE IsActive = 1
|
||||
ORDER BY SortOrder, DisplayName;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.GivenName_Recognise
|
||||
@NormalisedName nvarchar(120),
|
||||
@LocaleCode nvarchar(16) = NULL,
|
||||
@BirthYear int = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
DECLARE @GivenNameID int;
|
||||
DECLARE @NameLocaleID int;
|
||||
|
||||
SELECT TOP (1) @GivenNameID = GivenNameID
|
||||
FROM dbo.GivenNames
|
||||
WHERE NormalisedName = @NormalisedName COLLATE Latin1_General_100_CI_AI_SC
|
||||
AND IsActive = 1;
|
||||
|
||||
SELECT TOP (1) @NameLocaleID = NameLocaleID
|
||||
FROM dbo.NameLocales
|
||||
WHERE IsActive = 1
|
||||
AND LocaleCode = COALESCE(@LocaleCode, N'en-US');
|
||||
|
||||
IF @NameLocaleID IS NULL
|
||||
SELECT TOP (1) @NameLocaleID = NameLocaleID FROM dbo.NameLocales WHERE LocaleCode = N'en-US' AND IsActive = 1;
|
||||
|
||||
SELECT *
|
||||
FROM dbo.GivenNames
|
||||
WHERE GivenNameID = @GivenNameID;
|
||||
|
||||
IF @GivenNameID IS NOT NULL
|
||||
BEGIN
|
||||
SELECT TOP (1)
|
||||
u.*,
|
||||
l.LocaleCode,
|
||||
l.DisplayName AS LocaleDisplayName,
|
||||
CASE
|
||||
WHEN @BirthYear IS NULL THEN N'General'
|
||||
WHEN @BirthYear BETWEEN u.BirthYearFrom AND u.BirthYearTo THEN N'ExactEra'
|
||||
ELSE N'NearestEra'
|
||||
END AS YearMatchKind
|
||||
FROM dbo.NameUsages u
|
||||
INNER JOIN dbo.NameLocales l ON l.NameLocaleID = u.NameLocaleID
|
||||
WHERE u.GivenNameID = @GivenNameID
|
||||
AND u.NameLocaleID = @NameLocaleID
|
||||
AND u.IsActive = 1
|
||||
ORDER BY
|
||||
CASE WHEN @BirthYear IS NOT NULL AND @BirthYear BETWEEN u.BirthYearFrom AND u.BirthYearTo THEN 0 ELSE 1 END,
|
||||
CASE WHEN @BirthYear IS NULL THEN ABS(1960 - ((u.BirthYearFrom + u.BirthYearTo) / 2)) ELSE ABS(@BirthYear - ((u.BirthYearFrom + u.BirthYearTo) / 2)) END,
|
||||
u.UsageWeight DESC;
|
||||
|
||||
EXEC dbo.GivenName_Relationships @GivenNameID = @GivenNameID;
|
||||
END;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.GivenName_Relationships
|
||||
@GivenNameID int
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
SELECT
|
||||
r.NameRelationshipID,
|
||||
r.RelationshipKey,
|
||||
r.FromGivenNameID,
|
||||
r.ToGivenNameID,
|
||||
f.DisplayName AS FromDisplayName,
|
||||
t.DisplayName AS ToDisplayName,
|
||||
r.RelationshipType,
|
||||
r.Strength,
|
||||
r.IsBidirectional,
|
||||
r.IsActive,
|
||||
r.CurationSource,
|
||||
r.Notes
|
||||
FROM dbo.NameRelationships r
|
||||
INNER JOIN dbo.GivenNames f ON f.GivenNameID = r.FromGivenNameID
|
||||
INNER JOIN dbo.GivenNames t ON t.GivenNameID = r.ToGivenNameID
|
||||
WHERE r.IsActive = 1
|
||||
AND (r.FromGivenNameID = @GivenNameID OR (r.IsBidirectional = 1 AND r.ToGivenNameID = @GivenNameID))
|
||||
ORDER BY r.Strength DESC, t.DisplayName;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.GivenName_Search
|
||||
@UserID int,
|
||||
@ProjectID int,
|
||||
@BookID int = NULL,
|
||||
@Search nvarchar(120) = NULL,
|
||||
@LocaleCode nvarchar(16) = NULL,
|
||||
@BirthYear int = NULL,
|
||||
@GenderPreference nvarchar(12) = NULL,
|
||||
@GenderThreshold decimal(9,4) = NULL,
|
||||
@NameType nvarchar(40) = NULL,
|
||||
@MinimumUsage decimal(9,4) = NULL,
|
||||
@UsedScope nvarchar(20) = N'Project',
|
||||
@UsedOnly bit = NULL,
|
||||
@Page int = 1,
|
||||
@PageSize int = 25
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND UserID = @UserID AND IsActive = 1)
|
||||
BEGIN
|
||||
SELECT TOP (0) CAST(NULL AS int) AS GivenNameID;
|
||||
RETURN;
|
||||
END;
|
||||
|
||||
IF @Page < 1 SET @Page = 1;
|
||||
IF @PageSize < 1 SET @PageSize = 25;
|
||||
IF @PageSize > 100 SET @PageSize = 100;
|
||||
|
||||
DECLARE @NameLocaleID int;
|
||||
SELECT TOP (1) @NameLocaleID = NameLocaleID FROM dbo.NameLocales WHERE LocaleCode = COALESCE(@LocaleCode, N'en-US') AND IsActive = 1;
|
||||
|
||||
;WITH UsageChoice AS
|
||||
(
|
||||
SELECT
|
||||
u.*,
|
||||
l.LocaleCode,
|
||||
l.DisplayName AS LocaleDisplayName,
|
||||
ROW_NUMBER() OVER (PARTITION BY u.GivenNameID ORDER BY
|
||||
CASE WHEN @BirthYear IS NOT NULL AND @BirthYear BETWEEN u.BirthYearFrom AND u.BirthYearTo THEN 0 ELSE 1 END,
|
||||
CASE WHEN @BirthYear IS NULL THEN ABS(1960 - ((u.BirthYearFrom + u.BirthYearTo) / 2)) ELSE ABS(@BirthYear - ((u.BirthYearFrom + u.BirthYearTo) / 2)) END,
|
||||
u.UsageWeight DESC) AS rn
|
||||
FROM dbo.NameUsages u
|
||||
INNER JOIN dbo.NameLocales l ON l.NameLocaleID = u.NameLocaleID
|
||||
WHERE u.IsActive = 1
|
||||
AND (@NameLocaleID IS NULL OR u.NameLocaleID = @NameLocaleID)
|
||||
),
|
||||
Used AS
|
||||
(
|
||||
SELECT g.GivenNameID, COUNT(DISTINCT c.CharacterID) AS UsedCount,
|
||||
STRING_AGG(CONVERT(nvarchar(max), c.CharacterName + N' (' + p.ProjectName + COALESCE(N' / ' + b.BookTitle, N'') + N')'), N'; ') AS UsedSummary
|
||||
FROM dbo.GivenNames g
|
||||
INNER JOIN
|
||||
(
|
||||
SELECT c.CharacterID, c.ProjectID, c.CharacterName, c.CharacterName AS MatchedName
|
||||
FROM dbo.Characters c
|
||||
WHERE c.IsArchived = 0
|
||||
UNION ALL
|
||||
SELECT c.CharacterID, c.ProjectID, c.CharacterName, ca.Alias AS MatchedName
|
||||
FROM dbo.CharacterAliases ca
|
||||
INNER JOIN dbo.Characters c ON c.CharacterID = ca.CharacterID
|
||||
WHERE c.IsArchived = 0
|
||||
) c ON UPPER(LTRIM(RTRIM(c.MatchedName))) = g.NormalisedName COLLATE Latin1_General_100_CI_AI_SC
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = c.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID AND pua.UserID = @UserID AND pua.IsActive = 1
|
||||
LEFT JOIN dbo.Books b ON b.ProjectID = c.ProjectID AND b.IsArchived = 0
|
||||
WHERE (@UsedScope = N'Library' OR c.ProjectID = @ProjectID)
|
||||
AND (@BookID IS NULL OR @UsedScope <> N'Book' OR b.BookID = @BookID)
|
||||
GROUP BY g.GivenNameID
|
||||
),
|
||||
Related AS
|
||||
(
|
||||
SELECT x.GivenNameID,
|
||||
STRING_AGG(CONVERT(nvarchar(max), x.DisplayName), N', ') AS RelatedForms
|
||||
FROM
|
||||
(
|
||||
SELECT r.FromGivenNameID AS GivenNameID, t.DisplayName
|
||||
FROM dbo.NameRelationships r
|
||||
INNER JOIN dbo.GivenNames t ON t.GivenNameID = r.ToGivenNameID
|
||||
WHERE r.IsActive = 1
|
||||
UNION
|
||||
SELECT r.ToGivenNameID AS GivenNameID, f.DisplayName
|
||||
FROM dbo.NameRelationships r
|
||||
INNER JOIN dbo.GivenNames f ON f.GivenNameID = r.FromGivenNameID
|
||||
WHERE r.IsActive = 1 AND r.IsBidirectional = 1
|
||||
) x
|
||||
GROUP BY x.GivenNameID
|
||||
),
|
||||
Filtered AS
|
||||
(
|
||||
SELECT
|
||||
g.GivenNameID, g.GivenNameKey, g.DisplayName, g.NormalisedName, g.NameType, g.IsWordAmbiguous,
|
||||
u.MaleWeight, u.FemaleWeight, u.UsageWeight, u.Confidence, u.BirthYearFrom, u.BirthYearTo, u.LocaleCode, u.LocaleDisplayName, u.SourceType,
|
||||
rel.RelatedForms,
|
||||
COALESCE(used.UsedSummary, N'') AS UsedSummary,
|
||||
COALESCE(used.UsedCount, 0) AS UsedCount,
|
||||
COUNT(1) OVER() AS TotalRows
|
||||
FROM dbo.GivenNames g
|
||||
LEFT JOIN UsageChoice u ON u.GivenNameID = g.GivenNameID AND u.rn = 1
|
||||
LEFT JOIN Used used ON used.GivenNameID = g.GivenNameID
|
||||
LEFT JOIN Related rel ON rel.GivenNameID = g.GivenNameID
|
||||
WHERE g.IsActive = 1
|
||||
AND (@Search IS NULL OR g.NormalisedName LIKE UPPER(LTRIM(RTRIM(@Search))) + N'%' COLLATE Latin1_General_100_CI_AI_SC OR g.DisplayName LIKE @Search + N'%')
|
||||
AND (@NameType IS NULL OR g.NameType = @NameType)
|
||||
AND (@MinimumUsage IS NULL OR u.UsageWeight >= @MinimumUsage)
|
||||
AND (@UsedOnly IS NULL OR (@UsedOnly = 1 AND COALESCE(used.UsedCount, 0) > 0) OR (@UsedOnly = 0 AND COALESCE(used.UsedCount, 0) = 0))
|
||||
AND (
|
||||
@GenderPreference IS NULL
|
||||
OR @GenderThreshold IS NULL
|
||||
OR (@GenderPreference = N'Female' AND u.FemaleWeight >= @GenderThreshold)
|
||||
OR (@GenderPreference = N'Male' AND u.MaleWeight >= @GenderThreshold)
|
||||
OR (@GenderPreference = N'Mixed' AND u.MaleWeight >= (1 - @GenderThreshold) AND u.FemaleWeight >= (1 - @GenderThreshold))
|
||||
)
|
||||
)
|
||||
SELECT *
|
||||
FROM Filtered
|
||||
ORDER BY
|
||||
CASE WHEN @Search IS NOT NULL AND NormalisedName = UPPER(LTRIM(RTRIM(@Search))) COLLATE Latin1_General_100_CI_AI_SC THEN 0 ELSE 1 END,
|
||||
UsageWeight DESC,
|
||||
DisplayName
|
||||
OFFSET (@Page - 1) * @PageSize ROWS FETCH NEXT @PageSize ROWS ONLY;
|
||||
END;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE dbo.GivenName_UsedDecorations
|
||||
@UserID int,
|
||||
@ProjectID int,
|
||||
@BookID int = NULL,
|
||||
@NormalisedName nvarchar(120),
|
||||
@UsedScope nvarchar(20) = N'Project'
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF NOT EXISTS (SELECT 1 FROM dbo.ProjectUserAccess WHERE ProjectID = @ProjectID AND UserID = @UserID AND IsActive = 1)
|
||||
RETURN;
|
||||
|
||||
SELECT
|
||||
rows.MatchedName,
|
||||
rows.MatchType,
|
||||
c.CharacterID,
|
||||
c.CharacterName,
|
||||
p.ProjectID,
|
||||
p.ProjectName,
|
||||
b.BookID,
|
||||
b.BookTitle,
|
||||
b.Subtitle AS BookSubtitle,
|
||||
CASE WHEN b.BookID IS NOT NULL THEN N'Book' ELSE N'Project' END AS Scope
|
||||
FROM
|
||||
(
|
||||
SELECT c.CharacterID, c.ProjectID, c.CharacterName AS MatchedName, N'Character' AS MatchType
|
||||
FROM dbo.Characters c
|
||||
WHERE c.IsArchived = 0
|
||||
UNION ALL
|
||||
SELECT ca.CharacterID, c.ProjectID, ca.Alias AS MatchedName, N'Alias' AS MatchType
|
||||
FROM dbo.CharacterAliases ca
|
||||
INNER JOIN dbo.Characters c ON c.CharacterID = ca.CharacterID
|
||||
WHERE c.IsArchived = 0
|
||||
) rows
|
||||
INNER JOIN dbo.Characters c ON c.CharacterID = rows.CharacterID
|
||||
INNER JOIN dbo.Projects p ON p.ProjectID = c.ProjectID
|
||||
INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID AND pua.UserID = @UserID AND pua.IsActive = 1
|
||||
LEFT JOIN dbo.SceneCharacters sc ON sc.CharacterID = c.CharacterID
|
||||
LEFT JOIN dbo.Scenes s ON s.SceneID = sc.SceneID AND s.IsArchived = 0
|
||||
LEFT JOIN dbo.Chapters ch ON ch.ChapterID = s.ChapterID AND ch.IsArchived = 0
|
||||
LEFT JOIN dbo.Books b ON b.BookID = ch.BookID AND b.IsArchived = 0
|
||||
WHERE UPPER(LTRIM(RTRIM(rows.MatchedName))) = @NormalisedName COLLATE Latin1_General_100_CI_AI_SC
|
||||
AND (@UsedScope = N'Library' OR c.ProjectID = @ProjectID)
|
||||
AND (@BookID IS NULL OR @UsedScope <> N'Book' OR b.BookID = @BookID)
|
||||
ORDER BY p.ProjectName, b.SortOrder, c.CharacterName;
|
||||
END;
|
||||
GO
|
||||
17
PlotLine/Sql/Admin/ArchiveCurrentGeneratedIllustrations.sql
Normal file
17
PlotLine/Sql/Admin/ArchiveCurrentGeneratedIllustrations.sql
Normal file
@ -0,0 +1,17 @@
|
||||
/*
|
||||
Archive the current demand-generated OpenAI illustration set out of normal
|
||||
reuse/display without deleting files or prompt history.
|
||||
|
||||
Run after 147_Phase22C_SafeIllustrationGenerationPolicy.sql has been applied.
|
||||
The default target is the existing demand-generated library created before the
|
||||
safe-generation policy. Narrow with @ProjectID, @BookID, or @ImportSessionID
|
||||
when those source metadata fields are present on newer rows.
|
||||
*/
|
||||
|
||||
EXEC dbo.IllustrationLibraryItem_ArchiveGeneratedSet
|
||||
@ProjectID = NULL,
|
||||
@BookID = NULL,
|
||||
@ImportSessionID = NULL,
|
||||
@CreatedBeforeUtc = '2026-08-18T23:59:59',
|
||||
@StableCodePrefix = N'demand-',
|
||||
@Reason = N'Archived pre-22C generated demand images before safer Story Intelligence import testing.';
|
||||
14
PlotLine/ViewModels/NamesViewModels.cs
Normal file
14
PlotLine/ViewModels/NamesViewModels.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using PlotLine.Models;
|
||||
|
||||
namespace PlotLine.ViewModels;
|
||||
|
||||
public sealed class NamesLibraryViewModel
|
||||
{
|
||||
public Project Project { get; init; } = new();
|
||||
public NameSearchFilter Filter { get; init; } = new();
|
||||
public IReadOnlyList<NameLocale> Locales { get; init; } = [];
|
||||
public IReadOnlyList<Book> Books { get; init; } = [];
|
||||
public IReadOnlyList<NameSearchResult> Results { get; init; } = [];
|
||||
public int TotalRows => Results.FirstOrDefault()?.TotalRows ?? 0;
|
||||
public int TotalPages => Filter.PageSize <= 0 ? 1 : Math.Max(1, (int)Math.Ceiling(TotalRows / (double)Filter.PageSize));
|
||||
}
|
||||
@ -213,6 +213,7 @@ public sealed class StoryIntelligenceCharacterReviewCandidateViewModel
|
||||
public string Confidence { get; init; } = "Unknown";
|
||||
public IReadOnlyList<string> PossibleAliases { get; init; } = [];
|
||||
public string? ExampleFirstAppearance { get; init; }
|
||||
public string? NameIntelligenceWarning { get; init; }
|
||||
public int? ExistingCharacterID { get; init; }
|
||||
public string? ExistingCharacterName { get; init; }
|
||||
public bool IsExistingMatch => ExistingCharacterID.HasValue;
|
||||
|
||||
@ -17,6 +17,15 @@
|
||||
<span class="muted"> / Admin</span>
|
||||
</nav>
|
||||
|
||||
@if (TempData["AdminMessage"] is string adminMessage)
|
||||
{
|
||||
<div class="alert alert-success">@adminMessage</div>
|
||||
}
|
||||
@if (TempData["AdminError"] is string adminError)
|
||||
{
|
||||
<div class="alert alert-danger">@adminError</div>
|
||||
}
|
||||
|
||||
<section class="edit-panel">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
@ -91,6 +100,25 @@
|
||||
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceIllustrationLibrary">Illustration library</a>
|
||||
</div>
|
||||
</article>
|
||||
<article class="project-card">
|
||||
<div class="project-card__body">
|
||||
<p class="eyebrow">Reference data</p>
|
||||
<h2>Name Intelligence</h2>
|
||||
<p class="project-card__description">Validate or import the global given-name reference package.</p>
|
||||
</div>
|
||||
<div class="project-card__actions">
|
||||
<form asp-action="ValidateNameIntelligence" method="post">
|
||||
<button class="btn btn-outline-primary btn-sm" type="submit">Validate</button>
|
||||
</form>
|
||||
<form asp-action="ImportNameIntelligence" method="post">
|
||||
<button class="btn btn-primary btn-sm" type="submit">Import</button>
|
||||
</form>
|
||||
<form asp-action="ImportNameIntelligence" method="post">
|
||||
<input type="hidden" name="force" value="true" />
|
||||
<button class="btn btn-outline-secondary btn-sm" type="submit">Force re-import</button>
|
||||
</form>
|
||||
</div>
|
||||
</article>
|
||||
<article class="project-card">
|
||||
<div class="project-card__body">
|
||||
<p class="eyebrow">Placeholder</p>
|
||||
|
||||
230
PlotLine/Views/Names/Index.cshtml
Normal file
230
PlotLine/Views/Names/Index.cshtml
Normal file
@ -0,0 +1,230 @@
|
||||
@model NamesLibraryViewModel
|
||||
@{
|
||||
ViewData["Title"] = $"Names Library - {Model.Project.ProjectName}";
|
||||
ViewData["ProjectSection"] = "Names";
|
||||
var filter = Model.Filter;
|
||||
var previousPage = Math.Max(1, filter.Page - 1);
|
||||
var nextPage = Math.Min(Model.TotalPages, filter.Page + 1);
|
||||
var genderPreset = (filter.GenderPreference, filter.GenderThreshold) switch
|
||||
{
|
||||
("Female", >= 0.9m) => "female-very-strong",
|
||||
("Female", >= 0.7m) => "female-strong",
|
||||
("Female", _) => "female-mostly",
|
||||
("Male", >= 0.9m) => "male-very-strong",
|
||||
("Male", >= 0.7m) => "male-strong",
|
||||
("Male", _) => "male-mostly",
|
||||
("Mixed", _) => "mixed-balanced",
|
||||
_ => string.Empty
|
||||
};
|
||||
|
||||
string GenderLabel(NameSearchResult row)
|
||||
{
|
||||
if (!row.FemaleWeight.HasValue || !row.MaleWeight.HasValue)
|
||||
{
|
||||
return "No profile";
|
||||
}
|
||||
|
||||
if (row.FemaleWeight >= 0.7m)
|
||||
{
|
||||
return "strong female prior";
|
||||
}
|
||||
|
||||
if (row.MaleWeight >= 0.7m)
|
||||
{
|
||||
return "strong male prior";
|
||||
}
|
||||
|
||||
return "mixed prior";
|
||||
}
|
||||
|
||||
string EraLabel(NameSearchResult row)
|
||||
=> row.BirthYearFrom.HasValue && row.BirthYearTo.HasValue
|
||||
? $"established for {row.BirthYearFrom}-{row.BirthYearTo}"
|
||||
: "no era profile";
|
||||
}
|
||||
|
||||
<nav class="breadcrumb-trail" aria-label="Breadcrumb">
|
||||
<a asp-controller="Projects" asp-action="Index">Projects</a>
|
||||
<a asp-controller="Projects" asp-action="Details" asp-route-id="@Model.Project.ProjectID">@Model.Project.ProjectName</a>
|
||||
<span>Names Library</span>
|
||||
</nav>
|
||||
|
||||
<partial name="_ProjectSectionNav" model="Model.Project" />
|
||||
|
||||
<div class="page-heading compact">
|
||||
<div>
|
||||
<p class="eyebrow">Characters</p>
|
||||
<h1>Names Library</h1>
|
||||
<p class="lead-text">Search established given-name forms with era, locale and current-project usage context.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="edit-panel names-library-panel names-library-panel--search">
|
||||
<form method="get" asp-action="Index" data-name-search-form>
|
||||
<input type="hidden" name="ProjectID" value="@Model.Project.ProjectID" />
|
||||
<input type="hidden" name="Page" value="1" />
|
||||
<input type="hidden" id="GenderPreference" name="GenderPreference" value="@filter.GenderPreference" data-gender-preference />
|
||||
<input type="hidden" id="GenderThreshold" name="GenderThreshold" value="@filter.GenderThreshold" data-gender-threshold />
|
||||
<div class="row g-3 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label" for="Search">Search</label>
|
||||
<input class="form-control" id="Search" name="Search" value="@filter.Search" autocomplete="off" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="LocaleCode">Locale</label>
|
||||
<select class="form-select" id="LocaleCode" name="LocaleCode">
|
||||
@foreach (var locale in Model.Locales)
|
||||
{
|
||||
<option value="@locale.LocaleCode" selected="@(string.Equals(filter.LocaleCode, locale.LocaleCode, StringComparison.OrdinalIgnoreCase))">@locale.DisplayName</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<label class="form-label" for="BirthYear">Birth year</label>
|
||||
<input class="form-control" id="BirthYear" name="BirthYear" value="@filter.BirthYear" inputmode="numeric" />
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="GenderPreset">Gender prior</label>
|
||||
<select class="form-select" id="GenderPreset" data-gender-preset>
|
||||
<option value="" selected="@(genderPreset == string.Empty)">Any</option>
|
||||
<option value="female-mostly" data-preference="Female" data-threshold="0.5" selected="@(genderPreset == "female-mostly")">Mostly female</option>
|
||||
<option value="female-strong" data-preference="Female" data-threshold="0.7" selected="@(genderPreset == "female-strong")">Strongly female</option>
|
||||
<option value="female-very-strong" data-preference="Female" data-threshold="0.9" selected="@(genderPreset == "female-very-strong")">Very strongly female</option>
|
||||
<option value="male-mostly" data-preference="Male" data-threshold="0.5" selected="@(genderPreset == "male-mostly")">Mostly male</option>
|
||||
<option value="male-strong" data-preference="Male" data-threshold="0.7" selected="@(genderPreset == "male-strong")">Strongly male</option>
|
||||
<option value="male-very-strong" data-preference="Male" data-threshold="0.9" selected="@(genderPreset == "male-very-strong")">Very strongly male</option>
|
||||
<option value="mixed-balanced" data-preference="Mixed" data-threshold="0.65" selected="@(genderPreset == "mixed-balanced")">Balanced / mixed</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="NameType">Name type</label>
|
||||
<select class="form-select" id="NameType" name="NameType">
|
||||
<option value="">Any</option>
|
||||
@foreach (var type in new[] { "Canonical", "Independent", "Diminutive", "Nickname", "Variant" })
|
||||
{
|
||||
<option value="@type" selected="@(filter.NameType == type)">@type</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="MinimumUsage">Commonness</label>
|
||||
<select class="form-select" id="MinimumUsage" name="MinimumUsage">
|
||||
<option value="" selected="@(!filter.MinimumUsage.HasValue)">Any</option>
|
||||
<option value="0.01" selected="@(filter.MinimumUsage == 0.01m)">Rare or above</option>
|
||||
<option value="0.05" selected="@(filter.MinimumUsage == 0.05m)">Familiar</option>
|
||||
<option value="0.25" selected="@(filter.MinimumUsage == 0.25m)">Common</option>
|
||||
<option value="0.6" selected="@(filter.MinimumUsage == 0.6m)">Very common</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="BookID">Used scope</label>
|
||||
<select class="form-select" id="BookID" name="BookID">
|
||||
<option value="">Project</option>
|
||||
@foreach (var book in Model.Books)
|
||||
{
|
||||
<option value="@book.BookID" selected="@(filter.BookID == book.BookID)">@book.BookDisplayTitle</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="UsedOnly">Usage</label>
|
||||
<select class="form-select" id="UsedOnly" name="UsedOnly">
|
||||
<option value="">Used and unused</option>
|
||||
<option value="true" selected="@(filter.UsedOnly == true)">Already used</option>
|
||||
<option value="false" selected="@(filter.UsedOnly == false)">Unused</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<label class="form-label" for="PageSize">Rows</label>
|
||||
<select class="form-select" id="PageSize" name="PageSize">
|
||||
@foreach (var size in new[] { 25, 50, 100 })
|
||||
{
|
||||
<option value="@size" selected="@(filter.PageSize == size)">@size</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<button class="btn btn-primary w-100" type="submit">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="story-bible-section names-library-panel names-library-panel--results">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h2>Results</h2>
|
||||
<span class="muted">@Model.TotalRows.ToString("N0") names</span>
|
||||
</div>
|
||||
|
||||
@if (!Model.Results.Any())
|
||||
{
|
||||
<p class="muted">No matching names found.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-responsive">
|
||||
<table class="table align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Profile</th>
|
||||
<th>Related forms</th>
|
||||
<th>Current use</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var row in Model.Results)
|
||||
{
|
||||
<tr>
|
||||
<td>
|
||||
<strong>@row.DisplayName</strong>
|
||||
<div class="muted small">@row.NameType@if (row.IsWordAmbiguous){<span> · ambiguous word</span>}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span>@GenderLabel(row); @EraLabel(row)</span>
|
||||
<div class="muted small">@row.LocaleDisplayName · usage @row.UsageWeight?.ToString("0.000") · confidence @row.Confidence?.ToString("0.000")</div>
|
||||
</td>
|
||||
<td>@(string.IsNullOrWhiteSpace(row.RelatedForms) ? "None listed" : row.RelatedForms)</td>
|
||||
<td>
|
||||
@if (row.UsedCount > 0)
|
||||
{
|
||||
<span class="badge bg-warning text-dark">Already used</span>
|
||||
<div class="muted small">@row.UsedSummary</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-success">Unused here</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="button-row">
|
||||
<a class="btn btn-outline-secondary @(filter.Page <= 1 ? "disabled" : string.Empty)" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-search="@filter.Search" asp-route-localeCode="@filter.LocaleCode" asp-route-birthYear="@filter.BirthYear" asp-route-genderPreference="@filter.GenderPreference" asp-route-genderThreshold="@filter.GenderThreshold" asp-route-nameType="@filter.NameType" asp-route-minimumUsage="@filter.MinimumUsage" asp-route-bookId="@filter.BookID" asp-route-usedOnly="@filter.UsedOnly" asp-route-page="@previousPage" asp-route-pageSize="@filter.PageSize">Previous</a>
|
||||
<span class="muted">Page @filter.Page of @Model.TotalPages</span>
|
||||
<a class="btn btn-outline-secondary @(filter.Page >= Model.TotalPages ? "disabled" : string.Empty)" asp-action="Index" asp-route-projectId="@Model.Project.ProjectID" asp-route-search="@filter.Search" asp-route-localeCode="@filter.LocaleCode" asp-route-birthYear="@filter.BirthYear" asp-route-genderPreference="@filter.GenderPreference" asp-route-genderThreshold="@filter.GenderThreshold" asp-route-nameType="@filter.NameType" asp-route-minimumUsage="@filter.MinimumUsage" asp-route-bookId="@filter.BookID" asp-route-usedOnly="@filter.UsedOnly" asp-route-page="@nextPage" asp-route-pageSize="@filter.PageSize">Next</a>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@section Scripts {
|
||||
<script>
|
||||
(() => {
|
||||
const root = document.querySelector("[data-name-search-form]");
|
||||
if (!root) return;
|
||||
const preset = root.querySelector("[data-gender-preset]");
|
||||
const preference = root.querySelector("[data-gender-preference]");
|
||||
const threshold = root.querySelector("[data-gender-threshold]");
|
||||
const sync = () => {
|
||||
const selected = preset?.selectedOptions?.[0];
|
||||
preference.value = selected?.dataset.preference || "";
|
||||
threshold.value = selected?.dataset.threshold || "";
|
||||
};
|
||||
preset?.addEventListener("change", sync);
|
||||
sync();
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
@ -74,6 +74,10 @@
|
||||
<dl>
|
||||
<div><dt>First appearance</dt><dd>@Display(candidate.ExampleFirstAppearance)</dd></div>
|
||||
<div><dt>Possible aliases</dt><dd>@(candidate.PossibleAliases.Count == 0 ? "None detected" : string.Join(", ", candidate.PossibleAliases))</dd></div>
|
||||
@if (!string.IsNullOrWhiteSpace(candidate.NameIntelligenceWarning))
|
||||
{
|
||||
<div><dt>Name warning</dt><dd class="text-warning">@candidate.NameIntelligenceWarning</dd></div>
|
||||
}
|
||||
<div><dt>Existing match</dt><dd>@(candidate.ExistingCharacterName ?? "None")</dd></div>
|
||||
</dl>
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@
|
||||
<a class="pw-project-nav__link @(ActiveClass("Overview"))" asp-controller="Projects" asp-action="Details" asp-route-id="@projectId">Overview</a>
|
||||
<a class="pw-project-nav__link @(ActiveClass("Timeline"))" asp-controller="Timeline" asp-action="Index" asp-route-projectId="@projectId">Timeline</a>
|
||||
<a class="pw-project-nav__link @(ActiveClass("Characters"))" asp-controller="Characters" asp-action="Index" asp-route-projectId="@projectId">Characters</a>
|
||||
<a class="pw-project-nav__link @(ActiveClass("Names"))" asp-controller="Names" asp-action="Index" asp-route-projectId="@projectId">Names</a>
|
||||
<a class="pw-project-nav__link @(ActiveClass("Relationships"))" asp-controller="RelationshipMap" asp-action="Index" asp-route-projectId="@projectId">Relationships</a>
|
||||
<a class="pw-project-nav__link @(ActiveClass("Assets"))" asp-controller="StoryAssets" asp-action="Index" asp-route-projectId="@projectId">Assets</a>
|
||||
<a class="pw-project-nav__link @(ActiveClass("Locations"))" asp-controller="Locations" asp-action="Index" asp-route-projectId="@projectId">Locations</a>
|
||||
|
||||
@ -27,7 +27,17 @@
|
||||
"StoryIntelligence": {
|
||||
"ChapterStructureModel": "gpt-5-mini",
|
||||
"SceneIntelligenceModel": "gpt-5-mini",
|
||||
"SceneIntelligenceMaxOutputTokens": 10000
|
||||
"SceneIntelligenceMaxOutputTokens": 10000,
|
||||
"AutomaticImageGenerationDuringImportEnabled": false,
|
||||
"RequireImageGenerationApproval": true,
|
||||
"MaxImagesPerImport": 12,
|
||||
"MaxCharacterImagesPerImport": 4,
|
||||
"MaxLocationImagesPerImport": 6,
|
||||
"MaxAssetImagesPerImport": 2,
|
||||
"ImageGenerationModel": "gpt-image-1.5",
|
||||
"ImageGenerationQuality": "medium",
|
||||
"ImageGenerationSize": "1024x1024",
|
||||
"ImageGenerationOutputFormat": "webp"
|
||||
},
|
||||
"Site": {
|
||||
"PublicBaseUrl": "https://plotdirector.com"
|
||||
|
||||
@ -490,6 +490,18 @@ td.text-end {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.names-library-panel {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.names-library-panel--search {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.names-library-panel--results {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.feature-request-admin-table {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
BIN
plotdirector-generated-images-1218.zip
Normal file
BIN
plotdirector-generated-images-1218.zip
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user