Add name intelligence and generation safeguards

This commit is contained in:
Nick Beckley 2026-08-24 12:01:07 +00:00
parent 77369a86fd
commit 25a8ce65fa
43 changed files with 98827 additions and 57 deletions

View File

@ -31,9 +31,15 @@ var tests = new (string Name, Action Test)[]
("Knowledge signals map to existing knowledge states", KnowledgeSignalsMapToExistingStates), ("Knowledge signals map to existing knowledge states", KnowledgeSignalsMapToExistingStates),
("Knowledge duplicate statements share canonical keys", KnowledgeDuplicateStatementsShareCanonicalKeys), ("Knowledge duplicate statements share canonical keys", KnowledgeDuplicateStatementsShareCanonicalKeys),
("Illustration prompt builder separates spec and prompt", IllustrationPromptBuilderSeparatesSpecAndPrompt), ("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 specification validation catches missing typed fields", IllustrationSpecificationValidationCatchesMissingTypedFields),
("Illustration category validation rejects unknown values", IllustrationCategoryValidationRejectsUnknownValues), ("Illustration category validation rejects unknown values", IllustrationCategoryValidationRejectsUnknownValues),
("Illustration status transitions protect approval flow", IllustrationStatusTransitionsProtectApprovalFlow), ("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 storage codes are normalised safely", IllustrationStorageCodesAreNormalisedSafely),
("Illustration starter batch has required category counts", IllustrationStarterBatchHasRequiredCategoryCounts), ("Illustration starter batch has required category counts", IllustrationStarterBatchHasRequiredCategoryCounts),
("Illustration starter generation plan skips successful items", IllustrationStarterGenerationPlanSkipsSuccessfulItems), ("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), ("Manuscript scan preview keeps only auto-selected character candidates", ManuscriptScanPreviewKeepsOnlyAutoSelectedCharacterCandidates),
("Onboarding wizard keeps character pages inside one main step", OnboardingWizardKeepsCharacterPagesInsideOneMainStep), ("Onboarding wizard keeps character pages inside one main step", OnboardingWizardKeepsCharacterPagesInsideOneMainStep),
("Scan review aliases feed Story Intelligence known character context", ScanReviewAliasesFeedStoryIntelligenceKnownCharacterContext), ("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), ("Scan review post supports full-book form submissions", ScanReviewPostSupportsFullBookFormSubmissions),
("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel), ("Story Intelligence experience boot does not serialise live model", StoryIntelligenceExperienceBootDoesNotSerialiseLiveModel),
("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics), ("Live visualisation strips illustration diagnostics", LiveVisualisationStripsIllustrationDiagnostics),
@ -789,12 +799,12 @@ static void IllustrationPromptBuilderSeparatesSpecAndPrompt()
var result = builder.Build(spec); var result = builder.Build(spec);
Assert(result.TemplateVersion == IllustrationPromptBuilder.CurrentTemplateVersion, "Template version was not recorded."); 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("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("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("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("realistic cinematic still", StringComparison.OrdinalIgnoreCase), "Prompt did not include the realistic cinematic 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("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("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("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."); 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."); 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() static void IllustrationSpecificationValidationCatchesMissingTypedFields()
{ {
var invalid = new IllustrationGenerationSpecification var invalid = new IllustrationGenerationSpecification
@ -835,6 +855,58 @@ static void IllustrationStatusTransitionsProtectApprovalFlow()
Assert(!IllustrationLibraryStatuses.CanTransition(IllustrationLibraryStatuses.Queued, IllustrationLibraryStatuses.Approved), "Queued should not approve directly."); 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() static void IllustrationStorageCodesAreNormalisedSafely()
{ {
Assert(IllustrationLibraryStorageService.SafeSegment("Asset Letter_01.png") == "asset-letter-01-png", "Storage code was not normalised."); 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 == "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("beth", StringComparison.OrdinalIgnoreCase), "Demand key must not include character name.");
Assert(!key.Contains("eye", StringComparison.OrdinalIgnoreCase), "Demand key must not include eye colour."); 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() static void IllustrationAssignmentRepairEnforcesExplicitEvidenceGroupsAndStrictSubtypes()
@ -1203,7 +1275,7 @@ static void IllustrationProviderReportsMissingImageModel()
{ {
var provider = new OpenAIIllustrationImageProvider( var provider = new OpenAIIllustrationImageProvider(
new HttpClient(), new HttpClient(),
Options.Create(new StoryIntelligenceOptions { ApiKey = "configured-for-test" }), Options.Create(new StoryIntelligenceOptions { ApiKey = "configured-for-test", ImageGenerationModel = "" }),
NullLogger<OpenAIIllustrationImageProvider>.Instance); NullLogger<OpenAIIllustrationImageProvider>.Instance);
Assert(!provider.IsConfigured, "Provider should not be configured without an image model."); 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("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("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(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("AutomaticImageGenerationDuringImportEnabled", StringComparison.Ordinal), "Live imports must gate automatic image generation behind configuration.");
Assert(persistedRunner.Contains("MaxDemandToQueue = 24", StringComparison.Ordinal), "Live imports must cap automatic demand generation."); 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("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("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("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("CategoryCap(options, specification.Category)", StringComparison.Ordinal), "Automatic demand generation should use category-specific caps.");
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("QueuedIllustrationLibraryItemID.HasValue || item.GeneratedIllustrationLibraryItemID.HasValue", StringComparison.Ordinal), "Demand generation cap must count already queued or generated automatic demand for the import.");
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("\"Flyover\"", StringComparison.Ordinal) && storyMemory.Contains("concrete overpass", StringComparison.Ordinal), "Location prompts must distinguish road-under-flyover scenes from generic roads."); 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(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."); 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."); 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() static JsonSerializerOptions JsonOptions()
=> new() => new()
{ {

View File

@ -21,6 +21,7 @@ public sealed class AdminController(
IStoryIntelligenceExistingChapterQueueService existingChapterQueue, IStoryIntelligenceExistingChapterQueueService existingChapterQueue,
IStoryIntelligenceFullChapterTestService fullChapterTest, IStoryIntelligenceFullChapterTestService fullChapterTest,
IStoryIntelligenceImportCommitService storyIntelligenceImportCommit, IStoryIntelligenceImportCommitService storyIntelligenceImportCommit,
INameIntelligenceService nameIntelligence,
IIllustrationLibraryService illustrationLibrary) : Controller IIllustrationLibraryService illustrationLibrary) : Controller
{ {
[HttpGet("")] [HttpGet("")]
@ -39,6 +40,34 @@ public sealed class AdminController(
return View(); 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")] [HttpGet("story-intelligence/illustration-library")]
public async Task<IActionResult> StoryIntelligenceIllustrationLibrary([FromQuery] IllustrationLibraryFilter filter) public async Task<IActionResult> StoryIntelligenceIllustrationLibrary([FromQuery] IllustrationLibraryFilter filter)
{ {

View 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);
}
}

View File

@ -107,6 +107,9 @@ public sealed class IllustrationLibraryRepository(ISqlConnectionFactory connecti
image.MimeType, image.MimeType,
Provider = providerResult.Provider, Provider = providerResult.Provider,
Model = providerResult.Model, Model = providerResult.Model,
providerResult.Quality,
providerResult.Size,
providerResult.OutputFormat,
PromptText = prompt, PromptText = prompt,
PromptTemplateVersion = templateVersion, PromptTemplateVersion = templateVersion,
ProviderRequestID = providerResult.ProviderRequestID ProviderRequestID = providerResult.ProviderRequestID

View 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();
}

View 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.

View 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.

View 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.

View 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.

File diff suppressed because it is too large Load Diff

View 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"
}

View 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
1 locale_code display_name iso_country_code evidence_basis default_confidence sort_order is_active
2 en-GB United Kingdom GBR Direct UK registration series 0.88 1 true
3 en-IE Ireland IRL Northern Ireland evidence plus UK/US smoothing 0.67 2 true
4 en-US United States USA Direct SSA registration series 0.92 3 true
5 en-CA Canada CAN US/UK evidence blend 0.64 4 true
6 en-AU Australia AUS UK/US evidence blend 0.62 5 true
7 en-NZ New Zealand NZL UK/US evidence blend 0.62 6 true

View 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
1 relationship_key from_given_name_key to_given_name_key relationship_type strength is_bidirectional is_active curation_source notes
2 nr_000001 gn_abbey gn_abigail SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
3 nr_000002 gn_abbie gn_abigail Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
4 nr_000003 gn_abby gn_abigail Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
5 nr_000004 gn_abby gn_abigail Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
6 nr_000005 gn_abe gn_abraham Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
7 nr_000006 gn_addie gn_adelaide Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
8 nr_000007 gn_addie gn_adeline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
9 nr_000008 gn_al gn_alan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
10 nr_000009 gn_al gn_albert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
11 nr_000010 gn_alec gn_alexander Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
12 nr_000011 gn_alex gn_alexander Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
13 nr_000012 gn_alex gn_alexander ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
14 nr_000013 gn_alex gn_alexandra ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
15 nr_000014 gn_alf gn_alfred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
16 nr_000015 gn_alfie gn_alfred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
17 nr_000016 gn_alfie gn_alfred Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
18 nr_000017 gn_ali gn_alison ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
19 nr_000018 gn_allie gn_alison Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
20 nr_000019 gn_allison gn_alison SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
21 nr_000020 gn_ally gn_alexandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
22 nr_000021 gn_andy gn_andrew Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
23 nr_000022 gn_andy gn_andrew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
24 nr_000023 gn_angie gn_angela Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
25 nr_000024 gn_angie gn_angela Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
26 nr_000025 gn_angie gn_angelina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
27 nr_000026 gn_anne gn_ann SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
28 nr_000027 gn_annie gn_ann Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
29 nr_000028 gn_annie gn_ann Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
30 nr_000029 gn_annie gn_anna Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
31 nr_000030 gn_annie gn_anna Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
32 nr_000031 gn_ant gn_anthony ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
33 nr_000032 gn_archie gn_archibald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
34 nr_000033 gn_arnie gn_arnold Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
35 nr_000034 gn_art gn_arthur Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
36 nr_000035 gn_art gn_arthur ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
37 nr_000036 gn_artie gn_arthur Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
38 nr_000037 gn_artie gn_arthur Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
39 nr_000038 gn_babs gn_barbara Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
40 nr_000039 gn_barb gn_barbara Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
41 nr_000040 gn_barb gn_barbara ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
42 nr_000041 gn_barbie gn_barbara Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
43 nr_000042 gn_barbie gn_barbara Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
44 nr_000043 gn_barney gn_bernard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
45 nr_000044 gn_bastian gn_sebastian ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
46 nr_000045 gn_bea gn_beatrice ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
47 nr_000046 gn_becca gn_rebecca Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
48 nr_000047 gn_becca gn_rebecca ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
49 nr_000048 gn_becky gn_rebecca Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
50 nr_000049 gn_becky gn_rebecca Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
51 nr_000050 gn_bella gn_isabel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
52 nr_000051 gn_bella gn_isabella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
53 nr_000052 gn_belle gn_isabella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
54 nr_000053 gn_ben gn_benjamin Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
55 nr_000054 gn_ben gn_benjamin ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
56 nr_000055 gn_benny gn_benjamin Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
57 nr_000056 gn_benny gn_benjamin Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
58 nr_000057 gn_bernie gn_bernard Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
59 nr_000058 gn_bernie gn_bernard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
60 nr_000059 gn_bert gn_albert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
61 nr_000060 gn_bert gn_herbert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
62 nr_000061 gn_bert gn_robert Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
63 nr_000062 gn_bertie gn_albert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
64 nr_000063 gn_bess gn_elizabeth Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
65 nr_000064 gn_bessie gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
66 nr_000065 gn_beth gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
67 nr_000066 gn_beth gn_elizabeth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
68 nr_000067 gn_betsy gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
69 nr_000068 gn_betty gn_elizabeth Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
70 nr_000069 gn_bex gn_rebecca Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
71 nr_000070 gn_bill gn_william Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
72 nr_000071 gn_billie gn_wilhelmina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
73 nr_000072 gn_billy gn_william Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
74 nr_000073 gn_billy gn_william Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
75 nr_000074 gn_bob gn_robert Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
76 nr_000075 gn_bobbie gn_roberta Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
77 nr_000076 gn_bobby gn_robert Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
78 nr_000077 gn_bobby gn_robert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
79 nr_000078 gn_bridie gn_bridget Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
80 nr_000079 gn_cam gn_cameron ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
81 nr_000080 gn_carol gn_caroline ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
82 nr_000081 gn_carrie gn_carol Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
83 nr_000082 gn_carrie gn_caroline Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
84 nr_000083 gn_carrie gn_caroline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
85 nr_000084 gn_cass gn_cassandra Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
86 nr_000085 gn_cass gn_cassandra ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
87 nr_000086 gn_cassie gn_cassandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
88 nr_000087 gn_cat gn_catherine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
89 nr_000088 gn_cathy gn_catherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
90 nr_000089 gn_cathy gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
91 nr_000090 gn_charlie gn_charles Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
92 nr_000091 gn_charlie gn_charles Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
93 nr_000092 gn_charlie gn_charlotte Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
94 nr_000093 gn_charlie gn_charlotte Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
95 nr_000094 gn_chas gn_charles ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
96 nr_000095 gn_chris gn_christina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
97 nr_000096 gn_chris gn_christina ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
98 nr_000097 gn_chris gn_christine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
99 nr_000098 gn_chris gn_christine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
100 nr_000099 gn_chris gn_christopher Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
101 nr_000100 gn_chris gn_christopher ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
102 nr_000101 gn_chrissie gn_christine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
103 nr_000102 gn_chrissy gn_christina Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
104 nr_000103 gn_chuck gn_charles Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
105 nr_000104 gn_cindy gn_cynthia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
106 nr_000105 gn_cindy gn_lucinda Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
107 nr_000106 gn_clare gn_claire SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
108 nr_000107 gn_clint gn_clinton Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
109 nr_000108 gn_connie gn_constance Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
110 nr_000109 gn_dan gn_daniel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
111 nr_000110 gn_dan gn_daniel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
112 nr_000111 gn_dani gn_danielle ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
113 nr_000112 gn_danny gn_daniel Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
114 nr_000113 gn_danny gn_daniel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
115 nr_000114 gn_danny gn_danielle Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
116 nr_000115 gn_dave gn_david Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
117 nr_000116 gn_dave gn_david ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
118 nr_000117 gn_davey gn_david Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
119 nr_000118 gn_davy gn_david Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
120 nr_000119 gn_davy gn_david Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
121 nr_000120 gn_deb gn_deborah Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
122 nr_000121 gn_deb gn_deborah ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
123 nr_000122 gn_debbie gn_deborah Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
124 nr_000123 gn_debbie gn_deborah Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
125 nr_000124 gn_debby gn_deborah SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
126 nr_000125 gn_della gn_adelaide Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
127 nr_000126 gn_dick gn_richard Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
128 nr_000127 gn_dolly gn_dorothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
129 nr_000128 gn_dom gn_dominic ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
130 nr_000129 gn_don gn_donald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
131 nr_000130 gn_donnie gn_donald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
132 nr_000131 gn_donny gn_donald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
133 nr_000132 gn_dora gn_theodora Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
134 nr_000133 gn_dot gn_dorothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
135 nr_000134 gn_dot gn_dorothy ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
136 nr_000135 gn_dottie gn_dorothy Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
137 nr_000136 gn_dottie gn_dorothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
138 nr_000137 gn_drew gn_andrew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
139 nr_000138 gn_drew gn_andrew ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
140 nr_000139 gn_ed gn_edmund ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
141 nr_000140 gn_ed gn_edward Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
142 nr_000141 gn_ed gn_edward ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
143 nr_000142 gn_eddie gn_edmund Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
144 nr_000143 gn_eddie gn_edward Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
145 nr_000144 gn_eddie gn_edward Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
146 nr_000145 gn_edie gn_edith Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
147 nr_000146 gn_eli gn_elias Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
148 nr_000147 gn_eli gn_elijah Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
149 nr_000148 gn_eliza gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
150 nr_000149 gn_eliza gn_elizabeth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
151 nr_000150 gn_ella gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
152 nr_000151 gn_ellie gn_eleanor Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
153 nr_000152 gn_ellie gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
154 nr_000153 gn_ellie gn_ellen Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
155 nr_000154 gn_elliot gn_elliott SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
156 nr_000155 gn_elsie gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
157 nr_000156 gn_emmy gn_emily Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
158 nr_000157 gn_ernie gn_ernest Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
159 nr_000158 gn_etta gn_henrietta Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
160 nr_000159 gn_fanny gn_frances Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
161 nr_000160 gn_flo gn_florence Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
162 nr_000161 gn_flo gn_florence ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
163 nr_000162 gn_florrie gn_florence Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
164 nr_000163 gn_fran gn_frances Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
165 nr_000164 gn_fran gn_frances ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
166 nr_000165 gn_frank gn_francis Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
167 nr_000166 gn_frankie gn_frances Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
168 nr_000167 gn_frankie gn_francis Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
169 nr_000168 gn_frankie gn_francis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
170 nr_000169 gn_frankie gn_frank Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
171 nr_000170 gn_fred gn_alfred ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
172 nr_000171 gn_fred gn_frederick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
173 nr_000172 gn_fred gn_frederick ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
174 nr_000173 gn_freddie gn_alfred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
175 nr_000174 gn_freddie gn_frederick Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
176 nr_000175 gn_freddie gn_frederick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
177 nr_000176 gn_freddy gn_frederick Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
178 nr_000177 gn_freddy gn_frederick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
179 nr_000178 gn_gabby gn_gabrielle Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
180 nr_000179 gn_gabe gn_gabriel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
181 nr_000180 gn_gabe gn_gabriel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
182 nr_000181 gn_gabi gn_gabrielle SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
183 nr_000182 gn_gail gn_abigail Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
184 nr_000183 gn_gail gn_abigail ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
185 nr_000184 gn_gene gn_eugene Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
186 nr_000185 gn_geoff gn_geoffrey ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
187 nr_000186 gn_georgie gn_george Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
188 nr_000187 gn_georgie gn_georgia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
189 nr_000188 gn_gerry gn_gerald Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
190 nr_000189 gn_gerry gn_gerald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
191 nr_000190 gn_gerry gn_geraldine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
192 nr_000191 gn_gigi gn_virginia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
193 nr_000192 gn_ginger gn_virginia Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
194 nr_000193 gn_ginny gn_virginia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
195 nr_000194 gn_ginny gn_virginia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
196 nr_000195 gn_greg gn_gregory Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
197 nr_000196 gn_greg gn_gregory ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
198 nr_000197 gn_gus gn_augustus Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
199 nr_000198 gn_gus gn_gustavo Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
200 nr_000199 gn_gwen gn_gwendoline ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
201 nr_000200 gn_gwen gn_gwendolyn Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
202 nr_000201 gn_hal gn_harold Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
203 nr_000202 gn_hal gn_henry Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
204 nr_000203 gn_hank gn_henry Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
205 nr_000204 gn_harry gn_harold Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
206 nr_000205 gn_harry gn_henry AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
207 nr_000206 gn_harry gn_henry Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
208 nr_000207 gn_hattie gn_harriet Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
209 nr_000208 gn_hattie gn_harriet Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
210 nr_000209 gn_isabella gn_isabel SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
211 nr_000210 gn_isabelle gn_isabel SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
212 nr_000211 gn_izzy gn_isabel Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
213 nr_000212 gn_izzy gn_isabella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
214 nr_000213 gn_jack gn_john Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
215 nr_000214 gn_jackie gn_jacqueline Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
216 nr_000215 gn_jackie gn_jacqueline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
217 nr_000216 gn_jake gn_jacob Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
218 nr_000217 gn_jamie gn_james Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
219 nr_000218 gn_janey gn_jane Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
220 nr_000219 gn_jay gn_james Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
221 nr_000220 gn_jay gn_jason ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
222 nr_000221 gn_jeff gn_jeffrey Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
223 nr_000222 gn_jeffrey gn_geoffrey SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
224 nr_000223 gn_jen gn_jennifer Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
225 nr_000224 gn_jen gn_jennifer ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
226 nr_000225 gn_jenny gn_jennifer Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
227 nr_000226 gn_jenny gn_jennifer Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
228 nr_000227 gn_jerry gn_gerald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
229 nr_000228 gn_jerry gn_gerald SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
230 nr_000229 gn_jerry gn_jeremy Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
231 nr_000230 gn_jess gn_jessica Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
232 nr_000231 gn_jess gn_jessica ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
233 nr_000232 gn_jessie gn_jessica Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
234 nr_000233 gn_jessie gn_jessica Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
235 nr_000234 gn_jez gn_jeremy Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
236 nr_000235 gn_jim gn_james Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
237 nr_000236 gn_jimmy gn_james Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
238 nr_000237 gn_jimmy gn_james Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
239 nr_000238 gn_jo gn_joanne Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
240 nr_000239 gn_jo gn_josephine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
241 nr_000240 gn_jo gn_josephine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
242 nr_000241 gn_joe gn_joseph Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
243 nr_000242 gn_joe gn_joseph ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
244 nr_000243 gn_joey gn_joseph Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
245 nr_000244 gn_joey gn_joseph Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
246 nr_000245 gn_johnny gn_john Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
247 nr_000246 gn_johnny gn_john Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
248 nr_000247 gn_johnny gn_jonathan Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
249 nr_000248 gn_jon gn_jonathan ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
250 nr_000249 gn_jonny gn_jonathan Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
251 nr_000250 gn_josh gn_joshua Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
252 nr_000251 gn_josh gn_joshua ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
253 nr_000252 gn_josie gn_josephine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
254 nr_000253 gn_josie gn_josephine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
255 nr_000254 gn_joy gn_joyce Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
256 nr_000255 gn_judy gn_judith Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
257 nr_000256 gn_judy gn_judith Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
258 nr_000257 gn_julie gn_julia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
259 nr_000258 gn_julie gn_julia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
260 nr_000259 gn_kaitlin gn_caitlin SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
261 nr_000260 gn_kate gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
262 nr_000261 gn_kate gn_catherine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
263 nr_000262 gn_kate gn_katherine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
264 nr_000263 gn_katelyn gn_caitlin SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
265 nr_000264 gn_katherine gn_catherine SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
266 nr_000265 gn_kathryn gn_catherine SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
267 nr_000266 gn_kathy gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
268 nr_000267 gn_kathy gn_katherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
269 nr_000268 gn_katie gn_catherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
270 nr_000269 gn_katie gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
271 nr_000270 gn_katie gn_katherine Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
272 nr_000271 gn_kay gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
273 nr_000272 gn_ken gn_kenneth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
274 nr_000273 gn_ken gn_kenneth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
275 nr_000274 gn_kenny gn_kenneth Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
276 nr_000275 gn_kenny gn_kenneth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
277 nr_000276 gn_kim gn_kimberly ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
278 nr_000277 gn_kit gn_christopher Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
279 nr_000278 gn_kitty gn_catherine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
280 nr_000279 gn_kris gn_christina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
281 nr_000280 gn_larry gn_laurence Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
282 nr_000281 gn_larry gn_lawrence Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
283 nr_000282 gn_laurie gn_laura Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
284 nr_000283 gn_laurie gn_laura Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
285 nr_000284 gn_laurie gn_lauren Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
286 nr_000285 gn_laurie gn_laurence Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
287 nr_000286 gn_lawrence gn_laurence SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
288 nr_000287 gn_len gn_leonard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
289 nr_000288 gn_lena gn_helena Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
290 nr_000289 gn_lenny gn_leonard Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
291 nr_000290 gn_lenny gn_leonard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
292 nr_000291 gn_leo gn_leonard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
293 nr_000292 gn_leo gn_leonard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
294 nr_000293 gn_leo gn_leonardo Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
295 nr_000294 gn_lettie gn_charlotte Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
296 nr_000295 gn_lettie gn_letitia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
297 nr_000296 gn_lewis gn_louis SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
298 nr_000297 gn_lex gn_alexander Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
299 nr_000298 gn_lexi gn_alexandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
300 nr_000299 gn_lexi gn_alexandra Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
301 nr_000300 gn_lexi gn_alexis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
302 nr_000301 gn_lexie gn_alexandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
303 nr_000302 gn_liam gn_william ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
304 nr_000303 gn_libby gn_elizabeth Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
305 nr_000304 gn_lillie gn_lillian SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
306 nr_000305 gn_lily gn_lillian Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
307 nr_000306 gn_lisa gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
308 nr_000307 gn_liv gn_olivia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
309 nr_000308 gn_liv gn_olivia ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
310 nr_000309 gn_livvy gn_olivia Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
311 nr_000310 gn_liz gn_elizabeth Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
312 nr_000311 gn_liz gn_elizabeth ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
313 nr_000312 gn_lizzie gn_elizabeth Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
314 nr_000313 gn_lizzy gn_elizabeth SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
315 nr_000314 gn_lori gn_lorraine Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
316 nr_000315 gn_lottie gn_charlotte Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
317 nr_000316 gn_lottie gn_charlotte Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
318 nr_000317 gn_lou gn_louis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
319 nr_000318 gn_lou gn_louis ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
320 nr_000319 gn_lou gn_louise ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
321 nr_000320 gn_louie gn_louis Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
322 nr_000321 gn_lucy gn_lucille Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
323 nr_000322 gn_lucy gn_lucinda ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
324 nr_000323 gn_lulu gn_louise Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
325 nr_000324 gn_lulu gn_lucille Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
326 nr_000325 gn_maddie gn_madeline Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
327 nr_000326 gn_mae gn_mary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
328 nr_000327 gn_maggie gn_margaret Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
329 nr_000328 gn_maggie gn_margaret Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
330 nr_000329 gn_maisie gn_margaret Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
331 nr_000330 gn_mamie gn_mary Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
332 nr_000331 gn_mandy gn_amanda Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
333 nr_000332 gn_mandy gn_amanda Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
334 nr_000333 gn_margo gn_margaret ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
335 nr_000334 gn_mari gn_marianne ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
336 nr_000335 gn_marie gn_maria SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
337 nr_000336 gn_marty gn_martin Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
338 nr_000337 gn_mary gn_maria AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
339 nr_000338 gn_matt gn_matthew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
340 nr_000339 gn_matt gn_matthew ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
341 nr_000340 gn_mattie gn_matilda Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
342 nr_000341 gn_matty gn_matthew Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
343 nr_000342 gn_matty gn_matthew Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
344 nr_000343 gn_max gn_maximilian Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
345 nr_000344 gn_max gn_maxwell Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
346 nr_000345 gn_meg gn_margaret Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
347 nr_000346 gn_meg gn_margaret ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
348 nr_000347 gn_mel gn_melissa ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
349 nr_000348 gn_mick gn_michael Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
350 nr_000349 gn_mickey gn_michael Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
351 nr_000350 gn_mike gn_michael Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
352 nr_000351 gn_mike gn_michael ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
353 nr_000352 gn_mikey gn_michael Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
354 nr_000353 gn_millie gn_mildred Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
355 nr_000354 gn_millie gn_millicent Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
356 nr_000355 gn_mimi gn_miriam Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
357 nr_000356 gn_mindy gn_melinda Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
358 nr_000357 gn_minnie gn_minerva Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
359 nr_000358 gn_missy gn_melissa Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
360 nr_000359 gn_mitch gn_michelle Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
361 nr_000360 gn_mitch gn_mitchell Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
362 nr_000361 gn_molly gn_mary Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
363 nr_000362 gn_monty gn_montgomery Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
364 nr_000363 gn_nat gn_natalie ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
365 nr_000364 gn_nate gn_nathan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
366 nr_000365 gn_nate gn_nathaniel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
367 nr_000366 gn_nate gn_nathaniel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
368 nr_000367 gn_nathan gn_nathaniel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
369 nr_000368 gn_ned gn_edmund Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
370 nr_000369 gn_ned gn_edward Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
371 nr_000370 gn_nell gn_eleanor Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
372 nr_000371 gn_nell gn_ellen Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
373 nr_000372 gn_nell gn_helen Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
374 nr_000373 gn_nell gn_penelope Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
375 nr_000374 gn_nellie gn_eleanor Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
376 nr_000375 gn_nellie gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
377 nr_000376 gn_nellie gn_helen Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
378 nr_000377 gn_nellie gn_helen Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
379 nr_000378 gn_nelly gn_helen Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
380 nr_000379 gn_nic gn_nicole ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
381 nr_000380 gn_nick gn_dominic Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
382 nr_000381 gn_nick gn_nicholas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
383 nr_000382 gn_nick gn_nicholas ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
384 nr_000383 gn_nicky gn_nicholas Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
385 nr_000384 gn_nicky gn_nicholas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
386 nr_000385 gn_nicky gn_nicole Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
387 nr_000386 gn_nico gn_nicholas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
388 nr_000387 gn_nikki gn_nicole Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
389 nr_000388 gn_nora gn_eleanor Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
390 nr_000389 gn_paddy gn_patrick Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
391 nr_000390 gn_pat gn_patricia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
392 nr_000391 gn_pat gn_patricia ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
393 nr_000392 gn_pat gn_patrick Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
394 nr_000393 gn_pat gn_patrick ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
395 nr_000394 gn_patsy gn_patricia Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
396 nr_000395 gn_peggy gn_margaret Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
397 nr_000396 gn_penny gn_penelope Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
398 nr_000397 gn_penny gn_penelope Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
399 nr_000398 gn_pete gn_peter Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
400 nr_000399 gn_pete gn_peter ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
401 nr_000400 gn_phil gn_philip Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
402 nr_000401 gn_phil gn_philip ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
403 nr_000402 gn_phillip gn_philip SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
404 nr_000403 gn_polly gn_mary Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
405 nr_000404 gn_ray gn_raymond Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
406 nr_000405 gn_ray gn_raymond ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
407 nr_000406 gn_reg gn_reginald ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
408 nr_000407 gn_reggie gn_reginald Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
409 nr_000408 gn_reggie gn_reginald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
410 nr_000409 gn_rich gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
411 nr_000410 gn_rich gn_richard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
412 nr_000411 gn_richie gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
413 nr_000412 gn_rick gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
414 nr_000413 gn_rick gn_richard ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
415 nr_000414 gn_ricky gn_richard Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
416 nr_000415 gn_ricky gn_richard Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
417 nr_000416 gn_rob gn_robert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
418 nr_000417 gn_rob gn_robert ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
419 nr_000418 gn_robbie gn_robert Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
420 nr_000419 gn_robbie gn_robert Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
421 nr_000420 gn_rodger gn_roger SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
422 nr_000421 gn_ron gn_ronald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
423 nr_000422 gn_ron gn_ronald ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
424 nr_000423 gn_ronnie gn_ronald Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
425 nr_000424 gn_ronnie gn_ronald Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
426 nr_000425 gn_rose gn_rosemary ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
427 nr_000426 gn_rosie gn_rose Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
428 nr_000427 gn_rosie gn_rosemary Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
429 nr_000428 gn_rosie gn_rosemary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
430 nr_000429 gn_roxie gn_roxanne Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
431 nr_000430 gn_roxy gn_roxanne Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
432 nr_000431 gn_sadie gn_sarah Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
433 nr_000432 gn_sally gn_sarah Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
434 nr_000433 gn_sam gn_samantha Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
435 nr_000434 gn_sam gn_samantha ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
436 nr_000435 gn_sam gn_samuel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
437 nr_000436 gn_sam gn_samuel ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
438 nr_000437 gn_sammie gn_samantha Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
439 nr_000438 gn_sammy gn_samantha Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
440 nr_000439 gn_sammy gn_samuel Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
441 nr_000440 gn_sammy gn_samuel Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
442 nr_000441 gn_sandra gn_cassandra ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
443 nr_000442 gn_sandy gn_alexander Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
444 nr_000443 gn_sandy gn_sandra Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
445 nr_000444 gn_sandy gn_sandra Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
446 nr_000445 gn_sara gn_sarah SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
447 nr_000446 gn_sasha gn_alexandra AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
448 nr_000447 gn_seb gn_sebastian ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
449 nr_000448 gn_shelly gn_michelle Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
450 nr_000449 gn_sofia gn_sophia SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
451 nr_000450 gn_sophie gn_sophia AlternativeForm 0.8800 true true PrototypeCurated Potential relationship only; never auto-merge characters
452 nr_000451 gn_stella gn_estella Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
453 nr_000452 gn_steph gn_stephanie ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
454 nr_000453 gn_steve gn_stephen ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
455 nr_000454 gn_steve gn_steven Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
456 nr_000455 gn_steven gn_stephen SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
457 nr_000456 gn_stevie gn_stephanie Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
458 nr_000457 gn_stevie gn_stephen Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
459 nr_000458 gn_stevie gn_steven Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
460 nr_000459 gn_sue gn_susan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
461 nr_000460 gn_sue gn_susan ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
462 nr_000461 gn_sue gn_suzanne ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
463 nr_000462 gn_susie gn_susan Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
464 nr_000463 gn_susie gn_susan Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
465 nr_000464 gn_susy gn_susan SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
466 nr_000465 gn_suzie gn_susan SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
467 nr_000466 gn_suzie gn_suzanne Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
468 nr_000467 gn_suzy gn_susan SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
469 nr_000468 gn_suzy gn_suzanne Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
470 nr_000469 gn_tammy gn_tamara Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
471 nr_000470 gn_ted gn_edward Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
472 nr_000471 gn_ted gn_theodore Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
473 nr_000472 gn_teddy gn_edward Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
474 nr_000473 gn_teddy gn_edward Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
475 nr_000474 gn_teddy gn_theodore Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
476 nr_000475 gn_teddy gn_theodore Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
477 nr_000476 gn_terri gn_teresa Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
478 nr_000477 gn_tess gn_teresa Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
479 nr_000478 gn_tessa gn_teresa Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
480 nr_000479 gn_tessa gn_teresa Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
481 nr_000480 gn_theo gn_theodore Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
482 nr_000481 gn_theo gn_theodore ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
483 nr_000482 gn_theresa gn_teresa SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
484 nr_000483 gn_tilly gn_matilda Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
485 nr_000484 gn_tim gn_timothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
486 nr_000485 gn_tim gn_timothy ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
487 nr_000486 gn_timmy gn_timothy Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
488 nr_000487 gn_timmy gn_timothy Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
489 nr_000488 gn_tina gn_christina Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
490 nr_000489 gn_tina gn_christina ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
491 nr_000490 gn_tina gn_christine ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
492 nr_000491 gn_tom gn_thomas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
493 nr_000492 gn_tom gn_thomas ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
494 nr_000493 gn_tommy gn_thomas Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
495 nr_000494 gn_tommy gn_thomas Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
496 nr_000495 gn_toni gn_antoinette Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
497 nr_000496 gn_toni gn_antonia Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
498 nr_000497 gn_tony gn_anthony Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
499 nr_000498 gn_tony gn_anthony Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
500 nr_000499 gn_topher gn_christopher ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
501 nr_000500 gn_tori gn_victoria Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
502 nr_000501 gn_tori gn_victoria ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
503 nr_000502 gn_tricia gn_patricia ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
504 nr_000503 gn_trixie gn_beatrice Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
505 nr_000504 gn_trudy gn_gertrude Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
506 nr_000505 gn_vicki gn_victoria SpellingVariant 0.9700 true true PrototypeCurated Potential relationship only; never auto-merge characters
507 nr_000506 gn_vicky gn_victoria Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
508 nr_000507 gn_vicky gn_victoria Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
509 nr_000508 gn_vince gn_vincent ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
510 nr_000509 gn_vinny gn_vincent Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
511 nr_000510 gn_wally gn_walter Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
512 nr_000511 gn_wally gn_walter Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
513 nr_000512 gn_walt gn_walter Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
514 nr_000513 gn_walt gn_walter ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
515 nr_000514 gn_wendy gn_gwendoline Nickname 0.7200 false true PrototypeCurated Potential relationship only; never auto-merge characters
516 nr_000515 gn_wendy gn_gwendolyn Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
517 nr_000516 gn_will gn_william Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
518 nr_000517 gn_will gn_william ShortForm 0.9200 false true PrototypeCurated Potential relationship only; never auto-merge characters
519 nr_000518 gn_willie gn_william Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
520 nr_000519 gn_willie gn_william Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
521 nr_000520 gn_winnie gn_winifred Diminutive 0.9000 false true PrototypeCurated Potential relationship only; never auto-merge characters
522 nr_000521 gn_winnie gn_winifred Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
523 nr_000522 gn_woody gn_woodrow Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
524 nr_000523 gn_xander gn_alexander Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
525 nr_000524 gn_zach gn_zachary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters
526 nr_000525 gn_zack gn_zachary Nickname 0.7800 false true DxdcCuratedNicknameMap Potential relationship only; never auto-merge characters

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
{
"ok": true,
"counts": {
"locales": 6,
"givenNames": 3067,
"relationships": 525,
"usages": 92010
},
"coverage": {
"usageGroups": 18402,
"expectedUsageGroups": 18402
},
"errors": [],
"warnings": []
}

View File

@ -23,6 +23,7 @@ public interface IStoryMemoryRepository
Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request); Task UpsertAssignmentAsync(StoryMemoryIllustrationAssignmentSave request);
Task InvalidateAssignmentAsync(int assignmentId, string reason); Task InvalidateAssignmentAsync(int assignmentId, string reason);
Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request); Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request);
Task<IReadOnlyList<StoryMemoryIllustrationDemand>> ListDemandsAsync(int importSessionId);
Task<string> GetAppearancePreferenceAsync(int importSessionId); Task<string> GetAppearancePreferenceAsync(int importSessionId);
Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId); Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId);
Task SetAppearancePreferenceAsync(int importSessionId, string preference); Task SetAppearancePreferenceAsync(int importSessionId, string preference);
@ -363,6 +364,18 @@ public sealed class StoryMemoryRepository(ISqlConnectionFactory connectionFactor
request); 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) public async Task<string> GetAppearancePreferenceAsync(int importSessionId)
=> (await GetImportContextAsync(importSessionId)).AppearancePreference; => (await GetImportContextAsync(importSessionId)).AppearancePreference;

View 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.

View File

@ -34,6 +34,7 @@ public static class IllustrationLibraryStatuses
public const string Failed = "Failed"; public const string Failed = "Failed";
public const string Superseded = "Superseded"; public const string Superseded = "Superseded";
public const string Disabled = "Disabled"; public const string Disabled = "Disabled";
public const string Archived = "Archived";
public static readonly IReadOnlySet<string> All = new HashSet<string>(StringComparer.OrdinalIgnoreCase) public static readonly IReadOnlySet<string> All = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
@ -45,7 +46,8 @@ public static class IllustrationLibraryStatuses
Rejected, Rejected,
Failed, Failed,
Superseded, Superseded,
Disabled Disabled,
Archived
}; };
public static bool IsValid(string? status) public static bool IsValid(string? status)
@ -60,10 +62,13 @@ public static class IllustrationLibraryStatuses
(Generating, Failed) => true, (Generating, Failed) => true,
(Generated, Approved) => true, (Generated, Approved) => true,
(Generated, Rejected) => true, (Generated, Rejected) => true,
(Generated, Archived) => true,
(Approved, Superseded) => true, (Approved, Superseded) => true,
(Approved, Archived) => true,
(Rejected, Queued) => true, (Rejected, Queued) => true,
(Failed, Queued) => true, (Failed, Queued) => true,
(Approved, Disabled) => true, (Approved, Disabled) => true,
(Disabled, Archived) => true,
(Disabled, Queued) => true, (Disabled, Queued) => true,
_ when string.Equals(from, to, StringComparison.OrdinalIgnoreCase) => true, _ when string.Equals(from, to, StringComparison.OrdinalIgnoreCase) => true,
_ => false _ => false
@ -95,6 +100,18 @@ public sealed class IllustrationGenerationSpecification
public IReadOnlyList<string> Palette { get; init; } = []; public IReadOnlyList<string> Palette { get; init; } = [];
public IReadOnlyList<string> MetadataTags { get; init; } = []; public IReadOnlyList<string> MetadataTags { get; init; } = [];
public Dictionary<string, string> Metadata { get; init; } = new(StringComparer.OrdinalIgnoreCase); 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() public IReadOnlyList<string> Validate()
{ {
@ -173,6 +190,28 @@ public sealed class IllustrationLibraryItem
public string? Prompt { get; set; } public string? Prompt { get; set; }
public string? PromptTemplateVersion { get; set; } public string? PromptTemplateVersion { get; set; }
public string? ProviderRequestID { 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 AttemptCount { get; set; }
public int GenerationAttemptCount { get; set; } public int GenerationAttemptCount { get; set; }
public int? ParentItemID { get; set; } public int? ParentItemID { get; set; }
@ -200,6 +239,7 @@ public sealed class IllustrationLibrarySummary
public int FailedCount { get; set; } public int FailedCount { get; set; }
public int SupersededCount { get; set; } public int SupersededCount { get; set; }
public int DisabledCount { get; set; } public int DisabledCount { get; set; }
public int ArchivedCount { get; set; }
} }
public sealed class IllustrationLibraryFilter public sealed class IllustrationLibraryFilter
@ -229,6 +269,9 @@ public sealed record IllustrationImageGenerationResult(
string? Provider = null, string? Provider = null,
string? Model = null, string? Model = null,
string? ProviderRequestID = null, string? ProviderRequestID = null,
string? Quality = null,
string? Size = null,
string? OutputFormat = null,
string? ErrorMessage = null) string? ErrorMessage = null)
{ {
[JsonIgnore] [JsonIgnore]

View 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);

View File

@ -99,8 +99,16 @@ public sealed class StoryIntelligenceOptions
public string Model { get; init; } = string.Empty; public string Model { get; init; } = string.Empty;
public string ChapterStructureModel { get; init; } = string.Empty; public string ChapterStructureModel { get; init; } = string.Empty;
public string SceneIntelligenceModel { 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 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 MaxOutputTokens { get; init; } = 4000;
public int? SceneIntelligenceMaxOutputTokens { get; init; } public int? SceneIntelligenceMaxOutputTokens { get; init; }
public int TimeoutSeconds { get; init; } = 120; public int TimeoutSeconds { get; init; } = 120;

View File

@ -220,6 +220,9 @@ public sealed record StoryMemoryRebuildOptions
public bool RebuildAll { get; init; } public bool RebuildAll { get; init; }
public bool QueueDemand { get; init; } public bool QueueDemand { get; init; }
public int? MaxDemandToQueue { 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 AppearancePreference { get; init; } = StoryMemoryAppearancePreferences.Balanced;
public string PrimaryAgeGroup { get; init; } = string.Empty; public string PrimaryAgeGroup { get; init; } = string.Empty;
public string StoryEra { 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 bool QueueDemand { get; init; }
public int? MaxDemandToQueue { 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 AppearancePreference { get; init; } = StoryMemoryAppearancePreferences.Balanced;
public string PrimaryAgeGroup { get; init; } = string.Empty; public string PrimaryAgeGroup { get; init; } = string.Empty;
public string StoryEra { get; init; } = string.Empty; public string StoryEra { get; init; } = string.Empty;

View File

@ -25,6 +25,14 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
</Content> </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>
<ItemGroup> <ItemGroup>

View File

@ -151,6 +151,7 @@ public class Program
builder.Services.AddScoped<IStoryIntelligenceIllustrationMatchingRepository, StoryIntelligenceIllustrationMatchingRepository>(); builder.Services.AddScoped<IStoryIntelligenceIllustrationMatchingRepository, StoryIntelligenceIllustrationMatchingRepository>();
builder.Services.AddScoped<IStoryMemoryRepository, StoryMemoryRepository>(); builder.Services.AddScoped<IStoryMemoryRepository, StoryMemoryRepository>();
builder.Services.AddScoped<ICharacterIntelligenceRepository, CharacterIntelligenceRepository>(); builder.Services.AddScoped<ICharacterIntelligenceRepository, CharacterIntelligenceRepository>();
builder.Services.AddScoped<INameIntelligenceRepository, NameIntelligenceRepository>();
builder.Services.AddScoped<IIllustrationLibraryRepository, IllustrationLibraryRepository>(); builder.Services.AddScoped<IIllustrationLibraryRepository, IllustrationLibraryRepository>();
builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>(); builder.Services.AddScoped<IProjectAccessRepository, ProjectAccessRepository>();
builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>(); builder.Services.AddScoped<IProjectCollaborationRepository, ProjectCollaborationRepository>();
@ -227,6 +228,7 @@ public class Program
builder.Services.AddScoped<IStoryIntelligenceVisualisationSnapshotService, StoryIntelligenceVisualisationSnapshotService>(); builder.Services.AddScoped<IStoryIntelligenceVisualisationSnapshotService, StoryIntelligenceVisualisationSnapshotService>();
builder.Services.AddScoped<IStoryMemoryService, StoryMemoryService>(); builder.Services.AddScoped<IStoryMemoryService, StoryMemoryService>();
builder.Services.AddScoped<ICharacterIntelligenceService, CharacterIntelligenceService>(); builder.Services.AddScoped<ICharacterIntelligenceService, CharacterIntelligenceService>();
builder.Services.AddScoped<INameIntelligenceService, NameIntelligenceService>();
builder.Services.AddScoped<IStoryIntelligencePipelineStateService, StoryIntelligencePipelineStateService>(); builder.Services.AddScoped<IStoryIntelligencePipelineStateService, StoryIntelligencePipelineStateService>();
builder.Services.AddScoped<IStoryIntelligenceProgressNotifier, StoryIntelligenceProgressNotifier>(); builder.Services.AddScoped<IStoryIntelligenceProgressNotifier, StoryIntelligenceProgressNotifier>();
builder.Services.AddScoped<ISceneImportResolver, SceneImportResolver>(); builder.Services.AddScoped<ISceneImportResolver, SceneImportResolver>();

View File

@ -16,7 +16,7 @@ public interface IIllustrationPromptBuilder
public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
{ {
public const string CurrentTemplateVersion = "21J.3"; public const string CurrentTemplateVersion = "22C.1";
private static readonly JsonSerializerOptions JsonOptions = new() private static readonly JsonSerializerOptions JsonOptions = new()
{ {
@ -57,7 +57,7 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
var categoryRules = CategoryRules(specification.Category); var categoryRules = CategoryRules(specification.Category);
var prompt = $""" 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} Category: {specification.Category}
Library code: {specification.Code} Library code: {specification.Code}
Title: {specification.Title} Title: {specification.Title}
@ -71,16 +71,16 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
Category composition rules: {categoryRules} Category composition rules: {categoryRules}
Requirements: Requirements:
- representative visual identifier only, not a canonical depiction of any manuscript - realistic grounded visual identifier only, not a canonical depiction of any manuscript
- modern cinematic digital illustration with premium narrative game concept art quality - naturalistic cinematic still, close to believable production photography while remaining safe for a reusable UI library
- subdued atmospheric background treatment with contemporary polished rendering, realistic anatomy and believable proportions - 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 - 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 - 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 - 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 realistic rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
- subtle depth of field, clean modern rendering, and enough exposure to remain clear inside PlotDirector's dark navy interface
- no visible text, captions, logos, watermarks, UI, signatures, or typography - 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, 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 - 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 - 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) private static string CategoryRules(string category)
=> category switch => 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.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; 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.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.", 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 _ => string.Empty
}; };
@ -333,6 +333,9 @@ public interface IIllustrationImageProvider
{ {
string ProviderName { get; } string ProviderName { get; }
string EffectiveModel { get; } string EffectiveModel { get; }
string EffectiveQuality { get; }
string EffectiveSize { get; }
string EffectiveOutputFormat { get; }
bool IsConfigured { get; } bool IsConfigured { get; }
IReadOnlyList<string> MissingConfiguration { get; } IReadOnlyList<string> MissingConfiguration { get; }
Task<IllustrationImageGenerationResult> GenerateAsync(IllustrationImageGenerationRequest request, CancellationToken cancellationToken); Task<IllustrationImageGenerationResult> GenerateAsync(IllustrationImageGenerationRequest request, CancellationToken cancellationToken);
@ -347,6 +350,9 @@ public sealed class OpenAIIllustrationImageProvider(
public string ProviderName => "OpenAI"; public string ProviderName => "OpenAI";
public string EffectiveModel => string.IsNullOrWhiteSpace(_options.ImageGenerationModel) ? "not configured" : _options.ImageGenerationModel; 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 bool IsConfigured => MissingConfiguration.Count == 0;
public IReadOnlyList<string> MissingConfiguration 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)}."); 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"); using var httpRequest = new HttpRequestMessage(HttpMethod.Post, "https://api.openai.com/v1/images/generations");
httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiKey); httpRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.ApiKey);
httpRequest.Content = new StringContent(JsonSerializer.Serialize(new httpRequest.Content = new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json");
{
model = _options.ImageGenerationModel,
prompt = request.Prompt,
size = string.IsNullOrWhiteSpace(_options.ImageGenerationSize) ? "1024x1024" : _options.ImageGenerationSize,
n = 1
}), Encoding.UTF8, "application/json");
using var response = await httpClient.SendAsync(httpRequest, cancellationToken); 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) if (!response.IsSuccessStatusCode)
{ {
var error = OpenAIImageError.FromResponseBody(body); var error = OpenAIImageError.FromResponseBody(responseBody);
logger.LogWarning( logger.LogWarning(
"OpenAI image generation failed for illustration {ItemID} with HTTP {StatusCode}, error code {ErrorCode}: {ErrorMessage}", "OpenAI image generation failed for illustration {ItemID} with HTTP {StatusCode}, error code {ErrorCode}: {ErrorMessage}",
request.IllustrationLibraryItemID, 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()}"); 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 requestId = response.Headers.TryGetValues("x-request-id", out var values) ? values.FirstOrDefault() : null;
var b64 = document.RootElement.GetProperty("data")[0].GetProperty("b64_json").GetString(); var b64 = document.RootElement.GetProperty("data")[0].GetProperty("b64_json").GetString();
if (string.IsNullOrWhiteSpace(b64)) 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(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) private static string TrimForStorage(string value)
@ -560,6 +580,16 @@ public sealed class IllustrationGenerationRunner(
var prompt = promptBuilder.Build(specification); var prompt = promptBuilder.Build(specification);
var request = new IllustrationImageGenerationRequest(item.IllustrationLibraryItemID, item.StableCode, item.Category, prompt.Prompt, prompt.TemplateVersion); 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); var result = await provider.GenerateAsync(request, cancellationToken);
if (!result.Succeeded || !result.HasImage) if (!result.Succeeded || !result.HasImage)
{ {
@ -928,6 +958,8 @@ public sealed class IllustrationLibraryService(
var items = await repository.ListAsync(new IllustrationLibraryFilter()); var items = await repository.ListAsync(new IllustrationLibraryFilter());
var lookup = items var lookup = items
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated) .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) .OrderBy(item => item.Status == IllustrationLibraryStatuses.Approved ? 0 : 1)
.ThenByDescending(item => item.UpdatedUtc) .ThenByDescending(item => item.UpdatedUtc)
.GroupBy(item => item.StableCode, StringComparer.OrdinalIgnoreCase) .GroupBy(item => item.StableCode, StringComparer.OrdinalIgnoreCase)

View 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; }
}
}

View File

@ -344,11 +344,21 @@ public sealed class PersistedStoryIntelligenceRunner(
{ {
try try
{ {
var queueImageDemand = settings.AutomaticImageGenerationDuringImportEnabled
&& !settings.RequireImageGenerationApproval
&& settings.MaxImagesPerImport > 0;
await storyMemory.ProcessSceneResultAsync( await storyMemory.ProcessSceneResultAsync(
importSessionId.Value, importSessionId.Value,
run.UserID, run.UserID,
sceneResultId, sceneResultId,
new StoryMemoryResolveOptions { QueueDemand = true, MaxDemandToQueue = 24 }, new StoryMemoryResolveOptions
{
QueueDemand = queueImageDemand,
MaxDemandToQueue = settings.MaxImagesPerImport,
MaxCharacterDemandToQueue = settings.MaxCharacterImagesPerImport,
MaxLocationDemandToQueue = settings.MaxLocationImagesPerImport,
MaxAssetDemandToQueue = settings.MaxAssetImagesPerImport
},
cancellationToken); cancellationToken);
} }
catch (Exception ex) when (ex is not OperationCanceledException && !IsFatal(ex)) catch (Exception ex) when (ex is not OperationCanceledException && !IsFatal(ex))

View File

@ -17,6 +17,7 @@ public sealed class StoryIntelligenceCharacterImportService(
ICharacterRepository characters, ICharacterRepository characters,
ISceneRepository scenes, ISceneRepository scenes,
IStoryIntelligencePipelineStateService pipelineState, IStoryIntelligencePipelineStateService pipelineState,
INameIntelligenceService nameIntelligence,
ILogger<StoryIntelligenceCharacterImportService> logger) : IStoryIntelligenceCharacterImportService ILogger<StoryIntelligenceCharacterImportService> logger) : IStoryIntelligenceCharacterImportService
{ {
private static readonly JsonSerializerOptions JsonOptions = new() private static readonly JsonSerializerOptions JsonOptions = new()
@ -70,6 +71,8 @@ public sealed class StoryIntelligenceCharacterImportService(
var data = await BuildCandidateDataAsync(batch); var data = await BuildCandidateDataAsync(batch);
var decidedKeys = batch.CharacterDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase); var decidedKeys = batch.CharacterDecisions.Select(decision => decision.Key).ToHashSet(StringComparer.OrdinalIgnoreCase);
var visibleCandidates = data.Candidates.Where(candidate => !decidedKeys.Contains(candidate.Key)).ToList(); 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 return new StoryIntelligenceCharacterReviewViewModel
{ {
HasCommittedScenes = data.HasCommittedScenes, HasCommittedScenes = data.HasCommittedScenes,
@ -86,11 +89,74 @@ public sealed class StoryIntelligenceCharacterImportService(
PossibleAliases = candidate.Aliases.OrderBy(value => value).ToList(), PossibleAliases = candidate.Aliases.OrderBy(value => value).ToList(),
ExampleFirstAppearance = candidate.FirstAppearanceNote, ExampleFirstAppearance = candidate.FirstAppearanceNote,
ExistingCharacterID = candidate.ExistingCharacterID, ExistingCharacterID = candidate.ExistingCharacterID,
ExistingCharacterName = candidate.ExistingCharacterName ExistingCharacterName = candidate.ExistingCharacterName,
NameIntelligenceWarning = nameWarnings.GetValueOrDefault(candidate.Key)
}).ToList() }).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) public async Task<StoryIntelligenceImportCommitResult> ImportAsync(OnboardingStoryIntelligenceBatch batch, StoryIntelligenceCharacterImportForm form)
{ {
var data = await BuildCandidateDataAsync(batch); var data = await BuildCandidateDataAsync(batch);
@ -877,6 +943,8 @@ public sealed class StoryIntelligenceCharacterImportService(
private sealed class CharacterIndex private sealed class CharacterIndex
{ {
private readonly Dictionary<string, CharacterMatch> byName = new(StringComparer.OrdinalIgnoreCase); 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) public void Add(string? name, int characterId, string characterName)
{ {
@ -884,6 +952,7 @@ public sealed class StoryIntelligenceCharacterImportService(
if (!string.IsNullOrWhiteSpace(key)) if (!string.IsNullOrWhiteSpace(key))
{ {
byName.TryAdd(key, new CharacterMatch(characterId, characterName)); 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 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) private sealed class CharacterCandidate(string key, string displayName, int? existingCharacterId, string? existingCharacterName)
{ {

View File

@ -154,6 +154,37 @@ public static class StoryIntelligenceIllustrationCompatibility
FirstMetadata(metadata, "generationSource", "source") ?? string.Empty); 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) public static IReadOnlyList<string> CharacterHardRejections(CharacterEvidenceProfile evidence, IllustrationCharacterMetadata candidate, bool isNamedCharacter, bool alreadyAssignedToAnotherSignificantCharacter)
{ {
var rejected = new List<string>(); var rejected = new List<string>();
@ -445,6 +476,19 @@ public static class StoryIntelligenceIllustrationCompatibility
|| (requestedType == "Note" && candidateType is "Letter" or "Document") || (requestedType == "Note" && candidateType is "Letter" or "Document")
|| (requestedType == "Rucksack" && candidateType == "Rucksack"); || (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) public static bool IsGroupEntity(string? value)
{ {
var clean = (value ?? string.Empty).Trim().ToLowerInvariant(); 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) private static string? FirstMetadata(Dictionary<string, string>? metadata, params string[] keys)
=> keys.Select(key => metadata?.GetValueOrDefault(key)).FirstOrDefault(value => !string.IsNullOrWhiteSpace(value)); => 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) private static bool BoolMetadata(Dictionary<string, string>? metadata, string key)
=> bool.TryParse(metadata?.GetValueOrDefault(key), out var value) && value; => bool.TryParse(metadata?.GetValueOrDefault(key), out var value) && value;
@ -821,3 +868,8 @@ public sealed record IllustrationCharacterMetadata(
bool IsSuitableForNamedCharacter, bool IsSuitableForNamedCharacter,
string? PromptTemplateVersion, string? PromptTemplateVersion,
string GenerationSource); string GenerationSource);
public sealed record IllustrationLocationMetadata(
string BaseType,
bool IsFallbackClassification,
string ReuseScope);

View File

@ -2,6 +2,7 @@ using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Microsoft.Extensions.Options;
using PlotLine.Data; using PlotLine.Data;
using PlotLine.Models; using PlotLine.Models;
using PlotLine.ViewModels; using PlotLine.ViewModels;
@ -20,6 +21,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
IStoryIntelligenceIllustrationMatchingRepository assignments, IStoryIntelligenceIllustrationMatchingRepository assignments,
IIllustrationPromptBuilder promptBuilder, IIllustrationPromptBuilder promptBuilder,
IUploadStorageService uploadStorage, IUploadStorageService uploadStorage,
IOptions<StoryIntelligenceOptions> options,
ILogger<StoryIntelligenceIllustrationMatchingService> logger) : IStoryIntelligenceIllustrationMatchingService ILogger<StoryIntelligenceIllustrationMatchingService> logger) : IStoryIntelligenceIllustrationMatchingService
{ {
private const int SuitableScore = 48; private const int SuitableScore = 48;
@ -30,6 +32,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = false WriteIndented = false
}; };
private readonly StoryIntelligenceOptions settings = options.Value;
public async Task ApplyCharacterIllustrationsAsync(int projectId, StoryIntelligenceExperiencePrototypeViewModel prototype) 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 })) var catalogue = (await library.ListAsync(new IllustrationLibraryFilter { Category = IllustrationLibraryCategories.Character }))
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated) .Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated)
.Where(item => item.IsActive && !item.IsArchived)
.Where(item => !item.IsFallbackClassification)
.Select(CharacterCandidate.From) .Select(CharacterCandidate.From)
.Where(candidate => candidate is not null) .Where(candidate => candidate is not null)
.Cast<CharacterCandidate>() .Cast<CharacterCandidate>()
@ -171,6 +176,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
var catalogue = (await library.ListAsync(new IllustrationLibraryFilter { Category = IllustrationLibraryCategories.Character })) var catalogue = (await library.ListAsync(new IllustrationLibraryFilter { Category = IllustrationLibraryCategories.Character }))
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated) .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)) .Where(item => !string.IsNullOrWhiteSpace(item.StableCode))
.GroupBy(item => item.IllustrationLibraryItemID) .GroupBy(item => item.IllustrationLibraryItemID)
.ToDictionary(group => group.Key, group => group.First()); .ToDictionary(group => group.Key, group => group.First());
@ -225,6 +232,8 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
var items = await library.ListAsync(new IllustrationLibraryFilter()); var items = await library.ListAsync(new IllustrationLibraryFilter());
var lookup = items var lookup = items
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated) .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) .GroupBy(item => item.StableCode, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase); .ToDictionary(group => group.Key, group => group.ToList(), StringComparer.OrdinalIgnoreCase);
@ -282,7 +291,9 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
"Unknown", "Unknown",
"Unknown", "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 = WithDemandDiagnostics(
location.ImageResolution, location.ImageResolution,
demandKey, demandKey,
@ -339,7 +350,9 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
"Unknown", "Unknown",
"Unknown", "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 = WithDemandDiagnostics(
asset.ImageResolution, asset.ImageResolution,
demandKey, demandKey,
@ -434,6 +447,11 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
return await library.QueueAsync(item.IllustrationLibraryItemID, null) > 0; return await library.QueueAsync(item.IllustrationLibraryItemID, null) > 0;
} }
private bool ShouldQueueAutomaticDemand()
=> settings.AutomaticImageGenerationDuringImportEnabled
&& !settings.RequireImageGenerationApproval
&& settings.MaxImagesPerImport > 0;
private MatchScore? ChooseAssignment( private MatchScore? ChooseAssignment(
CharacterObservation observation, CharacterObservation observation,
StoryIntelligenceCharacterIllustrationAssignment? existing, StoryIntelligenceCharacterIllustrationAssignment? existing,

View File

@ -80,6 +80,9 @@ public sealed class StoryMemoryService(
{ {
QueueDemand = options.QueueDemand, QueueDemand = options.QueueDemand,
MaxDemandToQueue = options.MaxDemandToQueue, MaxDemandToQueue = options.MaxDemandToQueue,
MaxCharacterDemandToQueue = options.MaxCharacterDemandToQueue,
MaxLocationDemandToQueue = options.MaxLocationDemandToQueue,
MaxAssetDemandToQueue = options.MaxAssetDemandToQueue,
AppearancePreference = options.AppearancePreference, AppearancePreference = options.AppearancePreference,
PrimaryAgeGroup = options.PrimaryAgeGroup, PrimaryAgeGroup = options.PrimaryAgeGroup,
StoryEra = options.StoryEra, StoryEra = options.StoryEra,
@ -149,6 +152,8 @@ public sealed class StoryMemoryService(
var assignments = await memory.ListAssignmentsAsync(importSessionId); var assignments = await memory.ListAssignmentsAsync(importSessionId);
var catalogue = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter())) var catalogue = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter()))
.Where(item => item.Status is IllustrationLibraryStatuses.Approved or IllustrationLibraryStatuses.Generated) .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) .Where(item => ResolveUploadUrl(item) is not null)
.ToList(); .ToList();
@ -201,7 +206,7 @@ public sealed class StoryMemoryService(
continue; 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)); 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 requestedType = StoryIntelligenceIllustrationCompatibility.LocationType($"{location.DisplayName} {location.SemanticType} {location.Subtype}");
var chosen = catalogue var chosen = catalogue
.Where(item => item.Category == IllustrationLibraryCategories.Location) .Where(item => item.Category == IllustrationLibraryCategories.Location)
.Select(item => (Item: item, Type: StoryIntelligenceIllustrationCompatibility.LocationType($"{item.StableCode} {item.DisplayTitle} {item.ShortDescription} {item.MetadataJson} {item.SpecificationJson}"))) .Select(item => (Item: item, Metadata: StoryIntelligenceIllustrationCompatibility.LocationMetadataFrom(item)))
.Where(item => StoryIntelligenceIllustrationCompatibility.LocationCompatible(requestedType, item.Type)) .Where(item => !item.Metadata.IsFallbackClassification)
.OrderByDescending(item => string.Equals(item.Type, requestedType, StringComparison.OrdinalIgnoreCase) ? 100 : 50) .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) .ThenBy(item => item.Item.StableCode)
.FirstOrDefault(); .FirstOrDefault();
if (chosen.Item is not null) 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 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)); 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 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)); 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)); var recorded = await memory.UpsertDemandAsync(new StoryMemoryIllustrationDemandSave(importSessionId, projectId, entityType, archetypeKey, 1, "Recorded", null, null));
if (!options.QueueDemand) 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(); return DemandResolution.Recorded();
} }
@ -936,6 +962,7 @@ public sealed class StoryMemoryService(
.FirstOrDefault(item => item.IllustrationLibraryItemID == existingId); .FirstOrDefault(item => item.IllustrationLibraryItemID == existingId);
if (existingItem is not null if (existingItem is not null
&& existingItem.IsActive && existingItem.IsActive
&& !existingItem.IsArchived
&& string.Equals(existingItem.StableCode, specification.Code, StringComparison.OrdinalIgnoreCase) && string.Equals(existingItem.StableCode, specification.Code, StringComparison.OrdinalIgnoreCase)
&& existingItem.Status is IllustrationLibraryStatuses.Planned && existingItem.Status is IllustrationLibraryStatuses.Planned
or IllustrationLibraryStatuses.Queued or IllustrationLibraryStatuses.Queued
@ -943,23 +970,70 @@ public sealed class StoryMemoryService(
or IllustrationLibraryStatuses.Generated or IllustrationLibraryStatuses.Generated
or IllustrationLibraryStatuses.Approved) 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}."); 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) if (options.MaxDemandToQueue is int maxDemandToQueue && maxDemandToQueue > 0)
{ {
var categoryCap = maxDemandToQueue;
var queuedDemandItems = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter())) var queuedDemandItems = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter()))
.Count(item => string.Equals(item.Category, specification.Category, StringComparison.OrdinalIgnoreCase) .Count(item => string.Equals(item.Category, specification.Category, StringComparison.OrdinalIgnoreCase)
&& item.IsActive && item.IsActive
&& !item.IsArchived
&& item.StableCode.StartsWith("demand-", StringComparison.OrdinalIgnoreCase) && item.StableCode.StartsWith("demand-", StringComparison.OrdinalIgnoreCase)
&& item.Status is IllustrationLibraryStatuses.Planned && item.Status is IllustrationLibraryStatuses.Planned
or IllustrationLibraryStatuses.Queued or IllustrationLibraryStatuses.Queued
or IllustrationLibraryStatuses.Generating); 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", "Queued",
planned.IllustrationLibraryItemID, planned.IllustrationLibraryItemID,
null)); 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}."); 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 code = DemandStableCode(StoryMemoryEntityTypes.Character, character.CanonicalIdentityKey, evidence.AgeBand, evidence.Presentation, evidence.HairColour, evidence.HairLength, evidence.SkinTone);
var age = UnknownToDefault(evidence.AgeBand, NormaliseAgeContext(options.PrimaryAgeGroup)); var age = UnknownToDefault(evidence.AgeBand, NormaliseAgeContext(options.PrimaryAgeGroup));
@ -1008,11 +1101,22 @@ public sealed class StoryMemoryService(
ClothingStyle = $"{era} {location} everyday naturalistic", ClothingStyle = $"{era} {location} everyday naturalistic",
Palette = ["clear blue", "warm amber", "natural skin tones"], Palette = ["clear blue", "warm amber", "natural skin tones"],
MetadataTags = ["character", "story-memory-demand"], 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 code = DemandStableCode(StoryMemoryEntityTypes.Location, location.CanonicalIdentityKey, requestedType);
var locationType = UnknownToDefault(requestedType, "UnknownInterior"); var locationType = UnknownToDefault(requestedType, "UnknownInterior");
@ -1033,11 +1137,22 @@ public sealed class StoryMemoryService(
Setting = location.InteriorExterior ?? InferInteriorExterior(locationType), Setting = location.InteriorExterior ?? InferInteriorExterior(locationType),
Palette = ["clear navy", "warm light", "balanced natural colour"], Palette = ["clear navy", "warm light", "balanced natural colour"],
MetadataTags = ["location", "story-memory-demand"], 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 code = DemandStableCode(StoryMemoryEntityTypes.Asset, asset.CanonicalIdentityKey, requestedType, requestedColour);
var objectType = UnknownToDefault(requestedType, "UnknownObject"); var objectType = UnknownToDefault(requestedType, "UnknownObject");
@ -1056,20 +1171,62 @@ public sealed class StoryMemoryService(
Era = asset.Era ?? "1980s", Era = asset.Era ?? "1980s",
Palette = ["clean blue", "warm light", "natural object colour"], Palette = ["clean blue", "warm light", "natural object colour"],
MetadataTags = ["asset", "story-memory-demand"], 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) var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{ {
["category"] = category, ["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", ["generationSource"] = "Story Memory demand",
["libraryPurpose"] = "Live import demand fulfilment", ["libraryPurpose"] = "Live import demand fulfilment",
["privacy"] = "generic-no-manuscript-data", ["privacy"] = "generic-no-manuscript-data",
["storyMemoryEntityHash"] = ShortHash(entityKey) ["storyMemoryEntityHash"] = ShortHash(entityKey)
}; };
if (sceneId.HasValue)
{
metadata["sourceSceneId"] = sceneId.Value.ToString("N0");
}
Add("primaryAgeGroup", options.PrimaryAgeGroup); Add("primaryAgeGroup", options.PrimaryAgeGroup);
Add("storyEra", options.StoryEra); Add("storyEra", options.StoryEra);
Add("storyLocation", options.StoryLocation); 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 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"; : "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) 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"; => 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";

View File

@ -40,7 +40,8 @@ public interface IWordCompanionService
public sealed class WordCompanionService( public sealed class WordCompanionService(
IWordCompanionRepository repository, IWordCompanionRepository repository,
IManuscriptDocumentRepository manuscriptDocuments, IManuscriptDocumentRepository manuscriptDocuments,
ICurrentUserService currentUser) : IWordCompanionService ICurrentUserService currentUser,
INameIntelligenceService nameIntelligence) : IWordCompanionService
{ {
private const int CharacterDiscoveryMinimumMentions = 2; private const int CharacterDiscoveryMinimumMentions = 2;
private const int CharacterCreationSelectionLimit = 250; private const int CharacterCreationSelectionLimit = 250;
@ -539,7 +540,8 @@ public sealed class WordCompanionService(
return null; 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 return new WordCompanionDiscoverCharactersResponse
{ {
ProjectId = request.ProjectId, 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) public Task<WordCompanionCreateDiscoveredCharactersResponse?> CreateDiscoveredCharactersAsync(WordCompanionCreateDiscoveredCharactersRequest request)
{ {
if (request.ProjectId <= 0 || request.CandidateNames.Count == 0) if (request.ProjectId <= 0 || request.CandidateNames.Count == 0)

View 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

View 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

View 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.';

View 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));
}

View File

@ -213,6 +213,7 @@ public sealed class StoryIntelligenceCharacterReviewCandidateViewModel
public string Confidence { get; init; } = "Unknown"; public string Confidence { get; init; } = "Unknown";
public IReadOnlyList<string> PossibleAliases { get; init; } = []; public IReadOnlyList<string> PossibleAliases { get; init; } = [];
public string? ExampleFirstAppearance { get; init; } public string? ExampleFirstAppearance { get; init; }
public string? NameIntelligenceWarning { get; init; }
public int? ExistingCharacterID { get; init; } public int? ExistingCharacterID { get; init; }
public string? ExistingCharacterName { get; init; } public string? ExistingCharacterName { get; init; }
public bool IsExistingMatch => ExistingCharacterID.HasValue; public bool IsExistingMatch => ExistingCharacterID.HasValue;

View File

@ -17,6 +17,15 @@
<span class="muted"> / Admin</span> <span class="muted"> / Admin</span>
</nav> </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"> <section class="edit-panel">
<div class="row g-3"> <div class="row g-3">
<div class="col-md-4"> <div class="col-md-4">
@ -91,6 +100,25 @@
<a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceIllustrationLibrary">Illustration library</a> <a class="btn btn-outline-primary btn-sm" asp-action="StoryIntelligenceIllustrationLibrary">Illustration library</a>
</div> </div>
</article> </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"> <article class="project-card">
<div class="project-card__body"> <div class="project-card__body">
<p class="eyebrow">Placeholder</p> <p class="eyebrow">Placeholder</p>

View 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>
}

View File

@ -74,6 +74,10 @@
<dl> <dl>
<div><dt>First appearance</dt><dd>@Display(candidate.ExampleFirstAppearance)</dd></div> <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> <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> <div><dt>Existing match</dt><dd>@(candidate.ExistingCharacterName ?? "None")</dd></div>
</dl> </dl>

View File

@ -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("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("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("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("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("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> <a class="pw-project-nav__link @(ActiveClass("Locations"))" asp-controller="Locations" asp-action="Index" asp-route-projectId="@projectId">Locations</a>

View File

@ -27,7 +27,17 @@
"StoryIntelligence": { "StoryIntelligence": {
"ChapterStructureModel": "gpt-5-mini", "ChapterStructureModel": "gpt-5-mini",
"SceneIntelligenceModel": "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": { "Site": {
"PublicBaseUrl": "https://plotdirector.com" "PublicBaseUrl": "https://plotdirector.com"

View File

@ -490,6 +490,18 @@ td.text-end {
max-width: none; 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 { .feature-request-admin-table {
table-layout: fixed; table-layout: fixed;
} }

Binary file not shown.