diff --git a/.DS_Store b/.DS_Store index 1c9f1e8a..adff79f6 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Damselfly.Core/Services/ImageRecognitionService.cs b/Damselfly.Core/Services/ImageRecognitionService.cs index fa5d08d4..47a69096 100644 --- a/Damselfly.Core/Services/ImageRecognitionService.cs +++ b/Damselfly.Core/Services/ImageRecognitionService.cs @@ -254,21 +254,22 @@ private async Task LoadPersonCache(bool force = false) using var db = scope.ServiceProvider.GetService(); var identifiedPeople = await db.People.Where(x => !string.IsNullOrEmpty(x.PersonGuid)) + .Include(x => x.FaceData) + .Where( x => x.FaceData.Count > 0) .AsNoTracking() .ToListAsync(); if( identifiedPeople.Any() ) { - // Merge the items into the people cache. Note that we use - // the indexer to avoid dupe key issues. TODO: Should the table be unique? - foreach( var pair in identifiedPeople.ToDictionary( x => x.PersonGuid!, x => x) ) - _peopleCache[pair.Key] = pair.Value; - - // Now create the dictionary of embeddings. Parse the floats out from the comma-separated string, - // and load them into the ONNX embeddings collection. - var embeddings = identifiedPeople.ToDictionary(x => x.PersonGuid, - x => x.FaceData.Select( x => x.Embeddings) ); - + // Populate the people cache + foreach( var person in identifiedPeople ) + _peopleCache[person.PersonGuid] = person; + + // Now populate the embeddings lookup + var embeddings = identifiedPeople.ToDictionary( + x => x.PersonGuid, + x => x.FaceData.Select( e => e.Embeddings)); + _faceOnnxService.LoadFaceEmbeddings(embeddings); Logging.LogTrace("Pre-loaded cach with {0} people.", _peopleCache.Count()); @@ -310,12 +311,13 @@ public async Task CreateMissingPeople(IEnumerable detectedFac State = Person.PersonState.Unknown, LastUpdated = DateTime.UtcNow, PersonGuid = x.PersonGuid, - FaceData = new List { new PersonFaceData { Embeddings = string.Join( ",", x.Embeddings) } }, + FaceData = new List { new() { Embeddings = string.Join( ",", x.Embeddings) } }, }).ToList(); if ( newPeople.Any() ) { - await db.BulkInsert(db.People, newPeople); + await db.People.AddRangeAsync( newPeople ); + await db.SaveChangesAsync(); // Add or replace the new people in the cache (this should always add) newPeople.ForEach(x => _peopleCache[x.PersonGuid] = x); diff --git a/Damselfly.ML.FaceONNX/Embeddings.cs b/Damselfly.ML.FaceONNX/Embeddings.cs index 8686b873..c22f0fc0 100644 --- a/Damselfly.ML.FaceONNX/Embeddings.cs +++ b/Damselfly.ML.FaceONNX/Embeddings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using UMapx.Core; namespace FaceEmbeddingsClassification; @@ -9,7 +10,7 @@ namespace FaceEmbeddingsClassification; /// public class Embeddings { - private Dictionary> VectorLookup = new(); + private readonly Dictionary> VectorLookup = new(); public Embeddings() { @@ -23,7 +24,16 @@ public Embeddings() /// Labels public Embeddings(Dictionary> vectorLookups) { - VectorLookup = vectorLookups; + Clear(); + + foreach( var pair in vectorLookups ) + VectorLookup[pair.Key] = pair.Value; + } + + private float[] GetVectorFromString( string s ) + { + return s.Split(",", StringSplitOptions.TrimEntries) + .Select( fl => (float)Convert.ToDouble(fl)).ToArray(); } /// @@ -31,16 +41,18 @@ public Embeddings(Dictionary> vectorLookups) /// /// Label /// Vector - public void Add(string label, float[] vectors) + public void Add(string label, IEnumerable embeddings) { + var vectors = embeddings.Select( x => GetVectorFromString(x)).ToList(); + if( VectorLookup.TryGetValue(label, out var existingList) ) { - existingList.Add( vectors); + existingList.AddRange( vectors ); return; } else { - existingList = new List { vectors }; + existingList = new List(vectors); VectorLookup[label] = existingList; } } diff --git a/Damselfly.ML.FaceONNX/FaceONNXService.cs b/Damselfly.ML.FaceONNX/FaceONNXService.cs index 764a331b..fb649a50 100644 --- a/Damselfly.ML.FaceONNX/FaceONNXService.cs +++ b/Damselfly.ML.FaceONNX/FaceONNXService.cs @@ -23,7 +23,7 @@ public class FaceONNXService : IDisposable private FaceDetector _faceDetector; private FaceLandmarksExtractor _faceLandmarksExtractor; private FaceEmbedder _faceEmbedder; - private Embeddings _embeddings; + private readonly Embeddings _embeddings = new(); public FaceONNXService(IServiceScopeFactory scopeFactory, ILogger logger ) { @@ -50,7 +50,6 @@ public void StartService() _faceDetector = new FaceDetector(); _faceLandmarksExtractor = new FaceLandmarksExtractor(); _faceEmbedder = new FaceEmbedder(); - _embeddings = new Embeddings(); } catch ( Exception ex ) { @@ -59,11 +58,6 @@ public void StartService() Logging.LogError($"Inner exception: {ex.InnerException}"); } } - - private List GetEmbeddingsFromString( IEnumerable strings ) - { - return strings.Select( x => x.Split(",").Select( fl => (float)Convert.ToDouble(fl)).ToArray() ).ToList(); - } /// /// Takes a list of person GUIDs, each with a list of one or more sets of embeddings @@ -73,10 +67,8 @@ private List GetEmbeddingsFromString( IEnumerable strings ) /// public void LoadFaceEmbeddings(Dictionary> personIDAndEmbeddings) { - var convertedDict = personIDAndEmbeddings.ToDictionary( x => x.Key, - x => GetEmbeddingsFromString(x.Value)); - - _embeddings = new Embeddings(convertedDict); + foreach( var pair in personIDAndEmbeddings ) + _embeddings.Add( pair.Key, pair.Value); } private class FaceONNXFace @@ -170,8 +162,9 @@ public async Task> DetectFaces(Image image) { // No match, so create a new person GUID face.PersonGuid = Guid.NewGuid().ToString(); + var vectorStr = string.Join(",", face.Embeddings); // Add it to the embeddings DB - _embeddings.Add(face.PersonGuid, face.Embeddings); + _embeddings.Add(face.PersonGuid, new[] {vectorStr}); } else { diff --git a/Damselfly.Migrations.Sqlite/Migrations/20240306215424_MultipleEmbeddings.Designer.cs b/Damselfly.Migrations.Sqlite/Migrations/20240306215424_MultipleEmbeddings.Designer.cs new file mode 100644 index 00000000..5e9d69b6 --- /dev/null +++ b/Damselfly.Migrations.Sqlite/Migrations/20240306215424_MultipleEmbeddings.Designer.cs @@ -0,0 +1,1146 @@ +// +using System; +using Damselfly.Core.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Damselfly.Core.Migrations +{ + [DbContext(typeof(ImageContext))] + [Migration("20240306215424_MultipleEmbeddings")] + partial class MultipleEmbeddings + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.1"); + + modelBuilder.Entity("Damselfly.Core.DbModels.Authentication.AppIdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationRoleId") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ApplicationRoleId"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("Damselfly.Core.DbModels.Authentication.ApplicationRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = 1, + ConcurrencyStamp = "0db5fbed-a42c-43d0-a05e-187ea7b1f47f", + Name = "User", + NormalizedName = "USER" + }, + new + { + Id = 2, + ConcurrencyStamp = "bf19c01e-bba2-467c-a87a-97fa0e762a5d", + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = 3, + ConcurrencyStamp = "b74a44e3-34d6-4e4a-8629-e0616d7e16c6", + Name = "ReadOnly", + NormalizedName = "READONLY" + }); + }); + + modelBuilder.Entity("Damselfly.Core.DbModels.Authentication.ApplicationUserRole", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("ApplicationRoleId") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("ApplicationRoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Basket", b => + { + b.Property("BasketId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("BasketId"); + + b.HasIndex("UserId"); + + b.ToTable("Baskets"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.BasketEntry", b => + { + b.Property("BasketEntryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BasketId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.HasKey("BasketEntryId"); + + b.HasIndex("BasketId"); + + b.HasIndex("ImageId", "BasketId") + .IsUnique(); + + b.ToTable("BasketEntries"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Camera", b => + { + b.Property("CameraId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Make") + .HasColumnType("TEXT"); + + b.Property("Model") + .HasColumnType("TEXT"); + + b.Property("Serial") + .HasColumnType("TEXT"); + + b.HasKey("CameraId"); + + b.ToTable("Cameras"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ConfigSetting", b => + { + b.Property("ConfigSettingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("ConfigSettingId"); + + b.HasIndex("UserId"); + + b.ToTable("ConfigSettings"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ExifOperation", b => + { + b.Property("ExifOperationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("Operation") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("ExifOperationId"); + + b.HasIndex("TimeStamp"); + + b.HasIndex("UserId"); + + b.HasIndex("ImageId", "Text"); + + b.ToTable("KeywordOperations"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ExportConfig", b => + { + b.Property("ExportConfigId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("KeepFolders") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Size") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WatermarkText") + .HasColumnType("TEXT"); + + b.HasKey("ExportConfigId"); + + b.ToTable("DownloadConfigs"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.FTSTag", b => + { + b.Property("FTSTagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .HasColumnType("TEXT"); + + b.HasKey("FTSTagId"); + + b.ToTable("FTSTags"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Folder", b => + { + b.Property("FolderId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FolderScanDate") + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.Property("Path") + .HasColumnType("TEXT"); + + b.HasKey("FolderId"); + + b.HasIndex("FolderScanDate"); + + b.HasIndex("ParentId"); + + b.HasIndex("Path"); + + b.ToTable("Folders"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Hash", b => + { + b.Property("HashId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("MD5ImageHash") + .HasColumnType("TEXT"); + + b.Property("PerceptualHex1") + .HasColumnType("TEXT"); + + b.Property("PerceptualHex2") + .HasColumnType("TEXT"); + + b.Property("PerceptualHex3") + .HasColumnType("TEXT"); + + b.Property("PerceptualHex4") + .HasColumnType("TEXT"); + + b.HasKey("HashId"); + + b.HasIndex("ImageId") + .IsUnique(); + + b.HasIndex("MD5ImageHash"); + + b.HasIndex("PerceptualHex1", "PerceptualHex2", "PerceptualHex3", "PerceptualHex4"); + + b.ToTable("Hashes"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Image", b => + { + b.Property("ImageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClassificationId") + .HasColumnType("INTEGER"); + + b.Property("ClassificationScore") + .HasColumnType("REAL"); + + b.Property("FileCreationDate") + .HasColumnType("TEXT"); + + b.Property("FileLastModDate") + .HasColumnType("TEXT"); + + b.Property("FileName") + .HasColumnType("TEXT"); + + b.Property("FileSizeBytes") + .HasColumnType("INTEGER"); + + b.Property("FolderId") + .HasColumnType("INTEGER"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("SortDate") + .HasColumnType("TEXT"); + + b.HasKey("ImageId"); + + b.HasIndex("FileLastModDate"); + + b.HasIndex("FileName"); + + b.HasIndex("FolderId"); + + b.HasIndex("LastUpdated"); + + b.HasIndex("SortDate"); + + b.HasIndex("FileName", "FolderId") + .IsUnique(); + + b.ToTable("Images"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageClassification", b => + { + b.Property("ClassificationId") + .HasColumnType("INTEGER"); + + b.Property("Label") + .HasColumnType("TEXT"); + + b.HasKey("ClassificationId"); + + b.HasIndex("Label") + .IsUnique(); + + b.ToTable("ImageClassifications"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageMetaData", b => + { + b.Property("MetaDataId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AILastUpdated") + .HasColumnType("TEXT"); + + b.Property("AspectRatio") + .HasColumnType("REAL"); + + b.Property("AverageColor") + .HasColumnType("TEXT"); + + b.Property("CameraId") + .HasColumnType("INTEGER"); + + b.Property("Caption") + .HasColumnType("TEXT"); + + b.Property("Copyright") + .HasColumnType("TEXT"); + + b.Property("Credit") + .HasColumnType("TEXT"); + + b.Property("DateTaken") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("DominantColor") + .HasColumnType("TEXT"); + + b.Property("Exposure") + .HasColumnType("TEXT"); + + b.Property("FNum") + .HasColumnType("TEXT"); + + b.Property("FlashFired") + .HasColumnType("INTEGER"); + + b.Property("Height") + .HasColumnType("INTEGER"); + + b.Property("ISO") + .HasColumnType("TEXT"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("Latitude") + .HasColumnType("REAL"); + + b.Property("LensId") + .HasColumnType("INTEGER"); + + b.Property("Longitude") + .HasColumnType("REAL"); + + b.Property("Rating") + .HasColumnType("INTEGER"); + + b.Property("ThumbLastUpdated") + .HasColumnType("TEXT"); + + b.Property("Width") + .HasColumnType("INTEGER"); + + b.HasKey("MetaDataId"); + + b.HasIndex("AILastUpdated"); + + b.HasIndex("AspectRatio"); + + b.HasIndex("CameraId"); + + b.HasIndex("DateTaken"); + + b.HasIndex("ImageId") + .IsUnique(); + + b.HasIndex("LensId"); + + b.HasIndex("Rating"); + + b.HasIndex("ThumbLastUpdated"); + + b.ToTable("ImageMetaData"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageObject", b => + { + b.Property("ImageObjectId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.Property("RecogntionSource") + .HasColumnType("INTEGER"); + + b.Property("RectHeight") + .HasColumnType("INTEGER"); + + b.Property("RectWidth") + .HasColumnType("INTEGER"); + + b.Property("RectX") + .HasColumnType("INTEGER"); + + b.Property("RectY") + .HasColumnType("INTEGER"); + + b.Property("Score") + .HasColumnType("REAL"); + + b.Property("TagId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("TEXT"); + + b.HasKey("ImageObjectId"); + + b.HasIndex("ImageId"); + + b.HasIndex("PersonId"); + + b.HasIndex("TagId"); + + b.ToTable("ImageObjects"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageTag", b => + { + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("TagId") + .HasColumnType("INTEGER"); + + b.HasKey("ImageId", "TagId"); + + b.HasIndex("TagId"); + + b.HasIndex("ImageId", "TagId") + .IsUnique(); + + b.ToTable("ImageTags"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Lens", b => + { + b.Property("LensId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Make") + .HasColumnType("TEXT"); + + b.Property("Model") + .HasColumnType("TEXT"); + + b.Property("Serial") + .HasColumnType("TEXT"); + + b.HasKey("LensId"); + + b.ToTable("Lenses"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Person", b => + { + b.Property("PersonId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("LastUpdated") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonGuid") + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("PersonId"); + + b.HasIndex("State"); + + b.ToTable("People"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.PersonFaceData", b => + { + b.Property("FaceDataId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Embeddings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("FaceDataId"); + + b.HasIndex("PersonId"); + + b.ToTable("FaceData"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Tag", b => + { + b.Property("TagId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Favourite") + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TagType") + .HasColumnType("INTEGER"); + + b.Property("TimeStamp") + .HasColumnType("TEXT"); + + b.HasKey("TagId"); + + b.HasIndex("Keyword") + .IsUnique(); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Transformations", b => + { + b.Property("TransformationId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ImageId") + .HasColumnType("INTEGER"); + + b.Property("TransformsJson") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("TransformationId"); + + b.HasIndex("ImageId") + .IsUnique(); + + b.ToTable("Transformations"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.DataProtection.EntityFrameworkCore.DataProtectionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("FriendlyName") + .HasColumnType("TEXT"); + + b.Property("Xml") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("DataProtectionKeys"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("RoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("UserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("UserTokens", (string)null); + }); + + modelBuilder.Entity("Damselfly.Core.DbModels.Authentication.AppIdentityUser", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.ApplicationRole", null) + .WithMany("AspNetUsers") + .HasForeignKey("ApplicationRoleId"); + }); + + modelBuilder.Entity("Damselfly.Core.DbModels.Authentication.ApplicationUserRole", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.ApplicationRole", null) + .WithMany("UserRoles") + .HasForeignKey("ApplicationRoleId"); + + b.HasOne("Damselfly.Core.DbModels.Authentication.ApplicationRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Damselfly.Core.DbModels.Authentication.AppIdentityUser", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Basket", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.AppIdentityUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.BasketEntry", b => + { + b.HasOne("Damselfly.Core.Models.Basket", "Basket") + .WithMany("BasketEntries") + .HasForeignKey("BasketId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Damselfly.Core.Models.Image", "Image") + .WithMany("BasketEntries") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Basket"); + + b.Navigation("Image"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ConfigSetting", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.AppIdentityUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ExifOperation", b => + { + b.HasOne("Damselfly.Core.Models.Image", "Image") + .WithMany() + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Damselfly.Core.DbModels.Authentication.AppIdentityUser", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Image"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Folder", b => + { + b.HasOne("Damselfly.Core.Models.Folder", "Parent") + .WithMany("Children") + .HasForeignKey("ParentId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Hash", b => + { + b.HasOne("Damselfly.Core.Models.Image", "Image") + .WithOne("Hash") + .HasForeignKey("Damselfly.Core.Models.Hash", "ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Image", b => + { + b.HasOne("Damselfly.Core.Models.Folder", "Folder") + .WithMany("Images") + .HasForeignKey("FolderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Folder"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageClassification", b => + { + b.HasOne("Damselfly.Core.Models.Image", null) + .WithOne("Classification") + .HasForeignKey("Damselfly.Core.Models.ImageClassification", "ClassificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageMetaData", b => + { + b.HasOne("Damselfly.Core.Models.Camera", "Camera") + .WithMany() + .HasForeignKey("CameraId"); + + b.HasOne("Damselfly.Core.Models.Image", "Image") + .WithOne("MetaData") + .HasForeignKey("Damselfly.Core.Models.ImageMetaData", "ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Damselfly.Core.Models.Lens", "Lens") + .WithMany() + .HasForeignKey("LensId"); + + b.Navigation("Camera"); + + b.Navigation("Image"); + + b.Navigation("Lens"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageObject", b => + { + b.HasOne("Damselfly.Core.Models.Image", "Image") + .WithMany("ImageObjects") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Damselfly.Core.Models.Person", "Person") + .WithMany() + .HasForeignKey("PersonId"); + + b.HasOne("Damselfly.Core.Models.Tag", "Tag") + .WithMany("ImageObjects") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("Person"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.ImageTag", b => + { + b.HasOne("Damselfly.Core.Models.Image", "Image") + .WithMany("ImageTags") + .HasForeignKey("ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Damselfly.Core.Models.Tag", "Tag") + .WithMany("ImageTags") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.PersonFaceData", b => + { + b.HasOne("Damselfly.Core.Models.Person", "Person") + .WithMany("FaceData") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Transformations", b => + { + b.HasOne("Damselfly.Core.Models.Image", "Image") + .WithOne("Transforms") + .HasForeignKey("Damselfly.Core.Models.Transformations", "ImageId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Image"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.ApplicationRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.AppIdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.AppIdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Damselfly.Core.DbModels.Authentication.AppIdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Damselfly.Core.DbModels.Authentication.AppIdentityUser", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("Damselfly.Core.DbModels.Authentication.ApplicationRole", b => + { + b.Navigation("AspNetUsers"); + + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Basket", b => + { + b.Navigation("BasketEntries"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Folder", b => + { + b.Navigation("Children"); + + b.Navigation("Images"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Image", b => + { + b.Navigation("BasketEntries"); + + b.Navigation("Classification") + .IsRequired(); + + b.Navigation("Hash") + .IsRequired(); + + b.Navigation("ImageObjects"); + + b.Navigation("ImageTags"); + + b.Navigation("MetaData") + .IsRequired(); + + b.Navigation("Transforms"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Person", b => + { + b.Navigation("FaceData"); + }); + + modelBuilder.Entity("Damselfly.Core.Models.Tag", b => + { + b.Navigation("ImageObjects"); + + b.Navigation("ImageTags"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Damselfly.Migrations.Sqlite/Migrations/20240306215424_MultipleEmbeddings.cs b/Damselfly.Migrations.Sqlite/Migrations/20240306215424_MultipleEmbeddings.cs new file mode 100644 index 00000000..30d91f82 --- /dev/null +++ b/Damselfly.Migrations.Sqlite/Migrations/20240306215424_MultipleEmbeddings.cs @@ -0,0 +1,57 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Damselfly.Core.Migrations +{ + /// + public partial class MultipleEmbeddings : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Embeddings", + table: "People"); + + migrationBuilder.CreateTable( + name: "FaceData", + columns: table => new + { + FaceDataId = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + PersonId = table.Column(type: "INTEGER", nullable: false), + Embeddings = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FaceData", x => x.FaceDataId); + table.ForeignKey( + name: "FK_FaceData_People_PersonId", + column: x => x.PersonId, + principalTable: "People", + principalColumn: "PersonId", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_FaceData_PersonId", + table: "FaceData", + column: "PersonId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FaceData"); + + migrationBuilder.AddColumn( + name: "Embeddings", + table: "People", + type: "TEXT", + nullable: false, + defaultValue: ""); + } + } +} diff --git a/Damselfly.Migrations.Sqlite/Migrations/ImageContextModelSnapshot.cs b/Damselfly.Migrations.Sqlite/Migrations/ImageContextModelSnapshot.cs index e41a5e6b..4e09bba8 100644 --- a/Damselfly.Migrations.Sqlite/Migrations/ImageContextModelSnapshot.cs +++ b/Damselfly.Migrations.Sqlite/Migrations/ImageContextModelSnapshot.cs @@ -1,7 +1,6 @@ // using System; using Damselfly.Core.Database; -using Damselfly.Core.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; @@ -118,21 +117,21 @@ protected override void BuildModel(ModelBuilder modelBuilder) new { Id = 1, - ConcurrencyStamp = "fae01211-a4b9-4584-bb20-d9184bd91b87", + ConcurrencyStamp = "0db5fbed-a42c-43d0-a05e-187ea7b1f47f", Name = "User", NormalizedName = "USER" }, new { Id = 2, - ConcurrencyStamp = "778c0f63-9aba-4491-a825-a9895455aaad", + ConcurrencyStamp = "bf19c01e-bba2-467c-a87a-97fa0e762a5d", Name = "Admin", NormalizedName = "ADMIN" }, new { Id = 3, - ConcurrencyStamp = "9781e8b2-ad6c-4dce-a68c-d4ce998502bf", + ConcurrencyStamp = "b74a44e3-34d6-4e4a-8629-e0616d7e16c6", Name = "ReadOnly", NormalizedName = "READONLY" }); @@ -644,10 +643,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("Embeddings") - .IsRequired() - .HasColumnType("TEXT"); - b.Property("LastUpdated") .HasColumnType("TEXT"); @@ -668,6 +663,26 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("People"); }); + modelBuilder.Entity("Damselfly.Core.Models.PersonFaceData", b => + { + b.Property("FaceDataId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Embeddings") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("INTEGER"); + + b.HasKey("FaceDataId"); + + b.HasIndex("PersonId"); + + b.ToTable("FaceData"); + }); + modelBuilder.Entity("Damselfly.Core.Models.Tag", b => { b.Property("TagId") @@ -1009,6 +1024,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Tag"); }); + modelBuilder.Entity("Damselfly.Core.Models.PersonFaceData", b => + { + b.HasOne("Damselfly.Core.Models.Person", "Person") + .WithMany("FaceData") + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Person"); + }); + modelBuilder.Entity("Damselfly.Core.Models.Transformations", b => { b.HasOne("Damselfly.Core.Models.Image", "Image") @@ -1100,6 +1126,11 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("Transforms"); }); + modelBuilder.Entity("Damselfly.Core.Models.Person", b => + { + b.Navigation("FaceData"); + }); + modelBuilder.Entity("Damselfly.Core.Models.Tag", b => { b.Navigation("ImageObjects"); diff --git a/Damselfly.Web.Client/wwwroot/version.js b/Damselfly.Web.Client/wwwroot/version.js index 573a9429..0a32c517 100644 --- a/Damselfly.Web.Client/wwwroot/version.js +++ b/Damselfly.Web.Client/wwwroot/version.js @@ -1 +1 @@ -const CACHE_VERSION='4.1.0-20240306155719' +const CACHE_VERSION='4.1.0-20240306215536'