From 87ceaa805b04c416b0afd3708356dc3e5f2c1ef2 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Wed, 14 May 2025 19:53:35 +0300 Subject: [PATCH 01/10] prepare to build --- Azaion.Common/Services/GpsMatcherClient.cs | 2 +- Azaion.Common/Services/InferenceClient.cs | 2 +- Azaion.Suite.sln | 1 + Azaion.Suite/Azaion.Suite.csproj | 8 ++++---- build/publish.cmd | 1 - 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Azaion.Common/Services/GpsMatcherClient.cs b/Azaion.Common/Services/GpsMatcherClient.cs index 12db99d..fcb5077 100644 --- a/Azaion.Common/Services/GpsMatcherClient.cs +++ b/Azaion.Common/Services/GpsMatcherClient.cs @@ -60,7 +60,7 @@ public class GpsMatcherClient : IGpsMatcherClient process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; - //process.Start(); + process.Start(); } catch (Exception e) { diff --git a/Azaion.Common/Services/InferenceClient.cs b/Azaion.Common/Services/InferenceClient.cs index 6341d1a..28ad8fe 100644 --- a/Azaion.Common/Services/InferenceClient.cs +++ b/Azaion.Common/Services/InferenceClient.cs @@ -59,7 +59,7 @@ public class InferenceClient : IInferenceClient, IResourceLoader process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; - //process.Start(); + process.Start(); } catch (Exception e) { diff --git a/Azaion.Suite.sln b/Azaion.Suite.sln index 00324f8..3870dd3 100644 --- a/Azaion.Suite.sln +++ b/Azaion.Suite.sln @@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CF141A48 build\publish.cmd = build\publish.cmd build\installer.full.iss = build\installer.full.iss build\installer.iterative.iss = build\installer.iterative.iss + build\publish-full.cmd = build\publish-full.cmd EndProjectSection EndProject Global diff --git a/Azaion.Suite/Azaion.Suite.csproj b/Azaion.Suite/Azaion.Suite.csproj index 0a1e040..cde203f 100644 --- a/Azaion.Suite/Azaion.Suite.csproj +++ b/Azaion.Suite/Azaion.Suite.csproj @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + diff --git a/build/publish.cmd b/build/publish.cmd index 48f4fbf..3678741 100644 --- a/build/publish.cmd +++ b/build/publish.cmd @@ -12,7 +12,6 @@ call ..\gps-denied\image-matcher\build_gps call build\download_models echo building installer... -iscc build\installer.full.iss iscc build\installer.iterative.iss popd \ No newline at end of file From d02550f5a0b6f7c6c4e3ad94a92dc36e787863be Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sat, 17 May 2025 19:25:33 +0300 Subject: [PATCH 02/10] huge queue refactoring: 3 queues -> 1 queue send delete validate updates --- Azaion.Annotator/Annotator.xaml.cs | 5 +- Azaion.Annotator/AnnotatorEventHandler.cs | 21 ++- Azaion.Common/Constants.cs | 4 +- Azaion.Common/DTO/AnnotationThumbnail.cs | 1 + .../DTO/Queue/AnnotationCreatedMessage.cs | 26 +-- Azaion.Common/Database/Annotation.cs | 4 +- Azaion.Common/Database/AnnotationName.cs | 6 - .../Database/AnnotationQueueRecord.cs | 8 + Azaion.Common/Database/AnnotationsDb.cs | 2 +- Azaion.Common/Database/DbFactory.cs | 17 +- .../Events/AnnotationsDeletedEvent.cs | 5 +- Azaion.Common/Services/AnnotationService.cs | 126 +++++++++---- Azaion.Common/Services/FailsafeProducer.cs | 176 +++++++++--------- Azaion.Common/Services/GpsMatcherClient.cs | 2 +- Azaion.Dataset/DatasetExplorer.xaml | 8 +- Azaion.Dataset/DatasetExplorer.xaml.cs | 5 +- Azaion.Dataset/DatasetExplorerEventHandler.cs | 5 +- Azaion.Inference/remote_command.pxd | 4 +- Azaion.Suite/App.xaml.cs | 6 +- Azaion.Suite/Azaion.Suite.csproj | 8 +- 20 files changed, 246 insertions(+), 193 deletions(-) delete mode 100644 Azaion.Common/Database/AnnotationName.cs create mode 100644 Azaion.Common/Database/AnnotationQueueRecord.cs diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index d7d1a19..700c4c7 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -243,9 +243,9 @@ public partial class Annotator return; var res = DgAnnotations.SelectedItems.Cast().ToList(); - var annotations = res.Select(x => x.Annotation).ToList(); + var annotationNames = res.Select(x => x.Annotation.Name).ToList(); - await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); + await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames)); break; } }; @@ -555,6 +555,7 @@ public partial class Annotator LvFiles.Items.Refresh(); IsInferenceNow = false; + StatusHelp.Text = "Розпізнавання зваершено"; AIDetectBtn.IsEnabled = true; } diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 56c3f4c..05fce65 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -278,15 +278,18 @@ public class AnnotatorEventHandler( public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { - var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x); - foreach (var ann in notification.Annotations) - { - if (!annResDict.TryGetValue(ann.Name, out var value)) - continue; + var namesSet = notification.AnnotationNames.ToHashSet(); - formState.AnnotationResults.Remove(value); - mainWindow.TimedAnnotations.Remove(ann); - } + var remainAnnotations = formState.AnnotationResults + .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList(); + formState.AnnotationResults.Clear(); + foreach (var ann in remainAnnotations) + formState.AnnotationResults.Add(ann); + + var timedAnnsToRemove = mainWindow.TimedAnnotations + .Where(x => namesSet.Contains(x.Value.Name)) + .Select(x => x.Value).ToList(); + mainWindow.TimedAnnotations.Remove(timedAnnsToRemove); if (formState.AnnotationResults.Count == 0) { @@ -307,7 +310,7 @@ public class AnnotatorEventHandler( mainWindow.AddAnnotation(e.Annotation); var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det => - $"Розпізнавання: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " + + $"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " + $"xy=({det.CenterX:F2},{det.CenterY:F2}), " + $"розмір=({det.Width:F2}, {det.Height:F2}), " + $"conf: {det.Confidence*100:F0}%")); diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 84a9927..ceadd67 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -1,6 +1,4 @@ using System.Windows; -using System.Windows.Media; -using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Extensions; @@ -10,7 +8,7 @@ namespace Azaion.Common; public class Constants { public const string JPG_EXT = ".jpg"; - + public const string TXT_EXT = ".txt"; #region DirectoriesConfig public const string DEFAULT_VIDEO_DIR = "video"; diff --git a/Azaion.Common/DTO/AnnotationThumbnail.cs b/Azaion.Common/DTO/AnnotationThumbnail.cs index 988a267..73c9f17 100644 --- a/Azaion.Common/DTO/AnnotationThumbnail.cs +++ b/Azaion.Common/DTO/AnnotationThumbnail.cs @@ -28,6 +28,7 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged } public string ImageName => Path.GetFileName(Annotation.ImagePath); + public string CreatedEmail => Annotation.CreatedEmail; public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created; public event PropertyChangedEventHandler? PropertyChanged; diff --git a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs index 30a5fec..2b2a108 100644 --- a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs +++ b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs @@ -5,23 +5,23 @@ namespace Azaion.Common.DTO.Queue; using MessagePack; [MessagePackObject] -public class AnnotationCreatedMessage +public class AnnotationMessage { - [Key(0)] public DateTime CreatedDate { get; set; } - [Key(1)] public string Name { get; set; } = null!; - [Key(2)] public string OriginalMediaName { get; set; } = null!; - [Key(3)] public TimeSpan Time { get; set; } - [Key(4)] public string ImageExtension { get; set; } = null!; - [Key(5)] public string Detections { get; set; } = null!; - [Key(6)] public byte[] Image { get; set; } = null!; - [Key(7)] public RoleEnum CreatedRole { get; set; } - [Key(8)] public string CreatedEmail { get; set; } = null!; - [Key(9)] public SourceEnum Source { get; set; } + [Key(0)] public DateTime CreatedDate { get; set; } + [Key(1)] public string Name { get; set; } = null!; + [Key(2)] public string OriginalMediaName { get; set; } = null!; + [Key(3)] public TimeSpan Time { get; set; } + [Key(4)] public string ImageExtension { get; set; } = null!; + [Key(5)] public string Detections { get; set; } = null!; + [Key(6)] public byte[] Image { get; set; } = null!; + [Key(7)] public RoleEnum Role { get; set; } + [Key(8)] public string Email { get; set; } = null!; + [Key(9)] public SourceEnum Source { get; set; } [Key(10)] public AnnotationStatus Status { get; set; } } [MessagePackObject] -public class AnnotationValidatedMessage +public class AnnotationBulkMessage { - [Key(0)] public string Name { get; set; } = null!; + [Key(0)] public string[] AnnotationNames { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index 5b5c04c..2d0425a 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -59,5 +59,7 @@ public enum AnnotationStatus { None = 0, Created = 10, - Validated = 20 + Validated = 20, + ValidatedEdited = 25, + Deleted = 30 } \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationName.cs b/Azaion.Common/Database/AnnotationName.cs deleted file mode 100644 index 709066f..0000000 --- a/Azaion.Common/Database/AnnotationName.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Azaion.Common.Database; - -public class AnnotationName -{ - public string Name { get; set; } = null!; -} \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationQueueRecord.cs b/Azaion.Common/Database/AnnotationQueueRecord.cs new file mode 100644 index 0000000..8d95f6d --- /dev/null +++ b/Azaion.Common/Database/AnnotationQueueRecord.cs @@ -0,0 +1,8 @@ +namespace Azaion.Common.Database; + +public class AnnotationQueueRecord +{ + public DateTime DateTime { get; set; } + public AnnotationStatus Operation { get; set; } + public List AnnotationNames { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationsDb.cs b/Azaion.Common/Database/AnnotationsDb.cs index 69ed6ae..febcf8d 100644 --- a/Azaion.Common/Database/AnnotationsDb.cs +++ b/Azaion.Common/Database/AnnotationsDb.cs @@ -7,7 +7,7 @@ namespace Azaion.Common.Database; public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions) { public ITable Annotations => this.GetTable(); - public ITable AnnotationsQueue => this.GetTable(); + public ITable AnnotationsQueueRecords => this.GetTable(); public ITable Detections => this.GetTable(); public ITable QueueOffsets => this.GetTable(); } \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index fdce46d..25f0104 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -9,6 +9,7 @@ using LinqToDB.DataProvider.SQLite; using LinqToDB.Mapping; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json; namespace Azaion.Common.Database; @@ -17,7 +18,6 @@ public interface IDbFactory Task Run(Func> func); Task Run(Func func); void SaveToDisk(); - Task DeleteAnnotations(List annotations, CancellationToken cancellationToken = default); Task DeleteAnnotations(List annotationNames, CancellationToken cancellationToken = default); } @@ -63,7 +63,7 @@ public class DbFactory : IDbFactory SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); using var db = new AnnotationsDb(_fileDataOptions); db.CreateTable(); - db.CreateTable(); + db.CreateTable(); db.CreateTable(); db.CreateTable(); db.QueueOffsets.BulkCopy(new List @@ -98,12 +98,6 @@ public class DbFactory : IDbFactory _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); } - public async Task DeleteAnnotations(List annotations, CancellationToken cancellationToken = default) - { - var names = annotations.Select(x => x.Name).ToList(); - await DeleteAnnotations(names, cancellationToken); - } - public async Task DeleteAnnotations(List annotationNames, CancellationToken cancellationToken = default) { await Run(async db => @@ -142,8 +136,11 @@ public static class AnnotationsDbSchemaHolder builder.Entity() .HasTableName(Constants.DETECTIONS_TABLENAME); - builder.Entity() - .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME); + builder.Entity() + .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME) + .Property(x => x.AnnotationNames) + .HasDataType(DataType.NVarChar) + .HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject>(str) ?? new List()); builder.Build(); } diff --git a/Azaion.Common/Events/AnnotationsDeletedEvent.cs b/Azaion.Common/Events/AnnotationsDeletedEvent.cs index 0b93a76..c597e2d 100644 --- a/Azaion.Common/Events/AnnotationsDeletedEvent.cs +++ b/Azaion.Common/Events/AnnotationsDeletedEvent.cs @@ -3,9 +3,10 @@ using MediatR; namespace Azaion.Common.Events; -public class AnnotationsDeletedEvent(List annotations) : INotification +public class AnnotationsDeletedEvent(List annotationNames, bool fromQueue = false) : INotification { - public List Annotations { get; set; } = annotations; + public List AnnotationNames { get; set; } = annotationNames; + public bool FromQueue { get; set; } = fromQueue; } public class AnnotationAddedEvent(Annotation annotation) : INotification diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 4447584..0e11315 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -30,13 +30,16 @@ public class AnnotationService : IAnnotationService, INotificationHandler queueConfig, IOptions uiConfig, + IOptions directoriesConfig, IGalleryService galleryService, IMediator mediator, IAzaionApi api) @@ -48,11 +51,12 @@ public class AnnotationService : IAnnotationService, INotificationHandler await Init()).Wait(); + Task.Run(async () => await InitQueueConsumer()).Wait(); } - private async Task Init(CancellationToken cancellationToken = default) + private async Task InitQueueConsumer(CancellationToken cancellationToken = default) { if (!_api.CurrentUser.Role.IsValidator()) return; @@ -72,29 +76,40 @@ public class AnnotationService : IAnnotationService, INotificationHandler { - var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + var email = (string)message.ApplicationProperties[nameof(User.Email)]!; + if (email == _api.CurrentUser.Email) //Don't process messages by yourself + return; + var annotationStatus = (AnnotationStatus)message.ApplicationProperties[nameof(AnnotationStatus)]; + if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + await SaveAnnotationInner( + msg.CreatedDate, + msg.OriginalMediaName, + msg.Time, + JsonConvert.DeserializeObject>(msg.Detections) ?? [], + msg.Source, + new MemoryStream(msg.Image), + msg.Role, + msg.Email, + fromQueue: true, + token: cancellationToken); + } + else + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + if (annotationStatus == AnnotationStatus.Validated) + await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); + if (annotationStatus == AnnotationStatus.Deleted) + await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); + } offsets.AnnotationsOffset = context.Offset; ThrottleExt.Throttle(() => { _api.UpdateOffsets(offsets); return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true); - - if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself - return; - - await SaveAnnotationInner( - msg.CreatedDate, - msg.OriginalMediaName, - msg.Time, - JsonConvert.DeserializeObject>(msg.Detections) ?? [], - msg.Source, - new MemoryStream(msg.Image), - msg.CreatedRole, - msg.CreatedEmail, - fromQueue: true, - token: cancellationToken); + }, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true); } }); } @@ -120,7 +135,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler { @@ -128,25 +143,41 @@ public class AnnotationService : IAnnotationService, INotificationHandler x.Detections) .FirstOrDefaultAsync(x => x.Name == fName, token: token); - status = userRole.IsValidator() && source == SourceEnum.Manual - ? AnnotationStatus.Validated - : AnnotationStatus.Created; + if (userRole.IsValidator() && source == SourceEnum.Manual) + status = AnnotationStatus.Validated; if (fromQueue && ann is { AnnotationStatus: AnnotationStatus.Validated }) return ann; await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); - if (ann != null) + if (ann != null) //Annotation is already exists { - await db.Annotations + var annotationUpdatable = db.Annotations .Where(x => x.Name == fName) .Set(x => x.Source, source) + .Set(x => x.CreatedRole, userRole); + + if (status == AnnotationStatus.Validated) + { + if (status == AnnotationStatus.Validated) + status = AnnotationStatus.ValidatedEdited; //For further processing mark Annotations *edited* by Validator, not just simply Validated by button. + + annotationUpdatable = annotationUpdatable + .Set(x => x.ValidateDate, createdDate) + .Set(x => x.ValidateEmail, createdEmail); + } + else + { + annotationUpdatable = annotationUpdatable + .Set(x => x.CreatedDate, createdDate) + .Set(x => x.CreatedEmail, createdEmail); + } + + await annotationUpdatable .Set(x => x.AnnotationStatus, status) - .Set(x => x.CreatedDate, createdDate) - .Set(x => x.CreatedEmail, createdEmail) - .Set(x => x.CreatedRole, userRole) .UpdateAsync(token: token); + ann.Detections = detections; } else @@ -184,10 +215,11 @@ public class AnnotationService : IAnnotationService, INotificationHandler { _dbFactory.SaveToDisk(); @@ -196,12 +228,12 @@ public class AnnotationService : IAnnotationService, INotificationHandler annotations, CancellationToken token = default) + public async Task ValidateAnnotations(List annotationNames, bool fromQueue = false, CancellationToken token = default) { if (!_api.CurrentUser.Role.IsValidator()) return; - var annNames = annotations.Select(x => x.Name).ToHashSet(); + var annNames = annotationNames.ToHashSet(); await _dbFactory.Run(async db => { await db.Annotations @@ -211,6 +243,9 @@ public class AnnotationService : IAnnotationService, INotificationHandler x.ValidateEmail, _api.CurrentUser.Email) .UpdateAsync(token: token); }); + if (!fromQueue) + await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token); + ThrottleExt.Throttle(async () => { _dbFactory.SaveToDisk(); @@ -218,15 +253,25 @@ public class AnnotationService : IAnnotationService, INotificationHandler + { + _dbFactory.SaveToDisk(); + await Task.CompletedTask; + }, SaveTaskId, TimeSpan.FromSeconds(5), true); } } @@ -234,6 +279,5 @@ public interface IAnnotationService { Task SaveAnnotation(AnnotationImage a, CancellationToken ct = default); Task SaveAnnotation(string originalMediaName, TimeSpan time, List detections, Stream? stream = null, CancellationToken token = default); - Task ValidateAnnotations(List annotations, CancellationToken token = default); - + Task ValidateAnnotations(List annotationNames, bool fromQueue = false, CancellationToken token = default); } \ No newline at end of file diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index c46fcc2..750be53 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -3,12 +3,16 @@ using System.Net; using Azaion.Common.Database; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; +using Azaion.Common.Extensions; +using Azaion.CommonSecurity.DTO; +using Azaion.CommonSecurity.Services; using LinqToDB; using MessagePack; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using RabbitMQ.Stream.Client; +using RabbitMQ.Stream.Client.AMQP; using RabbitMQ.Stream.Client.Reliable; namespace Azaion.Common.Services; @@ -17,16 +21,15 @@ public class FailsafeAnnotationsProducer { private readonly ILogger _logger; private readonly IDbFactory _dbFactory; + private readonly IAzaionApi _azaionApi; private readonly QueueConfig _queueConfig; - private Producer _annotationProducer = null!; - private Producer _annotationConfirmProducer = null!; - - public FailsafeAnnotationsProducer(ILogger logger, IDbFactory dbFactory, IOptions queueConfig) + public FailsafeAnnotationsProducer(ILogger logger, IDbFactory dbFactory, IOptions queueConfig, IAzaionApi azaionApi) { _logger = logger; _dbFactory = dbFactory; + _azaionApi = azaionApi; _queueConfig = queueConfig.Value; Task.Run(async () => await ProcessQueue()); } @@ -41,107 +44,104 @@ public class FailsafeAnnotationsProducer }); } - private async Task Init(CancellationToken cancellationToken = default) + private async Task ProcessQueue(CancellationToken ct = default) { _annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE)); - _annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE)); - } - - private async Task ProcessQueue(CancellationToken cancellationToken = default) - { - await Init(cancellationToken); - while (!cancellationToken.IsCancellationRequested) + while (!ct.IsCancellationRequested) { - var messages = await GetFromInnerQueue(cancellationToken); - foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10 - { - var sent = false; - while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send - { - try - { - var createdMessages = messagesChunk - .Where(x => x.Status == AnnotationStatus.Created) - .Select(x => new Message(MessagePackSerializer.Serialize(x))) - .ToList(); - if (createdMessages.Any()) - await _annotationProducer.Send(createdMessages, CompressionType.Gzip); - - var validatedMessages = messagesChunk - .Where(x => x.Status == AnnotationStatus.Validated) - .Select(x => new Message(MessagePackSerializer.Serialize(x))) - .ToList(); - if (validatedMessages.Any()) - await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip); - - await _dbFactory.Run(async db => - await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken)); - sent = true; - _dbFactory.SaveToDisk(); - } - catch (Exception e) - { - _logger.LogError(e, e.Message); - await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); - } - await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); - } - } - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); - } - } - - private async Task> GetFromInnerQueue(CancellationToken cancellationToken = default) - { - return await _dbFactory.Run(async db => - { - var annotations = await db.AnnotationsQueue.Join( - db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a) - .ToListAsync(token: cancellationToken); - - var messages = new List(); - var badImages = new List(); - foreach (var annotation in annotations) + var sent = false; + while (!sent || !ct.IsCancellationRequested) //Waiting for send { try { - var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); - var annCreateMessage = new AnnotationCreatedMessage + var result = await _dbFactory.Run(async db => { - Name = annotation.Name, - OriginalMediaName = annotation.OriginalMediaName, - Time = annotation.Time, - CreatedRole = annotation.CreatedRole, - CreatedEmail = annotation.CreatedEmail, - CreatedDate = annotation.CreatedDate, - Status = annotation.AnnotationStatus, + var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct); + var editedCreatedNames = records + .Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) + .Select(x => x.AnnotationNames.FirstOrDefault()) + .ToList(); - ImageExtension = annotation.ImageExtension, - Image = image, - Detections = JsonConvert.SerializeObject(annotation.Detections), - Source = annotation.Source, - }; - messages.Add(annCreateMessage); + var annotationsDict = await db.Annotations.LoadWith(x => x.Detections) + .Where(x => editedCreatedNames.Contains(x.Name)) + .ToDictionaryAsync(a => a.Name, token: ct); + + var messages = new List(); + foreach (var record in records) + { + var appProperties = new ApplicationProperties + { + { nameof(AnnotationStatus), record.Operation }, + { nameof(User.Email), _azaionApi.CurrentUser.Email } + }; + + if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted)) + { + var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage + { + AnnotationNames = record.AnnotationNames.ToArray() + })) { ApplicationProperties = appProperties }; + + messages.Add(message); + } + else + { + var annotation = annotationsDict.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); + if (annotation == null) + continue; + + var image = await File.ReadAllBytesAsync(annotation.ImagePath, ct); + var annMessage = new AnnotationMessage + { + Name = annotation.Name, + OriginalMediaName = annotation.OriginalMediaName, + Time = annotation.Time, + Role = annotation.CreatedRole, + Email = annotation.CreatedEmail, + CreatedDate = annotation.CreatedDate, + Status = annotation.AnnotationStatus, + + ImageExtension = annotation.ImageExtension, + Image = image, + Detections = JsonConvert.SerializeObject(annotation.Detections), + Source = annotation.Source, + }; + var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties }; + + messages.Add(message); + } + } + + return (messages, records); + }); + + if (result.messages.Any()) + { + await _annotationProducer.Send(result.messages, CompressionType.Gzip); + await _dbFactory.Run(async db => await db.DeleteAsync(result.records, token: ct)); + sent = true; + _dbFactory.SaveToDisk(); + } } catch (Exception e) { _logger.LogError(e, e.Message); - badImages.Add(annotation.Name); + await Task.Delay(TimeSpan.FromSeconds(10), ct); } + await Task.Delay(TimeSpan.FromSeconds(10), ct); } - - if (badImages.Any()) - { - await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken); - _dbFactory.SaveToDisk(); - } - return messages; - }); + } + await Task.Delay(TimeSpan.FromSeconds(5), ct); } - public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default) + public async Task SendToInnerQueue(List annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default) { await _dbFactory.Run(async db => - await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken)); + await db.InsertAsync(new AnnotationQueueRecord + { + DateTime = DateTime.UtcNow, + Operation = status, + AnnotationNames = annotationNames + }, token: cancellationToken)); } } \ No newline at end of file diff --git a/Azaion.Common/Services/GpsMatcherClient.cs b/Azaion.Common/Services/GpsMatcherClient.cs index fcb5077..12db99d 100644 --- a/Azaion.Common/Services/GpsMatcherClient.cs +++ b/Azaion.Common/Services/GpsMatcherClient.cs @@ -60,7 +60,7 @@ public class GpsMatcherClient : IGpsMatcherClient process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; - process.Start(); + //process.Start(); } catch (Exception e) { diff --git a/Azaion.Dataset/DatasetExplorer.xaml b/Azaion.Dataset/DatasetExplorer.xaml index 73eeb63..80ed4b5 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml +++ b/Azaion.Dataset/DatasetExplorer.xaml @@ -30,7 +30,8 @@ - + + + diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 8989eac..6e132ad 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -273,10 +273,9 @@ public partial class DatasetExplorer if (result != MessageBoxResult.Yes) return; - var annotations = ThumbnailsView.SelectedItems.Cast().Select(x => x.Annotation) - .ToList(); + var annotationNames = ThumbnailsView.SelectedItems.Cast().Select(x => x.Annotation.Name).ToList(); - await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); + await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames)); ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected); } diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 0c222d5..b0ac4e9 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -99,7 +99,7 @@ public class DatasetExplorerEventHandler( var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast() .Select(x => x.Annotation) .ToList(); - await annotationService.ValidateAnnotations(annotations, cancellationToken); + await annotationService.ValidateAnnotations(annotations.Select(x => x.Name).ToList(), token: cancellationToken); foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation))) { ann.Annotation.AnnotationStatus = AnnotationStatus.Validated; @@ -143,9 +143,8 @@ public class DatasetExplorerEventHandler( public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { - var names = notification.Annotations.Select(x => x.Name).ToList(); var annThumbs = datasetExplorer.SelectedAnnotationDict - .Where(x => names.Contains(x.Key)) + .Where(x => notification.AnnotationNames.Contains(x.Key)) .Select(x => x.Value) .ToList(); foreach (var annThumb in annThumbs) diff --git a/Azaion.Inference/remote_command.pxd b/Azaion.Inference/remote_command.pxd index 2ead3a3..5f5b708 100644 --- a/Azaion.Inference/remote_command.pxd +++ b/Azaion.Inference/remote_command.pxd @@ -5,8 +5,8 @@ cdef enum CommandType: INFERENCE = 30 INFERENCE_DATA = 35 STOP_INFERENCE = 40 - AI_AVAILABILITY_CHECK = 80, - AI_AVAILABILITY_RESULT = 85, + AI_AVAILABILITY_CHECK = 80 + AI_AVAILABILITY_RESULT = 85 ERROR = 90 EXIT = 100 diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index ecb2d8d..f0dfe4d 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -46,7 +46,7 @@ public partial class App private readonly ICache _cache = new MemoryCache(); private IAzaionApi _azaionApi = null!; - private CancellationTokenSource _mainCancelTokenSource = new(); + private CancellationTokenSource _mainCTokenSource = new(); private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { @@ -89,7 +89,7 @@ public partial class App new ConfigUpdater().CheckConfig(); var secureAppConfig = ReadSecureAppConfig(); var apiDir = secureAppConfig.DirectoriesConfig.ApiResourcesDirectory; - _inferenceClient = new InferenceClient(new OptionsWrapper(secureAppConfig.InferenceClientConfig), _mainCancelTokenSource.Token); + _inferenceClient = new InferenceClient(new OptionsWrapper(secureAppConfig.InferenceClientConfig), _mainCTokenSource.Token); var login = new Login(); var loader = (IResourceLoader)_inferenceClient; @@ -244,7 +244,7 @@ public partial class App { var args = (KeyEventArgs)e; var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow); - ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), KeyPressTaskId, TimeSpan.FromMilliseconds(50)); + ThrottleExt.Throttle(() => _mediator.Publish(keyEvent, _mainCTokenSource.Token), KeyPressTaskId, TimeSpan.FromMilliseconds(50)); //e.Handled = true; } diff --git a/Azaion.Suite/Azaion.Suite.csproj b/Azaion.Suite/Azaion.Suite.csproj index cde203f..0a1e040 100644 --- a/Azaion.Suite/Azaion.Suite.csproj +++ b/Azaion.Suite/Azaion.Suite.csproj @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + From dae342b70ee1f4d97b84d56d4596448527a3c698 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sat, 17 May 2025 19:34:02 +0300 Subject: [PATCH 03/10] visual fixes --- Azaion.Common/DTO/AnnotationThumbnail.cs | 5 +++-- Azaion.Dataset/DatasetExplorer.xaml.cs | 9 +++++++-- Azaion.Dataset/DatasetExplorerEventHandler.cs | 9 +++++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/Azaion.Common/DTO/AnnotationThumbnail.cs b/Azaion.Common/DTO/AnnotationThumbnail.cs index 73c9f17..72ee3ea 100644 --- a/Azaion.Common/DTO/AnnotationThumbnail.cs +++ b/Azaion.Common/DTO/AnnotationThumbnail.cs @@ -7,9 +7,10 @@ using Azaion.Common.Extensions; namespace Azaion.Common.DTO; -public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged +public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged { public Annotation Annotation { get; set; } = annotation; + public bool IsValidator { get; set; } = isValidator; private BitmapImage? _thumbnail; public BitmapImage? Thumbnail @@ -29,7 +30,7 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged public string ImageName => Path.GetFileName(Annotation.ImagePath); public string CreatedEmail => Annotation.CreatedEmail; - public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created; + public bool IsSeed => IsValidator && Annotation.AnnotationStatus == AnnotationStatus.Created; public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 6e132ad..87bf21f 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -8,6 +8,7 @@ using Azaion.Common.DTO.Config; using Azaion.Common.Events; using Azaion.Common.Services; using Azaion.CommonSecurity.DTO; +using Azaion.CommonSecurity.Services; using LinqToDB; using MediatR; using Microsoft.Extensions.Logging; @@ -36,6 +37,7 @@ public partial class DatasetExplorer private readonly IMediator _mediator; public readonly List AnnotationsClasses; + private IAzaionApi _azaionApi; public bool ThumbnailLoading { get; set; } @@ -49,7 +51,8 @@ public partial class DatasetExplorer IGalleryService galleryService, FormState formState, IDbFactory dbFactory, - IMediator mediator) + IMediator mediator, + IAzaionApi azaionApi) { InitializeComponent(); @@ -59,6 +62,7 @@ public partial class DatasetExplorer _galleryService = galleryService; _dbFactory = dbFactory; _mediator = mediator; + _azaionApi = azaionApi; var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast().ToList(); _annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id)) @@ -87,6 +91,7 @@ public partial class DatasetExplorer ThumbnailsView.SelectionChanged += (_, _) => { StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}"; + ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast().Any(x => x.IsSeed) ? Visibility.Visible : Visibility.Hidden; @@ -288,7 +293,7 @@ public partial class DatasetExplorer .OrderBy(x => x.Value.AnnotationStatus) .ThenByDescending(x => x.Value.CreatedDate)) { - var annThumb = new AnnotationThumbnail(ann.Value); + var annThumb = new AnnotationThumbnail(ann.Value, _azaionApi.CurrentUser.Role.IsValidator()); SelectedAnnotations.Add(annThumb); SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); } diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index b0ac4e9..752cb88 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -7,18 +7,23 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Queue; using Azaion.Common.Events; using Azaion.Common.Services; +using Azaion.CommonSecurity.DTO; +using Azaion.CommonSecurity.Services; using MediatR; namespace Azaion.Dataset; public class DatasetExplorerEventHandler( DatasetExplorer datasetExplorer, - IAnnotationService annotationService) : + IAnnotationService annotationService, + IAzaionApi azaionApi) : INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler { + private readonly IAzaionApi _azaionApi = azaionApi; + private readonly Dictionary _keysControlEnumDict = new() { { Key.Enter, PlaybackControlEnum.SaveAnnotations }, @@ -122,7 +127,7 @@ public class DatasetExplorerEventHandler( if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) { var index = 0; - var annThumb = new AnnotationThumbnail(annotation); + var annThumb = new AnnotationThumbnail(annotation, _azaionApi.CurrentUser.Role.IsValidator()); if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name)) { datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); From cf563571c8592e3ba81e4161ae8c3400cf3a361b Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sat, 17 May 2025 19:38:07 +0300 Subject: [PATCH 04/10] log queue errors --- Azaion.Common/Services/AnnotationService.cs | 75 ++++++++++++--------- 1 file changed, 43 insertions(+), 32 deletions(-) diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 0e11315..843cd86 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -13,6 +13,7 @@ using LinqToDB; using LinqToDB.Data; using MediatR; using MessagePack; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using RabbitMQ.Stream.Client; @@ -27,6 +28,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler _logger; private readonly QueueConfig _queueConfig; private Consumer _consumer = null!; private readonly UIConfig _uiConfig; @@ -42,13 +44,15 @@ public class AnnotationService : IAnnotationService, INotificationHandler directoriesConfig, IGalleryService galleryService, IMediator mediator, - IAzaionApi api) + IAzaionApi api, + ILogger logger) { _dbFactory = dbFactory; _producer = producer; _galleryService = galleryService; _mediator = mediator; _api = api; + _logger = logger; _queueConfig = queueConfig.Value; _uiConfig = uiConfig.Value; _dirConfig = directoriesConfig.Value; @@ -76,40 +80,47 @@ public class AnnotationService : IAnnotationService, INotificationHandler { - var email = (string)message.ApplicationProperties[nameof(User.Email)]!; - if (email == _api.CurrentUser.Email) //Don't process messages by yourself - return; - var annotationStatus = (AnnotationStatus)message.ApplicationProperties[nameof(AnnotationStatus)]; - if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) + try { - var msg = MessagePackSerializer.Deserialize(message.Data.Contents); - await SaveAnnotationInner( - msg.CreatedDate, - msg.OriginalMediaName, - msg.Time, - JsonConvert.DeserializeObject>(msg.Detections) ?? [], - msg.Source, - new MemoryStream(msg.Image), - msg.Role, - msg.Email, - fromQueue: true, - token: cancellationToken); - } - else - { - var msg = MessagePackSerializer.Deserialize(message.Data.Contents); - if (annotationStatus == AnnotationStatus.Validated) - await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); - if (annotationStatus == AnnotationStatus.Deleted) - await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); - } + var email = (string)message.ApplicationProperties[nameof(User.Email)]!; + if (email == _api.CurrentUser.Email) //Don't process messages by yourself + return; + var annotationStatus = (AnnotationStatus)message.ApplicationProperties[nameof(AnnotationStatus)]; + if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + await SaveAnnotationInner( + msg.CreatedDate, + msg.OriginalMediaName, + msg.Time, + JsonConvert.DeserializeObject>(msg.Detections) ?? [], + msg.Source, + new MemoryStream(msg.Image), + msg.Role, + msg.Email, + fromQueue: true, + token: cancellationToken); + } + else + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + if (annotationStatus == AnnotationStatus.Validated) + await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); + if (annotationStatus == AnnotationStatus.Deleted) + await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); + } - offsets.AnnotationsOffset = context.Offset; - ThrottleExt.Throttle(() => + offsets.AnnotationsOffset = context.Offset; + ThrottleExt.Throttle(() => + { + _api.UpdateOffsets(offsets); + return Task.CompletedTask; + }, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true); + } + catch (Exception e) { - _api.UpdateOffsets(offsets); - return Task.CompletedTask; - }, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true); + _logger.LogError(e, e.Message); + } } }); } From c5e81ebcc6be1905a1c1b2417481eff642a6536f Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sun, 18 May 2025 20:11:19 +0300 Subject: [PATCH 05/10] fixed bugs with queue handling. At least most of them --- Azaion.Annotator/Annotator.xaml.cs | 5 +- Azaion.Annotator/AnnotatorEventHandler.cs | 54 ++++++++----- Azaion.Common/Constants.cs | 5 -- Azaion.Common/Database/Annotation.cs | 7 +- .../Database/AnnotationQueueRecord.cs | 5 +- Azaion.Common/Database/AnnotationsDb.cs | 1 - Azaion.Common/Database/DbFactory.cs | 33 ++++---- Azaion.Common/Database/QueueOffset.cs | 7 -- Azaion.Common/Services/AnnotationService.cs | 76 +++++++++---------- Azaion.Common/Services/FailsafeProducer.cs | 20 ++++- Azaion.Common/Services/GPSMatcherService.cs | 2 +- Azaion.Common/Services/InferenceClient.cs | 2 +- Azaion.Dataset/DatasetExplorerEventHandler.cs | 37 +++++---- Azaion.Suite/App.xaml.cs | 3 + Azaion.Suite/config.json | 2 +- 15 files changed, 135 insertions(+), 124 deletions(-) delete mode 100644 Azaion.Common/Database/QueueOffset.cs diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 700c4c7..76b901f 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -417,7 +417,8 @@ public partial class Annotator mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); AllMediaFiles = new ObservableCollection(allFiles); - MediaFilesDict = AllMediaFiles.ToDictionary(x => x.FName); + MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name) + .ToDictionary(gr => gr.Key, gr => gr.First()); LvFiles.ItemsSource = AllMediaFiles; DataContext = this; } @@ -463,7 +464,7 @@ public partial class Annotator { Title = "Open Video folder", IsFolderPicker = true, - InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + InitialDirectory = Path.GetDirectoryName(_appConfig.DirectoriesConfig.VideosDirectory) }; var dialogResult = dlg.ShowDialog(); diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 05fce65..c2258dc 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -1,6 +1,7 @@ using System.IO; using System.Windows; using System.Windows.Input; +using System.Windows.Threading; using Azaion.Annotator.DTO; using Azaion.Common; using Azaion.Common.DTO; @@ -276,31 +277,42 @@ public class AnnotatorEventHandler( mainWindow.AddAnnotation(annotation); } - public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + public Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { - var namesSet = notification.AnnotationNames.ToHashSet(); - - var remainAnnotations = formState.AnnotationResults - .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList(); - formState.AnnotationResults.Clear(); - foreach (var ann in remainAnnotations) - formState.AnnotationResults.Add(ann); - - var timedAnnsToRemove = mainWindow.TimedAnnotations - .Where(x => namesSet.Contains(x.Value.Name)) - .Select(x => x.Value).ToList(); - mainWindow.TimedAnnotations.Remove(timedAnnsToRemove); - - if (formState.AnnotationResults.Count == 0) + try { - var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name); - if (media != null) + mainWindow.Dispatcher.Invoke(() => { - media.HasAnnotations = false; - mainWindow.LvFiles.Items.Refresh(); - } + var namesSet = notification.AnnotationNames.ToHashSet(); + + var remainAnnotations = formState.AnnotationResults + .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList(); + formState.AnnotationResults.Clear(); + foreach (var ann in remainAnnotations) + formState.AnnotationResults.Add(ann); + + var timedAnnsToRemove = mainWindow.TimedAnnotations + .Where(x => namesSet.Contains(x.Value.Name)) + .Select(x => x.Value).ToList(); + mainWindow.TimedAnnotations.Remove(timedAnnsToRemove); + + if (formState.AnnotationResults.Count == 0) + { + var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name); + if (media != null) + { + media.HasAnnotations = false; + mainWindow.LvFiles.Items.Refresh(); + } + } + }); } - await Task.CompletedTask; + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; + } + return Task.CompletedTask; } public Task Handle(AnnotationAddedEvent e, CancellationToken cancellationToken) diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index ceadd67..e59e1d7 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -97,12 +97,7 @@ public class Constants #endregion - #region Queue - public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; - public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm"; - - #endregion #region Database diff --git a/Azaion.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index 2d0425a..013f4a6 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -59,7 +59,8 @@ public enum AnnotationStatus { None = 0, Created = 10, - Validated = 20, - ValidatedEdited = 25, - Deleted = 30 + ValidatedEdited = 20, + + Validated = 30, + Deleted = 40 } \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationQueueRecord.cs b/Azaion.Common/Database/AnnotationQueueRecord.cs index 8d95f6d..0e1524b 100644 --- a/Azaion.Common/Database/AnnotationQueueRecord.cs +++ b/Azaion.Common/Database/AnnotationQueueRecord.cs @@ -2,7 +2,8 @@ namespace Azaion.Common.Database; public class AnnotationQueueRecord { - public DateTime DateTime { get; set; } - public AnnotationStatus Operation { get; set; } + public Guid Id { get; set; } + public DateTime DateTime { get; set; } + public AnnotationStatus Operation { get; set; } public List AnnotationNames { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationsDb.cs b/Azaion.Common/Database/AnnotationsDb.cs index febcf8d..c8434d9 100644 --- a/Azaion.Common/Database/AnnotationsDb.cs +++ b/Azaion.Common/Database/AnnotationsDb.cs @@ -9,5 +9,4 @@ public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions public ITable Annotations => this.GetTable(); public ITable AnnotationsQueueRecords => this.GetTable(); public ITable Detections => this.GetTable(); - public ITable QueueOffsets => this.GetTable(); } \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index 25f0104..6b33fbe 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -53,32 +53,24 @@ public class DbFactory : IDbFactory .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); if (!File.Exists(_annConfig.AnnotationsDbFile)) - CreateDb(); + SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); + RecreateTables(); + _fileConnection.Open(); _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); } - private void CreateDb() + private void RecreateTables() { - SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); using var db = new AnnotationsDb(_fileDataOptions); - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - db.QueueOffsets.BulkCopy(new List - { - new() - { - Offset = 0, - QueueName = Constants.MQ_ANNOTATIONS_QUEUE - }, - new() - { - Offset = 0, - QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE - } - }); + var schema = db.DataProvider.GetSchemaProvider().GetSchema(db); + var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet(); + if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME)) + db.CreateTable(); + if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME)) + db.CreateTable(); + if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME)) + db.CreateTable(); } public async Task Run(Func> func) @@ -138,6 +130,7 @@ public static class AnnotationsDbSchemaHolder builder.Entity() .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME) + .HasPrimaryKey(x => x.Id) .Property(x => x.AnnotationNames) .HasDataType(DataType.NVarChar) .HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject>(str) ?? new List()); diff --git a/Azaion.Common/Database/QueueOffset.cs b/Azaion.Common/Database/QueueOffset.cs deleted file mode 100644 index 1fe3d0f..0000000 --- a/Azaion.Common/Database/QueueOffset.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Azaion.Common.Database; - -public class QueueOffset -{ - public string QueueName { get; set; } = null!; - public ulong Offset { get; set; } -} \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 843cd86..64c9c23 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -77,40 +77,43 @@ public class AnnotationService : IAnnotationService, INotificationHandler { try { var email = (string)message.ApplicationProperties[nameof(User.Email)]!; - if (email == _api.CurrentUser.Email) //Don't process messages by yourself + if (!Enum.TryParse((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus)) return; - var annotationStatus = (AnnotationStatus)message.ApplicationProperties[nameof(AnnotationStatus)]; - if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) - { - var msg = MessagePackSerializer.Deserialize(message.Data.Contents); - await SaveAnnotationInner( - msg.CreatedDate, - msg.OriginalMediaName, - msg.Time, - JsonConvert.DeserializeObject>(msg.Detections) ?? [], - msg.Source, - new MemoryStream(msg.Image), - msg.Role, - msg.Email, - fromQueue: true, - token: cancellationToken); - } - else - { - var msg = MessagePackSerializer.Deserialize(message.Data.Contents); - if (annotationStatus == AnnotationStatus.Validated) - await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); - if (annotationStatus == AnnotationStatus.Deleted) - await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); - } - offsets.AnnotationsOffset = context.Offset; + if (email != _api.CurrentUser.Email) //Don't process messages by yourself + { + if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + await SaveAnnotationInner( + msg.CreatedDate, + msg.OriginalMediaName, + msg.Time, + JsonConvert.DeserializeObject>(msg.Detections) ?? [], + msg.Source, + new MemoryStream(msg.Image), + msg.Role, + msg.Email, + fromQueue: true, + token: cancellationToken); + } + else + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + if (annotationStatus == AnnotationStatus.Validated) + await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); + if (annotationStatus == AnnotationStatus.Deleted) + await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); + } + } + + offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message ThrottleExt.Throttle(() => { _api.UpdateOffsets(offsets); @@ -154,12 +157,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler x.Detections) .FirstOrDefaultAsync(x => x.Name == fName, token: token); - if (userRole.IsValidator() && source == SourceEnum.Manual) - status = AnnotationStatus.Validated; - - if (fromQueue && ann is { AnnotationStatus: AnnotationStatus.Validated }) - return ann; - await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); if (ann != null) //Annotation is already exists @@ -169,10 +166,9 @@ public class AnnotationService : IAnnotationService, INotificationHandler x.Source, source) .Set(x => x.CreatedRole, userRole); - if (status == AnnotationStatus.Validated) + if (userRole.IsValidator() && source == SourceEnum.Manual) { - if (status == AnnotationStatus.Validated) - status = AnnotationStatus.ValidatedEdited; //For further processing mark Annotations *edited* by Validator, not just simply Validated by button. + status = AnnotationStatus.ValidatedEdited; annotationUpdatable = annotationUpdatable .Set(x => x.ValidateDate, createdDate) @@ -212,9 +208,6 @@ public class AnnotationService : IAnnotationService, INotificationHandler @@ -275,7 +268,8 @@ public class AnnotationService : IAnnotationService, INotificationHandler diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index 750be53..8e93f08 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -23,14 +23,22 @@ public class FailsafeAnnotationsProducer private readonly IDbFactory _dbFactory; private readonly IAzaionApi _azaionApi; private readonly QueueConfig _queueConfig; + private readonly UIConfig _uiConfig; + private Producer _annotationProducer = null!; - public FailsafeAnnotationsProducer(ILogger logger, IDbFactory dbFactory, IOptions queueConfig, IAzaionApi azaionApi) + + public FailsafeAnnotationsProducer(ILogger logger, + IDbFactory dbFactory, + IOptions queueConfig, + IOptions uiConfig, + IAzaionApi azaionApi) { _logger = logger; _dbFactory = dbFactory; _azaionApi = azaionApi; _queueConfig = queueConfig.Value; + _uiConfig = uiConfig.Value; Task.Run(async () => await ProcessQueue()); } @@ -71,7 +79,7 @@ public class FailsafeAnnotationsProducer { var appProperties = new ApplicationProperties { - { nameof(AnnotationStatus), record.Operation }, + { nameof(AnnotationStatus), record.Operation.ToString() }, { nameof(User.Email), _azaionApi.CurrentUser.Email } }; @@ -86,7 +94,7 @@ public class FailsafeAnnotationsProducer } else { - var annotation = annotationsDict.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); + var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); if (annotation == null) continue; @@ -118,7 +126,8 @@ public class FailsafeAnnotationsProducer if (result.messages.Any()) { await _annotationProducer.Send(result.messages, CompressionType.Gzip); - await _dbFactory.Run(async db => await db.DeleteAsync(result.records, token: ct)); + var ids = result.records.Select(x => x.Id).ToList(); + var removed = await _dbFactory.Run(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct)); sent = true; _dbFactory.SaveToDisk(); } @@ -136,9 +145,12 @@ public class FailsafeAnnotationsProducer public async Task SendToInnerQueue(List annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default) { + if (_uiConfig.SilentDetection) + return; await _dbFactory.Run(async db => await db.InsertAsync(new AnnotationQueueRecord { + Id = Guid.NewGuid(), DateTime = DateTime.UtcNow, Operation = status, AnnotationNames = annotationNames diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index f633caf..03c4370 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -41,7 +41,7 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow var indexOffset = 0; while (routeFiles.Any()) { - //await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken); + await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken); gpsMatcherClient.StartMatching(new StartMatchingEvent { ImagesCount = POINTS_COUNT, diff --git a/Azaion.Common/Services/InferenceClient.cs b/Azaion.Common/Services/InferenceClient.cs index 28ad8fe..0d6771d 100644 --- a/Azaion.Common/Services/InferenceClient.cs +++ b/Azaion.Common/Services/InferenceClient.cs @@ -126,7 +126,7 @@ public class InferenceClient : IInferenceClient, IResourceLoader _waitFileCancelSource.Cancel(); } - bytes = command.Data; + bytes = command.Data!; _waitFileCancelSource.Cancel(); } _waitFileCancelSource.Token.WaitForCancel(timeout ?? TimeSpan.FromSeconds(15)); diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 752cb88..f0cd874 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -1,19 +1,17 @@ -using System.IO; -using System.Windows; -using System.Windows.Input; -using System.Windows.Threading; +using System.Windows.Input; using Azaion.Common.Database; using Azaion.Common.DTO; -using Azaion.Common.DTO.Queue; using Azaion.Common.Events; using Azaion.Common.Services; using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.Services; using MediatR; +using Microsoft.Extensions.Logging; namespace Azaion.Dataset; public class DatasetExplorerEventHandler( + ILogger logger, DatasetExplorer datasetExplorer, IAnnotationService annotationService, IAzaionApi azaionApi) : @@ -22,8 +20,6 @@ public class DatasetExplorerEventHandler( INotificationHandler, INotificationHandler { - private readonly IAzaionApi _azaionApi = azaionApi; - private readonly Dictionary _keysControlEnumDict = new() { { Key.Enter, PlaybackControlEnum.SaveAnnotations }, @@ -127,7 +123,7 @@ public class DatasetExplorerEventHandler( if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) { var index = 0; - var annThumb = new AnnotationThumbnail(annotation, _azaionApi.CurrentUser.Role.IsValidator()); + var annThumb = new AnnotationThumbnail(annotation, azaionApi.CurrentUser.Role.IsValidator()); if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name)) { datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); @@ -148,14 +144,25 @@ public class DatasetExplorerEventHandler( public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { - var annThumbs = datasetExplorer.SelectedAnnotationDict - .Where(x => notification.AnnotationNames.Contains(x.Key)) - .Select(x => x.Value) - .ToList(); - foreach (var annThumb in annThumbs) + try { - datasetExplorer.SelectedAnnotations.Remove(annThumb); - datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); + datasetExplorer.Dispatcher.Invoke(() => + { + var annThumbs = datasetExplorer.SelectedAnnotationDict + .Where(x => notification.AnnotationNames.Contains(x.Key)) + .Select(x => x.Value) + .ToList(); + foreach (var annThumb in annThumbs) + { + datasetExplorer.SelectedAnnotations.Remove(annThumb); + datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); + } + }); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; } await Task.CompletedTask; } diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index f0dfe4d..6eeb083 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -145,6 +145,9 @@ public partial class App StartMain(); _host.Start(); EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new RoutedEventHandler(GlobalKeyHandler)); + var datasetExplorer = _host.Services.GetRequiredService(); + datasetExplorer.Show(); + datasetExplorer.Hide(); _host.Services.GetRequiredService().Show(); }; login.Closed += (sender, args) => diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index f5c76fd..d4652ef 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -27,6 +27,6 @@ "LeftPanelWidth": 220.0, "RightPanelWidth": 230.0, "GenerateAnnotatedImage": true, - "SilentDetection": true + "SilentDetection": false } } \ No newline at end of file From f2b57dccc093614ad0dbc20504963b0d86b01a14 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sun, 18 May 2025 20:14:19 +0300 Subject: [PATCH 06/10] security fix --- Azaion.Suite/Azaion.Suite.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Azaion.Suite/Azaion.Suite.csproj b/Azaion.Suite/Azaion.Suite.csproj index 0a1e040..cde203f 100644 --- a/Azaion.Suite/Azaion.Suite.csproj +++ b/Azaion.Suite/Azaion.Suite.csproj @@ -33,8 +33,8 @@ - - + + @@ -62,8 +62,8 @@ - - + + From 66bfe474c2aa9b0d6d66862784c58245d8f4cf1e Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sun, 18 May 2025 20:31:43 +0300 Subject: [PATCH 07/10] fix security 2 --- Azaion.Suite/App.xaml.cs | 6 +++--- build/installer.full.iss | 4 ++-- build/installer.iterative.iss | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 6eeb083..2d0c1ce 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -145,9 +145,6 @@ public partial class App StartMain(); _host.Start(); EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new RoutedEventHandler(GlobalKeyHandler)); - var datasetExplorer = _host.Services.GetRequiredService(); - datasetExplorer.Show(); - datasetExplorer.Hide(); _host.Services.GetRequiredService().Show(); }; login.Closed += (sender, args) => @@ -233,6 +230,9 @@ public partial class App .Build(); Annotation.InitializeDirs(_host.Services.GetRequiredService>().Value); + var datasetExplorer = _host.Services.GetRequiredService(); + datasetExplorer.Show(); + datasetExplorer.Hide(); _mediator = _host.Services.GetRequiredService(); if (!string.IsNullOrEmpty(_loadErrors)) diff --git a/build/installer.full.iss b/build/installer.full.iss index 2e7ba94..edea07d 100644 --- a/build/installer.full.iss +++ b/build/installer.full.iss @@ -1,12 +1,12 @@ [Setup] AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} AppName=Azaion Suite -AppVersion=1.4.5 +AppVersion=1.4.6 AppPublisher=Azaion Ukraine DefaultDirName={localappdata}\Azaion\Azaion Suite DefaultGroupName=Azaion Suite OutputDir=..\ -OutputBaseFilename=AzaionSuite.Full.1.4.5 +OutputBaseFilename=AzaionSuite.Full.1.4.6 SetupIconFile=..\dist-azaion\logo.ico UninstallDisplayName=Azaion Suite UninstallDisplayIcon={app}\Azaion.Suite.exe diff --git a/build/installer.iterative.iss b/build/installer.iterative.iss index 6285322..86f9106 100644 --- a/build/installer.iterative.iss +++ b/build/installer.iterative.iss @@ -1,12 +1,12 @@ [Setup] AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} AppName=Azaion Suite -AppVersion=1.4.5 +AppVersion=1.4.6 AppPublisher=Azaion Ukraine DefaultDirName={localappdata}\Azaion\Azaion Suite DefaultGroupName=Azaion Suite OutputDir=..\ -OutputBaseFilename=AzaionSuite.Iterative.1.4.5 +OutputBaseFilename=AzaionSuite.Iterative.1.4.6 SetupIconFile=..\dist-azaion\logo.ico UninstallDisplayName=Azaion Suite UninstallDisplayIcon={app}\Azaion.Suite.exe From a5fcb0988b51b5892f477a6a2e9accc2105b0360 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Tue, 20 May 2025 11:02:24 +0300 Subject: [PATCH 08/10] fixed sorting in datasetexplorer, also show date make annotationstatus more clear --- Azaion.Common/DTO/AnnotationThumbnail.cs | 6 +++++- Azaion.Common/Database/Annotation.cs | 3 +-- Azaion.Common/Services/AnnotationService.cs | 15 ++++----------- Azaion.Common/Services/FailsafeProducer.cs | 2 +- Azaion.Dataset/DatasetExplorer.xaml | 10 ++++++++-- Azaion.Dataset/DatasetExplorer.xaml.cs | 18 +++++++++++------- 6 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Azaion.Common/DTO/AnnotationThumbnail.cs b/Azaion.Common/DTO/AnnotationThumbnail.cs index 72ee3ea..f3e00e4 100644 --- a/Azaion.Common/DTO/AnnotationThumbnail.cs +++ b/Azaion.Common/DTO/AnnotationThumbnail.cs @@ -4,6 +4,7 @@ using System.Runtime.CompilerServices; using System.Windows.Media.Imaging; using Azaion.Common.Database; using Azaion.Common.Extensions; +using Azaion.CommonSecurity.DTO; namespace Azaion.Common.DTO; @@ -29,8 +30,11 @@ public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INot } public string ImageName => Path.GetFileName(Annotation.ImagePath); + public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}"; public string CreatedEmail => Annotation.CreatedEmail; - public bool IsSeed => IsValidator && Annotation.AnnotationStatus == AnnotationStatus.Created; + public bool IsSeed => IsValidator && + Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) && + !Annotation.CreatedRole.IsValidator(); public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) diff --git a/Azaion.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index 013f4a6..42c780c 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -59,8 +59,7 @@ public enum AnnotationStatus { None = 0, Created = 10, - ValidatedEdited = 20, - + Edited = 20, Validated = 30, Deleted = 40 } \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 64c9c23..63f0ddf 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -88,7 +88,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler(message.Data.Contents); await SaveAnnotationInner( @@ -161,25 +161,18 @@ public class AnnotationService : IAnnotationService, INotificationHandler x.Name == fName) - .Set(x => x.Source, source) - .Set(x => x.CreatedRole, userRole); + .Set(x => x.Source, source); if (userRole.IsValidator() && source == SourceEnum.Manual) { - status = AnnotationStatus.ValidatedEdited; - annotationUpdatable = annotationUpdatable .Set(x => x.ValidateDate, createdDate) .Set(x => x.ValidateEmail, createdEmail); } - else - { - annotationUpdatable = annotationUpdatable - .Set(x => x.CreatedDate, createdDate) - .Set(x => x.CreatedEmail, createdEmail); - } await annotationUpdatable .Set(x => x.AnnotationStatus, status) diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index 8e93f08..4a2d877 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -66,7 +66,7 @@ public class FailsafeAnnotationsProducer { var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct); var editedCreatedNames = records - .Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.ValidatedEdited)) + .Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited)) .Select(x => x.AnnotationNames.FirstOrDefault()) .ToList(); diff --git a/Azaion.Dataset/DatasetExplorer.xaml b/Azaion.Dataset/DatasetExplorer.xaml index 80ed4b5..c15c4ec 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml +++ b/Azaion.Dataset/DatasetExplorer.xaml @@ -46,8 +46,14 @@ + Foreground="Gray"> + + + + + + + diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 87bf21f..154be41 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -288,14 +288,18 @@ public partial class DatasetExplorer { SelectedAnnotations.Clear(); SelectedAnnotationDict.Clear(); - var annotations = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId]; - foreach (var ann in annotations - .OrderBy(x => x.Value.AnnotationStatus) - .ThenByDescending(x => x.Value.CreatedDate)) + var annThumbnails = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId] + .Select(x => new AnnotationThumbnail(x.Value, _azaionApi.CurrentUser.Role.IsValidator())) + .OrderBy(x => !x.IsSeed) + .ThenByDescending(x =>x.Annotation.CreatedDate); + + //var dict = annThumbnails.Take(20).ToDictionary(x => x.Annotation.Name, x => x.IsSeed); + + + foreach (var thumb in annThumbnails) { - var annThumb = new AnnotationThumbnail(ann.Value, _azaionApi.CurrentUser.Role.IsValidator()); - SelectedAnnotations.Add(annThumb); - SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); + SelectedAnnotations.Add(thumb); + SelectedAnnotationDict.Add(thumb.Annotation.Name, thumb); } await Task.CompletedTask; } From edd803c30439692607ac060d661fcab7b1cd13e1 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Tue, 20 May 2025 12:03:52 +0300 Subject: [PATCH 09/10] fix classes colors, add caponier --- Azaion.Suite/config.system.json | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Azaion.Suite/config.system.json b/Azaion.Suite/config.system.json index a0beb77..e3a280c 100644 --- a/Azaion.Suite/config.system.json +++ b/Azaion.Suite/config.system.json @@ -1,22 +1,23 @@ { "AnnotationConfig": { "DetectionClasses": [ - { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#FF0000" }, - { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00FF00" }, - { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000FF" }, - { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#FFFF00" }, - { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#FF00FF" }, - { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00FFFF" }, - { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021" }, - { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000" }, - { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000" }, - { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, - { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#000080" }, - { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, - { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#800080" }, - { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" }, - { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" }, - { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" } + { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#ff0000" }, + { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00ff00" }, + { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000ff" }, + { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#ffff00" }, + { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#ff00ff" }, + { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00ffff" }, + { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021" }, + { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000" }, + { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000" }, + { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, + { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#a52a2a" }, + { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, + { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#87ceeb" }, + { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" }, + { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" }, + { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" }, + { "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500" } ], "VideoFormats": [ ".mp4", ".mov", ".avi" ], "ImageFormats": [ ".jpg", ".jpeg", ".png", ".bmp" ], From 522af51a8d442990e745134a39019128f6bc9d3e Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Tue, 20 May 2025 12:31:18 +0300 Subject: [PATCH 10/10] don't send image to the queue on editing --- Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs | 2 +- Azaion.Common/Services/AnnotationService.cs | 7 +++---- Azaion.Common/Services/FailsafeProducer.cs | 5 ++++- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs index 2b2a108..98d3bab 100644 --- a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs +++ b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs @@ -13,7 +13,7 @@ public class AnnotationMessage [Key(3)] public TimeSpan Time { get; set; } [Key(4)] public string ImageExtension { get; set; } = null!; [Key(5)] public string Detections { get; set; } = null!; - [Key(6)] public byte[] Image { get; set; } = null!; + [Key(6)] public byte[]? Image { get; set; } = null!; [Key(7)] public RoleEnum Role { get; set; } [Key(8)] public string Email { get; set; } = null!; [Key(9)] public SourceEnum Source { get; set; } diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 63f0ddf..92fc516 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -97,7 +97,7 @@ public class AnnotationService : IAnnotationService, INotificationHandler>(msg.Detections) ?? [], msg.Source, - new MemoryStream(msg.Image), + msg.Image == null ? null : new MemoryStream(msg.Image), msg.Role, msg.Email, fromQueue: true, @@ -141,9 +141,8 @@ public class AnnotationService : IAnnotationService, INotificationHandler Validated -> stream: azaion-annotations-confirm - // AI, Manual save from Operators -> Created -> stream: azaion-annotations - private async Task SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List detections, SourceEnum source, Stream? stream, + private async Task SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, + List detections, SourceEnum source, Stream? stream, RoleEnum userRole, string createdEmail, bool fromQueue = false, diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index 4a2d877..5beafc1 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -98,7 +98,10 @@ public class FailsafeAnnotationsProducer if (annotation == null) continue; - var image = await File.ReadAllBytesAsync(annotation.ImagePath, ct); + var image = record.Operation == AnnotationStatus.Created + ? await File.ReadAllBytesAsync(annotation.ImagePath, ct) + : null; + var annMessage = new AnnotationMessage { Name = annotation.Name,