CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_ClaimNext @LeaseMinutes int = 90 AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @RunID int; BEGIN TRANSACTION; SELECT TOP (1) @RunID = CharacterEnrichmentRunID FROM dbo.CharacterEnrichmentRuns WITH (UPDLOCK, HOLDLOCK, READPAST) WHERE IsCurrent = 1 AND ( Status = N'Queued' OR (Status = N'Running' AND StartedUtc < DATEADD(minute, -ABS(@LeaseMinutes), SYSUTCDATETIME())) ) ORDER BY QueuedUtc, CharacterEnrichmentRunID; IF @RunID IS NULL BEGIN COMMIT TRANSACTION; SELECT TOP (0) * FROM dbo.CharacterEnrichmentRuns; RETURN; END; UPDATE dbo.CharacterEnrichmentRuns SET Status = N'Running', StartedUtc = SYSUTCDATETIME(), AttemptCount = AttemptCount + 1, CurrentStage = N'Preparing character analysis', CurrentMessage = N'Preparing character analysis...', UpdatedUtc = SYSUTCDATETIME() WHERE CharacterEnrichmentRunID = @RunID AND IsCurrent = 1 AND ( Status = N'Queued' OR (Status = N'Running' AND StartedUtc < DATEADD(minute, -ABS(@LeaseMinutes), SYSUTCDATETIME())) ); IF @@ROWCOUNT = 0 BEGIN COMMIT TRANSACTION; SELECT TOP (0) * FROM dbo.CharacterEnrichmentRuns; RETURN; END; SELECT * FROM dbo.CharacterEnrichmentRuns WHERE CharacterEnrichmentRunID = @RunID; COMMIT TRANSACTION; END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentRun_Complete @CharacterEnrichmentRunID int, @Model nvarchar(100), @InputTokens int = NULL, @OutputTokens int = NULL, @TotalTokens int = NULL, @DurationMs bigint = NULL, @ResultsJson nvarchar(max) AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @ProjectID int, @BookID int, @PipelineID int; DECLARE @Results TABLE ( CharacterID int NOT NULL PRIMARY KEY, Summary nvarchar(max) NULL, Confidence decimal(5,4) NOT NULL, DateOfBirthValue nvarchar(120) NULL, DateOfBirthConfidence decimal(5,4) NULL, DateOfBirthEvidence nvarchar(700) NULL, DateOfBirthSceneID int NULL, AgeValue nvarchar(120) NULL, AgeConfidence decimal(5,4) NULL, AgeEvidence nvarchar(700) NULL, AgeSceneID int NULL, SexValue nvarchar(120) NULL, SexConfidence decimal(5,4) NULL, SexEvidence nvarchar(700) NULL, SexSceneID int NULL, HeightValue nvarchar(120) NULL, HeightConfidence decimal(5,4) NULL, HeightEvidence nvarchar(700) NULL, HeightSceneID int NULL, EyeColourValue nvarchar(120) NULL, EyeColourConfidence decimal(5,4) NULL, EyeColourEvidence nvarchar(700) NULL, EyeColourSceneID int NULL ); INSERT @Results SELECT CharacterID, NULLIF(LTRIM(RTRIM(Summary)), N''), Confidence, NULLIF(LTRIM(RTRIM(DateOfBirthValue)), N''), DateOfBirthConfidence, NULLIF(LTRIM(RTRIM(DateOfBirthEvidence)), N''), DateOfBirthSceneID, NULLIF(LTRIM(RTRIM(AgeValue)), N''), AgeConfidence, NULLIF(LTRIM(RTRIM(AgeEvidence)), N''), AgeSceneID, CASE WHEN LOWER(NULLIF(LTRIM(RTRIM(SexValue)), N'')) = N'female' THEN N'Female' WHEN LOWER(NULLIF(LTRIM(RTRIM(SexValue)), N'')) = N'male' THEN N'Male' WHEN LOWER(NULLIF(LTRIM(RTRIM(SexValue)), N'')) = N'other' THEN N'Other' ELSE NULL END, SexConfidence, NULLIF(LTRIM(RTRIM(SexEvidence)), N''), SexSceneID, NULLIF(LTRIM(RTRIM(HeightValue)), N''), HeightConfidence, NULLIF(LTRIM(RTRIM(HeightEvidence)), N''), HeightSceneID, NULLIF(LTRIM(RTRIM(EyeColourValue)), N''), EyeColourConfidence, NULLIF(LTRIM(RTRIM(EyeColourEvidence)), N''), EyeColourSceneID FROM OPENJSON(@ResultsJson) WITH ( CharacterID int '$.characterID', Summary nvarchar(max) '$.summary', Confidence decimal(5,4) '$.confidence', DateOfBirthValue nvarchar(120) '$.dateOfBirth.value', DateOfBirthConfidence decimal(5,4) '$.dateOfBirth.confidence', DateOfBirthEvidence nvarchar(700) '$.dateOfBirth.evidence', DateOfBirthSceneID int '$.dateOfBirth.sceneID', AgeValue nvarchar(120) '$.ageAtStartOfSeries.value', AgeConfidence decimal(5,4) '$.ageAtStartOfSeries.confidence', AgeEvidence nvarchar(700) '$.ageAtStartOfSeries.evidence', AgeSceneID int '$.ageAtStartOfSeries.sceneID', SexValue nvarchar(120) '$.sex.value', SexConfidence decimal(5,4) '$.sex.confidence', SexEvidence nvarchar(700) '$.sex.evidence', SexSceneID int '$.sex.sceneID', HeightValue nvarchar(120) '$.height.value', HeightConfidence decimal(5,4) '$.height.confidence', HeightEvidence nvarchar(700) '$.height.evidence', HeightSceneID int '$.height.sceneID', EyeColourValue nvarchar(120) '$.eyeColour.value', EyeColourConfidence decimal(5,4) '$.eyeColour.confidence', EyeColourEvidence nvarchar(700) '$.eyeColour.evidence', EyeColourSceneID int '$.eyeColour.sceneID' ) WHERE CharacterID IS NOT NULL; DECLARE @SexValues TABLE (SexName nvarchar(120) NOT NULL PRIMARY KEY, CharacterSexValueID int NOT NULL); INSERT @SexValues (SexName, CharacterSexValueID) SELECT SexName, CharacterSexValueID FROM dbo.CharacterSexValues WHERE IsSystem = 1 AND IsActive = 1 AND SexName IN (N'Male', N'Female', N'Other'); BEGIN TRANSACTION; SELECT @ProjectID = ProjectID, @BookID = BookID, @PipelineID = StoryIntelligenceBookPipelineID FROM dbo.CharacterEnrichmentRuns WITH (UPDLOCK, HOLDLOCK) WHERE CharacterEnrichmentRunID = @CharacterEnrichmentRunID; UPDATE c SET DefaultDescription = CASE WHEN NULLIF(LTRIM(RTRIM(c.DefaultDescription)), N'') IS NULL THEN r.Summary ELSE c.DefaultDescription END, BirthDate = CASE WHEN c.BirthDate IS NULL THEN TRY_CONVERT(date, r.DateOfBirthValue) ELSE c.BirthDate END, AgeAtSeriesStart = CASE WHEN c.AgeAtSeriesStart IS NULL THEN TRY_CONVERT(int, r.AgeValue) ELSE c.AgeAtSeriesStart END, SexValueID = CASE WHEN c.SexValueID IS NULL AND (NULLIF(LTRIM(RTRIM(c.Sex)), N'') IS NULL OR c.Sex = N'Unknown') AND sexValue.CharacterSexValueID IS NOT NULL THEN sexValue.CharacterSexValueID ELSE c.SexValueID END, Sex = CASE WHEN c.SexValueID IS NULL AND (NULLIF(LTRIM(RTRIM(c.Sex)), N'') IS NULL OR c.Sex = N'Unknown') AND sexValue.CharacterSexValueID IS NOT NULL THEN r.SexValue ELSE c.Sex END, Height = CASE WHEN NULLIF(LTRIM(RTRIM(c.Height)), N'') IS NULL THEN r.HeightValue ELSE c.Height END, EyeColour = CASE WHEN NULLIF(LTRIM(RTRIM(c.EyeColour)), N'') IS NULL THEN r.EyeColourValue ELSE c.EyeColour END, UpdatedDate = SYSUTCDATETIME() FROM dbo.Characters c INNER JOIN @Results r ON r.CharacterID = c.CharacterID LEFT JOIN @SexValues sexValue ON sexValue.SexName = r.SexValue WHERE c.ProjectID = @ProjectID; MERGE dbo.CharacterIntelligenceProfiles WITH (HOLDLOCK) AS target USING ( SELECT ImportSessionID = @PipelineID, ProjectID = @ProjectID, BookID = @BookID, CanonicalIdentityKey = CONCAT(N'character-enrichment-', r.CharacterID), CanonicalName = c.CharacterName, ProfileJson = (SELECT r.* FOR JSON PATH, WITHOUT_ARRAY_WRAPPER), Confidence = r.Confidence, NarrativeImportance = CAST(NULL AS nvarchar(60)) FROM @Results r INNER JOIN dbo.Characters c ON c.CharacterID = r.CharacterID WHERE @PipelineID IS NOT NULL ) AS source ON target.ImportSessionID = source.ImportSessionID AND target.CanonicalIdentityKey = source.CanonicalIdentityKey WHEN MATCHED THEN UPDATE SET CanonicalName = source.CanonicalName, ProfileJson = source.ProfileJson, Confidence = source.Confidence, EvidenceVersion = N'CharacterEnrichmentV1', UpdatedUtc = SYSUTCDATETIME() WHEN NOT MATCHED THEN INSERT (ImportSessionID, ProjectID, BookID, CanonicalIdentityKey, CanonicalName, ProfileJson, Confidence, NarrativeImportance, IsRecurring, EvidenceVersion, LastUpdatedSceneResultID) VALUES (source.ImportSessionID, source.ProjectID, source.BookID, source.CanonicalIdentityKey, source.CanonicalName, source.ProfileJson, source.Confidence, source.NarrativeImportance, 1, N'CharacterEnrichmentV1', NULL); DELETE evidence FROM dbo.CharacterEnrichmentEvidence evidence INNER JOIN @Results r ON r.CharacterID = evidence.CharacterID WHERE evidence.CharacterEnrichmentRunID = @CharacterEnrichmentRunID; INSERT dbo.CharacterEnrichmentEvidence (CharacterEnrichmentRunID, CharacterID, SceneID, FieldName, Value, Evidence, Confidence) SELECT @CharacterEnrichmentRunID, CharacterID, NULL, N'Summary', Summary, NULL, Confidence FROM @Results WHERE Summary IS NOT NULL UNION ALL SELECT @CharacterEnrichmentRunID, CharacterID, DateOfBirthSceneID, N'DateOfBirth', DateOfBirthValue, DateOfBirthEvidence, COALESCE(DateOfBirthConfidence, 0) FROM @Results WHERE DateOfBirthValue IS NOT NULL AND DateOfBirthSceneID IS NOT NULL UNION ALL SELECT @CharacterEnrichmentRunID, CharacterID, AgeSceneID, N'AgeAtStartOfSeries', AgeValue, AgeEvidence, COALESCE(AgeConfidence, 0) FROM @Results WHERE AgeValue IS NOT NULL AND AgeSceneID IS NOT NULL UNION ALL SELECT @CharacterEnrichmentRunID, CharacterID, SexSceneID, N'Sex', SexValue, SexEvidence, COALESCE(SexConfidence, 0) FROM @Results WHERE SexValue IS NOT NULL AND SexSceneID IS NOT NULL UNION ALL SELECT @CharacterEnrichmentRunID, CharacterID, HeightSceneID, N'Height', HeightValue, HeightEvidence, COALESCE(HeightConfidence, 0) FROM @Results WHERE HeightValue IS NOT NULL AND HeightSceneID IS NOT NULL UNION ALL SELECT @CharacterEnrichmentRunID, CharacterID, EyeColourSceneID, N'EyeColour', EyeColourValue, EyeColourEvidence, COALESCE(EyeColourConfidence, 0) FROM @Results WHERE EyeColourValue IS NOT NULL AND EyeColourSceneID IS NOT NULL; UPDATE dbo.CharacterEnrichmentRuns SET Status = N'Completed', ProcessedCharacters = TotalCharacters, CurrentStage = N'Complete', CurrentMessage = N'Character summaries are ready.', CompletedUtc = SYSUTCDATETIME(), Model = @Model, InputTokens = @InputTokens, OutputTokens = @OutputTokens, TotalTokens = @TotalTokens, DurationMs = @DurationMs, ErrorMessage = NULL, ErrorDetail = NULL, UpdatedUtc = SYSUTCDATETIME() WHERE CharacterEnrichmentRunID = @CharacterEnrichmentRunID; COMMIT TRANSACTION; END; GO CREATE OR ALTER PROCEDURE dbo.CharacterEnrichmentCharacterStatus_Get @CharacterID int, @UserID int AS BEGIN SET NOCOUNT ON; DECLARE @ProjectID int; SELECT @ProjectID = c.ProjectID FROM dbo.Characters c INNER JOIN dbo.ProjectUserAccess pua ON pua.ProjectID = c.ProjectID WHERE c.CharacterID = @CharacterID AND c.IsArchived = 0 AND pua.UserID = @UserID AND pua.AccessRole = N'Owner' AND pua.IsActive = 1; IF @ProjectID IS NULL BEGIN SELECT TOP (0) CharacterID = CAST(NULL AS int), BookID = CAST(NULL AS int), CharacterEnrichmentRunID = CAST(NULL AS int), Status = CAST(NULL AS nvarchar(30)), CurrentStage = CAST(NULL AS nvarchar(120)), CurrentMessage = CAST(NULL AS nvarchar(300)), LastAnalysedUtc = CAST(NULL AS datetime2), ErrorMessage = CAST(NULL AS nvarchar(max)), Summary = CAST(NULL AS nvarchar(max)), DateOfBirth = CAST(NULL AS nvarchar(500)), AgeAtStartOfSeries = CAST(NULL AS nvarchar(500)), Sex = CAST(NULL AS nvarchar(500)), Height = CAST(NULL AS nvarchar(500)), EyeColour = CAST(NULL AS nvarchar(500)), UpdatedUtc = CAST(NULL AS datetime2); RETURN; END; DECLARE @Attempt TABLE ( CharacterEnrichmentRunID int NULL, BookID int NULL, Status nvarchar(30) NULL, CurrentStage nvarchar(120) NULL, CurrentMessage nvarchar(300) NULL, ErrorMessage nvarchar(max) NULL, UpdatedUtc datetime2 NULL ); INSERT @Attempt SELECT TOP (1) runRows.CharacterEnrichmentRunID, runRows.BookID, runRows.Status, runRows.CurrentStage, runRows.CurrentMessage, runRows.ErrorMessage, runRows.UpdatedUtc FROM dbo.CharacterEnrichmentRuns runRows WHERE runRows.UserID = @UserID AND runRows.ProjectID = @ProjectID AND runRows.CharacterID = @CharacterID AND runRows.IsCurrent = 1 ORDER BY CASE runRows.Status WHEN N'Queued' THEN 0 WHEN N'Running' THEN 0 WHEN N'Failed' THEN 1 WHEN N'Completed' THEN 2 ELSE 9 END, runRows.UpdatedUtc DESC, runRows.CharacterEnrichmentRunID DESC; DECLARE @SuccessRunID int, @SuccessBookID int, @LastAnalysedUtc datetime2; SELECT TOP (1) @SuccessRunID = runRows.CharacterEnrichmentRunID, @SuccessBookID = runRows.BookID, @LastAnalysedUtc = runRows.CompletedUtc FROM dbo.CharacterEnrichmentRuns runRows WHERE runRows.UserID = @UserID AND runRows.ProjectID = @ProjectID AND runRows.Status = N'Completed' AND runRows.CompletedUtc IS NOT NULL AND ( runRows.CharacterID = @CharacterID OR EXISTS ( SELECT 1 FROM dbo.CharacterEnrichmentEvidence evidence WHERE evidence.CharacterEnrichmentRunID = runRows.CharacterEnrichmentRunID AND evidence.CharacterID = @CharacterID ) ) ORDER BY runRows.CompletedUtc DESC, runRows.CharacterEnrichmentRunID DESC; SELECT CharacterID = @CharacterID, BookID = COALESCE((SELECT TOP (1) BookID FROM @Attempt), @SuccessBookID), CharacterEnrichmentRunID = COALESCE((SELECT TOP (1) CharacterEnrichmentRunID FROM @Attempt), @SuccessRunID), Status = COALESCE((SELECT TOP (1) Status FROM @Attempt), CASE WHEN @SuccessRunID IS NULL THEN N'Not analysed' ELSE N'Completed' END), CurrentStage = (SELECT TOP (1) CurrentStage FROM @Attempt), CurrentMessage = (SELECT TOP (1) CurrentMessage FROM @Attempt), LastAnalysedUtc = @LastAnalysedUtc, ErrorMessage = (SELECT TOP (1) ErrorMessage FROM @Attempt), Summary = MAX(CASE WHEN evidence.FieldName = N'Summary' THEN evidence.Value END), DateOfBirth = MAX(CASE WHEN evidence.FieldName = N'DateOfBirth' THEN evidence.Value END), AgeAtStartOfSeries = MAX(CASE WHEN evidence.FieldName = N'AgeAtStartOfSeries' THEN evidence.Value END), Sex = MAX(CASE WHEN evidence.FieldName = N'Sex' THEN evidence.Value END), Height = MAX(CASE WHEN evidence.FieldName = N'Height' THEN evidence.Value END), EyeColour = MAX(CASE WHEN evidence.FieldName = N'EyeColour' THEN evidence.Value END), UpdatedUtc = COALESCE((SELECT TOP (1) UpdatedUtc FROM @Attempt), @LastAnalysedUtc, SYSUTCDATETIME()) FROM (SELECT 1 AS Anchor) anchor LEFT JOIN dbo.CharacterEnrichmentEvidence evidence ON evidence.CharacterEnrichmentRunID = @SuccessRunID AND evidence.CharacterID = @CharacterID; END; GO ;WITH LatestEvidence AS ( SELECT evidence.CharacterID, evidence.FieldName, evidence.Value, runRows.ProjectID, rn = ROW_NUMBER() OVER ( PARTITION BY evidence.CharacterID, evidence.FieldName ORDER BY runRows.CompletedUtc DESC, evidence.CharacterEnrichmentEvidenceID DESC ) FROM dbo.CharacterEnrichmentEvidence evidence INNER JOIN dbo.CharacterEnrichmentRuns runRows ON runRows.CharacterEnrichmentRunID = evidence.CharacterEnrichmentRunID WHERE runRows.Status = N'Completed' AND evidence.FieldName IN (N'DateOfBirth', N'AgeAtStartOfSeries', N'Height', N'EyeColour', N'Sex') ), Pivoted AS ( SELECT CharacterID, ProjectID = MAX(ProjectID), DateOfBirthValue = MAX(CASE WHEN FieldName = N'DateOfBirth' THEN Value END), AgeValue = MAX(CASE WHEN FieldName = N'AgeAtStartOfSeries' THEN Value END), HeightValue = MAX(CASE WHEN FieldName = N'Height' THEN Value END), EyeColourValue = MAX(CASE WHEN FieldName = N'EyeColour' THEN Value END), SexValue = MAX(CASE WHEN FieldName = N'Sex' THEN Value END) FROM LatestEvidence WHERE rn = 1 GROUP BY CharacterID ), Normalised AS ( SELECT p.CharacterID, p.ProjectID, DateOfBirthValue = TRY_CONVERT(date, p.DateOfBirthValue), AgeValue = TRY_CONVERT(int, CASE WHEN p.AgeValue IS NULL THEN NULL WHEN PATINDEX(N'%[^0-9]%', p.AgeValue + N'x') = 1 THEN NULL ELSE LEFT(p.AgeValue, PATINDEX(N'%[^0-9]%', p.AgeValue + N'x') - 1) END), p.HeightValue, p.EyeColourValue, SexValue = CASE WHEN LOWER(NULLIF(LTRIM(RTRIM(p.SexValue)), N'')) = N'female' THEN N'Female' WHEN LOWER(NULLIF(LTRIM(RTRIM(p.SexValue)), N'')) = N'male' THEN N'Male' WHEN LOWER(NULLIF(LTRIM(RTRIM(p.SexValue)), N'')) = N'other' THEN N'Other' ELSE NULL END FROM Pivoted p ), SexValues AS ( SELECT SexName, CharacterSexValueID FROM dbo.CharacterSexValues WHERE IsSystem = 1 AND IsActive = 1 AND SexName IN (N'Male', N'Female', N'Other') ) UPDATE c SET BirthDate = CASE WHEN c.BirthDate IS NULL THEN n.DateOfBirthValue ELSE c.BirthDate END, AgeAtSeriesStart = CASE WHEN c.AgeAtSeriesStart IS NULL THEN n.AgeValue ELSE c.AgeAtSeriesStart END, Height = CASE WHEN NULLIF(LTRIM(RTRIM(c.Height)), N'') IS NULL THEN n.HeightValue ELSE c.Height END, EyeColour = CASE WHEN NULLIF(LTRIM(RTRIM(c.EyeColour)), N'') IS NULL THEN n.EyeColourValue ELSE c.EyeColour END, SexValueID = CASE WHEN c.SexValueID IS NULL AND (NULLIF(LTRIM(RTRIM(c.Sex)), N'') IS NULL OR c.Sex = N'Unknown') AND sexValue.CharacterSexValueID IS NOT NULL THEN sexValue.CharacterSexValueID ELSE c.SexValueID END, Sex = CASE WHEN c.SexValueID IS NULL AND (NULLIF(LTRIM(RTRIM(c.Sex)), N'') IS NULL OR c.Sex = N'Unknown') AND sexValue.CharacterSexValueID IS NOT NULL THEN n.SexValue ELSE c.Sex END, UpdatedDate = SYSUTCDATETIME() FROM dbo.Characters c INNER JOIN Normalised n ON n.CharacterID = c.CharacterID LEFT JOIN SexValues sexValue ON sexValue.SexName = n.SexValue WHERE c.ProjectID = n.ProjectID AND ( (c.BirthDate IS NULL AND n.DateOfBirthValue IS NOT NULL) OR (c.AgeAtSeriesStart IS NULL AND n.AgeValue IS NOT NULL) OR (NULLIF(LTRIM(RTRIM(c.Height)), N'') IS NULL AND n.HeightValue IS NOT NULL) OR (NULLIF(LTRIM(RTRIM(c.EyeColour)), N'') IS NULL AND n.EyeColourValue IS NOT NULL) OR (c.SexValueID IS NULL AND (NULLIF(LTRIM(RTRIM(c.Sex)), N'') IS NULL OR c.Sex = N'Unknown') AND sexValue.CharacterSexValueID IS NOT NULL) ); GO