From 1287c13516a17d5fa54e2de967aa766b3f68a8b5 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Mon, 14 Apr 2025 19:43:14 +0300 Subject: [PATCH 001/203] fix ui bugs, fix RefreshThumbnails method --- Azaion.Common/Services/AnnotationService.cs | 3 +- Azaion.Common/Services/GalleryService.cs | 29 +++++++++++--- Azaion.Dataset/DatasetExplorerEventHandler.cs | 39 ++++++++++--------- Azaion.Suite/App.xaml.cs | 4 +- Azaion.Suite/postbuild.cmd | 14 ++++++- 5 files changed, 62 insertions(+), 27 deletions(-) diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 5b528bf..30bad22 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -147,7 +147,7 @@ public class AnnotationService : INotificationHandler : AnnotationStatus.Created; await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); - await db.BulkCopyAsync(detections, cancellationToken: token); + if (ann != null) { await db.Annotations @@ -177,6 +177,7 @@ public class AnnotationService : INotificationHandler }; await db.InsertAsync(ann, token: token); } + await db.BulkCopyAsync(detections, cancellationToken: token); return ann; }); diff --git a/Azaion.Common/Services/GalleryService.cs b/Azaion.Common/Services/GalleryService.cs index 5331e6b..82616de 100644 --- a/Azaion.Common/Services/GalleryService.cs +++ b/Azaion.Common/Services/GalleryService.cs @@ -73,7 +73,7 @@ public class GalleryService( await _updateLock.WaitAsync(); var existingAnnotations = new ConcurrentDictionary(await dbFactory.Run(async db => await db.Annotations.ToDictionaryAsync(x => x.Name))); - var missedAnnotations = new ConcurrentBag(); + var missedAnnotations = new ConcurrentDictionary(); try { var prefixLen = Constants.THUMBNAIL_PREFIX.Length; @@ -89,7 +89,7 @@ public class GalleryService( await ParallelExt.ForEachAsync(files, async (file, cancellationToken) => { - var fName = Path.GetFileNameWithoutExtension(file.Name); + var fName = file.Name.ToFName(); try { var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt"); @@ -136,7 +136,7 @@ public class GalleryService( { Time = time, OriginalMediaName = originalMediaName, - Name = file.Name.ToFName(), + Name = fName, ImageExtension = Path.GetExtension(file.Name), Detections = detections, CreatedDate = File.GetCreationTimeUtc(file.FullName), @@ -146,8 +146,15 @@ public class GalleryService( AnnotationStatus = AnnotationStatus.Validated }; + //Remove duplicates if (!existingAnnotations.ContainsKey(fName)) - missedAnnotations.Add(annotation); + { + if (missedAnnotations.ContainsKey(fName)) + Console.WriteLine($"{fName} is already exists! Duplicate!"); + else + missedAnnotations.TryAdd(fName, annotation); + } + if (!thumbnails.Contains(fName)) await CreateThumbnail(annotation, cancellationToken); @@ -181,10 +188,20 @@ public class GalleryService( { MaxBatchSize = 50 }; + + //Db could be updated during the long files scraping + existingAnnotations = new ConcurrentDictionary(await dbFactory.Run(async db => + await db.Annotations.ToDictionaryAsync(x => x.Name))); + var insertedDuplicates = missedAnnotations.Where(x => existingAnnotations.ContainsKey(x.Key)).ToList(); + var annotationsToInsert = missedAnnotations + .Where(a => !existingAnnotations.ContainsKey(a.Key)) + .Select(x => x.Value) + .ToList(); + await dbFactory.Run(async db => { - await db.BulkCopyAsync(copyOptions, missedAnnotations); - await db.BulkCopyAsync(copyOptions, missedAnnotations.SelectMany(x => x.Detections)); + await db.BulkCopyAsync(copyOptions, annotationsToInsert); + await db.BulkCopyAsync(copyOptions, annotationsToInsert.SelectMany(x => x.Detections)); }); dbFactory.SaveToDisk(); _updateLock.Release(); diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 03d69b8..09b41e4 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -1,6 +1,7 @@ using System.IO; using System.Windows; using System.Windows.Input; +using System.Windows.Threading; using Azaion.Common.DTO; using Azaion.Common.DTO.Queue; using Azaion.Common.Events; @@ -103,26 +104,28 @@ public class DatasetExplorerEventHandler( public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) { - var annotation = notification.Annotation; - var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber; - - //TODO: For editing existing need to handle updates - datasetExplorer.AddAnnotationToDict(annotation); - if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) + datasetExplorer.Dispatcher.Invoke(async () => { - var annThumb = new AnnotationThumbnail(annotation); - if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name)) - { - datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); - var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name); - if (ann != null) - datasetExplorer.SelectedAnnotations.Remove(ann); - } + var annotation = notification.Annotation; + var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber; - datasetExplorer.SelectedAnnotations.Insert(0, annThumb); - datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); - } - await Task.CompletedTask; + //TODO: For editing existing need to handle updates + datasetExplorer.AddAnnotationToDict(annotation); + if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) + { + var annThumb = new AnnotationThumbnail(annotation); + if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name)) + { + datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); + var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name); + if (ann != null) + datasetExplorer.SelectedAnnotations.Remove(ann); + } + + datasetExplorer.SelectedAnnotations.Insert(0, annThumb); + datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); + } + }); } public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index b86517e..8ca24e1 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -173,7 +173,9 @@ public partial class App services.AddSingleton(_resourceLoader); services.AddSingleton(_authProvider); services.AddSingleton(); - + services.AddSingleton(); + services.AddSingleton(); + services.AddHttpClient(); #endregion services.AddSingleton(); diff --git a/Azaion.Suite/postbuild.cmd b/Azaion.Suite/postbuild.cmd index da1b574..7423da5 100644 --- a/Azaion.Suite/postbuild.cmd +++ b/Azaion.Suite/postbuild.cmd @@ -9,7 +9,19 @@ call upload-file %FILE1_TO_UPLOAD% %RESOURCES_FOLDER% set FILE2_TO_UPLOAD=%cd%\..\Azaion.Dataset\bin\%CONFIG%\net8.0-windows\Azaion.Dataset.dll call upload-file %FILE2_TO_UPLOAD% %RESOURCES_FOLDER% -set DESTINATION=%cd%\bin\Debug\net8.0-windows\gps-denied +set SUITE_FOLDER=%cd%\bin\%CONFIG%\net8.0-windows\ + +rem Inference + +set INFERENCE_PATH=%cd%\..\Azaion.Inference +xcopy /E %INFERENCE_PATH%\dist\azaion-inference %SUITE_FOLDER% +copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvinfer_10.dll %SUITE_FOLDER% +copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvinfer_plugin_10.dll %SUITE_FOLDER% +copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvonnxparser_10.dll %SUITE_FOLDER% +copy %INFERENCE_PATH%\config.yaml %SUITE_FOLDER% + +rem Gps Denied +set DESTINATION=%SUITE_FOLDER%\gps-denied set GPS_DENIED=%cd%\..\..\gps-denied\ rmdir %DESTINATION% /s /q From 0237e279a57031cec71d57af532067e91b1607eb Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Thu, 17 Apr 2025 00:18:40 +0300 Subject: [PATCH 002/203] don't send user dto back --- Azaion.Inference/main.pyx | 1 - 1 file changed, 1 deletion(-) diff --git a/Azaion.Inference/main.pyx b/Azaion.Inference/main.pyx index 592ad0c..acf5fe2 100644 --- a/Azaion.Inference/main.pyx +++ b/Azaion.Inference/main.pyx @@ -62,7 +62,6 @@ cdef class CommandProcessor: cdef User user self.api_client.set_credentials(Credentials.from_msgpack(command.data)) user = self.api_client.get_user() - self.remote_handler.send(command.client_id, user.serialize()) cdef load_file(self, RemoteCommand command): cdef FileData file_data = FileData.from_msgpack(command.data) From 0c66607ed7768cced91892a0a9a1c50b4c06ebe8 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Thu, 17 Apr 2025 01:19:48 +0300 Subject: [PATCH 003/203] failsafe load dlls add user config queue offsets throttle improvements --- Azaion.Annotator/Annotator.xaml.cs | 4 +- Azaion.Annotator/AnnotatorEventHandler.cs | 4 +- Azaion.Common/Constants.cs | 55 ++++++-- Azaion.Common/DTO/Config/AppConfig.cs | 25 +--- Azaion.Common/Extensions/ColorExtensions.cs | 3 + .../Extensions/ThrottleExtensions.cs | 57 ++------- Azaion.Common/Services/AnnotationService.cs | 33 +++-- Azaion.Common/Services/GPSMatcherService.cs | 1 + Azaion.Common/Services/InferenceService.cs | 3 +- Azaion.Common/Services/SatelliteDownloader.cs | 1 + .../Azaion.CommonSecurity.csproj | 2 + .../DTO/BusinessExceptionDto.cs | 7 ++ .../DTO}/DirectoriesConfig.cs | 4 +- .../DTO/ExternalClientsConfig.cs | 5 +- Azaion.CommonSecurity/DTO/HardwareInfo.cs | 2 - Azaion.CommonSecurity/DTO/SecureAppConfig.cs | 1 + Azaion.CommonSecurity/DTO/User.cs | 22 +++- Azaion.CommonSecurity/SecurityConstants.cs | 17 ++- .../Services/AuthProvider.cs | 117 ++++++++++++++++-- Azaion.CommonSecurity/Services/Cache.cs | 27 ++++ .../Services/HardwareService.cs | 29 ++--- .../Services/InferenceClient.cs | 8 +- .../Services/ResourceLoader.cs | 2 +- Azaion.Dataset/DatasetExplorer.xaml.cs | 1 + Azaion.Inference/azaion-inference.spec | 2 + Azaion.Suite/App.xaml.cs | 54 +++++--- Azaion.Suite/MainSuite.xaml.cs | 9 +- Azaion.Suite/config.json | 4 +- Azaion.Suite/config.production.json | 4 +- Azaion.Suite/config.system.json | 2 +- Azaion.Suite/postbuild.cmd | 2 +- Azaion.Test/GetTilesTest.cs | 1 + 32 files changed, 320 insertions(+), 188 deletions(-) create mode 100644 Azaion.CommonSecurity/DTO/BusinessExceptionDto.cs rename {Azaion.Common/DTO/Config => Azaion.CommonSecurity/DTO}/DirectoriesConfig.cs (80%) create mode 100644 Azaion.CommonSecurity/Services/Cache.cs diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 9ba1775..3b4b39b 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -261,11 +261,11 @@ public partial class Annotator _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; - await ThrottleExt.ThrottleRunFirst(() => + await ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; - }, SaveConfigTaskId, TimeSpan.FromSeconds(5)); + }, TimeSpan.FromSeconds(5)); } private void ShowTimeAnnotations(TimeSpan time) diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 28deefa..e0961e8 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -1,15 +1,13 @@ using System.IO; -using System.Reflection.Metadata; using System.Windows; using System.Windows.Input; using Azaion.Annotator.DTO; using Azaion.Common; using Azaion.Common.DTO; -using Azaion.Common.DTO.Config; -using Azaion.Common.DTO.Queue; using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.Common.Services; +using Azaion.CommonSecurity.DTO; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Logging; diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 1237b9a..917f3b1 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -1,5 +1,9 @@ using System.Windows; +using System.Windows.Media; +using Azaion.Common.Database; using Azaion.Common.DTO; +using Azaion.Common.DTO.Config; +using Azaion.Common.Extensions; namespace Azaion.Common; @@ -21,20 +25,32 @@ public class Constants #region AnnotatorConfig + public static readonly AnnotationConfig DefaultAnnotationConfig = new() + { + DetectionClasses = DefaultAnnotationClasses, + VideoFormats = DefaultVideoFormats, + ImageFormats = DefaultImageFormats, + AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE + }; + public static readonly List DefaultAnnotationClasses = [ - new() { Id = 0, Name = "Броньована техніка", ShortName = "Бронь" }, - new() { Id = 1, Name = "Вантажівка", ShortName = "Вантаж" }, - new() { Id = 2, Name = "Машина легкова", ShortName = "Машина" }, - new() { Id = 3, Name = "Артилерія", ShortName = "Арта" }, - new() { Id = 4, Name = "Тінь від техніки", ShortName = "Тінь" }, - new() { Id = 5, Name = "Окопи", ShortName = "Окопи" }, - new() { Id = 6, Name = "Військовий", ShortName = "Військов" }, - new() { Id = 7, Name = "Накати", ShortName = "Накати" }, - new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" }, - new() { Id = 9, Name = "Дим", ShortName = "Дим" }, - new() { Id = 10, Name = "Літак", ShortName = "Літак" }, - new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" } + new() { Id = 0, Name = "ArmorVehicle", ShortName = "Броня", Color = "#FF0000".ToColor() }, + new() { Id = 1, Name = "Truck", ShortName = "Вантаж.", Color = "#00FF00".ToColor() }, + new() { Id = 2, Name = "Vehicle", ShortName = "Машина", Color = "#0000FF".ToColor() }, + new() { Id = 3, Name = "Atillery", ShortName = "Арта", Color = "#FFFF00".ToColor() }, + new() { Id = 4, Name = "Shadow", ShortName = "Тінь", Color = "#FF00FF".ToColor() }, + new() { Id = 5, Name = "Trenches", ShortName = "Окопи", Color = "#00FFFF".ToColor() }, + new() { Id = 6, Name = "MilitaryMan", ShortName = "Військов", Color = "#188021".ToColor() }, + new() { Id = 7, Name = "TyreTracks", ShortName = "Накати", Color = "#800000".ToColor() }, + new() { Id = 8, Name = "AdditArmoredTank", ShortName = "Танк.захист", Color = "#008000".ToColor() }, + new() { Id = 9, Name = "Smoke", ShortName = "Дим", Color = "#000080".ToColor() }, + new() { Id = 10, Name = "Plane", ShortName = "Літак", Color = "#000080".ToColor() }, + new() { Id = 11, Name = "Moto", ShortName = "Мото", Color = "#808000".ToColor() }, + new() { Id = 12, Name = "CamouflageNet", ShortName = "Сітка", Color = "#800080".ToColor() }, + new() { Id = 13, Name = "CamouflageBranches", ShortName = "Гілки", Color = "#2f4f4f".ToColor() }, + new() { Id = 14, Name = "Roof", ShortName = "Дах", Color = "#1e90ff".ToColor() }, + new() { Id = 15, Name = "Building", ShortName = "Будівля", Color = "#ffb6c1".ToColor() } ]; public static readonly List DefaultVideoFormats = ["mp4", "mov", "avi"]; @@ -49,6 +65,15 @@ public class Constants # region AIRecognitionConfig + public static readonly AIRecognitionConfig DefaultAIRecognitionConfig = new() + { + FrameRecognitionSeconds = DEFAULT_FRAME_RECOGNITION_SECONDS, + TrackingDistanceConfidence = TRACKING_DISTANCE_CONFIDENCE, + TrackingProbabilityIncrease = TRACKING_PROBABILITY_INCREASE, + TrackingIntersectionThreshold = TRACKING_INTERSECTION_THRESHOLD, + FramePeriodRecognition = DEFAULT_FRAME_PERIOD_RECOGNITION + }; + public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2; public const double TRACKING_DISTANCE_CONFIDENCE = 0.15; public const double TRACKING_PROBABILITY_INCREASE = 15; @@ -60,6 +85,12 @@ public class Constants #region Thumbnails + public static readonly ThumbnailConfig DefaultThumbnailConfig = new() + { + Size = DefaultThumbnailSize, + Border = DEFAULT_THUMBNAIL_BORDER + }; + public static readonly Size DefaultThumbnailSize = new(240, 135); public const int DEFAULT_THUMBNAIL_BORDER = 10; diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index 701170e..b03f988 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -45,14 +45,7 @@ public class ConfigUpdater : IConfigUpdater var appConfig = new AppConfig { - AnnotationConfig = new AnnotationConfig - { - DetectionClasses = Constants.DefaultAnnotationClasses, - VideoFormats = Constants.DefaultVideoFormats, - ImageFormats = Constants.DefaultImageFormats, - - AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE - }, + AnnotationConfig = Constants.DefaultAnnotationConfig, UIConfig = new UIConfig { @@ -72,20 +65,8 @@ public class ConfigUpdater : IConfigUpdater GpsRouteDirectory = Constants.DEFAULT_GPS_ROUTE_DIRECTORY }, - ThumbnailConfig = new ThumbnailConfig - { - Size = Constants.DefaultThumbnailSize, - Border = Constants.DEFAULT_THUMBNAIL_BORDER - }, - - AIRecognitionConfig = new AIRecognitionConfig - { - FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS, - TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE, - TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE, - TrackingIntersectionThreshold = Constants.TRACKING_INTERSECTION_THRESHOLD, - FramePeriodRecognition = Constants.DEFAULT_FRAME_PERIOD_RECOGNITION - } + ThumbnailConfig = Constants.DefaultThumbnailConfig, + AIRecognitionConfig = Constants.DefaultAIRecognitionConfig }; Save(appConfig); } diff --git a/Azaion.Common/Extensions/ColorExtensions.cs b/Azaion.Common/Extensions/ColorExtensions.cs index 30839c9..76e56d6 100644 --- a/Azaion.Common/Extensions/ColorExtensions.cs +++ b/Azaion.Common/Extensions/ColorExtensions.cs @@ -12,4 +12,7 @@ public static class ColorExtensions color.A = (byte)(MIN_ALPHA + (int)Math.Round(confidence * (MAX_ALPHA - MIN_ALPHA))); return color; } + + public static Color ToColor(this string hexColor) => + (Color)ColorConverter.ConvertFromString(hexColor); } \ No newline at end of file diff --git a/Azaion.Common/Extensions/ThrottleExtensions.cs b/Azaion.Common/Extensions/ThrottleExtensions.cs index 72cd29e..b6c12e7 100644 --- a/Azaion.Common/Extensions/ThrottleExtensions.cs +++ b/Azaion.Common/Extensions/ThrottleExtensions.cs @@ -1,57 +1,22 @@ -using System.Collections.Concurrent; - -namespace Azaion.Common.Extensions; +namespace Azaion.Common.Extensions; public static class ThrottleExt { - private static ConcurrentDictionary _taskStates = new(); + private static readonly Dictionary LastExecution = new(); + private static readonly object Lock = new(); - public static async Task ThrottleRunFirst(Func func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default) + public static async Task Throttle(this Func func, TimeSpan interval, CancellationToken ct = default) { - if (_taskStates.ContainsKey(actionId) && _taskStates[actionId]) - return; + ArgumentNullException.ThrowIfNull(func); - _taskStates[actionId] = true; - try + lock (Lock) { - await func(); + if (LastExecution.ContainsKey(func) && DateTime.UtcNow - LastExecution[func] < interval) + return; + + func(); + LastExecution[func] = DateTime.UtcNow; } - catch (Exception e) - { - Console.WriteLine(e); - } - - _ = Task.Run(async () => - { - await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); - _taskStates[actionId] = false; - }, cancellationToken); - } - - public static async Task ThrottleRunAfter(Func func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default) - { - if (_taskStates.ContainsKey(actionId) && _taskStates[actionId]) - return; - - _taskStates[actionId] = true; - _ = Task.Run(async () => - { - try - { - await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); - await func(); - } - catch (Exception) - { - _taskStates[actionId] = false; - } - finally - { - _taskStates[actionId] = false; - } - - }, cancellationToken); - await Task.CompletedTask; } } \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 30bad22..0f45792 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -26,15 +26,13 @@ public class AnnotationService : INotificationHandler private readonly FailsafeAnnotationsProducer _producer; private readonly IGalleryService _galleryService; private readonly IMediator _mediator; - private readonly IHardwareService _hardwareService; - private readonly IAuthProvider _authProvider; + private readonly IAzaionApi _api; private readonly QueueConfig _queueConfig; private Consumer _consumer = null!; private readonly UIConfig _uiConfig; private static readonly Guid SaveTaskId = Guid.NewGuid(); public AnnotationService( - IResourceLoader resourceLoader, IDbFactory dbFactory, FailsafeAnnotationsProducer producer, IOptions queueConfig, @@ -42,14 +40,13 @@ public class AnnotationService : INotificationHandler IGalleryService galleryService, IMediator mediator, IHardwareService hardwareService, - IAuthProvider authProvider) + IAzaionApi api) { _dbFactory = dbFactory; _producer = producer; _galleryService = galleryService; _mediator = mediator; - _hardwareService = hardwareService; - _authProvider = authProvider; + _api = api; _queueConfig = queueConfig.Value; _uiConfig = uiConfig.Value; @@ -58,7 +55,7 @@ public class AnnotationService : INotificationHandler private async Task Init(CancellationToken cancellationToken = default) { - if (!_authProvider.CurrentUser.Role.IsValidator()) + if (!_api.CurrentUser.Role.IsValidator()) return; var consumerSystem = await StreamSystem.Create(new StreamSystemConfig @@ -68,13 +65,11 @@ public class AnnotationService : INotificationHandler Password = _queueConfig.ConsumerPassword }); - var offset = (await _dbFactory.Run(db => db.QueueOffsets.FirstOrDefaultAsync( - x => x.QueueName == Constants.MQ_ANNOTATIONS_QUEUE, token: cancellationToken)) - )?.Offset ?? 0; + var offset = (ulong)(_api.CurrentUser.UserConfig?.QueueConfig?.AnnotationsOffset ?? 0); _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) { - Reference = _hardwareService.GetHardware().Hash, + Reference = _api.CurrentUser.Email, OffsetSpec = new OffsetTypeOffset(offset + 1), MessageHandler = async (_, _, context, message) => { @@ -84,13 +79,13 @@ public class AnnotationService : INotificationHandler .Set(x => x.Offset, context.Offset) .UpdateAsync(token: cancellationToken)); - await ThrottleExt.ThrottleRunAfter(() => + await ThrottleExt.Throttle(() => { _dbFactory.SaveToDisk(); return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), cancellationToken); + }, TimeSpan.FromSeconds(10), cancellationToken); - if (msg.CreatedEmail == _authProvider.CurrentUser.Email) //Don't process messages by yourself + if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself return; await SaveAnnotationInner( @@ -114,18 +109,18 @@ public class AnnotationService : INotificationHandler { a.Time = TimeSpan.FromMilliseconds(a.Milliseconds); return await SaveAnnotationInner(DateTime.Now, a.OriginalMediaName, a.Time, a.Detections.ToList(), - SourceEnum.AI, new MemoryStream(a.Image), _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, generateThumbnail: true, token: ct); + SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, generateThumbnail: true, token: ct); } //Manual public async Task SaveAnnotation(string originalMediaName, TimeSpan time, List detections, Stream? stream = null, CancellationToken token = default) => await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream, - _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, generateThumbnail: true, token: token); + _api.CurrentUser.Role, _api.CurrentUser.Email, generateThumbnail: true, token: token); //Manual Validate existing public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) => await SaveAnnotationInner(DateTime.UtcNow, annotation.OriginalMediaName, annotation.Time, annotation.Detections.ToList(), SourceEnum.Manual, null, - _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, token: token); + _api.CurrentUser.Role, _api.CurrentUser.Email, token: token); // Manual save from Validators -> Validated -> stream: azaion-annotations-confirm // AI, Manual save from Operators -> Created -> stream: azaion-annotations @@ -199,11 +194,11 @@ public class AnnotationService : INotificationHandler await _producer.SendToInnerQueue(annotation, token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); - await ThrottleExt.ThrottleRunAfter(() => + await ThrottleExt.Throttle(() => { _dbFactory.SaveToDisk(); return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), token); + }, TimeSpan.FromSeconds(5), token); return annotation; } diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index cd803d6..03c4370 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -3,6 +3,7 @@ using System.IO; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.CommonSecurity; +using Azaion.CommonSecurity.DTO; using Microsoft.Extensions.Options; namespace Azaion.Common.Services; diff --git a/Azaion.Common/Services/InferenceService.cs b/Azaion.Common/Services/InferenceService.cs index c66baff..3ce4ef6 100644 --- a/Azaion.Common/Services/InferenceService.cs +++ b/Azaion.Common/Services/InferenceService.cs @@ -17,10 +17,11 @@ public interface IInferenceService void StopInference(); } -public class InferenceService(ILogger logger, IInferenceClient client, IOptions aiConfigOptions) : IInferenceService +public class InferenceService(ILogger logger, IInferenceClient client, IAzaionApi azaionApi, IOptions aiConfigOptions) : IInferenceService { public async Task RunInference(List mediaPaths, Func processAnnotation, CancellationToken detectToken = default) { + client.Send(RemoteCommand.Create(CommandType.Login, azaionApi.Credentials)); var aiConfig = aiConfigOptions.Value; aiConfig.Paths = mediaPaths; diff --git a/Azaion.Common/Services/SatelliteDownloader.cs b/Azaion.Common/Services/SatelliteDownloader.cs index 4660820..7c7d0be 100644 --- a/Azaion.Common/Services/SatelliteDownloader.cs +++ b/Azaion.Common/Services/SatelliteDownloader.cs @@ -7,6 +7,7 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Extensions; using Azaion.CommonSecurity; +using Azaion.CommonSecurity.DTO; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; diff --git a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj index 4e4f70d..88fbb9f 100644 --- a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj +++ b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj @@ -7,9 +7,11 @@ + + diff --git a/Azaion.CommonSecurity/DTO/BusinessExceptionDto.cs b/Azaion.CommonSecurity/DTO/BusinessExceptionDto.cs new file mode 100644 index 0000000..5315856 --- /dev/null +++ b/Azaion.CommonSecurity/DTO/BusinessExceptionDto.cs @@ -0,0 +1,7 @@ +namespace Azaion.CommonSecurity.DTO; + +internal class BusinessExceptionDto +{ + public int ErrorCode { get; set; } + public string Message { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/DirectoriesConfig.cs b/Azaion.CommonSecurity/DTO/DirectoriesConfig.cs similarity index 80% rename from Azaion.Common/DTO/Config/DirectoriesConfig.cs rename to Azaion.CommonSecurity/DTO/DirectoriesConfig.cs index b36a57b..5e84edd 100644 --- a/Azaion.Common/DTO/Config/DirectoriesConfig.cs +++ b/Azaion.CommonSecurity/DTO/DirectoriesConfig.cs @@ -1,7 +1,9 @@ -namespace Azaion.Common.DTO.Config; +namespace Azaion.CommonSecurity.DTO; public class DirectoriesConfig { + public string ApiResourcesDirectory { get; set; } = null!; + public string VideosDirectory { get; set; } = null!; public string LabelsDirectory { get; set; } = null!; public string ImagesDirectory { get; set; } = null!; diff --git a/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs b/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs index aaf7d64..691a922 100644 --- a/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs +++ b/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs @@ -8,10 +8,7 @@ public abstract class ExternalClientConfig public int RetryCount {get;set;} } -public class InferenceClientConfig : ExternalClientConfig -{ - public string ResourcesFolder { get; set; } = ""; -} +public class InferenceClientConfig : ExternalClientConfig; public class GpsDeniedClientConfig : ExternalClientConfig { diff --git a/Azaion.CommonSecurity/DTO/HardwareInfo.cs b/Azaion.CommonSecurity/DTO/HardwareInfo.cs index 292c212..4b87a34 100644 --- a/Azaion.CommonSecurity/DTO/HardwareInfo.cs +++ b/Azaion.CommonSecurity/DTO/HardwareInfo.cs @@ -6,6 +6,4 @@ public class HardwareInfo public string GPU { get; set; } = null!; public string MacAddress { get; set; } = null!; public string Memory { get; set; } = null!; - - public string Hash { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/SecureAppConfig.cs b/Azaion.CommonSecurity/DTO/SecureAppConfig.cs index 80ea07f..a40d736 100644 --- a/Azaion.CommonSecurity/DTO/SecureAppConfig.cs +++ b/Azaion.CommonSecurity/DTO/SecureAppConfig.cs @@ -4,4 +4,5 @@ public class SecureAppConfig { public InferenceClientConfig InferenceClientConfig { get; set; } = null!; public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!; + public DirectoriesConfig DirectoriesConfig { get; set; } = null!; } \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/User.cs b/Azaion.CommonSecurity/DTO/User.cs index bb2a2ba..9f6add9 100644 --- a/Azaion.CommonSecurity/DTO/User.cs +++ b/Azaion.CommonSecurity/DTO/User.cs @@ -1,11 +1,21 @@ -using MessagePack; - namespace Azaion.CommonSecurity.DTO; -[MessagePackObject] public class User { - [Key("i")] public string Id { get; set; } = ""; - [Key("e")] public string Email { get; set; } = ""; - [Key("r")]public RoleEnum Role { get; set; } + public string Id { get; set; } = ""; + public string Email { get; set; } = ""; + public RoleEnum Role { get; set; } + public UserConfig? UserConfig { get; set; } = null!; +} + +public class UserConfig +{ + public UserQueueOffsets? QueueConfig { get; set; } = new(); +} + +public class UserQueueOffsets +{ + public int AnnotationsOffset { get; set; } + public int AnnotationsConfirmOffset { get; set; } + public int AnnotationsCommandsOffset { get; set; } } \ No newline at end of file diff --git a/Azaion.CommonSecurity/SecurityConstants.cs b/Azaion.CommonSecurity/SecurityConstants.cs index da0bffc..2498753 100644 --- a/Azaion.CommonSecurity/SecurityConstants.cs +++ b/Azaion.CommonSecurity/SecurityConstants.cs @@ -9,6 +9,9 @@ public class SecurityConstants public const string DUMMY_DIR = "dummy"; #region ExternalClientsConfig + //public const string API_URL = "http://localhost:5219"; + public const string API_URL = "https://api.azaion.com"; + public const string EXTERNAL_INFERENCE_PATH = "azaion-inference.exe"; public const string EXTERNAL_GPS_DENIED_FOLDER = "gps-denied"; public static readonly string ExternalGpsDeniedPath = Path.Combine(EXTERNAL_GPS_DENIED_FOLDER, "image-matcher.exe"); @@ -22,6 +25,13 @@ public class SecurityConstants public const int DEFAULT_RETRY_COUNT = 25; public const int DEFAULT_TIMEOUT_SECONDS = 5; + # region Cache keys + + public const string CURRENT_USER_CACHE_KEY = "CurrentUser"; + public const string HARDWARE_INFO_KEY = "HardwareInfo"; + + # endregion + public static readonly SecureAppConfig DefaultSecureAppConfig = new() { InferenceClientConfig = new InferenceClientConfig @@ -29,8 +39,7 @@ public class SecurityConstants ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST, ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT, OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS, - RetryCount = DEFAULT_RETRY_COUNT, - ResourcesFolder = "" + RetryCount = DEFAULT_RETRY_COUNT }, GpsDeniedClientConfig = new GpsDeniedClientConfig { @@ -38,6 +47,10 @@ public class SecurityConstants ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT, OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS, RetryCount = DEFAULT_RETRY_COUNT, + }, + DirectoriesConfig = new DirectoriesConfig + { + ApiResourcesDirectory = "" } }; #endregion ExternalClientsConfig diff --git a/Azaion.CommonSecurity/Services/AuthProvider.cs b/Azaion.CommonSecurity/Services/AuthProvider.cs index ebc5d0d..235a7e4 100644 --- a/Azaion.CommonSecurity/Services/AuthProvider.cs +++ b/Azaion.CommonSecurity/Services/AuthProvider.cs @@ -1,26 +1,117 @@ -using Azaion.CommonSecurity.DTO; -using Azaion.CommonSecurity.DTO.Commands; -using Microsoft.Extensions.DependencyInjection; +using System.Net; +using System.Net.Http.Headers; +using System.Text; +using Azaion.CommonSecurity.DTO; +using Newtonsoft.Json; namespace Azaion.CommonSecurity.Services; -public interface IAuthProvider +public interface IAzaionApi { - void Login(ApiCredentials credentials); + ApiCredentials Credentials { get; } User CurrentUser { get; } + T? Get(string url); + Stream GetResource(string filename); } -public class AuthProvider(IInferenceClient inferenceClient) : IAuthProvider +public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials, IHardwareService hardwareService) : IAzaionApi { - public User CurrentUser { get; private set; } = null!; + private string _jwtToken = null!; + const string APP_JSON = "application/json"; + public ApiCredentials Credentials => credentials; - public void Login(ApiCredentials credentials) + public User CurrentUser { - inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); - var user = inferenceClient.Get(); - if (user == null) - throw new Exception("Can't get user from Auth provider"); + get + { + var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY, + () => Get("currentUser")); + if (user == null) + throw new Exception("Can't get current user"); - CurrentUser = user; + return user; + } + + } + + private HttpResponseMessage Send(HttpRequestMessage request, CancellationToken ct = default) + { + if (string.IsNullOrEmpty(_jwtToken)) + Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken); + var response = client.Send(request); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken); + response = client.Send(request); + } + + if (response.IsSuccessStatusCode) + return response; + + var stream = response.Content.ReadAsStream(); + var content = new StreamReader(stream).ReadToEnd(); + if (response.StatusCode == HttpStatusCode.Conflict) + { + var result = JsonConvert.DeserializeObject(content); + throw new Exception($"Failed: {response.StatusCode}! Error Code: {result.ErrorCode}. Message: {result.Message}"); + } + throw new Exception($"Failed: {response.StatusCode}! Result: {content}"); + } + + public Stream GetResource(string filename) + { + var hardware = cache.GetFromCache(SecurityConstants.HARDWARE_INFO_KEY, hardwareService.GetHardware); + + var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{credentials.Folder}") + { + Content = new StringContent(JsonConvert.SerializeObject(new { filename, credentials.Password, hardware }), Encoding.UTF8, APP_JSON) + }); + return response.Content.ReadAsStream(); + } + + private void Authorize() + { + try + { + if (string.IsNullOrEmpty(credentials.Email) || credentials.Password.Length == 0) + throw new Exception("Email or password is empty! Please do EnterCredentials first!"); + + var payload = new + { + email = credentials.Email, + password = credentials.Password + }; + var content = new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, APP_JSON); + var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content }; + var response = client.Send(message); + + if (!response.IsSuccessStatusCode) + throw new Exception($"EnterCredentials failed: {response.StatusCode}"); + + var stream = response.Content.ReadAsStream(); + var json = new StreamReader(stream).ReadToEnd(); + var result = JsonConvert.DeserializeObject(json); + + if (string.IsNullOrEmpty(result?.Token)) + throw new Exception("JWT Token not found in response"); + + _jwtToken = result.Token; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public T? Get(string url) + { + var response = Send(new HttpRequestMessage(HttpMethod.Get, url)); + var stream = response.Content.ReadAsStream(); + var json = new StreamReader(stream).ReadToEnd(); + return JsonConvert.DeserializeObject(json); } } \ No newline at end of file diff --git a/Azaion.CommonSecurity/Services/Cache.cs b/Azaion.CommonSecurity/Services/Cache.cs new file mode 100644 index 0000000..b98d8a3 --- /dev/null +++ b/Azaion.CommonSecurity/Services/Cache.cs @@ -0,0 +1,27 @@ +using LazyCache; + +namespace Azaion.CommonSecurity.Services; + +public interface ICache +{ + T GetFromCache(string key, Func fetchFunc, TimeSpan? expiration = null); + void Invalidate(string key); +} + +public class MemoryCache : ICache +{ + private readonly IAppCache _cache = new CachingService(); + + public T GetFromCache(string key, Func fetchFunc, TimeSpan? expiration = null) + { + expiration ??= TimeSpan.FromHours(4); + return _cache.GetOrAdd(key, entry => + { + var result = fetchFunc(); + entry.AbsoluteExpirationRelativeToNow = expiration; + return result; + }); + } + + public void Invalidate(string key) => _cache.Remove(key); +} \ No newline at end of file diff --git a/Azaion.CommonSecurity/Services/HardwareService.cs b/Azaion.CommonSecurity/Services/HardwareService.cs index 3e1a815..e61cadf 100644 --- a/Azaion.CommonSecurity/Services/HardwareService.cs +++ b/Azaion.CommonSecurity/Services/HardwareService.cs @@ -38,29 +38,20 @@ public class HardwareService : IHardwareService .Replace("Name=", "") .Replace(" ", " ") .Trim() - .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); + .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries) + .Select(x => x.Trim()) + .ToArray(); - var memoryStr = "Unknown RAM"; - if (lines.Length > 0) - { - memoryStr = lines[0]; - if (int.TryParse(memoryStr, out var memKb)) - memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb"; - } + if (lines.Length < 3) + throw new Exception("Can't get hardware info"); - var macAddress = MacAddress(); var hardwareInfo = new HardwareInfo { - Memory = memoryStr, - CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1]) - ? "Unknown CPU" - : lines[1].Trim(), - GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2]) - ? "Unknown GPU" - : lines[2], - MacAddress = macAddress + CPU = lines[0], + GPU = lines[1], + Memory = lines[2], + MacAddress = GetMacAddress() }; - hardwareInfo.Hash = ToHash($"Az|{hardwareInfo.CPU}|{hardwareInfo.GPU}|{macAddress}"); return hardwareInfo; } catch (Exception ex) @@ -70,7 +61,7 @@ public class HardwareService : IHardwareService } } - private string MacAddress() + private string GetMacAddress() { var macAddress = NetworkInterface .GetAllNetworkInterfaces() diff --git a/Azaion.CommonSecurity/Services/InferenceClient.cs b/Azaion.CommonSecurity/Services/InferenceClient.cs index 401243a..7fc307f 100644 --- a/Azaion.CommonSecurity/Services/InferenceClient.cs +++ b/Azaion.CommonSecurity/Services/InferenceClient.cs @@ -71,9 +71,6 @@ public class InferenceClient : IInferenceClient _dealer.SendFrame(MessagePackSerializer.Serialize(command)); } - public void SendString(string text) => - Send(new RemoteCommand(CommandType.Load, MessagePackSerializer.Serialize(text))); - public T? Get(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class { var bytes = GetBytes(retries, tryTimeoutSeconds, ct); @@ -83,8 +80,9 @@ public class InferenceClient : IInferenceClient public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) { var tryNum = 0; - while (!ct.IsCancellationRequested && tryNum++ < retries) + while (!ct.IsCancellationRequested && tryNum < retries) { + tryNum++; if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes)) continue; @@ -92,7 +90,7 @@ public class InferenceClient : IInferenceClient } if (!ct.IsCancellationRequested) - throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each"); + throw new Exception($"Unable to get bytes after {tryNum - 1} retries, {tryTimeoutSeconds} seconds each"); return null; } diff --git a/Azaion.CommonSecurity/Services/ResourceLoader.cs b/Azaion.CommonSecurity/Services/ResourceLoader.cs index 7929ad1..38a4d70 100644 --- a/Azaion.CommonSecurity/Services/ResourceLoader.cs +++ b/Azaion.CommonSecurity/Services/ResourceLoader.cs @@ -13,7 +13,7 @@ public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERE public MemoryStream LoadFile(string fileName, string? folder = null) { inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder))); - var bytes = inferenceClient.GetBytes(); + var bytes = inferenceClient.GetBytes(2, 3); if (bytes == null) throw new Exception($"Unable to receive {fileName}"); diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 758eca1..a0e1e7f 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -7,6 +7,7 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Events; using Azaion.Common.Services; +using Azaion.CommonSecurity.DTO; using LinqToDB; using MediatR; using Microsoft.Extensions.Logging; diff --git a/Azaion.Inference/azaion-inference.spec b/Azaion.Inference/azaion-inference.spec index a01fda6..37f6901 100644 --- a/Azaion.Inference/azaion-inference.spec +++ b/Azaion.Inference/azaion-inference.spec @@ -4,6 +4,8 @@ from PyInstaller.utils.hooks import collect_all datas = [] binaries = [] hiddenimports = ['constants', 'annotation', 'credentials', 'file_data', 'user', 'security', 'secure_model', 'api_client', 'hardware_service', 'remote_command', 'ai_config', 'inference_engine', 'inference', 'remote_command_handler'] +tmp_ret = collect_all('pyyaml') +datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('jwt') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('requests') diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 8ca24e1..9561987 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -1,8 +1,12 @@ using System.IO; +using System.Net.Http; using System.Reflection; +using System.Text; +using System.Text.Unicode; using System.Windows; using System.Windows.Threading; using Azaion.Annotator; +using Azaion.Common; using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; @@ -11,8 +15,10 @@ using Azaion.Common.Extensions; using Azaion.Common.Services; using Azaion.CommonSecurity; using Azaion.CommonSecurity.DTO; +using Azaion.CommonSecurity.DTO.Commands; using Azaion.CommonSecurity.Services; using Azaion.Dataset; +using LazyCache; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Configuration; @@ -35,12 +41,14 @@ public partial class App private IInferenceClient _inferenceClient = null!; private IResourceLoader _resourceLoader = null!; - private IAuthProvider _authProvider = null!; - private Stream _securedConfig = null!; private Stream _systemConfig = null!; private static readonly Guid KeyPressTaskId = Guid.NewGuid(); + private readonly ICache _cache = new MemoryCache(); + private readonly IHardwareService _hardwareService = new HardwareService(); + private IAzaionApi _azaionApi = null!; + private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { _logger.LogError(e.Exception, e.Exception.Message); @@ -83,21 +91,32 @@ public partial class App var secureAppConfig = ReadSecureAppConfig(); _inferenceClient = new InferenceClient(new OptionsWrapper(secureAppConfig.InferenceClientConfig)); _resourceLoader = new ResourceLoader(_inferenceClient); - _authProvider = new AuthProvider(_inferenceClient); - var login = new Login(); - login.Closed += (sender, args) => - { - if (!login.MainSuiteOpened) - _inferenceClient.Stop(); - }; - login.CredentialsEntered += (_, credentials) => + login.CredentialsEntered += async (_, credentials) => { - credentials.Folder = secureAppConfig.InferenceClientConfig.ResourcesFolder; - _authProvider.Login(credentials); - _securedConfig = _resourceLoader.LoadFile("config.secured.json"); - _systemConfig = _resourceLoader.LoadFile("config.system.json"); + credentials.Folder = secureAppConfig.DirectoriesConfig.ApiResourcesDirectory; + + _inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); + _azaionApi = new AzaionApi(new HttpClient { BaseAddress = new Uri(SecurityConstants.API_URL) }, _cache, credentials, _hardwareService); + + try + { + _securedConfig = _resourceLoader.LoadFile("config.secured.json"); + _systemConfig = _resourceLoader.LoadFile("config.system.json"); + } + catch (Exception e) + { + Console.WriteLine(e); + _securedConfig = new MemoryStream("{}"u8.ToArray()); + var systemConfig = new + { + AnnotationConfig = Constants.DefaultAnnotationConfig, + AIRecognitionConfig = Constants.DefaultAIRecognitionConfig, + ThumbnailConfig = Constants.DefaultThumbnailConfig, + }; + _systemConfig = new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(systemConfig))); + } AppDomain.CurrentDomain.AssemblyResolve += (_, a) => { @@ -168,14 +187,13 @@ public partial class App services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); - services.AddSingleton(_inferenceClient); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(_resourceLoader); - services.AddSingleton(_authProvider); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddHttpClient(); + services.AddSingleton(_azaionApi); #endregion services.AddSingleton(); @@ -220,7 +238,7 @@ public partial class App { var args = (KeyEventArgs)e; var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow); - _ = ThrottleExt.ThrottleRunFirst(() => _mediator.Publish(keyEvent), KeyPressTaskId, TimeSpan.FromMilliseconds(50)); + _ = ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), TimeSpan.FromMilliseconds(50)); } protected override async void OnExit(ExitEventArgs e) diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs index c3854c1..14d527b 100644 --- a/Azaion.Suite/MainSuite.xaml.cs +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -25,7 +25,6 @@ public partial class MainSuite private readonly IGalleryService _galleryService; private readonly IDbFactory _dbFactory; private readonly Dictionary _openedWindows = new(); - private readonly IResourceLoader _resourceLoader; private readonly IInferenceClient _inferenceClient; private readonly IGpsMatcherClient _gpsMatcherClient; private static readonly Guid SaveConfigTaskId = Guid.NewGuid(); @@ -36,7 +35,6 @@ public partial class MainSuite IServiceProvider sp, IGalleryService galleryService, IDbFactory dbFactory, - IResourceLoader resourceLoader, IInferenceClient inferenceClient, IGpsMatcherClient gpsMatcherClient) { @@ -45,11 +43,10 @@ public partial class MainSuite _sp = sp; _galleryService = galleryService; _dbFactory = dbFactory; - _resourceLoader = resourceLoader; _inferenceClient = inferenceClient; _gpsMatcherClient = gpsMatcherClient; - _appConfig = appConfig.Value; + InitializeComponent(); Loaded += OnLoaded; Closed += OnFormClosed; @@ -135,11 +132,11 @@ public partial class MainSuite private async Task SaveUserSettings() { - await ThrottleExt.ThrottleRunFirst(() => + await ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; - }, SaveConfigTaskId, TimeSpan.FromSeconds(2)); + }, TimeSpan.FromSeconds(2)); } private void OnFormClosed(object? sender, EventArgs e) diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index da36fa7..3f0cdf1 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -3,8 +3,7 @@ "ZeroMqHost": "127.0.0.1", "ZeroMqPort": 5127, "RetryCount": 25, - "TimeoutSeconds": 5, - "ResourcesFolder": "stage" + "TimeoutSeconds": 5 }, "GpsDeniedClientConfig": { "ZeroMqHost": "127.0.0.1", @@ -14,6 +13,7 @@ "TimeoutSeconds": 5 }, "DirectoriesConfig": { + "ApiResourcesDirectory": "stage", "VideosDirectory": "E:\\Azaion6", "LabelsDirectory": "E:\\labels", "ImagesDirectory": "E:\\images", diff --git a/Azaion.Suite/config.production.json b/Azaion.Suite/config.production.json index dd879a7..b06b752 100644 --- a/Azaion.Suite/config.production.json +++ b/Azaion.Suite/config.production.json @@ -3,8 +3,7 @@ "ZeroMqHost": "127.0.0.1", "ZeroMqPort": 5131, "RetryCount": 25, - "TimeoutSeconds": 5, - "ResourcesFolder": "" + "TimeoutSeconds": 5 }, "GpsDeniedClientConfig": { "ZeroMqHost": "127.0.0.1", @@ -14,6 +13,7 @@ "TimeoutSeconds": 5 }, "DirectoriesConfig": { + "ApiResourcesDirectory": "", "VideosDirectory": "videos", "LabelsDirectory": "labels", "ImagesDirectory": "images", diff --git a/Azaion.Suite/config.system.json b/Azaion.Suite/config.system.json index bc78f06..a0beb77 100644 --- a/Azaion.Suite/config.system.json +++ b/Azaion.Suite/config.system.json @@ -13,7 +13,7 @@ { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#000080" }, { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, - { "Id": 12, "Name": "CamouflageNnet", "ShortName": "Сітка", "Color": "#800080" }, + { "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" } diff --git a/Azaion.Suite/postbuild.cmd b/Azaion.Suite/postbuild.cmd index 7423da5..93ca1b7 100644 --- a/Azaion.Suite/postbuild.cmd +++ b/Azaion.Suite/postbuild.cmd @@ -14,7 +14,7 @@ set SUITE_FOLDER=%cd%\bin\%CONFIG%\net8.0-windows\ rem Inference set INFERENCE_PATH=%cd%\..\Azaion.Inference -xcopy /E %INFERENCE_PATH%\dist\azaion-inference %SUITE_FOLDER% +xcopy /E /Y %INFERENCE_PATH%\dist\azaion-inference %SUITE_FOLDER% copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvinfer_10.dll %SUITE_FOLDER% copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvinfer_plugin_10.dll %SUITE_FOLDER% copy %INFERENCE_PATH%\venv\Lib\site-packages\tensorrt_libs\nvonnxparser_10.dll %SUITE_FOLDER% diff --git a/Azaion.Test/GetTilesTest.cs b/Azaion.Test/GetTilesTest.cs index d27e8d3..64e011f 100644 --- a/Azaion.Test/GetTilesTest.cs +++ b/Azaion.Test/GetTilesTest.cs @@ -1,5 +1,6 @@ using Azaion.Common.DTO.Config; using Azaion.Common.Services; +using Azaion.CommonSecurity.DTO; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; From 277aaf09b05786792ea6d7e6ba5b8d344b078d3d Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Thu, 17 Apr 2025 09:16:34 +0300 Subject: [PATCH 004/203] throttle reimplemented --- Azaion.Annotator/Annotator.xaml.cs | 4 +- .../Extensions/ThrottleExtensions.cs | 72 +++++++++++++++---- Azaion.Common/Services/AnnotationService.cs | 21 +++--- Azaion.CommonSecurity/DTO/User.cs | 8 +-- .../Services/AuthProvider.cs | 45 +++++++----- Azaion.Suite/App.xaml.cs | 4 +- Azaion.Suite/MainSuite.xaml.cs | 6 +- 7 files changed, 109 insertions(+), 51 deletions(-) diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 3b4b39b..e3b3f19 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -261,11 +261,11 @@ public partial class Annotator _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; - await ThrottleExt.Throttle(() => + ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; - }, TimeSpan.FromSeconds(5)); + }, SaveConfigTaskId, TimeSpan.FromSeconds(5)); } private void ShowTimeAnnotations(TimeSpan time) diff --git a/Azaion.Common/Extensions/ThrottleExtensions.cs b/Azaion.Common/Extensions/ThrottleExtensions.cs index b6c12e7..a210621 100644 --- a/Azaion.Common/Extensions/ThrottleExtensions.cs +++ b/Azaion.Common/Extensions/ThrottleExtensions.cs @@ -1,22 +1,70 @@ -namespace Azaion.Common.Extensions; +using System.Collections.Concurrent; + +namespace Azaion.Common.Extensions; public static class ThrottleExt { - private static readonly Dictionary LastExecution = new(); - private static readonly object Lock = new(); - - public static async Task Throttle(this Func func, TimeSpan interval, CancellationToken ct = default) + private class ThrottleState(Func action) { - ArgumentNullException.ThrowIfNull(func); + public Func Action { get; } = action ?? throw new ArgumentNullException(nameof(action)); + public bool IsCoolingDown = false; + public bool CallScheduledDuringCooldown = false; + public Task CooldownTask = Task.CompletedTask; + public readonly object StateLock = new(); + } - lock (Lock) + private static readonly ConcurrentDictionary ThrottlerStates = new(); + + public static void Throttle(Func action, Guid actionId, TimeSpan interval) + { + ArgumentNullException.ThrowIfNull(action); + if (actionId == Guid.Empty) + throw new ArgumentException("Throttle identifier cannot be empty.", nameof(actionId)); + if (interval <= TimeSpan.Zero) + throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive."); + + var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action)); + + lock (state.StateLock) { - if (LastExecution.ContainsKey(func) && DateTime.UtcNow - LastExecution[func] < interval) - return; - - func(); - LastExecution[func] = DateTime.UtcNow; + if (!state.IsCoolingDown) + { + state.IsCoolingDown = true; + state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state); + } + else + { + state.CallScheduledDuringCooldown = true; + } } } + private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state) + { + try + { + await state.Action(); + } + catch (Exception ex) + { + Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}"); + } + finally + { + await Task.Delay(interval); + + lock (state.StateLock) + { + if (state.CallScheduledDuringCooldown) + { + state.CallScheduledDuringCooldown = false; + state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state); + } + else + { + state.IsCoolingDown = false; + } + } + } + } } \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 0f45792..293ad4f 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -65,25 +65,22 @@ public class AnnotationService : INotificationHandler Password = _queueConfig.ConsumerPassword }); - var offset = (ulong)(_api.CurrentUser.UserConfig?.QueueConfig?.AnnotationsOffset ?? 0); + var offsets = _api.CurrentUser.UserConfig?.QueueOffsets ?? new UserQueueOffsets(); _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) { Reference = _api.CurrentUser.Email, - OffsetSpec = new OffsetTypeOffset(offset + 1), + OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset + 1), MessageHandler = async (_, _, context, message) => { var msg = MessagePackSerializer.Deserialize(message.Data.Contents); - await _dbFactory.Run(async db => await db.QueueOffsets - .Where(x => x.QueueName == Constants.MQ_ANNOTATIONS_QUEUE) - .Set(x => x.Offset, context.Offset) - .UpdateAsync(token: cancellationToken)); - await ThrottleExt.Throttle(() => + offsets.AnnotationsOffset = context.Offset; + ThrottleExt.Throttle(() => { - _dbFactory.SaveToDisk(); + _api.UpdateOffsets(offsets); return Task.CompletedTask; - }, TimeSpan.FromSeconds(10), cancellationToken); + }, SaveTaskId, TimeSpan.FromSeconds(10)); if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself return; @@ -194,11 +191,11 @@ public class AnnotationService : INotificationHandler await _producer.SendToInnerQueue(annotation, token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); - await ThrottleExt.Throttle(() => + ThrottleExt.Throttle(async () => { _dbFactory.SaveToDisk(); - return Task.CompletedTask; - }, TimeSpan.FromSeconds(5), token); + await Task.CompletedTask; + }, SaveTaskId, TimeSpan.FromSeconds(5)); return annotation; } diff --git a/Azaion.CommonSecurity/DTO/User.cs b/Azaion.CommonSecurity/DTO/User.cs index 9f6add9..2254574 100644 --- a/Azaion.CommonSecurity/DTO/User.cs +++ b/Azaion.CommonSecurity/DTO/User.cs @@ -10,12 +10,12 @@ public class User public class UserConfig { - public UserQueueOffsets? QueueConfig { get; set; } = new(); + public UserQueueOffsets? QueueOffsets { get; set; } = new(); } public class UserQueueOffsets { - public int AnnotationsOffset { get; set; } - public int AnnotationsConfirmOffset { get; set; } - public int AnnotationsCommandsOffset { get; set; } + public ulong AnnotationsOffset { get; set; } + public ulong AnnotationsConfirmOffset { get; set; } + public ulong AnnotationsCommandsOffset { get; set; } } \ No newline at end of file diff --git a/Azaion.CommonSecurity/Services/AuthProvider.cs b/Azaion.CommonSecurity/Services/AuthProvider.cs index 235a7e4..ca0ade1 100644 --- a/Azaion.CommonSecurity/Services/AuthProvider.cs +++ b/Azaion.CommonSecurity/Services/AuthProvider.cs @@ -10,7 +10,7 @@ public interface IAzaionApi { ApiCredentials Credentials { get; } User CurrentUser { get; } - T? Get(string url); + void UpdateOffsets(UserQueueOffsets offsets); Stream GetResource(string filename); } @@ -28,13 +28,27 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia () => Get("currentUser")); if (user == null) throw new Exception("Can't get current user"); - return user; } - } - private HttpResponseMessage Send(HttpRequestMessage request, CancellationToken ct = default) + public Stream GetResource(string filename) + { + var hardware = cache.GetFromCache(SecurityConstants.HARDWARE_INFO_KEY, hardwareService.GetHardware); + + var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{credentials.Folder}") + { + Content = new StringContent(JsonConvert.SerializeObject(new { filename, credentials.Password, hardware }), Encoding.UTF8, APP_JSON) + }); + return response.Content.ReadAsStream(); + } + + public void UpdateOffsets(UserQueueOffsets offsets) + { + Put($"/users/queue-offsets/{CurrentUser.Email}", offsets); + } + + private HttpResponseMessage Send(HttpRequestMessage request) { if (string.IsNullOrEmpty(_jwtToken)) Authorize(); @@ -61,15 +75,20 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia throw new Exception($"Failed: {response.StatusCode}! Result: {content}"); } - public Stream GetResource(string filename) + private T? Get(string url) { - var hardware = cache.GetFromCache(SecurityConstants.HARDWARE_INFO_KEY, hardwareService.GetHardware); + var response = Send(new HttpRequestMessage(HttpMethod.Get, url)); + var stream = response.Content.ReadAsStream(); + var json = new StreamReader(stream).ReadToEnd(); + return JsonConvert.DeserializeObject(json); + } - var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{credentials.Folder}") + private void Put(string url, T obj) + { + Send(new HttpRequestMessage(HttpMethod.Put, url) { - Content = new StringContent(JsonConvert.SerializeObject(new { filename, credentials.Password, hardware }), Encoding.UTF8, APP_JSON) + Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON) }); - return response.Content.ReadAsStream(); } private void Authorize() @@ -107,11 +126,5 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia } } - public T? Get(string url) - { - var response = Send(new HttpRequestMessage(HttpMethod.Get, url)); - var stream = response.Content.ReadAsStream(); - var json = new StreamReader(stream).ReadToEnd(); - return JsonConvert.DeserializeObject(json); - } + } \ No newline at end of file diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 9561987..85ddb8f 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -187,7 +187,7 @@ public partial class App services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); - services.AddSingleton(); + services.AddSingleton(_inferenceClient); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -238,7 +238,7 @@ public partial class App { var args = (KeyEventArgs)e; var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow); - _ = ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), TimeSpan.FromMilliseconds(50)); + ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), KeyPressTaskId, TimeSpan.FromMilliseconds(50)); } protected override async void OnExit(ExitEventArgs e) diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs index 14d527b..c377f00 100644 --- a/Azaion.Suite/MainSuite.xaml.cs +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -8,7 +8,6 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Extensions; using Azaion.Common.Services; -using Azaion.CommonSecurity; using Azaion.CommonSecurity.Services; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -130,13 +129,14 @@ public partial class MainSuite } } + private async Task SaveUserSettings() { - await ThrottleExt.Throttle(() => + ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; - }, TimeSpan.FromSeconds(2)); + }, SaveConfigTaskId, TimeSpan.FromSeconds(2)); } private void OnFormClosed(object? sender, EventArgs e) From d42409de7d91cf63bd4993c34bded8bbcba1e343 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Thu, 17 Apr 2025 19:40:09 +0300 Subject: [PATCH 005/203] fix throttle ext fix configs fix build scripts --- Azaion.Common/DTO/Config/AppConfig.cs | 14 +++--- .../Extensions/ThrottleExtensions.cs | 8 ++-- Azaion.Common/Services/AnnotationService.cs | 2 +- Azaion.Common/Services/GPSMatcherService.cs | 2 +- Azaion.Common/Services/SatelliteDownloader.cs | 45 ------------------- Azaion.Inference/azaion-inference.spec | 2 - Azaion.Suite/App.xaml.cs | 1 + Azaion.Suite/config.json | 4 +- build/build_downloader.cmd | 4 +- 9 files changed, 21 insertions(+), 61 deletions(-) diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index b03f988..7d94ab3 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -12,19 +12,19 @@ public class AppConfig public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!; - public QueueConfig QueueConfig { get; set; } = null!; + public QueueConfig QueueConfig { get; set; } = null!; - public DirectoriesConfig DirectoriesConfig { get; set; } = null!; + public DirectoriesConfig DirectoriesConfig { get; set; } = null!; - public AnnotationConfig AnnotationConfig { get; set; } = null!; + public AnnotationConfig AnnotationConfig { get; set; } = null!; - public UIConfig UIConfig { get; set; } = null!; + public UIConfig UIConfig { get; set; } = null!; - public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!; + public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!; - public ThumbnailConfig ThumbnailConfig { get; set; } = null!; + public ThumbnailConfig ThumbnailConfig { get; set; } = null!; - public MapConfig MapConfig{ get; set; } = null!; + public MapConfig MapConfig{ get; set; } = null!; } public interface IConfigUpdater diff --git a/Azaion.Common/Extensions/ThrottleExtensions.cs b/Azaion.Common/Extensions/ThrottleExtensions.cs index a210621..219374f 100644 --- a/Azaion.Common/Extensions/ThrottleExtensions.cs +++ b/Azaion.Common/Extensions/ThrottleExtensions.cs @@ -6,7 +6,7 @@ public static class ThrottleExt { private class ThrottleState(Func action) { - public Func Action { get; } = action ?? throw new ArgumentNullException(nameof(action)); + public Func Action { get; set; } = action ?? throw new ArgumentNullException(nameof(action)); public bool IsCoolingDown = false; public bool CallScheduledDuringCooldown = false; public Task CooldownTask = Task.CompletedTask; @@ -15,7 +15,7 @@ public static class ThrottleExt private static readonly ConcurrentDictionary ThrottlerStates = new(); - public static void Throttle(Func action, Guid actionId, TimeSpan interval) + public static void Throttle(Func action, Guid actionId, TimeSpan interval, bool scheduleCallAfterCooldown = false) { ArgumentNullException.ThrowIfNull(action); if (actionId == Guid.Empty) @@ -24,6 +24,7 @@ public static class ThrottleExt throw new ArgumentOutOfRangeException(nameof(interval), "Interval must be positive."); var state = ThrottlerStates.GetOrAdd(actionId, new ThrottleState(action)); + state.Action = action; lock (state.StateLock) { @@ -34,7 +35,8 @@ public static class ThrottleExt } else { - state.CallScheduledDuringCooldown = true; + if (scheduleCallAfterCooldown) + state.CallScheduledDuringCooldown = true; } } } diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 293ad4f..ab6fa21 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -80,7 +80,7 @@ public class AnnotationService : INotificationHandler { _api.UpdateOffsets(offsets); return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(10)); + }, SaveTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true); if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself return; diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index 03c4370..9883c5a 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -17,7 +17,7 @@ public interface IGpsMatcherService public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions dirConfig) : IGpsMatcherService { private const int ZOOM_LEVEL = 18; - private const int POINTS_COUNT = 10; + private const int POINTS_COUNT = 5; private const int DISTANCE_BETWEEN_POINTS_M = 100; private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1); diff --git a/Azaion.Common/Services/SatelliteDownloader.cs b/Azaion.Common/Services/SatelliteDownloader.cs index 7c7d0be..867980d 100644 --- a/Azaion.Common/Services/SatelliteDownloader.cs +++ b/Azaion.Common/Services/SatelliteDownloader.cs @@ -104,51 +104,6 @@ public class SatelliteDownloader( await Task.Run(() => Parallel.ForEach(cropTasks, action => action()), token); } - private async Task SplitToTiles_OLD(Image image, DownloadTilesResult bounds, CancellationToken token = default) - { - ArgumentNullException.ThrowIfNull(image); - ArgumentNullException.ThrowIfNull(bounds); - - if (bounds.LatMax <= bounds.LatMin || bounds.LonMax <= bounds.LonMin || image.Width <= 0 || image.Height <= 0) - throw new ArgumentException("Invalid coordinate bounds (LatMax <= LatMin or LonMax <= LonMin) or image dimensions (Width/Height <= 0)."); - - var latRange = bounds.LatMax - bounds.LatMin; - var lonRange = bounds.LonMax - bounds.LonMin; - var degreesPerPixelLat = latRange / image.Height; - var degreesPerPixelLon = lonRange / image.Width; - - var rowIndex = 0; - for (int top = 0; top <= image.Height - CROP_HEIGHT; top += STEP_Y) - { - token.ThrowIfCancellationRequested(); - int colIndex = 0; - for (int left = 0; left <= image.Width - CROP_WIDTH; left += STEP_X) - { - token.ThrowIfCancellationRequested(); - - var cropBox = new Rectangle(left, top, CROP_WIDTH, CROP_HEIGHT); - - using (var croppedImage = image.Clone(ctx => ctx.Crop(cropBox))) - { - var cropTlLat = bounds.LatMax - (top * degreesPerPixelLat); - var cropTlLon = bounds.LonMin + (left * degreesPerPixelLon); - var cropBrLat = cropTlLat - (CROP_HEIGHT * degreesPerPixelLat); - var cropBrLon = cropTlLon + (CROP_WIDTH * degreesPerPixelLon); - - var outputFilename = Path.Combine(_satDirectory, - $"map_{rowIndex:D4}_{colIndex:D4}_tl_{cropTlLat:F6}_{cropTlLon:F6}_br_{cropBrLat:F6}_{cropBrLon:F6}.tif" - ); - - using (var resizedImage = croppedImage.Clone(ctx => ctx.Resize(OUTPUT_TILE_SIZE, OUTPUT_TILE_SIZE, KnownResamplers.Lanczos3))) - await resizedImage.SaveAsTiffAsync(outputFilename, token); - } - colIndex++; - } - rowIndex++; - } - } - - private async Task?> ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default) { if (downloadedTiles.IsEmpty) diff --git a/Azaion.Inference/azaion-inference.spec b/Azaion.Inference/azaion-inference.spec index 37f6901..a01fda6 100644 --- a/Azaion.Inference/azaion-inference.spec +++ b/Azaion.Inference/azaion-inference.spec @@ -4,8 +4,6 @@ from PyInstaller.utils.hooks import collect_all datas = [] binaries = [] hiddenimports = ['constants', 'annotation', 'credentials', 'file_data', 'user', 'security', 'secure_model', 'api_client', 'hardware_service', 'remote_command', 'ai_config', 'inference_engine', 'inference', 'remote_command_handler'] -tmp_ret = collect_all('pyyaml') -datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('jwt') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('requests') diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 85ddb8f..7c41de7 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -182,6 +182,7 @@ public partial class App services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); #region External Services diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index 3f0cdf1..2fa431d 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -18,7 +18,9 @@ "LabelsDirectory": "E:\\labels", "ImagesDirectory": "E:\\images", "ResultsDirectory": "E:\\results", - "ThumbnailsDirectory": "E:\\thumbnails" + "ThumbnailsDirectory": "E:\\thumbnails", + "GpsSatDirectory": "satellitesDir", + "GpsRouteDirectory": "routeDir" }, "UIConfig": { "LeftPanelWidth": 220.0, diff --git a/build/build_downloader.cmd b/build/build_downloader.cmd index d8a0e03..94b0cd6 100644 --- a/build/build_downloader.cmd +++ b/build/build_downloader.cmd @@ -1,4 +1,6 @@ -pyinstaller --onefile --collect-all boto3 cdn_manager.py +python -m venv venv +venv\Scripts\pip install -r requirements.txt +venv\Scripts\pyinstaller --onefile --collect-all boto3 cdn_manager.py move dist\cdn_manager.exe .\cdn_manager.exe rmdir /s /q dist rmdir /s /q build From 70148bdfdf10cb56b9f728dbffaf0c9743529a68 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Sat, 19 Apr 2025 07:20:10 +0300 Subject: [PATCH 006/203] fix config and installer --- Azaion.Suite/config.production.json | 4 +++- build/cdn_manager.py | 10 ---------- build/installer.iss | 4 ++-- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/Azaion.Suite/config.production.json b/Azaion.Suite/config.production.json index b06b752..d8e778a 100644 --- a/Azaion.Suite/config.production.json +++ b/Azaion.Suite/config.production.json @@ -18,7 +18,9 @@ "LabelsDirectory": "labels", "ImagesDirectory": "images", "ResultsDirectory": "results", - "ThumbnailsDirectory": "thumbnails" + "ThumbnailsDirectory": "thumbnails", + "GpsSatDirectory": "satellitesDir", + "GpsRouteDirectory": "routeDir" }, "UIConfig": { "LeftPanelWidth": 170.0, diff --git a/build/cdn_manager.py b/build/cdn_manager.py index 60e820b..4b11d1b 100644 --- a/build/cdn_manager.py +++ b/build/cdn_manager.py @@ -34,16 +34,6 @@ class CDNManager: print(e) return False - def upload(self, bucket: str, filename: str, file_bytes: bytearray): - try: - self.upload_client.upload - self.upload_client.upload_fileobj(io.BytesIO(file_bytes), bucket, filename) - print(f'uploaded {filename} ({len(file_bytes)} bytes) to the {bucket}') - return True - except Exception as e: - print(e) - return False - def download(self, bucket: str, filename: str): try: self.download_client.download_file(bucket, filename, filename) diff --git a/build/installer.iss b/build/installer.iss index bbd2c70..6f40b9d 100644 --- a/build/installer.iss +++ b/build/installer.iss @@ -1,12 +1,12 @@ [Setup] AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} AppName=Azaion Suite -AppVersion=1.4.2 +AppVersion=1.4.3 AppPublisher=Azaion Ukraine DefaultDirName={localappdata}\Azaion\Azaion Suite DefaultGroupName=Azaion Suite OutputDir=..\ -OutputBaseFilename=AzaionSuite1.4.2 +OutputBaseFilename=AzaionSuite1.4.3 SetupIconFile=..\dist\logo.ico UninstallDisplayName=Azaion Suite UninstallDisplayIcon={app}\Azaion.Suite.exe From c68c29344821de220698d438868ece76ac5a8a77 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Mon, 21 Apr 2025 17:02:13 +0300 Subject: [PATCH 007/203] update validation logic --- Azaion.Common/DTO/AnnotationThumbnail.cs | 2 + Azaion.Common/Database/Annotation.cs | 3 ++ Azaion.Common/Services/AnnotationService.cs | 43 ++++++++++++------- Azaion.Dataset/DatasetExplorerEventHandler.cs | 11 ++++- Azaion.Suite/config.production.json | 3 +- 5 files changed, 43 insertions(+), 19 deletions(-) diff --git a/Azaion.Common/DTO/AnnotationThumbnail.cs b/Azaion.Common/DTO/AnnotationThumbnail.cs index 0fd7c8a..988a267 100644 --- a/Azaion.Common/DTO/AnnotationThumbnail.cs +++ b/Azaion.Common/DTO/AnnotationThumbnail.cs @@ -35,4 +35,6 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + public void UpdateUI() => OnPropertyChanged(nameof(IsSeed)); } \ No newline at end of file diff --git a/Azaion.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index b77e41e..5b5c04c 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -31,6 +31,9 @@ public class Annotation [IgnoreMember]public SourceEnum Source { get; set; } [IgnoreMember]public AnnotationStatus AnnotationStatus { get; set; } + [IgnoreMember]public DateTime ValidateDate { get; set; } + [IgnoreMember]public string ValidateEmail { get; set; } = null!; + [Key("d")] public IEnumerable Detections { get; set; } = null!; [Key("t")] public long Milliseconds { get; set; } diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index ab6fa21..8271588 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -94,7 +94,6 @@ public class AnnotationService : INotificationHandler new MemoryStream(msg.Image), msg.CreatedRole, msg.CreatedEmail, - generateThumbnail: true, fromQueue: true, token: cancellationToken); } @@ -106,17 +105,12 @@ public class AnnotationService : INotificationHandler { a.Time = TimeSpan.FromMilliseconds(a.Milliseconds); return await SaveAnnotationInner(DateTime.Now, a.OriginalMediaName, a.Time, a.Detections.ToList(), - SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, generateThumbnail: true, token: ct); + SourceEnum.AI, new MemoryStream(a.Image), _api.CurrentUser.Role, _api.CurrentUser.Email, token: ct); } //Manual public async Task SaveAnnotation(string originalMediaName, TimeSpan time, List detections, Stream? stream = null, CancellationToken token = default) => await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream, - _api.CurrentUser.Role, _api.CurrentUser.Email, generateThumbnail: true, token: token); - - //Manual Validate existing - public async Task ValidateAnnotation(Annotation annotation, CancellationToken token = default) => - await SaveAnnotationInner(DateTime.UtcNow, annotation.OriginalMediaName, annotation.Time, annotation.Detections.ToList(), SourceEnum.Manual, null, _api.CurrentUser.Role, _api.CurrentUser.Email, token: token); // Manual save from Validators -> Validated -> stream: azaion-annotations-confirm @@ -124,11 +118,9 @@ public class AnnotationService : INotificationHandler private async Task SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List detections, SourceEnum source, Stream? stream, RoleEnum userRole, string createdEmail, - bool generateThumbnail = false, bool fromQueue = false, CancellationToken token = default) { - AnnotationStatus status; var fName = originalMediaName.ToTimeName(time); var annotation = await _dbFactory.Run(async db => @@ -179,13 +171,10 @@ public class AnnotationService : INotificationHandler img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue } await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); - if (generateThumbnail) - { - await _galleryService.CreateThumbnail(annotation, token); - if (_uiConfig.GenerateAnnotatedImage) - await _galleryService.CreateAnnotatedImage(annotation, token); - } + await _galleryService.CreateThumbnail(annotation, token); + if (_uiConfig.GenerateAnnotatedImage) + await _galleryService.CreateAnnotatedImage(annotation, token); if (!fromQueue) //Send to queue only if we're not getting from queue already await _producer.SendToInnerQueue(annotation, token); @@ -195,10 +184,32 @@ public class AnnotationService : INotificationHandler { _dbFactory.SaveToDisk(); await Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5)); + }, SaveTaskId, TimeSpan.FromSeconds(5), true); return annotation; } + public async Task ValidateAnnotations(List annotations, CancellationToken token = default) + { + if (!_api.CurrentUser.Role.IsValidator()) + return; + + var annNames = annotations.Select(x => x.Name).ToHashSet(); + await _dbFactory.Run(async db => + { + await db.Annotations + .Where(x => annNames.Contains(x.Name)) + .Set(x => x.AnnotationStatus, AnnotationStatus.Validated) + .Set(x => x.ValidateDate, DateTime.UtcNow) + .Set(x => x.ValidateEmail, _api.CurrentUser.Email) + .UpdateAsync(token: token); + }); + ThrottleExt.Throttle(async () => + { + _dbFactory.SaveToDisk(); + await Task.CompletedTask; + }, SaveTaskId, TimeSpan.FromSeconds(5), true); + } + public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken); diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 09b41e4..de4b3c8 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -2,6 +2,7 @@ using System.Windows; using System.Windows.Input; using System.Windows.Threading; +using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Queue; using Azaion.Common.Events; @@ -96,8 +97,14 @@ public class DatasetExplorerEventHandler( var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast() .Select(x => x.Annotation) .ToList(); - foreach (var annotation in annotations) - await annotationService.ValidateAnnotation(annotation, cancellationToken); + await annotationService.ValidateAnnotations(annotations, cancellationToken); + foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation))) + { + ann.Annotation.AnnotationStatus = AnnotationStatus.Validated; + if (datasetExplorer.SelectedAnnotationDict.TryGetValue(ann.Annotation.Name, out var value)) + value.Annotation.AnnotationStatus = AnnotationStatus.Validated; + ann.UpdateUI(); + } break; } } diff --git a/Azaion.Suite/config.production.json b/Azaion.Suite/config.production.json index d8e778a..5c34cc2 100644 --- a/Azaion.Suite/config.production.json +++ b/Azaion.Suite/config.production.json @@ -24,6 +24,7 @@ }, "UIConfig": { "LeftPanelWidth": 170.0, - "RightPanelWidth": 120.0 + "RightPanelWidth": 120.0, + "GenerateAnnotatedImage": true } } \ No newline at end of file From e798af470b64c49465fd98bf81606cb4acf798f8 Mon Sep 17 00:00:00 2001 From: Alex Bezdieniezhnykh Date: Wed, 23 Apr 2025 23:20:08 +0300 Subject: [PATCH 008/203] read cdn yaml config from api automate tensorrt model conversion in case of no existing one for user's gpu --- .../Services/AuthProvider.cs | 6 +- .../Services/InferenceClient.cs | 2 +- Azaion.Inference/api_client.pxd | 9 +- Azaion.Inference/api_client.pyx | 64 +++++++++----- Azaion.Inference/build.cmd | 1 + Azaion.Inference/cdn_manager.pxd | 14 ++++ Azaion.Inference/cdn_manager.pyx | 41 +++++++++ Azaion.Inference/constants.pxd | 8 +- Azaion.Inference/constants.pyx | 9 +- Azaion.Inference/credentials.pxd | 1 - Azaion.Inference/credentials.pyx | 6 +- Azaion.Inference/inference.pxd | 4 + Azaion.Inference/inference.pyx | 42 ++++++++-- Azaion.Inference/inference_engine.pxd | 11 ++- Azaion.Inference/inference_engine.pyx | 84 ++++++++++++++----- Azaion.Inference/main.pyx | 5 +- Azaion.Inference/requirements.txt | 4 +- Azaion.Inference/setup.py | 1 + Azaion.Suite/App.xaml.cs | 9 +- Azaion.Suite/Login.xaml | 4 +- build/build_downloader.cmd | 7 -- build/cdn_manager.py | 22 ++++- build/publish.cmd | 4 +- 23 files changed, 265 insertions(+), 93 deletions(-) create mode 100644 Azaion.Inference/cdn_manager.pxd create mode 100644 Azaion.Inference/cdn_manager.pyx delete mode 100644 build/build_downloader.cmd diff --git a/Azaion.CommonSecurity/Services/AuthProvider.cs b/Azaion.CommonSecurity/Services/AuthProvider.cs index ca0ade1..c345927 100644 --- a/Azaion.CommonSecurity/Services/AuthProvider.cs +++ b/Azaion.CommonSecurity/Services/AuthProvider.cs @@ -11,7 +11,7 @@ public interface IAzaionApi ApiCredentials Credentials { get; } User CurrentUser { get; } void UpdateOffsets(UserQueueOffsets offsets); - Stream GetResource(string filename); + Stream GetResource(string filename, string folder); } public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials, IHardwareService hardwareService) : IAzaionApi @@ -32,11 +32,11 @@ public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentia } } - public Stream GetResource(string filename) + public Stream GetResource(string filename, string folder) { var hardware = cache.GetFromCache(SecurityConstants.HARDWARE_INFO_KEY, hardwareService.GetHardware); - var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{credentials.Folder}") + var response = Send(new HttpRequestMessage(HttpMethod.Post, $"/resources/get/{folder}") { Content = new StringContent(JsonConvert.SerializeObject(new { filename, credentials.Password, hardware }), Encoding.UTF8, APP_JSON) }); diff --git a/Azaion.CommonSecurity/Services/InferenceClient.cs b/Azaion.CommonSecurity/Services/InferenceClient.cs index 7fc307f..da75cff 100644 --- a/Azaion.CommonSecurity/Services/InferenceClient.cs +++ b/Azaion.CommonSecurity/Services/InferenceClient.cs @@ -45,7 +45,7 @@ public class InferenceClient : IInferenceClient 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.Inference/api_client.pxd b/Azaion.Inference/api_client.pxd index b53f75b..d05f072 100644 --- a/Azaion.Inference/api_client.pxd +++ b/Azaion.Inference/api_client.pxd @@ -1,9 +1,11 @@ from user cimport User from credentials cimport Credentials +from cdn_manager cimport CDNManager cdef class ApiClient: cdef Credentials credentials + cdef CDNManager cdn_manager cdef str token, folder, api_url cdef User user @@ -12,6 +14,7 @@ cdef class ApiClient: cdef set_token(self, str token) cdef get_user(self) - cdef load_bytes(self, str filename, str folder=*) - cdef upload_file(self, str filename, str folder=*) - cdef load_ai_model(self, bint is_tensor=*) + cdef load_bytes(self, str filename, str folder) + cdef upload_file(self, str filename, bytes resource, str folder) + cdef load_big_small_resource(self, str resource_name, str folder, str key) + cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key) diff --git a/Azaion.Inference/api_client.pyx b/Azaion.Inference/api_client.pyx index 6f3637d..c933ae5 100644 --- a/Azaion.Inference/api_client.pyx +++ b/Azaion.Inference/api_client.pyx @@ -1,12 +1,15 @@ import json from http import HTTPStatus +from os import path from uuid import UUID import jwt import requests cimport constants +import yaml + +from cdn_manager cimport CDNManager, CDNCredentials from hardware_service cimport HardwareService, HardwareInfo from security cimport Security -from io import BytesIO from user cimport User, RoleEnum cdef class ApiClient: @@ -15,9 +18,19 @@ cdef class ApiClient: self.credentials = None self.user = None self.token = None + self.cdn_manager = None cdef set_credentials(self, Credentials credentials): self.credentials = credentials + yaml_bytes = self.load_bytes(constants.CDN_CONFIG, '') + yaml_config = yaml.safe_load(yaml_bytes) + creds = CDNCredentials(yaml_config["host"], + yaml_config["downloader_access_key"], + yaml_config["downloader_access_secret"], + yaml_config["uploader_access_key"], + yaml_config["uploader_access_secret"]) + + self.cdn_manager = CDNManager(creds) cdef login(self): response = requests.post(f"{constants.API_URL}/login", @@ -57,13 +70,12 @@ cdef class ApiClient: self.login() return self.user - cdef upload_file(self, str filename, str folder=None): - folder = folder or self.credentials.folder + cdef upload_file(self, str filename, bytes resource, str folder): if self.token is None: self.login() url = f"{constants.API_URL}/resources/{folder}" headers = { "Authorization": f"Bearer {self.token}" } - files = dict(data=open(filename, 'rb')) + files = {'data': (filename, resource)} try: r = requests.post(url, headers=headers, files=files, allow_redirects=True) r.raise_for_status() @@ -71,9 +83,7 @@ cdef class ApiClient: except Exception as e: print(f"Upload fail: {e}") - cdef load_bytes(self, str filename, str folder=None): - folder = folder or self.credentials.folder - + cdef load_bytes(self, str filename, str folder): hardware_service = HardwareService() cdef HardwareInfo hardware = hardware_service.get_hardware_info() @@ -111,22 +121,30 @@ cdef class ApiClient: constants.log(f'Downloaded file: {filename}, {len(data)} bytes') return data - cdef load_ai_model(self, bint is_tensor=False): - if is_tensor: - big_file = constants.AI_TENSOR_MODEL_FILE_BIG - small_file = constants.AI_TENSOR_MODEL_FILE_SMALL - else: - big_file = constants.AI_ONNX_MODEL_FILE_BIG - small_file = constants.AI_ONNX_MODEL_FILE_SMALL + cdef load_big_small_resource(self, str resource_name, str folder, str key): + cdef str big_part = path.join(folder, f'{resource_name}.big') + cdef str small_part = f'{resource_name}.small' - with open(big_file, 'rb') as binary_file: + with open(big_part, 'rb') as binary_file: encrypted_bytes_big = binary_file.read() - print('read encrypted big file') - print(f'small file: {small_file}') - encrypted_bytes_small = self.load_bytes(small_file) - print('read encrypted small file') - encrypted_model_bytes = encrypted_bytes_small + encrypted_bytes_big - key = Security.get_model_encryption_key() - model_bytes = Security.decrypt_to(encrypted_model_bytes, key) - return model_bytes + encrypted_bytes_small = self.load_bytes(small_part, folder) + + encrypted_bytes = encrypted_bytes_small + encrypted_bytes_big + result = Security.decrypt_to(encrypted_bytes, key) + return result + + cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder, str key): + cdef str big_part_name = f'{resource_name}.big' + cdef str small_part_name = f'{resource_name}.small' + + resource_encrypted = Security.encrypt_to(resource, key) + part_small_size = min(constants.SMALL_SIZE_KB * 1024, int(0.3 * len(resource_encrypted))) + part_small = resource_encrypted[:part_small_size] # slice bytes for part1 + + part_big = resource_encrypted[part_small_size:] + + self.cdn_manager.upload(constants.MODELS_FOLDER, big_part_name, part_big) + with open(path.join(folder, big_part_name), 'wb') as f: + f.write(part_big) + self.upload_file(small_part_name, part_small, constants.MODELS_FOLDER) \ No newline at end of file diff --git a/Azaion.Inference/build.cmd b/Azaion.Inference/build.cmd index 80d5a8e..b9e6857 100644 --- a/Azaion.Inference/build.cmd +++ b/Azaion.Inference/build.cmd @@ -10,6 +10,7 @@ pyinstaller --name=azaion-inference ^ --collect-all onnxruntime ^ --collect-all tensorrt ^ --collect-all pycuda ^ +--collect-all pynvml ^ --collect-all re ^ --hidden-import constants ^ --hidden-import annotation ^ diff --git a/Azaion.Inference/cdn_manager.pxd b/Azaion.Inference/cdn_manager.pxd new file mode 100644 index 0000000..028c26d --- /dev/null +++ b/Azaion.Inference/cdn_manager.pxd @@ -0,0 +1,14 @@ +cdef class CDNCredentials: + cdef str host + cdef str downloader_access_key + cdef str downloader_access_secret + cdef str uploader_access_key + cdef str uploader_access_secret + +cdef class CDNManager: + cdef CDNCredentials creds + cdef object download_client + cdef object upload_client + + cdef upload(self, str bucket, str filename, bytes file_bytes) + cdef download(self, str bucket, str filename) \ No newline at end of file diff --git a/Azaion.Inference/cdn_manager.pyx b/Azaion.Inference/cdn_manager.pyx new file mode 100644 index 0000000..92b4d3f --- /dev/null +++ b/Azaion.Inference/cdn_manager.pyx @@ -0,0 +1,41 @@ +import io +import boto3 + + +cdef class CDNCredentials: + def __init__(self, host, downloader_access_key, downloader_access_secret, uploader_access_key, uploader_access_secret): + self.host = host + self.downloader_access_key = downloader_access_key + self.downloader_access_secret = downloader_access_secret + self.uploader_access_key = uploader_access_key + self.uploader_access_secret = uploader_access_secret + + +cdef class CDNManager: + def __init__(self, CDNCredentials credentials): + + self.creds = credentials + self.download_client = boto3.client('s3', endpoint_url=self.creds.host, + aws_access_key_id=self.creds.downloader_access_key, + aws_secret_access_key=self.creds.downloader_access_secret) + self.upload_client = boto3.client('s3', endpoint_url=self.creds.host, + aws_access_key_id=self.creds.uploader_access_key, + aws_secret_access_key=self.creds.uploader_access_secret) + + cdef upload(self, str bucket, str filename, bytes file_bytes): + try: + self.upload_client.upload_fileobj(io.BytesIO(file_bytes), bucket, filename) + print(f'uploaded {filename} ({len(file_bytes)} bytes) to the {bucket}') + return True + except Exception as e: + print(e) + return False + + cdef download(self, str bucket, str filename): + try: + self.download_client.download_file(bucket, filename, filename) + print(f'downloaded {filename} from the {bucket} to current folder') + return True + except Exception as e: + print(e) + return False diff --git a/Azaion.Inference/constants.pxd b/Azaion.Inference/constants.pxd index ebdae29..660c97e 100644 --- a/Azaion.Inference/constants.pxd +++ b/Azaion.Inference/constants.pxd @@ -7,12 +7,12 @@ cdef str ANNOTATIONS_QUEUE # Name of the annotations queue in rabbit cdef str API_URL # Base URL for the external API cdef str QUEUE_CONFIG_FILENAME # queue config filename to load from api -cdef str AI_ONNX_MODEL_FILE_BIG -cdef str AI_ONNX_MODEL_FILE_SMALL +cdef str AI_ONNX_MODEL_FILE -cdef str AI_TENSOR_MODEL_FILE_BIG -cdef str AI_TENSOR_MODEL_FILE_SMALL +cdef str CDN_CONFIG +cdef str MODELS_FOLDER +cdef int SMALL_SIZE_KB cdef bytes DONE_SIGNAL diff --git a/Azaion.Inference/constants.pyx b/Azaion.Inference/constants.pyx index ec4554a..a8f384c 100644 --- a/Azaion.Inference/constants.pyx +++ b/Azaion.Inference/constants.pyx @@ -9,11 +9,12 @@ cdef str ANNOTATIONS_QUEUE = "azaion-annotations" cdef str API_URL = "https://api.azaion.com" # Base URL for the external API cdef str QUEUE_CONFIG_FILENAME = "secured-config.json" -cdef str AI_ONNX_MODEL_FILE_BIG = "azaion.onnx.big" -cdef str AI_ONNX_MODEL_FILE_SMALL = "azaion.onnx.small" +cdef str AI_ONNX_MODEL_FILE = "azaion.onnx" -cdef str AI_TENSOR_MODEL_FILE_BIG = "azaion.engine.big" -cdef str AI_TENSOR_MODEL_FILE_SMALL = "azaion.engine.small" +cdef str CDN_CONFIG = "cdn.yaml" +cdef str MODELS_FOLDER = "models" + +cdef int SMALL_SIZE_KB = 3 cdef log(str log_message, bytes client_id=None): local_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) diff --git a/Azaion.Inference/credentials.pxd b/Azaion.Inference/credentials.pxd index cd6c090..74bc016 100644 --- a/Azaion.Inference/credentials.pxd +++ b/Azaion.Inference/credentials.pxd @@ -1,7 +1,6 @@ cdef class Credentials: cdef public str email cdef public str password - cdef public str folder @staticmethod cdef from_msgpack(bytes data) \ No newline at end of file diff --git a/Azaion.Inference/credentials.pyx b/Azaion.Inference/credentials.pyx index 2eb020c..ae0e17e 100644 --- a/Azaion.Inference/credentials.pyx +++ b/Azaion.Inference/credentials.pyx @@ -2,16 +2,14 @@ from msgpack import unpackb cdef class Credentials: - def __init__(self, str email, str password, str folder): + def __init__(self, str email, str password): self.email = email self.password = password - self.folder = folder @staticmethod cdef from_msgpack(bytes data): unpacked = unpackb(data, strict_map_key=False) return Credentials( unpacked.get("Email"), - unpacked.get("Password"), - unpacked.get("Folder")) + unpacked.get("Password")) diff --git a/Azaion.Inference/inference.pxd b/Azaion.Inference/inference.pxd index c85793c..ec2f2e0 100644 --- a/Azaion.Inference/inference.pxd +++ b/Azaion.Inference/inference.pxd @@ -17,7 +17,11 @@ cdef class Inference: cdef int model_width cdef int model_height + cdef build_tensor_engine(self) + cdef init_ai(self) + cdef bint is_building_engine cdef bint is_video(self, str filepath) + cdef run_inference(self, RemoteCommand cmd) cdef _process_video(self, RemoteCommand cmd, AIRecognitionConfig ai_config, str video_name) cdef _process_images(self, RemoteCommand cmd, AIRecognitionConfig ai_config, list[str] image_paths) diff --git a/Azaion.Inference/inference.pyx b/Azaion.Inference/inference.pyx index 9ebe14d..c32249a 100644 --- a/Azaion.Inference/inference.pyx +++ b/Azaion.Inference/inference.pyx @@ -1,14 +1,19 @@ import json import mimetypes +import os import subprocess +import time import cv2 import numpy as np + +cimport constants from remote_command cimport RemoteCommand from annotation cimport Detection, Annotation from ai_config cimport AIRecognitionConfig from inference_engine cimport OnnxEngine, TensorRTEngine from hardware_service cimport HardwareService +from security cimport Security cdef class Inference: def __init__(self, api_client, on_annotation): @@ -20,18 +25,41 @@ cdef class Inference: self.model_height = 0 self.engine = None self.class_names = None + self.is_building_engine = False - def init_ai(self): + cdef build_tensor_engine(self): + is_nvidia = HardwareService.has_nvidia_gpu() + if not is_nvidia: + return + + engine_filename = TensorRTEngine.get_engine_filename() + key = Security.get_model_encryption_key() + models_dir = constants.MODELS_FOLDER + if not os.path.exists(os.path.join( models_dir, f'{engine_filename}.big')): + self.is_building_engine = True + onnx_model = self.api_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir, key) + model_bytes = TensorRTEngine.convert_from_onnx(onnx_model) + self.api_client.upload_big_small_resource(model_bytes, engine_filename, models_dir, key) + self.is_building_engine = False + + + cdef init_ai(self): if self.engine is not None: return is_nvidia = HardwareService.has_nvidia_gpu() + key = Security.get_model_encryption_key() + models_dir = constants.MODELS_FOLDER if is_nvidia: - model_bytes = self.api_client.load_ai_model(is_tensor=True) - self.engine = TensorRTEngine(model_bytes, batch_size=4) + while self.is_building_engine: + time.sleep(1) + engine_filename = TensorRTEngine.get_engine_filename() + model_bytes = self.api_client.load_big_small_resource(engine_filename, models_dir, key) + self.engine = TensorRTEngine(model_bytes) + else: - model_bytes = self.api_client.load_ai_model() - self.engine = OnnxEngine(model_bytes, batch_size=4) + model_bytes = self.api_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir, key) + self.engine = OnnxEngine(model_bytes) self.model_height, self.model_width = self.engine.get_input_shape() self.class_names = self.engine.get_class_names() @@ -135,7 +163,7 @@ cdef class Inference: images.append(m) # images first, it's faster if len(images) > 0: - for chunk in self.split_list_extend(images, ai_config.model_batch_size): + for chunk in self.split_list_extend(images, self.engine.get_input_shape()): print(f'run inference on {" ".join(chunk)}...') self._process_images(cmd, ai_config, chunk) if len(videos) > 0: @@ -161,7 +189,7 @@ cdef class Inference: batch_frames.append(frame) batch_timestamps.append(int(v_input.get(cv2.CAP_PROP_POS_MSEC))) - if len(batch_frames) == ai_config.model_batch_size: + if len(batch_frames) == self.engine.get_input_shape(): input_blob = self.preprocess(batch_frames) outputs = self.engine.run(input_blob) diff --git a/Azaion.Inference/inference_engine.pxd b/Azaion.Inference/inference_engine.pxd index 1f76d05..0e74a46 100644 --- a/Azaion.Inference/inference_engine.pxd +++ b/Azaion.Inference/inference_engine.pxd @@ -26,4 +26,13 @@ cdef class TensorRTEngine(InferenceEngine): cdef object input_shape cdef object output_shape cdef object h_output - cdef object class_names \ No newline at end of file + cdef object class_names + + @staticmethod + cdef bytes convert_from_onnx(bytes onnx_model) + + @staticmethod + cdef unsigned long long get_gpu_memory_bytes(device_id=?) + + @staticmethod + cdef str get_engine_filename(device_id=?) \ No newline at end of file diff --git a/Azaion.Inference/inference_engine.pyx b/Azaion.Inference/inference_engine.pyx index 8785a68..6c61138 100644 --- a/Azaion.Inference/inference_engine.pyx +++ b/Azaion.Inference/inference_engine.pyx @@ -6,6 +6,7 @@ import onnxruntime as onnx import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit # required for automatically initialize CUDA, do not remove. +import pynvml cdef class InferenceEngine: @@ -28,13 +29,13 @@ cdef class InferenceEngine: cdef class OnnxEngine(InferenceEngine): def __init__(self, model_bytes: bytes, batch_size: int = 1, **kwargs): super().__init__(model_bytes, batch_size) - self.batch_size = batch_size + + self.session = onnx.InferenceSession(model_bytes, providers=["CUDAExecutionProvider", "CPUExecutionProvider"]) self.model_inputs = self.session.get_inputs() self.input_name = self.model_inputs[0].name self.input_shape = self.model_inputs[0].shape - if self.input_shape[0] != -1: - self.batch_size = self.input_shape[0] + self.batch_size = self.input_shape[0] if self.input_shape[0] != -1 else batch_size print(f'AI detection model input: {self.model_inputs} {self.input_shape}') model_meta = self.session.get_modelmeta() print("Metadata:", model_meta.custom_metadata_map) @@ -57,25 +58,12 @@ cdef class OnnxEngine(InferenceEngine): cdef class TensorRTEngine(InferenceEngine): def __init__(self, model_bytes: bytes, batch_size: int = 4, **kwargs): super().__init__(model_bytes, batch_size) - self.batch_size = batch_size print('Enter init TensorRT') try: logger = trt.Logger(trt.Logger.WARNING) - metadata_len = struct.unpack("(secureAppConfig.InferenceClientConfig)); _resourceLoader = new ResourceLoader(_inferenceClient); var login = new Login(); login.CredentialsEntered += async (_, credentials) => { - credentials.Folder = secureAppConfig.DirectoriesConfig.ApiResourcesDirectory; - _inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); _azaionApi = new AzaionApi(new HttpClient { BaseAddress = new Uri(SecurityConstants.API_URL) }, _cache, credentials, _hardwareService); try { - _securedConfig = _resourceLoader.LoadFile("config.secured.json"); - _systemConfig = _resourceLoader.LoadFile("config.system.json"); + _securedConfig = _resourceLoader.LoadFile("config.secured.json", apiDir); + _systemConfig = _resourceLoader.LoadFile("config.system.json", apiDir); } catch (Exception e) { @@ -125,7 +124,7 @@ public partial class App { try { - var stream = _resourceLoader.LoadFile($"{assemblyName}.dll"); + var stream = _resourceLoader.LoadFile($"{assemblyName}.dll", apiDir); return Assembly.Load(stream.ToArray()); } catch (Exception e) diff --git a/Azaion.Suite/Login.xaml b/Azaion.Suite/Login.xaml index 8b419d6..64b3bce 100644 --- a/Azaion.Suite/Login.xaml +++ b/Azaion.Suite/Login.xaml @@ -74,7 +74,7 @@ BorderBrush="DimGray" BorderThickness="0,0,0,1" HorizontalAlignment="Left" - Text="" + Text="admin@azaion.com" /> + Password="Az@1on1000Odm$n"/> + - diff --git a/Azaion.LoaderUI/Login.xaml.cs b/Azaion.LoaderUI/Login.xaml.cs index 09fea29..431f3e7 100644 --- a/Azaion.LoaderUI/Login.xaml.cs +++ b/Azaion.LoaderUI/Login.xaml.cs @@ -3,23 +3,27 @@ using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Input; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; + namespace Azaion.LoaderUI; public partial class Login { private readonly IAzaionApi _azaionApi; - private readonly DirectoriesConfig _dirConfig; + private readonly ILogger _logger; + private readonly DirectoriesConfig? _dirConfig; - public Login(IAzaionApi azaionApi, IOptions directoriesConfig) + public Login(IAzaionApi azaionApi, IOptions directoriesConfig, ILogger logger) { _azaionApi = azaionApi; + _logger = logger; _dirConfig = directoriesConfig.Value; InitializeComponent(); } - private void LoginClick(object sender, RoutedEventArgs e) + private async void LoginClick(object sender, RoutedEventArgs e) { var creds = new ApiCredentials(TbEmail.Text, TbPassword.Password); if (!creds.IsValid()) @@ -30,33 +34,43 @@ public partial class Login _azaionApi.Login(creds); - if (GetInstallerVer() > GetLocalVer()) - DownloadAndRunInstaller(); + var installerVersion = await GetInstallerVer(); + var localVersion = GetLocalVer(); + + if (installerVersion > localVersion) + { + TbStatus.Text = $"Updating from {localVersion} to {installerVersion}..."; + await DownloadAndRunInstaller(); + TbStatus.Text = $"Installed {installerVersion}!"; + } + else + TbStatus.Text = $"Your version is up to date!"; Process.Start(Constants.AZAION_SUITE_EXE, $"-e {creds.Email} -p {creds.Password}"); Close(); } - private void DownloadAndRunInstaller() + private async Task DownloadAndRunInstaller() { - var (installerName, stream) = _azaionApi.DownloadInstaller(_dirConfig.SuiteInstallerDirectory); + var (installerName, stream) = await _azaionApi.DownloadInstaller(_dirConfig?.SuiteInstallerDirectory ?? ""); var localFileStream = new FileStream(installerName, FileMode.Create, FileAccess.Write); - stream.CopyTo(localFileStream); + await stream.CopyToAsync(localFileStream); localFileStream.Close(); stream.Close(); - var processInfo = new ProcessStartInfo(installerName) { UseShellExecute = true, - Verb = "runas" + Arguments = "/VERYSILENT" }; - Process.Start(processInfo); + var process = Process.Start(processInfo); + await process!.WaitForExitAsync(); } - private Version GetInstallerVer() + private async Task GetInstallerVer() { - var installerName = _azaionApi.GetLastInstallerName(_dirConfig.SuiteInstallerDirectory); + TbStatus.Text = "Checking for the newer version..."; + var installerName = await _azaionApi.GetLastInstallerName(_dirConfig?.SuiteInstallerDirectory ?? Constants.SUITE_FOLDER); var version = installerName .Replace("AzaionSuite.Iterative.", "") .Replace(".exe", ""); diff --git a/Azaion.LoaderUI/loaderconfig.json b/Azaion.LoaderUI/loaderconfig.json deleted file mode 100644 index c77a864..0000000 --- a/Azaion.LoaderUI/loaderconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "DirectoriesConfig": - { - "SuiteInstallerDirectory": "" - } -} \ No newline at end of file diff --git a/Azaion.Suite.sln b/Azaion.Suite.sln index 1831e1a..515cc63 100644 --- a/Azaion.Suite.sln +++ b/Azaion.Suite.sln @@ -23,7 +23,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CF141A48 build\init.cmd = build\init.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 build\publish.cmd = build\publish.cmd build\requirements.txt = build\requirements.txt build\upload.cmd = build\upload.cmd diff --git a/build/installer.full.iss b/build/installer.full.iss index d05b189..f67a5d0 100644 --- a/build/installer.full.iss +++ b/build/installer.full.iss @@ -1,12 +1,15 @@ +#define MyAppVersion GetFileVersion("..\dist-azaion\Azaion.Suite.exe") + [Setup] AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} AppName=Azaion Suite -AppVersion=1.5.0 -AppPublisher=Azaion Ukraine +AppVersion={#MyAppVersion} +VersionInfoVersion={#MyAppVersion} +AppPublisher=Azaion LLC DefaultDirName={localappdata}\Azaion\Azaion Suite DefaultGroupName=Azaion Suite OutputDir=..\ -OutputBaseFilename=AzaionSuite.Full.1.5.0 +OutputBaseFilename=AzaionSuite.Full.{#MyAppVersion} SetupIconFile=..\dist-azaion\logo.ico UninstallDisplayName=Azaion Suite UninstallDisplayIcon={app}\Azaion.Suite.exe @@ -25,7 +28,7 @@ Source: "..\dist-dlls\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs Source: "..\dist-azaion\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; [Icons] -Name: "{group}\Azaion Suite"; Filename: "{app}\Azaion.Suite.exe" -Name: "{commondesktop}\Azaion Suite"; Filename: "{app}\Azaion.Suite.exe"; Tasks: desktopicon +Name: "{group}\Azaion Suite"; Filename: "{app}\Azaion.LoaderUI.exe" +Name: "{commondesktop}\Azaion Suite"; Filename: "{app}\Azaion.LoaderUI.exe"; Tasks: desktopicon [UninstallRun] \ No newline at end of file diff --git a/build/installer.iterative.iss b/build/installer.iterative.iss index 3f44d07..a6029b6 100644 --- a/build/installer.iterative.iss +++ b/build/installer.iterative.iss @@ -26,7 +26,7 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ Source: "..\dist-azaion\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; [Icons] -Name: "{group}\Azaion Suite"; Filename: "{app}\Azaion.Suite.exe" -Name: "{commondesktop}\Azaion Suite"; Filename: "{app}\Azaion.Suite.exe"; Tasks: desktopicon +Name: "{group}\Azaion Suite"; Filename: "{app}\Azaion.LoaderUI.exe" +Name: "{commondesktop}\Azaion Suite"; Filename: "{app}\Azaion.LoaderUI.exe"; Tasks: desktopicon [UninstallRun] \ No newline at end of file diff --git a/build/publish.cmd b/build/publish.cmd index 540be11..b672b92 100644 --- a/build/publish.cmd +++ b/build/publish.cmd @@ -15,10 +15,12 @@ call ..\gps-denied\image-matcher\build_gps call build\download_models -echo building installer... +echo building and upload iterative installer... iscc build\installer.iterative.iss - call build\upload.cmd "suite" +echo building full installer +iscc build\installer.full.iss + cd /d %CURRENT_DIR% echo Done! \ No newline at end of file