Strengthen story intelligence portrait context
This commit is contained in:
parent
61b4d9d288
commit
02bc4fd2de
@ -662,12 +662,12 @@ static void Phase21PUsesOneAuthoritativeCurrentVisualisationScene()
|
||||
Assert(view.Contains("Scenes = Model.Scenes", StringComparison.Ordinal), "Initial real page payload must include the same scene collection as polled snapshots.");
|
||||
}
|
||||
|
||||
static IllustrationCharacterMetadata Metadata(string code, string ageBand, string presentation, bool isUnknownFigure = false, string hairColour = "Unknown")
|
||||
static IllustrationCharacterMetadata Metadata(string code, string ageBand, string presentation, bool isUnknownFigure = false, string hairColour = "Unknown", string skinTone = "Unknown")
|
||||
=> new(
|
||||
ageBand,
|
||||
presentation,
|
||||
hairColour,
|
||||
"Unknown",
|
||||
skinTone,
|
||||
"Unknown",
|
||||
"Unknown",
|
||||
"Unknown",
|
||||
@ -1044,6 +1044,10 @@ static void Phase21UExtractsMandatoryCharacterAppearanceFixtures()
|
||||
|
||||
Assert(maggie.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.OlderTeen, $"Maggie should be OlderTeen, got {maggie.AgeBand}.");
|
||||
Assert(maggie.HairColour == StoryIntelligenceIllustrationCompatibility.HairColours.Red, $"Maggie should be Red, got {maggie.HairColour}.");
|
||||
var maggieMemory = StoryMemoryService.ResolveCharacterEvidenceFromAttributes("Maggie", [
|
||||
new StoryMemoryCharacterAttribute { AttributeType = "HairColour", NormalisedValue = StoryIntelligenceIllustrationCompatibility.HairColours.Red, Confidence = 0.95m, IsExplicit = true }
|
||||
]);
|
||||
Assert(maggieMemory.SkinTone == StoryIntelligenceIllustrationCompatibility.SkinTones.Light, $"Natural red hair should default unknown skin tone to Light, got {maggieMemory.SkinTone}.");
|
||||
Assert(beth.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.OlderTeen, $"Beth should be OlderTeen, got {beth.AgeBand}.");
|
||||
Assert(beth.HairColour == StoryIntelligenceIllustrationCompatibility.HairColours.Blonde, $"Beth should be Blonde, got {beth.HairColour}.");
|
||||
Assert(rosie.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult, $"Rosie should be YoungAdult, got {rosie.AgeBand}.");
|
||||
@ -1058,8 +1062,13 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
||||
|
||||
var mum = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("Mum", ["Mum is mother of teenage daughters."], []);
|
||||
var examiner = StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("driving examiner", ["the driving examiner"], []);
|
||||
var redHairedMaggie = StoryMemoryService.ResolveCharacterEvidenceFromAttributes("Maggie", [
|
||||
new StoryMemoryCharacterAttribute { AttributeType = "HairColour", NormalisedValue = StoryIntelligenceIllustrationCompatibility.HairColours.Red, Confidence = 0.95m, IsExplicit = true }
|
||||
]);
|
||||
var childCandidate = Metadata("char-child", "Child", "Feminine");
|
||||
var darkRedCandidate = Metadata("char-red-dark", "OlderTeen", "Feminine", hairColour: "Red", skinTone: "Dark");
|
||||
Assert(Rejected(mum, childCandidate).Any(reason => reason.Contains("age hard mismatch", StringComparison.OrdinalIgnoreCase)), "Parent role should reject child portraits.");
|
||||
Assert(Rejected(redHairedMaggie, darkRedCandidate).Any(reason => reason.Contains("skin tone hard mismatch", StringComparison.OrdinalIgnoreCase)), "Red-haired light-skin evidence must reject dark-skin generated portraits.");
|
||||
Assert(examiner.AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Adult, $"Driving examiner should infer Adult, got {examiner.AgeBand}.");
|
||||
Assert(StoryIntelligenceIllustrationCompatibility.InferCharacterEvidence("girl", ["girl"], []).AgeBand == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown, "Girl alone must not mean Child.");
|
||||
|
||||
@ -1087,7 +1096,13 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
||||
var scenePrompt = File.ReadAllText(Path.Combine(root, "Docs/AI/Scene-Prompt-V2.md"));
|
||||
|
||||
Assert(onboarding.Contains("name=\"appearancePreference\"", StringComparison.Ordinal), "Import setup must ask for appearance preference.");
|
||||
Assert(onboarding.Contains("name=\"primaryAgeGroup\"", StringComparison.Ordinal), "Import setup must ask for primary age group.");
|
||||
Assert(onboarding.Contains("name=\"storyEra\"", StringComparison.Ordinal), "Import setup must ask for era.");
|
||||
Assert(onboarding.Contains("name=\"storyLocation\"", StringComparison.Ordinal), "Import setup must ask for location.");
|
||||
Assert(storyMemory.Contains("CharacterAppearanceExtractionService.Extract", StringComparison.Ordinal), "Durable Story Memory must consume dedicated appearance extraction.");
|
||||
Assert(storyMemory.Contains("SkinToneInstruction(skinTone)", StringComparison.Ordinal), "Demand generation must use hard skin-tone wording.");
|
||||
Assert(storyMemory.Contains("HairColours.Red", StringComparison.Ordinal) && storyMemory.Contains("SkinTones.Light", StringComparison.Ordinal), "Red-hair demand evidence must force light skin when skin is otherwise unknown.");
|
||||
Assert(storyMemory.Contains("ApplyPrimaryAgeContext", StringComparison.Ordinal), "Primary age-group context must guide generic adult/unknown character ages.");
|
||||
Assert(persistedRunner.Contains("QueueDemand = true", StringComparison.Ordinal), "Live imports must queue missing illustration demand, not only record it.");
|
||||
Assert(persistedRunner.Contains("MaxDemandToQueue = 24", StringComparison.Ordinal), "Live imports must cap automatic demand generation.");
|
||||
Assert(storyMemory.Contains("UpsertPlannedAsync(specification", StringComparison.Ordinal), "Durable Story Memory demand must create reusable illustration library items.");
|
||||
@ -1101,6 +1116,7 @@ static void Phase21UEnforcesAliasAgeSemanticAndUiResetRules()
|
||||
Assert(resetSql.Contains("StoryIntelligence_DevelopmentAccountReset", StringComparison.Ordinal), "Phase 21U reset procedure is missing.");
|
||||
Assert(resetSql.Contains("ProjectHardDelete_ForOwner", StringComparison.Ordinal), "Reset must reuse owner-scoped project hard delete.");
|
||||
Assert(scenePrompt.Contains("characterAppearance", StringComparison.OrdinalIgnoreCase), "Scene prompt must request structured character appearance.");
|
||||
Assert(File.Exists(Path.Combine(root, "Sql/144_Phase21V_StoryMemoryImportContext.sql")), "Import context SQL migration is missing.");
|
||||
}
|
||||
|
||||
static void Phase21RSceneBrowserAndPanelsAreReplayFriendly()
|
||||
|
||||
@ -86,9 +86,13 @@ public sealed class OnboardingController(IOnboardingService onboarding, IOnboard
|
||||
|
||||
[HttpPost("story-intelligence/start")]
|
||||
[ValidateAntiForgeryToken]
|
||||
public async Task<IActionResult> StartStoryIntelligence([FromForm] string? appearancePreference)
|
||||
public async Task<IActionResult> StartStoryIntelligence(
|
||||
[FromForm] string? appearancePreference,
|
||||
[FromForm] string? primaryAgeGroup,
|
||||
[FromForm] string? storyEra,
|
||||
[FromForm] string? storyLocation)
|
||||
{
|
||||
var job = await storyIntelligence.StartAsync(appearancePreference);
|
||||
var job = await storyIntelligence.StartAsync(appearancePreference, primaryAgeGroup, storyEra, storyLocation);
|
||||
if (job is null)
|
||||
{
|
||||
TempData["OnboardingStoryIntelligenceError"] = "Review the manuscript scan before starting Story Intelligence.";
|
||||
|
||||
@ -24,7 +24,9 @@ public interface IStoryMemoryRepository
|
||||
Task InvalidateAssignmentAsync(int assignmentId, string reason);
|
||||
Task<StoryMemoryIllustrationDemand> UpsertDemandAsync(StoryMemoryIllustrationDemandSave request);
|
||||
Task<string> GetAppearancePreferenceAsync(int importSessionId);
|
||||
Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId);
|
||||
Task SetAppearancePreferenceAsync(int importSessionId, string preference);
|
||||
Task SetImportContextAsync(int importSessionId, string appearancePreference, string? primaryAgeGroup, string? storyEra, string? storyLocation);
|
||||
Task<StoryMemoryDiagnostics> GetDiagnosticsAsync(int importSessionId);
|
||||
}
|
||||
|
||||
@ -362,29 +364,75 @@ public sealed class StoryMemoryRepository(ISqlConnectionFactory connectionFactor
|
||||
}
|
||||
|
||||
public async Task<string> GetAppearancePreferenceAsync(int importSessionId)
|
||||
=> (await GetImportContextAsync(importSessionId)).AppearancePreference;
|
||||
|
||||
public async Task<StoryMemoryImportContext> GetImportContextAsync(int importSessionId)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
return await connection.QuerySingleOrDefaultAsync<string>(
|
||||
"SELECT AppearancePreference FROM dbo.StoryMemoryImportPreferences WHERE ImportSessionID = @ImportSessionID;",
|
||||
await EnsureImportPreferenceContextColumnsAsync(connection);
|
||||
return await connection.QuerySingleOrDefaultAsync<StoryMemoryImportContext>(
|
||||
"""
|
||||
SELECT
|
||||
AppearancePreference,
|
||||
COALESCE(PrimaryAgeGroup, N'') AS PrimaryAgeGroup,
|
||||
COALESCE(StoryEra, N'') AS StoryEra,
|
||||
COALESCE(StoryLocation, N'') AS StoryLocation
|
||||
FROM dbo.StoryMemoryImportPreferences
|
||||
WHERE ImportSessionID = @ImportSessionID;
|
||||
""",
|
||||
new { ImportSessionID = importSessionId })
|
||||
?? StoryMemoryAppearancePreferences.Balanced;
|
||||
?? new StoryMemoryImportContext(StoryMemoryAppearancePreferences.Balanced, string.Empty, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
public async Task SetAppearancePreferenceAsync(int importSessionId, string preference)
|
||||
=> await SetImportContextAsync(importSessionId, preference, null, null, null);
|
||||
|
||||
public async Task SetImportContextAsync(int importSessionId, string appearancePreference, string? primaryAgeGroup, string? storyEra, string? storyLocation)
|
||||
{
|
||||
using var connection = connectionFactory.CreateConnection();
|
||||
await EnsureImportPreferenceContextColumnsAsync(connection);
|
||||
await connection.ExecuteAsync(
|
||||
"""
|
||||
MERGE dbo.StoryMemoryImportPreferences WITH (HOLDLOCK) AS target
|
||||
USING (SELECT @ImportSessionID AS ImportSessionID) AS source
|
||||
ON target.ImportSessionID = source.ImportSessionID
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET AppearancePreference = @AppearancePreference, UpdatedUtc = SYSUTCDATETIME()
|
||||
UPDATE SET AppearancePreference = @AppearancePreference,
|
||||
PrimaryAgeGroup = COALESCE(@PrimaryAgeGroup, PrimaryAgeGroup),
|
||||
StoryEra = COALESCE(@StoryEra, StoryEra),
|
||||
StoryLocation = COALESCE(@StoryLocation, StoryLocation),
|
||||
UpdatedUtc = SYSUTCDATETIME()
|
||||
WHEN NOT MATCHED THEN
|
||||
INSERT (ImportSessionID, AppearancePreference)
|
||||
VALUES (@ImportSessionID, @AppearancePreference);
|
||||
INSERT (ImportSessionID, AppearancePreference, PrimaryAgeGroup, StoryEra, StoryLocation)
|
||||
VALUES (@ImportSessionID, @AppearancePreference, COALESCE(@PrimaryAgeGroup, N''), COALESCE(@StoryEra, N''), COALESCE(@StoryLocation, N''));
|
||||
""",
|
||||
new { ImportSessionID = importSessionId, AppearancePreference = preference });
|
||||
new
|
||||
{
|
||||
ImportSessionID = importSessionId,
|
||||
AppearancePreference = appearancePreference,
|
||||
PrimaryAgeGroup = TrimContext(primaryAgeGroup, 120),
|
||||
StoryEra = TrimContext(storyEra, 120),
|
||||
StoryLocation = TrimContext(storyLocation, 160)
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task EnsureImportPreferenceContextColumnsAsync(System.Data.IDbConnection connection)
|
||||
{
|
||||
await connection.ExecuteAsync(
|
||||
"""
|
||||
IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'PrimaryAgeGroup') IS NULL
|
||||
ALTER TABLE dbo.StoryMemoryImportPreferences ADD PrimaryAgeGroup nvarchar(120) NULL;
|
||||
IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'StoryEra') IS NULL
|
||||
ALTER TABLE dbo.StoryMemoryImportPreferences ADD StoryEra nvarchar(120) NULL;
|
||||
IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'StoryLocation') IS NULL
|
||||
ALTER TABLE dbo.StoryMemoryImportPreferences ADD StoryLocation nvarchar(160) NULL;
|
||||
""");
|
||||
}
|
||||
|
||||
private static string? TrimContext(string? value, int maxLength)
|
||||
{
|
||||
var clean = value?.Trim();
|
||||
return string.IsNullOrWhiteSpace(clean) ? null : clean[..Math.Min(clean.Length, maxLength)];
|
||||
}
|
||||
|
||||
public async Task<StoryMemoryDiagnostics> GetDiagnosticsAsync(int importSessionId)
|
||||
|
||||
@ -214,18 +214,30 @@ public sealed class StoryMemoryRebuildResult
|
||||
public IReadOnlyList<string> Warnings { get; init; } = [];
|
||||
}
|
||||
|
||||
public sealed class StoryMemoryRebuildOptions
|
||||
public sealed record StoryMemoryRebuildOptions
|
||||
{
|
||||
public bool DryRun { get; init; }
|
||||
public bool RebuildAll { get; init; }
|
||||
public bool QueueDemand { get; init; }
|
||||
public int? MaxDemandToQueue { get; init; }
|
||||
public string AppearancePreference { get; init; } = StoryMemoryAppearancePreferences.Balanced;
|
||||
public string PrimaryAgeGroup { get; init; } = string.Empty;
|
||||
public string StoryEra { get; init; } = string.Empty;
|
||||
public string StoryLocation { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class StoryMemoryResolveOptions
|
||||
public sealed record StoryMemoryResolveOptions
|
||||
{
|
||||
public bool QueueDemand { get; init; }
|
||||
public int? MaxDemandToQueue { get; init; }
|
||||
public string AppearancePreference { get; init; } = StoryMemoryAppearancePreferences.Balanced;
|
||||
public string PrimaryAgeGroup { get; init; } = string.Empty;
|
||||
public string StoryEra { get; init; } = string.Empty;
|
||||
public string StoryLocation { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record StoryMemoryImportContext(
|
||||
string AppearancePreference,
|
||||
string PrimaryAgeGroup,
|
||||
string StoryEra,
|
||||
string StoryLocation);
|
||||
|
||||
@ -247,7 +247,10 @@ public class Program
|
||||
builder.Services.AddHostedService<EmailQueueWorker>();
|
||||
builder.Services.AddHostedService<StoryIntelligenceWorker>();
|
||||
builder.Services.AddHostedService<PersistedStoryIntelligenceWorker>();
|
||||
if (builder.Configuration.GetValue("Illustrations:GenerationWorkerEnabled", true))
|
||||
{
|
||||
builder.Services.AddHostedService<IllustrationGenerationWorker>();
|
||||
}
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
- 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 anime, cartoon, Pixar-like, caricature, celebrity likeness, or photoreal identity claim
|
||||
- for character images, the specified hair colour, hair length, apparent age, era, location context and skin tone are hard requirements; do not reinterpret or diversify away from them
|
||||
- no bright flat studio backdrop, no plain colour-block background, no stock-photo lighting, and no cartoon simplicity
|
||||
- avoid private manuscript details, real names, excerpts, or recognisable copyrighted characters
|
||||
- avoid muddy olive and brown grading, heavy sepia tones, underexposure, excessive darkness, low-contrast lighting, flat colour, antique oil-painting texture, distressed canvas texture, rough brushwork, overly sombre default expressions, backgrounds that merge into the subject, and monochrome or near-monochrome results
|
||||
@ -115,7 +116,7 @@ public sealed class IllustrationPromptBuilder : IIllustrationPromptBuilder
|
||||
$"apparent age band {specification.ApparentAgeBand}",
|
||||
$"presentation {specification.Presentation}",
|
||||
$"hair {specification.HairLength} {specification.HairColour}",
|
||||
$"skin tone range {specification.SkinTone}",
|
||||
$"required skin tone {specification.SkinTone}",
|
||||
$"features {string.Join(", ", specification.Features)}",
|
||||
$"clothing {specification.ClothingEra} {specification.ClothingStyle}"
|
||||
}.Where(item => !item.EndsWith(" ", StringComparison.Ordinal))),
|
||||
|
||||
@ -9,7 +9,7 @@ namespace PlotLine.Services;
|
||||
public interface IOnboardingStoryIntelligenceService
|
||||
{
|
||||
Task<StoryIntelligenceOverviewViewModel> GetOverviewAsync();
|
||||
Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null);
|
||||
Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null, string? primaryAgeGroup = null, string? storyEra = null, string? storyLocation = null);
|
||||
Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid batchId);
|
||||
Task<StoryIntelligenceProgressViewModel?> CancelAsync(Guid batchId);
|
||||
Task<(StoryIntelligenceProgressViewModel? Progress, StoryIntelligenceImportCommitResult Result)> CommitAsync(Guid batchId, int runId);
|
||||
@ -40,6 +40,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
IStoryIntelligenceRelationshipImportService relationshipImport,
|
||||
IStoryIntelligenceKnowledgeImportService knowledgeImport,
|
||||
IStoryIntelligencePipelineStateService pipelineState,
|
||||
IStoryMemoryRepository storyMemory,
|
||||
IStoryIntelligenceClient client,
|
||||
IOnboardingStoryIntelligenceBatchStore batchStore,
|
||||
IStoryIntelligenceProgressNotifier notifier,
|
||||
@ -83,7 +84,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null)
|
||||
public async Task<OnboardingStoryIntelligenceBatch?> StartAsync(string? appearancePreference = null, string? primaryAgeGroup = null, string? storyEra = null, string? storyLocation = null)
|
||||
{
|
||||
var userId = RequireUserId();
|
||||
var context = await GetContextAsync(userId);
|
||||
@ -161,6 +162,7 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
ChapterID = chapterId,
|
||||
RunID = runId
|
||||
});
|
||||
await pipelineState.RecordChapterAnalysisQueuedAsync(preview.ProjectID, preview.BookID, runId);
|
||||
|
||||
await notifier.PublishAsync(new StoryIntelligenceRunProgressEvent
|
||||
{
|
||||
@ -190,9 +192,22 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
ProjectName = wizard.SelectedProjectName,
|
||||
BookTitle = wizard.SelectedBookTitle,
|
||||
AppearancePreference = NormaliseAppearancePreference(appearancePreference),
|
||||
PrimaryAgeGroup = CleanContext(primaryAgeGroup),
|
||||
StoryEra = CleanContext(storyEra),
|
||||
StoryLocation = CleanContext(storyLocation),
|
||||
Items = batchItems
|
||||
};
|
||||
|
||||
if (await pipelineState.GetForBookAsync(preview.BookID, userId) is { } pipeline)
|
||||
{
|
||||
await storyMemory.SetImportContextAsync(
|
||||
pipeline.StoryIntelligenceBookPipelineID,
|
||||
batch.AppearancePreference,
|
||||
batch.PrimaryAgeGroup,
|
||||
batch.StoryEra,
|
||||
batch.StoryLocation);
|
||||
}
|
||||
|
||||
await batchStore.SaveAsync(batch);
|
||||
logger.LogInformation(
|
||||
"Queued onboarding Story Intelligence batch {BatchID}. UserID={UserID} ProjectID={ProjectID} BookID={BookID} Chapters={ChapterCount}",
|
||||
@ -217,6 +232,12 @@ public sealed class OnboardingStoryIntelligenceService(
|
||||
_ => StoryMemoryAppearancePreferences.Balanced
|
||||
};
|
||||
|
||||
private static string CleanContext(string? value)
|
||||
{
|
||||
var clean = value?.Trim();
|
||||
return string.IsNullOrWhiteSpace(clean) ? string.Empty : clean[..Math.Min(clean.Length, 160)];
|
||||
}
|
||||
|
||||
public async Task<StoryIntelligenceProgressViewModel?> GetProgressAsync(Guid batchId)
|
||||
{
|
||||
var batch = await batchStore.GetAsync(RequireUserId(), batchId);
|
||||
@ -933,6 +954,9 @@ public sealed class OnboardingStoryIntelligenceBatch
|
||||
public string? ProjectName { get; init; }
|
||||
public string? BookTitle { get; init; }
|
||||
public string AppearancePreference { get; init; } = StoryMemoryAppearancePreferences.Balanced;
|
||||
public string PrimaryAgeGroup { get; init; } = string.Empty;
|
||||
public string StoryEra { get; init; } = string.Empty;
|
||||
public string StoryLocation { get; init; } = string.Empty;
|
||||
public DateTime CreatedUtc { get; init; } = DateTime.UtcNow;
|
||||
public IReadOnlyList<OnboardingStoryIntelligenceBatchItem> Items { get; init; } = [];
|
||||
public List<OnboardingStoryIntelligenceCharacterDecision> CharacterDecisions { get; } = [];
|
||||
|
||||
@ -191,6 +191,13 @@ public static class StoryIntelligenceIllustrationCompatibility
|
||||
rejected.Add($"hair colour hard mismatch: wanted {evidence.HairColour}, candidate {candidate.HairColour}");
|
||||
}
|
||||
|
||||
if (!string.Equals(evidence.SkinTone, SkinTones.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||
&& !string.Equals(candidate.SkinTone, SkinTones.Unknown, StringComparison.OrdinalIgnoreCase)
|
||||
&& KnownMismatch(evidence.SkinTone, candidate.SkinTone))
|
||||
{
|
||||
rejected.Add($"skin tone hard mismatch: wanted {evidence.SkinTone}, candidate {candidate.SkinTone}");
|
||||
}
|
||||
|
||||
if (alreadyAssignedToAnotherSignificantCharacter && isNamedCharacter)
|
||||
{
|
||||
rejected.Add("already assigned to another significant named character");
|
||||
|
||||
@ -591,7 +591,7 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
Code = $"char-demand-{StableHash(demand.DemandKey)}",
|
||||
Title = $"{age} {presentation.ToLowerInvariant()} character",
|
||||
Description = "Demand-created reusable character illustration for Story Intelligence matching.",
|
||||
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair and {skinTone.ToLowerInvariant()} skin tone range.",
|
||||
VisualSubject = $"{age} {presentation.ToLowerInvariant()} character with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {SkinToneInstruction(skinTone)}.",
|
||||
Composition = "clear head-and-shoulders cinematic portrait with readable face, subtle atmospheric story background, clean separation, suitable for circular cropping",
|
||||
Mood = "neutral, story-ready, emotionally readable, subdued and cinematic",
|
||||
ApparentAgeBand = age,
|
||||
@ -688,6 +688,15 @@ public sealed class StoryIntelligenceIllustrationMatchingService(
|
||||
};
|
||||
}
|
||||
|
||||
private static string SkinToneInstruction(string skinTone)
|
||||
=> skinTone switch
|
||||
{
|
||||
StoryIntelligenceIllustrationCompatibility.SkinTones.Light => "must have light/fair skin; do not depict dark or black skin unless explicit manuscript evidence says so",
|
||||
StoryIntelligenceIllustrationCompatibility.SkinTones.Medium => "must have medium skin tone; do not drift to dark or very pale skin",
|
||||
StoryIntelligenceIllustrationCompatibility.SkinTones.Dark => "must have dark skin tone",
|
||||
_ => "not specified"
|
||||
};
|
||||
|
||||
private static Dictionary<string, CharacterObservation> BuildCharacterObservations(StoryIntelligenceExperiencePrototypeViewModel prototype)
|
||||
{
|
||||
var observations = new Dictionary<string, CharacterObservation>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
@ -60,7 +60,7 @@ public sealed class StoryMemoryService(
|
||||
}
|
||||
|
||||
var identityAliases = BuildCrossSceneIdentityAliases(completedScenes);
|
||||
await memory.SetAppearancePreferenceAsync(importSessionId, options.AppearancePreference);
|
||||
await memory.SetImportContextAsync(importSessionId, options.AppearancePreference, options.PrimaryAgeGroup, options.StoryEra, options.StoryLocation);
|
||||
var processed = await memory.ListProcessedSceneResultIdsAsync(importSessionId, ProcessorVersion);
|
||||
var processedCount = 0;
|
||||
foreach (var scene in completedScenes)
|
||||
@ -80,7 +80,10 @@ public sealed class StoryMemoryService(
|
||||
{
|
||||
QueueDemand = options.QueueDemand,
|
||||
MaxDemandToQueue = options.MaxDemandToQueue,
|
||||
AppearancePreference = options.AppearancePreference
|
||||
AppearancePreference = options.AppearancePreference,
|
||||
PrimaryAgeGroup = options.PrimaryAgeGroup,
|
||||
StoryEra = options.StoryEra,
|
||||
StoryLocation = options.StoryLocation
|
||||
}, cancellationToken);
|
||||
|
||||
var diagnostics = await memory.GetDiagnosticsAsync(importSessionId);
|
||||
@ -118,11 +121,27 @@ public sealed class StoryMemoryService(
|
||||
|
||||
await UpsertSceneMemoryAsync(session, scene.Result, scene.Scene, new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase), cancellationToken);
|
||||
await memory.MarkSceneResultProcessedAsync(importSessionId, sceneResultId, ProcessorVersion, StableContentHash(scene.Result.ParsedJson));
|
||||
await ResolveIllustrationsAsync(importSessionId, options, cancellationToken);
|
||||
var context = await memory.GetImportContextAsync(importSessionId);
|
||||
await ResolveIllustrationsAsync(importSessionId, options with
|
||||
{
|
||||
AppearancePreference = string.IsNullOrWhiteSpace(options.AppearancePreference) ? context.AppearancePreference : options.AppearancePreference,
|
||||
PrimaryAgeGroup = string.IsNullOrWhiteSpace(options.PrimaryAgeGroup) ? context.PrimaryAgeGroup : options.PrimaryAgeGroup,
|
||||
StoryEra = string.IsNullOrWhiteSpace(options.StoryEra) ? context.StoryEra : options.StoryEra,
|
||||
StoryLocation = string.IsNullOrWhiteSpace(options.StoryLocation) ? context.StoryLocation : options.StoryLocation
|
||||
}, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task ResolveIllustrationsAsync(int importSessionId, StoryMemoryResolveOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var context = await memory.GetImportContextAsync(importSessionId);
|
||||
options = options with
|
||||
{
|
||||
AppearancePreference = context.AppearancePreference,
|
||||
PrimaryAgeGroup = FirstConfigured(context.PrimaryAgeGroup, options.PrimaryAgeGroup),
|
||||
StoryEra = FirstConfigured(context.StoryEra, options.StoryEra),
|
||||
StoryLocation = FirstConfigured(context.StoryLocation, options.StoryLocation)
|
||||
};
|
||||
|
||||
var characters = await memory.ListCharactersAsync(importSessionId);
|
||||
var attributes = await memory.ListCharacterAttributesAsync(importSessionId);
|
||||
var locations = await memory.ListLocationsAsync(importSessionId);
|
||||
@ -145,6 +164,7 @@ public sealed class StoryMemoryService(
|
||||
CharacterEvidence(character, attributes.Where(item => item.StoryMemoryCharacterID == character.StoryMemoryCharacterID)),
|
||||
character.CanonicalIdentityKey,
|
||||
options.AppearancePreference);
|
||||
evidence = ApplyPrimaryAgeContext(evidence, options.PrimaryAgeGroup);
|
||||
var existing = assignments.FirstOrDefault(item => item.EntityType == StoryMemoryEntityTypes.Character && item.EntityID == character.StoryMemoryCharacterID);
|
||||
var existingItem = existing?.IllustrationLibraryItemID is int existingId
|
||||
? catalogue.FirstOrDefault(item => item.IllustrationLibraryItemID == existingId)
|
||||
@ -181,7 +201,7 @@ public sealed class StoryMemoryService(
|
||||
continue;
|
||||
}
|
||||
|
||||
var demand = await RecordDemandAsync(importSessionId, character.ProjectID, StoryMemoryEntityTypes.Character, DemandKey(character.CanonicalIdentityKey, evidence.AgeBand, evidence.Presentation, evidence.HairColour, evidence.HairLength, evidence.SkinTone), options, CharacterDemandSpecification(character, evidence), 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(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));
|
||||
}
|
||||
|
||||
@ -202,7 +222,7 @@ public sealed class StoryMemoryService(
|
||||
}
|
||||
else
|
||||
{
|
||||
var demand = await RecordDemandAsync(importSessionId, location.ProjectID, StoryMemoryEntityTypes.Location, DemandKey(location.CanonicalIdentityKey, requestedType), options, LocationDemandSpecification(location, requestedType), cancellationToken);
|
||||
var demand = await RecordDemandAsync(importSessionId, location.ProjectID, StoryMemoryEntityTypes.Location, DemandKey(location.CanonicalIdentityKey, requestedType), options, LocationDemandSpecification(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));
|
||||
}
|
||||
}
|
||||
@ -227,7 +247,7 @@ public sealed class StoryMemoryService(
|
||||
}
|
||||
else
|
||||
{
|
||||
var demand = await RecordDemandAsync(importSessionId, asset.ProjectID, StoryMemoryEntityTypes.Asset, DemandKey(asset.CanonicalIdentityKey, requestedType, requestedColour), options, AssetDemandSpecification(asset, requestedType, requestedColour), cancellationToken);
|
||||
var demand = await RecordDemandAsync(importSessionId, asset.ProjectID, StoryMemoryEntityTypes.Asset, DemandKey(asset.CanonicalIdentityKey, requestedType, requestedColour), options, AssetDemandSpecification(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));
|
||||
}
|
||||
}
|
||||
@ -499,6 +519,12 @@ public sealed class StoryMemoryService(
|
||||
|
||||
private static CharacterEvidenceProfile ApplyAppearancePreference(CharacterEvidenceProfile evidence, string identityKey, string preference)
|
||||
{
|
||||
if (string.Equals(evidence.HairColour, StoryIntelligenceIllustrationCompatibility.HairColours.Red, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(evidence.SkinTone, StoryIntelligenceIllustrationCompatibility.SkinTones.Unknown, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return evidence with { SkinTone = StoryIntelligenceIllustrationCompatibility.SkinTones.Light };
|
||||
}
|
||||
|
||||
if (!string.Equals(evidence.SkinTone, StoryIntelligenceIllustrationCompatibility.SkinTones.Unknown, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return evidence;
|
||||
@ -519,6 +545,20 @@ public sealed class StoryMemoryService(
|
||||
: evidence with { SkinTone = skinTone };
|
||||
}
|
||||
|
||||
private static CharacterEvidenceProfile ApplyPrimaryAgeContext(CharacterEvidenceProfile evidence, string primaryAgeGroup)
|
||||
{
|
||||
var contextAge = StoryIntelligenceIllustrationCompatibility.NormalizeAgeBand(primaryAgeGroup);
|
||||
if (contextAge == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown)
|
||||
{
|
||||
return evidence;
|
||||
}
|
||||
|
||||
return evidence.AgeBand is StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown
|
||||
or StoryIntelligenceIllustrationCompatibility.AgeBands.Adult
|
||||
? evidence with { AgeBand = contextAge, AgeConfidence = Math.Max(evidence.AgeConfidence, 0.74m) }
|
||||
: evidence;
|
||||
}
|
||||
|
||||
private static string BalancedSkinTone(string key)
|
||||
{
|
||||
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
@ -545,6 +585,11 @@ public sealed class StoryMemoryService(
|
||||
var hairColour = PickAttribute(attributeList, "HairColour", inferred.HairColour, 0.1m);
|
||||
var skinTone = PickAttribute(attributeList, "SkinTone", inferred.SkinTone, 0.1m);
|
||||
var hairLength = PickAttribute(attributeList, "HairLength", inferred.HairLength, 0.1m);
|
||||
if (string.Equals(hairColour.Value, StoryIntelligenceIllustrationCompatibility.HairColours.Red, StringComparison.OrdinalIgnoreCase)
|
||||
&& string.Equals(skinTone.Value, StoryIntelligenceIllustrationCompatibility.SkinTones.Unknown, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
skinTone = (StoryIntelligenceIllustrationCompatibility.SkinTones.Light, Math.Max(skinTone.Confidence, 0.82m));
|
||||
}
|
||||
|
||||
return new(
|
||||
age.Value,
|
||||
@ -760,12 +805,26 @@ public sealed class StoryMemoryService(
|
||||
if (recorded.QueuedIllustrationLibraryItemID.HasValue || recorded.GeneratedIllustrationLibraryItemID.HasValue)
|
||||
{
|
||||
var existingId = recorded.GeneratedIllustrationLibraryItemID ?? recorded.QueuedIllustrationLibraryItemID;
|
||||
var existingItem = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter()))
|
||||
.FirstOrDefault(item => item.IllustrationLibraryItemID == existingId);
|
||||
if (existingItem is not null
|
||||
&& existingItem.IsActive
|
||||
&& string.Equals(existingItem.StableCode, specification.Code, StringComparison.OrdinalIgnoreCase)
|
||||
&& existingItem.Status is IllustrationLibraryStatuses.Planned
|
||||
or IllustrationLibraryStatuses.Queued
|
||||
or IllustrationLibraryStatuses.Generating
|
||||
or IllustrationLibraryStatuses.Generated
|
||||
or IllustrationLibraryStatuses.Approved)
|
||||
{
|
||||
return new(existingId, specification.Code, "DemandQueued", $"Demand already has queued illustration item {existingId:N0}.");
|
||||
}
|
||||
}
|
||||
|
||||
if (options.MaxDemandToQueue is int maxDemandToQueue && maxDemandToQueue > 0)
|
||||
{
|
||||
var categoryCap = Math.Max(1, maxDemandToQueue / 3);
|
||||
var categoryCap = specification.Category == IllustrationLibraryCategories.Character
|
||||
? maxDemandToQueue
|
||||
: Math.Max(1, maxDemandToQueue / 3);
|
||||
var queuedDemandItems = (await illustrationLibrary.ListAsync(new IllustrationLibraryFilter()))
|
||||
.Count(item => string.Equals(item.Category, specification.Category, StringComparison.OrdinalIgnoreCase)
|
||||
&& item.IsActive
|
||||
@ -796,21 +855,25 @@ public sealed class StoryMemoryService(
|
||||
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)
|
||||
private static IllustrationGenerationSpecification CharacterDemandSpecification(StoryMemoryCharacter character, CharacterEvidenceProfile evidence, StoryMemoryResolveOptions options)
|
||||
{
|
||||
var code = DemandStableCode(StoryMemoryEntityTypes.Character, character.CanonicalIdentityKey, evidence.AgeBand, evidence.Presentation, evidence.HairColour, evidence.HairLength, evidence.SkinTone);
|
||||
var age = UnknownToDefault(evidence.AgeBand, "Adult");
|
||||
var age = UnknownToDefault(evidence.AgeBand, NormaliseAgeContext(options.PrimaryAgeGroup));
|
||||
var presentation = UnknownToDefault(evidence.Presentation, "Androgynous");
|
||||
var hairColour = UnknownToDefault(evidence.HairColour, "Brown");
|
||||
var hairLength = UnknownToDefault(evidence.HairLength, "Medium");
|
||||
var skinTone = UnknownToDefault(evidence.SkinTone, "Medium");
|
||||
var era = UnknownToDefault(options.StoryEra, "1980s");
|
||||
var location = UnknownToDefault(options.StoryLocation, "UK");
|
||||
var skinInstruction = SkinToneInstruction(skinTone);
|
||||
var agePrompt = AgePromptText(age);
|
||||
return new IllustrationGenerationSpecification
|
||||
{
|
||||
Category = IllustrationLibraryCategories.Character,
|
||||
Code = code,
|
||||
Title = $"{age} {presentation} character",
|
||||
Description = "Reusable character portrait generated from durable Story Memory appearance evidence. Not canonical to a specific manuscript.",
|
||||
VisualSubject = $"A {age.ToLowerInvariant()} {presentation.ToLowerInvariant()} person with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair and {skinTone.ToLowerInvariant()} skin tone.",
|
||||
VisualSubject = $"A {agePrompt} {presentation.ToLowerInvariant()} person from {era} {location} with {hairLength.ToLowerInvariant()} {hairColour.ToLowerInvariant()} hair. Skin tone requirement: {skinInstruction}.",
|
||||
Composition = "single head-and-shoulders portrait with clear facial lighting, neutral story background, and strong thumbnail readability",
|
||||
Mood = "present, natural, story-aware",
|
||||
ApparentAgeBand = age,
|
||||
@ -818,15 +881,15 @@ public sealed class StoryMemoryService(
|
||||
HairColour = hairColour,
|
||||
HairLength = hairLength,
|
||||
SkinTone = skinTone,
|
||||
ClothingEra = "1980s",
|
||||
ClothingStyle = "everyday naturalistic",
|
||||
ClothingEra = era,
|
||||
ClothingStyle = $"{era} {location} everyday naturalistic",
|
||||
Palette = ["clear blue", "warm amber", "natural skin tones"],
|
||||
MetadataTags = ["character", "story-memory-demand"],
|
||||
Metadata = DemandMetadata("Character", character.CanonicalIdentityKey)
|
||||
Metadata = DemandMetadata("Character", character.CanonicalIdentityKey, options)
|
||||
};
|
||||
}
|
||||
|
||||
private static IllustrationGenerationSpecification LocationDemandSpecification(StoryMemoryLocation location, string requestedType)
|
||||
private static IllustrationGenerationSpecification LocationDemandSpecification(StoryMemoryLocation location, string requestedType, StoryMemoryResolveOptions options)
|
||||
{
|
||||
var code = DemandStableCode(StoryMemoryEntityTypes.Location, location.CanonicalIdentityKey, requestedType);
|
||||
var locationType = UnknownToDefault(requestedType, "UnknownInterior");
|
||||
@ -844,11 +907,11 @@ public sealed class StoryMemoryService(
|
||||
Setting = location.InteriorExterior ?? "Story setting",
|
||||
Palette = ["clear navy", "warm light", "balanced natural colour"],
|
||||
MetadataTags = ["location", "story-memory-demand"],
|
||||
Metadata = DemandMetadata("Location", location.CanonicalIdentityKey)
|
||||
Metadata = DemandMetadata("Location", location.CanonicalIdentityKey, options)
|
||||
};
|
||||
}
|
||||
|
||||
private static IllustrationGenerationSpecification AssetDemandSpecification(StoryMemoryAsset asset, string requestedType, string requestedColour)
|
||||
private static IllustrationGenerationSpecification AssetDemandSpecification(StoryMemoryAsset asset, string requestedType, string requestedColour, StoryMemoryResolveOptions options)
|
||||
{
|
||||
var code = DemandStableCode(StoryMemoryEntityTypes.Asset, asset.CanonicalIdentityKey, requestedType, requestedColour);
|
||||
var objectType = UnknownToDefault(requestedType, "UnknownObject");
|
||||
@ -867,12 +930,13 @@ public sealed class StoryMemoryService(
|
||||
Era = asset.Era ?? "1980s",
|
||||
Palette = ["clean blue", "warm light", "natural object colour"],
|
||||
MetadataTags = ["asset", "story-memory-demand"],
|
||||
Metadata = DemandMetadata("Asset", asset.CanonicalIdentityKey)
|
||||
Metadata = DemandMetadata("Asset", asset.CanonicalIdentityKey, options)
|
||||
};
|
||||
}
|
||||
|
||||
private static Dictionary<string, string> DemandMetadata(string category, string entityKey)
|
||||
=> new(StringComparer.OrdinalIgnoreCase)
|
||||
private static Dictionary<string, string> DemandMetadata(string category, string entityKey, StoryMemoryResolveOptions options)
|
||||
{
|
||||
var metadata = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["category"] = category,
|
||||
["generationSource"] = "Story Memory demand",
|
||||
@ -880,6 +944,19 @@ public sealed class StoryMemoryService(
|
||||
["privacy"] = "generic-no-manuscript-data",
|
||||
["storyMemoryEntityHash"] = ShortHash(entityKey)
|
||||
};
|
||||
Add("primaryAgeGroup", options.PrimaryAgeGroup);
|
||||
Add("storyEra", options.StoryEra);
|
||||
Add("storyLocation", options.StoryLocation);
|
||||
return metadata;
|
||||
|
||||
void Add(string key, string value)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
metadata[key] = value.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string DemandStableCode(string entityType, params string[] parts)
|
||||
=> $"demand-{StableKey(entityType)}-{ShortHash(string.Join("|", parts.Where(part => !string.IsNullOrWhiteSpace(part))))}";
|
||||
@ -892,6 +969,34 @@ public sealed class StoryMemoryService(
|
||||
? fallback
|
||||
: value.Trim();
|
||||
|
||||
private static string FirstConfigured(params string?[] values)
|
||||
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;
|
||||
|
||||
private static string NormaliseAgeContext(string? value)
|
||||
{
|
||||
var normalised = StoryIntelligenceIllustrationCompatibility.NormalizeAgeBand(value);
|
||||
return normalised == StoryIntelligenceIllustrationCompatibility.AgeBands.Unknown ? "Adult" : normalised;
|
||||
}
|
||||
|
||||
private static string AgePromptText(string age)
|
||||
=> age switch
|
||||
{
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.OlderTeen => "late-teen",
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.YoungAdult => "early-twenties",
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.YoungTeen => "young-teen",
|
||||
StoryIntelligenceIllustrationCompatibility.AgeBands.MatureAdult => "middle-aged adult",
|
||||
_ => age.ToLowerInvariant()
|
||||
};
|
||||
|
||||
private static string SkinToneInstruction(string skinTone)
|
||||
=> skinTone switch
|
||||
{
|
||||
StoryIntelligenceIllustrationCompatibility.SkinTones.Light => "must have light/fair skin; do not depict dark or black skin unless explicit manuscript evidence says so",
|
||||
StoryIntelligenceIllustrationCompatibility.SkinTones.Medium => "must have medium skin tone; do not drift to dark or very pale skin",
|
||||
StoryIntelligenceIllustrationCompatibility.SkinTones.Dark => "must have dark skin tone",
|
||||
_ => "not specified"
|
||||
};
|
||||
|
||||
private static string SplitWords(string? value)
|
||||
{
|
||||
var clean = UnknownToDefault(value, "Unknown");
|
||||
|
||||
11
PlotLine/Sql/144_Phase21V_StoryMemoryImportContext.sql
Normal file
11
PlotLine/Sql/144_Phase21V_StoryMemoryImportContext.sql
Normal file
@ -0,0 +1,11 @@
|
||||
IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'PrimaryAgeGroup') IS NULL
|
||||
ALTER TABLE dbo.StoryMemoryImportPreferences ADD PrimaryAgeGroup nvarchar(120) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'StoryEra') IS NULL
|
||||
ALTER TABLE dbo.StoryMemoryImportPreferences ADD StoryEra nvarchar(120) NULL;
|
||||
GO
|
||||
|
||||
IF COL_LENGTH(N'dbo.StoryMemoryImportPreferences', N'StoryLocation') IS NULL
|
||||
ALTER TABLE dbo.StoryMemoryImportPreferences ADD StoryLocation nvarchar(160) NULL;
|
||||
GO
|
||||
@ -91,6 +91,22 @@
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.PredominantlyDark" /> Predominantly dark-skinned cast</label>
|
||||
<label><input type="radio" name="appearancePreference" value="@StoryMemoryAppearancePreferences.CustomMix" /> Custom mix</label>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>Story context for generated portraits</legend>
|
||||
<p>These guide generated images when the manuscript is silent. Explicit manuscript evidence still wins.</p>
|
||||
<label>
|
||||
Primary character age group
|
||||
<input class="form-control" type="text" name="primaryAgeGroup" placeholder="Late teen / early 20s" />
|
||||
</label>
|
||||
<label>
|
||||
Era
|
||||
<input class="form-control" type="text" name="storyEra" placeholder="1980s" />
|
||||
</label>
|
||||
<label>
|
||||
Location
|
||||
<input class="form-control" type="text" name="storyLocation" placeholder="UK" />
|
||||
</label>
|
||||
</fieldset>
|
||||
<button class="btn btn-primary" type="submit" disabled="@(!Model.CanStart)">Analyse manuscript</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user