using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations.Schema; namespace MeetingAssistant.Speakers; public sealed class SpeakerIdentity { public int Id { get; set; } public string? CanonicalName { get; set; } public DateTimeOffset CreatedAt { get; set; } public DateTimeOffset UpdatedAt { get; set; } public int TranscriptionCount { get; set; } public List CandidateNames { get; set; } = []; public List Aliases { get; set; } = []; public List Snippets { get; set; } = []; public List References { get; set; } = []; [NotMapped] public int ReferenceCount => References.Count; public string? GetDisplayName() { if (!string.IsNullOrWhiteSpace(CanonicalName)) { return CanonicalName; } return Aliases .Select(alias => alias.Name) .Where(alias => !string.IsNullOrWhiteSpace(alias)) .Order(StringComparer.OrdinalIgnoreCase) .FirstOrDefault(); } } public sealed class SpeakerIdentityReference { public int Id { get; set; } public int SpeakerIdentityId { get; set; } public SpeakerIdentity? SpeakerIdentity { get; set; } public string MeetingNotePath { get; set; } = ""; public string TranscriptPath { get; set; } = ""; public DateTimeOffset CreatedAt { get; set; } } public static class SpeakerIdentityReferences { public static SpeakerIdentityReference Create( string meetingNotePath, string transcriptPath, DateTimeOffset createdAt) { return new SpeakerIdentityReference { MeetingNotePath = meetingNotePath, TranscriptPath = transcriptPath, CreatedAt = createdAt }; } public static void AddIfMissing( SpeakerIdentity identity, SpeakerIdentityReference reference, DateTimeOffset? createdAt = null) { if (IsEmpty(reference) || identity.References.Any(existing => IsSame(existing, reference))) { return; } identity.References.Add(Create( reference.MeetingNotePath, reference.TranscriptPath, createdAt ?? reference.CreatedAt)); } private static bool IsEmpty(SpeakerIdentityReference reference) { return string.IsNullOrWhiteSpace(reference.MeetingNotePath) && string.IsNullOrWhiteSpace(reference.TranscriptPath); } private static bool IsSame(SpeakerIdentityReference first, SpeakerIdentityReference second) { return string.Equals(first.MeetingNotePath, second.MeetingNotePath, StringComparison.OrdinalIgnoreCase) && string.Equals(first.TranscriptPath, second.TranscriptPath, StringComparison.OrdinalIgnoreCase); } } public sealed class SpeakerCandidateName { public int Id { get; set; } public int SpeakerIdentityId { get; set; } public SpeakerIdentity? SpeakerIdentity { get; set; } public string Name { get; set; } = ""; } public sealed class SpeakerSnippet { public int Id { get; set; } public int SpeakerIdentityId { get; set; } public SpeakerIdentity? SpeakerIdentity { get; set; } public byte[] WavBytes { get; set; } = []; public DateTimeOffset CreatedAt { get; set; } } public sealed class SpeakerAlias { public int Id { get; set; } public int SpeakerIdentityId { get; set; } public SpeakerIdentity? SpeakerIdentity { get; set; } public string Name { get; set; } = ""; } public sealed class SpeakerIdentityDbContext : DbContext { public SpeakerIdentityDbContext(DbContextOptions options) : base(options) { } public DbSet SpeakerIdentities => Set(); public DbSet SpeakerCandidateNames => Set(); public DbSet SpeakerAliases => Set(); public DbSet SpeakerSnippets => Set(); public DbSet SpeakerIdentityReferences => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => { entity.Property(identity => identity.TranscriptionCount) .IsRequired() .ValueGeneratedNever(); entity.HasMany(identity => identity.CandidateNames) .WithOne(candidate => candidate.SpeakerIdentity) .HasForeignKey(candidate => candidate.SpeakerIdentityId) .OnDelete(DeleteBehavior.Cascade); entity.HasMany(identity => identity.Aliases) .WithOne(alias => alias.SpeakerIdentity) .HasForeignKey(alias => alias.SpeakerIdentityId) .OnDelete(DeleteBehavior.Cascade); entity.HasMany(identity => identity.Snippets) .WithOne(snippet => snippet.SpeakerIdentity) .HasForeignKey(snippet => snippet.SpeakerIdentityId) .OnDelete(DeleteBehavior.Cascade); entity.HasMany(identity => identity.References) .WithOne(reference => reference.SpeakerIdentity) .HasForeignKey(reference => reference.SpeakerIdentityId) .OnDelete(DeleteBehavior.Cascade); }); modelBuilder.Entity() .HasIndex(candidate => new { candidate.SpeakerIdentityId, candidate.Name }) .IsUnique(); modelBuilder.Entity() .HasIndex(alias => new { alias.SpeakerIdentityId, alias.Name }) .IsUnique(); modelBuilder.Entity() .HasIndex(reference => new { reference.SpeakerIdentityId, reference.MeetingNotePath, reference.TranscriptPath }) .IsUnique(); } }