diff --git a/.gitignore b/.gitignore index ef6e7f6..788d8a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ .idea bin obj +*.dll +*.exe +*.log .vs *.DotSettings* *.user @@ -11,6 +14,11 @@ venv *.c *.pyd cython_debug* -dist -AzaionSuiteInstaller.exe -azaion\.*\.big \ No newline at end of file +dist-dlls +dist-azaion +Azaion*.exe +Azaion*.bin + +azaion\.*\.big +_internal +*.spec \ No newline at end of file diff --git a/Azaion.Annotator/Annotator.xaml b/Azaion.Annotator/Annotator.xaml index 53bd029..f77d1fa 100644 --- a/Azaion.Annotator/Annotator.xaml +++ b/Azaion.Annotator/Annotator.xaml @@ -500,7 +500,7 @@ Padding="2" Width="25" Height="25" ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black" - Click="AutoDetect"> + Click="AIDetectBtn_OnClick"> + { + { "disabled", aiDisabledText }, + { "downloading", "Будь ласка зачекайте, йде завантаження AI для Вашої відеокарти" }, + { "converting", "Будь ласка зачекайте, йде налаштування AI під Ваше залізо. (5-12 хвилин в залежності від моделі відеокарти, до 50 хв на старих GTX1650)" }, + { "uploading", "Будь ласка зачекайте, йде зберігання" }, + { "enabled", "AI готовий для розпізнавання" } + }; + + if (command.Message?.StartsWith("Error") ?? false) + { + _logger.LogError(command.Message); + StatusHelp.Text = command.Message; + } + + else + StatusHelp.Text = messagesDict!.GetValueOrDefault(command.Message, aiDisabledText); + + if (aiEnabled) + StatusHelp.Foreground = aiEnabled ? Brushes.White : Brushes.Red; + }); + }; + _inferenceClient.Send(RemoteCommand.Create(CommandType.AIAvailabilityCheck)); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); - MapMatcherComponent.Init(_appConfig, _gpsMatcherService); + MapMatcherComponent.Init(_appConfig, gpsMatcherService); } private void OnLoaded(object sender, RoutedEventArgs e) @@ -126,9 +155,6 @@ public partial class Annotator TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory; LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses); - - if (LvFiles.Items.IsEmpty) - BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]); } public void BlinkHelp(string helpText, int times = 2) @@ -175,8 +201,6 @@ public partial class Annotator LvFiles.MouseDoubleClick += async (_, _) => { - if (IsInferenceNow) - FollowAI = false; await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play)); }; @@ -199,9 +223,9 @@ public partial class Annotator Volume.ValueChanged += (_, newValue) => _mediator.Publish(new VolumeChangedEvent((int)newValue)); - SizeChanged += async (_, _) => await SaveUserSettings(); - LocationChanged += async (_, _) => await SaveUserSettings(); - StateChanged += async (_, _) => await SaveUserSettings(); + SizeChanged += (_, _) => SaveUserSettings(); + LocationChanged += (_, _) => SaveUserSettings(); + StateChanged += (_, _) => SaveUserSettings(); DgAnnotations.MouseDoubleClick += (sender, args) => { @@ -225,9 +249,9 @@ public partial class Annotator return; var res = DgAnnotations.SelectedItems.Cast().ToList(); - var annotations = res.Select(x => x.Annotation).ToList(); + var annotationNames = res.Select(x => x.Annotation.Name).ToList(); - await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); + await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames)); break; } }; @@ -238,8 +262,6 @@ public partial class Annotator public void OpenAnnotationResult(AnnotationResult res) { - if (IsInferenceNow) - FollowAI = false; _mediaPlayer.SetPause(true); Editor.RemoveAllAnns(); _mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds; @@ -253,7 +275,7 @@ public partial class Annotator ShowAnnotations(res.Annotation, showImage: true); } - private async Task SaveUserSettings() + private void SaveUserSettings() { if (_suspendLayout) return; @@ -261,7 +283,7 @@ public partial class Annotator _appConfig.UIConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; _appConfig.UIConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; - await ThrottleExt.ThrottleRunFirst(() => + ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; @@ -324,6 +346,10 @@ public partial class Annotator //Add manually public void AddAnnotation(Annotation annotation) { + var mediaInfo = (MediaFileInfo)LvFiles.SelectedItem; + if ((mediaInfo?.FName ?? "") != annotation.OriginalMediaName) + return; + var time = annotation.Time; var previousAnnotations = TimedAnnotations.Query(time); TimedAnnotations.Remove(previousAnnotations); @@ -341,10 +367,8 @@ public partial class Annotator _logger.LogError(e, e.Message); throw; } - } - var dict = _formState.AnnotationResults .Select((x, i) => new { x.Annotation.Time, Index = i }) .ToDictionary(x => x.Time, x => x.Index); @@ -398,11 +422,9 @@ public partial class Annotator mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); AllMediaFiles = new ObservableCollection(allFiles); + MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name) + .ToDictionary(gr => gr.Key, gr => gr.First()); LvFiles.ItemsSource = AllMediaFiles; - - BlinkHelp(AllMediaFiles.Count == 0 - ? HelpTexts.HelpTextsDict[HelpTextEnum.Initial] - : HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]); DataContext = this; } @@ -447,7 +469,7 @@ public partial class Annotator { Title = "Open Video folder", IsFolderPicker = true, - InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) + InitialDirectory = Path.GetDirectoryName(_appConfig.DirectoriesConfig.VideosDirectory) }; var dialogResult = dlg.ShowDialog(); @@ -462,14 +484,13 @@ public partial class Annotator private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) { FilteredMediaFiles = new ObservableCollection(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList()); + MediaFilesDict = FilteredMediaFiles.ToDictionary(x => x.FName); LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles; } private void PlayClick(object sender, RoutedEventArgs e) { - if (IsInferenceNow) - FollowAI = false; _mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play)); } @@ -492,7 +513,7 @@ public partial class Annotator _helpWindow.Activate(); } - private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => _ = SaveUserSettings(); + private void Thumb_OnDragCompleted(object sender, DragCompletedEventArgs e) => SaveUserSettings(); private void LvFilesContextOpening(object sender, ContextMenuEventArgs e) { @@ -500,13 +521,22 @@ public partial class Annotator LvFilesContextMenu.DataContext = listItem!.DataContext; } - public void AutoDetect(object sender, RoutedEventArgs e) + private async void AIDetectBtn_OnClick(object sender, RoutedEventArgs e) + { + try + { + await AutoDetect(); + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + } + } + + public async Task AutoDetect() { if (IsInferenceNow) - { - FollowAI = true; return; - } if (LvFiles.Items.IsEmpty) return; @@ -516,96 +546,23 @@ public partial class Annotator Dispatcher.Invoke(() => Editor.ResetBackground()); IsInferenceNow = true; - FollowAI = true; + AIDetectBtn.IsEnabled = false; + DetectionCancellationSource = new CancellationTokenSource(); - var detectToken = DetectionCancellationSource.Token; - _ = Task.Run(async () => - { - while (!detectToken.IsCancellationRequested) - { - var files = new List(); - await Dispatcher.Invoke(async () => - { - //Take all medias - files = (LvFiles.ItemsSource as IEnumerable)?.Skip(LvFiles.SelectedIndex) - //.Where(x => !x.HasAnnotations) - .Take(Constants.DETECTION_BATCH_SIZE) - .Select(x => x.Path) - .ToList() ?? []; - if (files.Count != 0) - { - await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), detectToken); - await ReloadAnnotations(); - } - }); - if (files.Count == 0) - break; - await _inferenceService.RunInference(files, async annotationImage => await ProcessDetection(annotationImage, detectToken), detectToken); + var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles) + .Skip(LvFiles.SelectedIndex) + .Select(x => x.Path) + .ToList(); + if (files.Count == 0) + return; - Dispatcher.Invoke(() => - { - if (LvFiles.SelectedIndex + files.Count >= LvFiles.Items.Count) - DetectionCancellationSource.Cancel(); - LvFiles.SelectedIndex += files.Count; - }); - } - Dispatcher.Invoke(() => - { - LvFiles.Items.Refresh(); - IsInferenceNow = false; - FollowAI = false; - }); - }); - } + await _inferenceService.RunInference(files, DetectionCancellationSource.Token); - private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default) - { - await Dispatcher.Invoke(async () => - { - try - { - var annotation = await _annotationService.SaveAnnotation(annotationImage, ct); - if (annotation.OriginalMediaName != _formState.CurrentMedia?.FName) - { - var nextFile = (LvFiles.ItemsSource as IEnumerable)? - .Select((info, i) => new - { - MediaInfo = info, - Index = i - }) - .FirstOrDefault(x => x.MediaInfo.FName == annotation.OriginalMediaName); - if (nextFile != null) - { - LvFiles.SelectedIndex = nextFile.Index; - await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), ct); - } - } - - AddAnnotation(annotation); - - if (FollowAI) - SeekTo(annotationImage.Milliseconds, false); - - var log = string.Join(Environment.NewLine, annotation.Detections.Select(det => - $"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " + - $"xy=({det.CenterX:F2},{det.CenterY:F2}), " + - $"size=({det.Width:F2}, {det.Height:F2}), " + - $"conf: {det.Confidence*100:F0}%")); - - Dispatcher.Invoke(() => - { - if (_formState.CurrentMedia != null) - _formState.CurrentMedia.HasAnnotations = true; - LvFiles.Items.Refresh(); - StatusHelp.Text = log; - }); - } - catch (Exception e) - { - _logger.LogError(e, e.Message); - } - }); + LvFiles.Items.Refresh(); + IsInferenceNow = false; + StatusHelp.Text = "Розпізнавання зваершено"; + AIDetectBtn.IsEnabled = true; } private void SwitchGpsPanel(object sender, RoutedEventArgs e) diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 28deefa..b8bcbfa 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -1,15 +1,18 @@ using System.IO; -using System.Reflection.Metadata; using System.Windows; using System.Windows.Input; +using System.Windows.Media; +using Azaion.Annotator.Controls; using Azaion.Annotator.DTO; using Azaion.Common; +using Azaion.Common.Database; 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 GMap.NET; +using GMap.NET.WindowsPresentation; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Logging; @@ -23,16 +26,23 @@ public class AnnotatorEventHandler( MediaPlayer mediaPlayer, Annotator mainWindow, FormState formState, - AnnotationService annotationService, + IAnnotationService annotationService, ILogger logger, IOptions dirConfig, - IInferenceService inferenceService) + IOptions annotationConfig, + IInferenceService inferenceService, + IDbFactory dbFactory, + IAzaionApi api, + FailsafeAnnotationsProducer producer) : INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler, - INotificationHandler + INotificationHandler, + INotificationHandler, + INotificationHandler, + INotificationHandler { private const int STEP = 20; private const int LARGE_STEP = 5000; @@ -50,7 +60,7 @@ public class AnnotatorEventHandler( { Key.PageDown, PlaybackControlEnum.Next }, }; - public async Task Handle(AnnClassSelectedEvent notification, CancellationToken cancellationToken) + public async Task Handle(AnnClassSelectedEvent notification, CancellationToken ct) { SelectClass(notification.DetectionClass); await Task.CompletedTask; @@ -64,7 +74,7 @@ public class AnnotatorEventHandler( mainWindow.LvClasses.SelectNum(detClass.Id); } - public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default) + public async Task Handle(KeyEvent keyEvent, CancellationToken ct = default) { if (keyEvent.WindowEnum != WindowEnum.Annotator) return; @@ -80,19 +90,19 @@ public class AnnotatorEventHandler( SelectClass((DetectionClass)mainWindow.LvClasses.DetectionDataGrid.Items[keyNumber.Value]!); if (_keysControlEnumDict.TryGetValue(key, out var value)) - await ControlPlayback(value, cancellationToken); + await ControlPlayback(value, ct); if (key == Key.R) - mainWindow.AutoDetect(null!, null!); + await mainWindow.AutoDetect(); #region Volume switch (key) { case Key.VolumeMute when mediaPlayer.Volume == 0: - await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken); + await ControlPlayback(PlaybackControlEnum.TurnOnVolume, ct); break; case Key.VolumeMute: - await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken); + await ControlPlayback(PlaybackControlEnum.TurnOffVolume, ct); break; case Key.Up: case Key.VolumeUp: @@ -110,9 +120,9 @@ public class AnnotatorEventHandler( #endregion } - public async Task Handle(AnnotatorControlEvent notification, CancellationToken cancellationToken = default) + public async Task Handle(AnnotatorControlEvent notification, CancellationToken ct = default) { - await ControlPlayback(notification.PlaybackControl, cancellationToken); + await ControlPlayback(notification.PlaybackControl, ct); mainWindow.VideoView.Focus(); } @@ -130,10 +140,6 @@ public class AnnotatorEventHandler( break; case PlaybackControlEnum.Pause: mediaPlayer.Pause(); - if (mainWindow.IsInferenceNow) - mainWindow.FollowAI = false; - if (!mediaPlayer.IsPlaying) - mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]); if (formState.BackgroundTime.HasValue) { @@ -203,7 +209,7 @@ public class AnnotatorEventHandler( await Play(ct); } - public async Task Handle(VolumeChangedEvent notification, CancellationToken cancellationToken) + public async Task Handle(VolumeChangedEvent notification, CancellationToken ct) { ChangeVolume(notification.Volume); await Task.CompletedTask; @@ -227,7 +233,6 @@ public class AnnotatorEventHandler( await Task.Delay(100, ct); mediaPlayer.Stop(); mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}"; - mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]); mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); if (formState.CurrentMedia.MediaType == MediaTypes.Image) mediaPlayer.SetPause(true); @@ -280,27 +285,113 @@ public class AnnotatorEventHandler( mainWindow.AddAnnotation(annotation); } - public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct) { - var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x); - foreach (var ann in notification.Annotations) + try { - if (!annResDict.TryGetValue(ann.Name, out var value)) - continue; - - formState.AnnotationResults.Remove(value); - mainWindow.TimedAnnotations.Remove(ann); - } - - if (formState.AnnotationResults.Count == 0) - { - var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name); - if (media != null) + mainWindow.Dispatcher.Invoke(() => { - media.HasAnnotations = false; - mainWindow.LvFiles.Items.Refresh(); + var namesSet = notification.AnnotationNames.ToHashSet(); + + var remainAnnotations = formState.AnnotationResults + .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList(); + formState.AnnotationResults.Clear(); + foreach (var ann in remainAnnotations) + formState.AnnotationResults.Add(ann); + + var timedAnnsToRemove = mainWindow.TimedAnnotations + .Where(x => namesSet.Contains(x.Value.Name)) + .Select(x => x.Value).ToList(); + mainWindow.TimedAnnotations.Remove(timedAnnsToRemove); + + if (formState.AnnotationResults.Count == 0) + { + var media = mainWindow.AllMediaFiles.FirstOrDefault(x => x.Name == formState.CurrentMedia?.Name); + if (media != null) + { + media.HasAnnotations = false; + mainWindow.LvFiles.Items.Refresh(); + } + } + }); + + await dbFactory.DeleteAnnotations(notification.AnnotationNames, ct); + + try + { + foreach (var name in notification.AnnotationNames) + { + File.Delete(Path.Combine(dirConfig.Value.ImagesDirectory, $"{name}{Constants.JPG_EXT}")); + File.Delete(Path.Combine(dirConfig.Value.LabelsDirectory, $"{name}{Constants.TXT_EXT}")); + File.Delete(Path.Combine(dirConfig.Value.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}")); + File.Delete(Path.Combine(dirConfig.Value.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}")); + } } + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; + } + + //Only validators can send Delete to the queue + if (!notification.FromQueue && api.CurrentUser.Role.IsValidator()) + await producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct); } - await Task.CompletedTask; + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; + } + } + + public Task Handle(AnnotationAddedEvent e, CancellationToken ct) + { + mainWindow.Dispatcher.Invoke(() => + { + mainWindow.AddAnnotation(e.Annotation); + + var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det => + $"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " + + $"xy=({det.CenterX:F2},{det.CenterY:F2}), " + + $"розмір=({det.Width:F2}, {det.Height:F2}), " + + $"conf: {det.Confidence*100:F0}%")); + + mainWindow.LvFiles.Items.Refresh(); + + var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName); + if (media != null) + media.HasAnnotations = true; + + mainWindow.LvFiles.Items.Refresh(); + mainWindow.StatusHelp.Text = log; + }); + return Task.CompletedTask; + } + + public Task Handle(SetStatusTextEvent e, CancellationToken cancellationToken) + { + mainWindow.Dispatcher.Invoke(() => + { + mainWindow.StatusHelp.Text = e.Text; + mainWindow.StatusHelp.Foreground = e.IsError ? Brushes.Red : Brushes.White; + }); + return Task.CompletedTask; + } + + public Task Handle(GPSMatcherResultEvent e, CancellationToken cancellationToken) + { + mainWindow.Dispatcher.Invoke(() => + { + var mapMatcher = mainWindow.MapMatcherComponent; + var marker = new GMapMarker(new PointLatLng(e.Latitude, e.Longitude)); + var ann = mapMatcher.Annotations[e.Index]; + marker.Shape = new CircleVisual(marker, size: 14, text: e.Image, background: Brushes.Blue); + mapMatcher.SatelliteMap.Markers.Add(marker); + ann.Lat = e.Latitude; + ann.Lon = e.Longitude; + mapMatcher.SatelliteMap.Position = new PointLatLng(e.Latitude, e.Longitude); + mapMatcher.SatelliteMap.ZoomAndCenterMarkers(null); + }); + return Task.CompletedTask; } } diff --git a/Azaion.Annotator/Azaion.Annotator.csproj b/Azaion.Annotator/Azaion.Annotator.csproj index 38968a5..03dd400 100644 --- a/Azaion.Annotator/Azaion.Annotator.csproj +++ b/Azaion.Annotator/Azaion.Annotator.csproj @@ -7,24 +7,27 @@ net8.0-windows + + $([System.DateTime]::UtcNow.ToString("yyyy.MM.dd")) + $([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes))) + + $(VersionDate).$(VersionSeconds) + $(AssemblyVersion) + $(AssemblyVersion) + Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved. + + - - - - - + + + + - - - - - - diff --git a/Azaion.Annotator/Controls/CircleVisual.cs b/Azaion.Annotator/Controls/CircleVisual.cs index b7ff86e..0d6ae2d 100644 --- a/Azaion.Annotator/Controls/CircleVisual.cs +++ b/Azaion.Annotator/Controls/CircleVisual.cs @@ -11,7 +11,7 @@ namespace Azaion.Annotator.Controls { public readonly GMapMarker Marker; - public CircleVisual(GMapMarker m, Brush background) + public CircleVisual(GMapMarker m, int size, string text, Brush background) { ShadowEffect = new DropShadowEffect(); Marker = m; @@ -22,14 +22,14 @@ namespace Azaion.Annotator.Controls MouseLeave += CircleVisual_MouseLeave; Loaded += OnLoaded; - Text = "?"; + Text = text; StrokeArrow.EndLineCap = PenLineCap.Triangle; StrokeArrow.LineJoin = PenLineJoin.Round; RenderTransform = _scale; - Width = Height = 22; + Width = Height = size; FontSize = Width / 1.55; Background = background; @@ -80,7 +80,7 @@ namespace Azaion.Annotator.Controls FontWeights.Bold, FontStretches.Normal); - FormattedText _fText; + FormattedText _fText = null!; private Brush _background = Brushes.Blue; @@ -178,16 +178,17 @@ namespace Azaion.Annotator.Controls void ForceUpdateText() { + _fText = new FormattedText(_text, CultureInfo.InvariantCulture, FlowDirection.LeftToRight, Font, FontSize, - Foreground); + Foreground, 1.0); IsChanged = true; } - string _text; + string _text = null!; public string Text { @@ -205,9 +206,9 @@ namespace Azaion.Annotator.Controls } } - Visual _child; + Visual _child = null!; - public virtual Visual Child + public virtual Visual? Child { get => _child; set @@ -228,7 +229,7 @@ namespace Azaion.Annotator.Controls } // cache the new child - _child = value; + _child = value!; InvalidateVisual(); } @@ -295,7 +296,7 @@ namespace Azaion.Annotator.Controls } } - protected override Visual GetVisualChild(int index) + protected override Visual? GetVisualChild(int index) { return Child; } diff --git a/Azaion.Annotator/Controls/MapMatcher.xaml.cs b/Azaion.Annotator/Controls/MapMatcher.xaml.cs index 20c6600..16d1e3b 100644 --- a/Azaion.Annotator/Controls/MapMatcher.xaml.cs +++ b/Azaion.Annotator/Controls/MapMatcher.xaml.cs @@ -1,12 +1,9 @@ using System.Collections.ObjectModel; using System.Diagnostics; -using System.Drawing; using System.IO; using System.Windows; using System.Windows.Controls; -using System.Windows.Input; using System.Windows.Media; -using Azaion.Common; using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; @@ -14,7 +11,6 @@ using Azaion.Common.Extensions; using Azaion.Common.Services; using GMap.NET; using GMap.NET.MapProviders; -using GMap.NET.WindowsPresentation; using Microsoft.WindowsAPICodePack.Dialogs; namespace Azaion.Annotator.Controls; @@ -23,7 +19,7 @@ public partial class MapMatcher : UserControl { private AppConfig _appConfig = null!; List _allMediaFiles = new(); - private Dictionary _annotations = new(); + public Dictionary Annotations = new(); private string _currentDir = null!; private IGpsMatcherService _gpsMatcherService = null!; @@ -46,8 +42,11 @@ public partial class MapMatcher : UserControl private async Task OpenGpsLocation(int gpsFilesIndex) { - var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo; - var ann = _annotations.GetValueOrDefault(gpsFilesIndex); + //var media = GpsFiles.Items[gpsFilesIndex] as MediaFileInfo; + var ann = Annotations.GetValueOrDefault(gpsFilesIndex); + if (ann == null) + return; + GpsImageEditor.Background = new ImageBrush { ImageSource = await Path.Combine(_currentDir, ann.Name).OpenImage() @@ -98,7 +97,7 @@ public partial class MapMatcher : UserControl _allMediaFiles = mediaFiles; GpsFiles.ItemsSource = new ObservableCollection(_allMediaFiles); - _annotations = mediaFiles.Select((x, i) => (i, new Annotation + Annotations = mediaFiles.Select((x, i) => (i, new Annotation { Name = x.Name, OriginalMediaName = x.Name @@ -107,41 +106,7 @@ public partial class MapMatcher : UserControl var initialLat = double.Parse(TbLat.Text); var initialLon = double.Parse(TbLon.Text); - await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon, async res => await SetMarker(res)); - } - - private async Task SetMarker(GpsMatchResult result) - { - await Dispatcher.Invoke(async () => - { - var marker = new GMapMarker(new PointLatLng(result.Latitude, result.Longitude)); - var ann = _annotations[result.Index]; - marker.Shape = new CircleVisual(marker, System.Windows.Media.Brushes.Blue) - { - Text = ann.Name - }; - SatelliteMap.Markers.Add(marker); - ann.Lat = result.Latitude; - ann.Lon = result.Longitude; - SatelliteMap.Position = new PointLatLng(result.Latitude, result.Longitude); - SatelliteMap.ZoomAndCenterMarkers(null); - }); - } - - private async Task SetFromCsv(List mediaFiles) - { - - var csvResults = GpsMatchResult.ReadFromCsv(Constants.CSV_PATH); - var csvDict = csvResults - .Where(x => x.MatchType == "stitched") - .ToDictionary(x => x.Index); - foreach (var ann in _annotations) - { - var csvRes = csvDict.GetValueOrDefault(ann.Key); - if (csvRes == null) - continue; - await SetMarker(csvRes); - } + await _gpsMatcherService.RunGpsMatching(dir.FullName, initialLat, initialLon); } private async void TestGps(object sender, RoutedEventArgs e) @@ -151,6 +116,6 @@ public partial class MapMatcher : UserControl var initialLat = double.Parse(TbLat.Text); var initialLon = double.Parse(TbLon.Text); - await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon, async res => await SetMarker(res)); + await _gpsMatcherService.RunGpsMatching(TbGpsMapFolder.Text, initialLat, initialLon); } } diff --git a/Azaion.Common/Azaion.Common.csproj b/Azaion.Common/Azaion.Common.csproj index 96f371b..ca47d48 100644 --- a/Azaion.Common/Azaion.Common.csproj +++ b/Azaion.Common/Azaion.Common.csproj @@ -7,25 +7,25 @@ + + - + - - - - + + + + + + - - - - diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index 1237b9a..874d418 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -1,12 +1,14 @@ using System.Windows; using Azaion.Common.DTO; +using Azaion.Common.DTO.Config; +using Azaion.Common.Extensions; namespace Azaion.Common; public class Constants { public const string JPG_EXT = ".jpg"; - + public const string TXT_EXT = ".txt"; #region DirectoriesConfig public const string DEFAULT_VIDEO_DIR = "video"; @@ -21,20 +23,33 @@ public class Constants #region AnnotatorConfig - public static readonly List DefaultAnnotationClasses = + public static readonly AnnotationConfig DefaultAnnotationConfig = new() + { + DetectionClasses = DefaultAnnotationClasses!, + VideoFormats = DefaultVideoFormats!, + ImageFormats = DefaultImageFormats!, + AnnotationsDbFile = DEFAULT_ANNOTATIONS_DB_FILE + }; + + private 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() }, + new() { Id = 16, Name = "Caponier", ShortName = "Капонір", Color = "#ffb6c1".ToColor() }, ]; public static readonly List DefaultVideoFormats = ["mp4", "mov", "avi"]; @@ -49,17 +64,31 @@ 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; public const double TRACKING_INTERSECTION_THRESHOLD = 0.8; public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4; - public const int DETECTION_BATCH_SIZE = 4; # endregion AIRecognitionConfig #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; @@ -69,12 +98,7 @@ public class Constants #endregion - #region Queue - public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; - public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm"; - - #endregion #region Database diff --git a/Azaion.Common/Controls/DetectionClasses.xaml b/Azaion.Common/Controls/DetectionClasses.xaml index 47951a8..a7b9925 100644 --- a/Azaion.Common/Controls/DetectionClasses.xaml +++ b/Azaion.Common/Controls/DetectionClasses.xaml @@ -52,6 +52,7 @@ CanUserResizeColumns="False" SelectionChanged="DetectionDataGrid_SelectionChanged" x:FieldModifier="public" + PreviewKeyDown="OnKeyBanActivity" > diff --git a/Azaion.Common/Controls/DetectionClasses.xaml.cs b/Azaion.Common/Controls/DetectionClasses.xaml.cs index d704fa5..8f7d4ce 100644 --- a/Azaion.Common/Controls/DetectionClasses.xaml.cs +++ b/Azaion.Common/Controls/DetectionClasses.xaml.cs @@ -1,6 +1,7 @@ using System.Collections.ObjectModel; using System.Windows; using System.Windows.Controls; +using System.Windows.Input; using Azaion.Common.DTO; using Azaion.Common.Extensions; @@ -86,4 +87,11 @@ public partial class DetectionClasses { DetectionDataGrid.SelectedIndex = keyNumber; } + + private void OnKeyBanActivity(object sender, KeyEventArgs e) + { + if (e.Key.In(Key.Enter, Key.Down, Key.Up, Key.PageDown, Key.PageUp)) + e.Handled = true; + } + } \ No newline at end of file diff --git a/Azaion.Common/DTO/AnnotationThumbnail.cs b/Azaion.Common/DTO/AnnotationThumbnail.cs index 0fd7c8a..54a1572 100644 --- a/Azaion.Common/DTO/AnnotationThumbnail.cs +++ b/Azaion.Common/DTO/AnnotationThumbnail.cs @@ -7,9 +7,10 @@ using Azaion.Common.Extensions; namespace Azaion.Common.DTO; -public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged +public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged { public Annotation Annotation { get; set; } = annotation; + public bool IsValidator { get; set; } = isValidator; private BitmapImage? _thumbnail; public BitmapImage? Thumbnail @@ -28,11 +29,17 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged } public string ImageName => Path.GetFileName(Annotation.ImagePath); - public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created; + public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}"; + public string CreatedEmail => Annotation.CreatedEmail; + public bool IsSeed => IsValidator && + Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) && + !Annotation.CreatedRole.IsValidator(); public event PropertyChangedEventHandler? PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } + + public void UpdateUI() => OnPropertyChanged(nameof(IsSeed)); } \ No newline at end of file diff --git a/Azaion.Common/DTO/ApiCredentials.cs b/Azaion.Common/DTO/ApiCredentials.cs new file mode 100644 index 0000000..d11ab90 --- /dev/null +++ b/Azaion.Common/DTO/ApiCredentials.cs @@ -0,0 +1,16 @@ +using CommandLine; +using MessagePack; + +namespace Azaion.Common.DTO; + +[MessagePackObject] +public class ApiCredentials : EventArgs +{ + [Key(nameof(Email))] + [Option('e', "email", Required = true, HelpText = "User Email")] + public string Email { get; set; } = null!; + + [Key(nameof(Password))] + [Option('p', "pass", Required = true, HelpText = "User Password")] + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/BusinessExceptionDto.cs b/Azaion.Common/DTO/BusinessExceptionDto.cs new file mode 100644 index 0000000..5a64178 --- /dev/null +++ b/Azaion.Common/DTO/BusinessExceptionDto.cs @@ -0,0 +1,7 @@ +namespace Azaion.Common.DTO; + +public 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/ClusterDistribution.cs b/Azaion.Common/DTO/ClusterDistribution.cs new file mode 100644 index 0000000..6545d51 --- /dev/null +++ b/Azaion.Common/DTO/ClusterDistribution.cs @@ -0,0 +1,11 @@ +using System.Windows.Media; + +namespace Azaion.Common.DTO; + +public class ClusterDistribution +{ + public string Label { get; set; } = ""; + public Color Color { get; set; } + public int ClassCount { get; set; } + public double BarWidth { get; set; } +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/AppConfig.cs b/Azaion.Common/DTO/Config/AppConfig.cs index 701170e..b4e2275 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -1,30 +1,31 @@ using System.IO; using System.Text; using Azaion.CommonSecurity; -using Azaion.CommonSecurity.DTO; using Newtonsoft.Json; namespace Azaion.Common.DTO.Config; public class AppConfig { + public LoaderClientConfig LoaderClientConfig { get; set; } = null!; + public InferenceClientConfig InferenceClientConfig { get; set; } = null!; 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 @@ -45,14 +46,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 +66,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); } @@ -95,6 +77,7 @@ public class ConfigUpdater : IConfigUpdater //Save only user's config var publicConfig = new { + config.LoaderClientConfig, config.InferenceClientConfig, config.GpsDeniedClientConfig, config.DirectoriesConfig, diff --git a/Azaion.Common/DTO/Config/UIConfig.cs b/Azaion.Common/DTO/Config/UIConfig.cs index 9b4036e..d1f75e7 100644 --- a/Azaion.Common/DTO/Config/UIConfig.cs +++ b/Azaion.Common/DTO/Config/UIConfig.cs @@ -5,4 +5,5 @@ public class UIConfig public double LeftPanelWidth { get; set; } public double RightPanelWidth { get; set; } public bool GenerateAnnotatedImage { get; set; } + public bool SilentDetection { get; set; } } diff --git a/Azaion.Common/DTO/Config/DirectoriesConfig.cs b/Azaion.Common/DTO/DirectoriesConfig.cs similarity index 82% rename from Azaion.Common/DTO/Config/DirectoriesConfig.cs rename to Azaion.Common/DTO/DirectoriesConfig.cs index b36a57b..c4d6f73 100644 --- a/Azaion.Common/DTO/Config/DirectoriesConfig.cs +++ b/Azaion.Common/DTO/DirectoriesConfig.cs @@ -1,7 +1,10 @@ -namespace Azaion.Common.DTO.Config; +namespace Azaion.Common.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.Common/DTO/ExternalClientsConfig.cs similarity index 52% rename from Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs rename to Azaion.Common/DTO/ExternalClientsConfig.cs index aaf7d64..4b9ccad 100644 --- a/Azaion.CommonSecurity/DTO/ExternalClientsConfig.cs +++ b/Azaion.Common/DTO/ExternalClientsConfig.cs @@ -1,19 +1,22 @@ -namespace Azaion.CommonSecurity.DTO; +namespace Azaion.Common.DTO; public abstract class ExternalClientConfig { public string ZeroMqHost { get; set; } = ""; public int ZeroMqPort { get; set; } - public double OneTryTimeoutSeconds { get; set; } - public int RetryCount {get;set;} +} + +public class LoaderClientConfig : ExternalClientConfig +{ + public string ApiUrl { get; set; } = null!; } public class InferenceClientConfig : ExternalClientConfig { - public string ResourcesFolder { get; set; } = ""; + public string ApiUrl { get; set; } = null!; } public class GpsDeniedClientConfig : ExternalClientConfig { - public int ZeroMqSubscriberPort { get; set; } + public int ZeroMqReceiverPort { get; set; } } \ No newline at end of file diff --git a/Azaion.Common/DTO/GpsMatchResult.cs b/Azaion.Common/DTO/GpsMatchResult.cs deleted file mode 100644 index 53f75fc..0000000 --- a/Azaion.Common/DTO/GpsMatchResult.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Azaion.Common.DTO; -using System.Collections.Generic; -using System.IO; - -public class GpsMatchResult -{ - public int Index { get; set; } - public string Image { get; set; } = null!; - public double Latitude { get; set; } - public double Longitude { get; set; } - public int KeyPoints { get; set; } - public int Rotation { get; set; } - public string MatchType { get; set; } = null!; - - public static List ReadFromCsv(string csvFilePath) - { - var imageDatas = new List(); - - using var reader = new StreamReader(csvFilePath); - //read header - reader.ReadLine(); - if (reader.EndOfStream) - return new List(); - - while (!reader.EndOfStream) - { - var line = reader.ReadLine(); - if (string.IsNullOrWhiteSpace(line)) - continue; - var values = line.Split(','); - if (values.Length == 6) - { - imageDatas.Add(new GpsMatchResult - { - Image = GetFilename(values[0]), - Latitude = double.Parse(values[1]), - Longitude = double.Parse(values[2]), - KeyPoints = int.Parse(values[3]), - Rotation = int.Parse(values[4]), - MatchType = values[5] - }); - } - } - - return imageDatas; - } - - private static string GetFilename(string imagePath) => - Path.GetFileNameWithoutExtension(imagePath) - .Replace("-small", ""); -} \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/IEnumerableExtensions.cs b/Azaion.Common/DTO/IEnumerableExtensions.cs similarity index 78% rename from Azaion.CommonSecurity/DTO/IEnumerableExtensions.cs rename to Azaion.Common/DTO/IEnumerableExtensions.cs index df3cf07..5386daa 100644 --- a/Azaion.CommonSecurity/DTO/IEnumerableExtensions.cs +++ b/Azaion.Common/DTO/IEnumerableExtensions.cs @@ -1,4 +1,4 @@ -namespace Azaion.Common.Extensions; +namespace Azaion.Common.DTO; public static class EnumerableExtensions { diff --git a/Azaion.Common/DTO/InitConfig.cs b/Azaion.Common/DTO/InitConfig.cs new file mode 100644 index 0000000..74fb984 --- /dev/null +++ b/Azaion.Common/DTO/InitConfig.cs @@ -0,0 +1,9 @@ +namespace Azaion.Common.DTO; + +public class InitConfig +{ + public LoaderClientConfig LoaderClientConfig { get; set; } = null!; + 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.Common/DTO/Label.cs b/Azaion.Common/DTO/Label.cs index fab95f2..341ba1a 100644 --- a/Azaion.Common/DTO/Label.cs +++ b/Azaion.Common/DTO/Label.cs @@ -146,8 +146,11 @@ public class YoloLabel : Label return null; var strings = s.Replace(',', '.').Split(' '); - if (strings.Length != 5) + if (strings.Length < 5) throw new Exception("Wrong labels format!"); + if (strings.Length > 5) + strings = strings[..5]; + var res = new YoloLabel { diff --git a/Azaion.CommonSecurity/DTO/LoginResponse.cs b/Azaion.Common/DTO/LoginResponse.cs similarity index 67% rename from Azaion.CommonSecurity/DTO/LoginResponse.cs rename to Azaion.Common/DTO/LoginResponse.cs index a9d070f..b2a027b 100644 --- a/Azaion.CommonSecurity/DTO/LoginResponse.cs +++ b/Azaion.Common/DTO/LoginResponse.cs @@ -1,4 +1,4 @@ -namespace Azaion.CommonSecurity.DTO; +namespace Azaion.Common.DTO; public class LoginResponse { diff --git a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs index 30a5fec..1f130f9 100644 --- a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs +++ b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs @@ -1,27 +1,29 @@ using Azaion.Common.Database; -using Azaion.CommonSecurity.DTO; namespace Azaion.Common.DTO.Queue; using MessagePack; [MessagePackObject] -public class AnnotationCreatedMessage +public class AnnotationMessage { - [Key(0)] public DateTime CreatedDate { get; set; } - [Key(1)] public string Name { get; set; } = null!; - [Key(2)] public string OriginalMediaName { get; set; } = null!; - [Key(3)] public TimeSpan Time { get; set; } - [Key(4)] public string ImageExtension { get; set; } = null!; - [Key(5)] public string Detections { get; set; } = null!; - [Key(6)] public byte[] Image { get; set; } = null!; - [Key(7)] public RoleEnum CreatedRole { get; set; } - [Key(8)] public string CreatedEmail { get; set; } = null!; - [Key(9)] public SourceEnum Source { get; set; } + [Key(0)] public DateTime CreatedDate { get; set; } + [Key(1)] public string Name { get; set; } = null!; + [Key(2)] public string OriginalMediaName { get; set; } = null!; + [Key(3)] public TimeSpan Time { get; set; } + [Key(4)] public string ImageExtension { get; set; } = null!; + [Key(5)] public string Detections { get; set; } = null!; + [Key(6)] public byte[]? Image { get; set; } = null!; + [Key(7)] public RoleEnum Role { get; set; } + [Key(8)] public string Email { get; set; } = null!; + [Key(9)] public SourceEnum Source { get; set; } [Key(10)] public AnnotationStatus Status { get; set; } } [MessagePackObject] -public class AnnotationValidatedMessage +public class AnnotationBulkMessage { - [Key(0)] public string Name { get; set; } = null!; + [Key(0)] public string[] AnnotationNames { get; set; } = null!; + [Key(1)] public AnnotationStatus AnnotationStatus { get; set; } + [Key(2)] public string Email { get; set; } = null!; + [Key(3)] public DateTime CreatedDate { get; set; } } \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/Commands/RemoteCommand.cs b/Azaion.Common/DTO/RemoteCommand.cs similarity index 55% rename from Azaion.CommonSecurity/DTO/Commands/RemoteCommand.cs rename to Azaion.Common/DTO/RemoteCommand.cs index 9fd9f7e..5edeea9 100644 --- a/Azaion.CommonSecurity/DTO/Commands/RemoteCommand.cs +++ b/Azaion.Common/DTO/RemoteCommand.cs @@ -1,9 +1,9 @@ using MessagePack; -namespace Azaion.CommonSecurity.DTO.Commands; +namespace Azaion.Common.DTO; [MessagePackObject] -public class RemoteCommand(CommandType commandType, byte[]? data = null) +public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null) { [Key("CommandType")] public CommandType CommandType { get; set; } = commandType; @@ -11,11 +11,16 @@ public class RemoteCommand(CommandType commandType, byte[]? data = null) [Key("Data")] public byte[]? Data { get; set; } = data; + [Key("Message")] + public string? Message { get; set; } = message; + public static RemoteCommand Create(CommandType commandType) => new(commandType); - public static RemoteCommand Create(CommandType commandType, T data) where T : class => - new(commandType, MessagePackSerializer.Serialize(data)); + public static RemoteCommand Create(CommandType commandType, T data, string? message = null) where T : class => + new(commandType, MessagePackSerializer.Serialize(data), message); + + public override string ToString() => $"({CommandType.ToString().ToUpper()}: Data: {Data?.Length ?? 0} bytes. Message: {Message})"; } [MessagePackObject] @@ -32,9 +37,19 @@ public class LoadFileData(string filename, string? folder = null ) public enum CommandType { None = 0, + Ok = 3, Login = 10, + ListRequest = 15, + ListFiles = 18, Load = 20, + LoadBigSmall = 22, + UploadBigSmall = 24, + DataBytes = 25, Inference = 30, + InferenceData = 35, StopInference = 40, - Exit = 100 + AIAvailabilityCheck = 80, + AIAvailabilityResult = 85, + Error = 90, + Exit = 100, } \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/RoleEnum.cs b/Azaion.Common/DTO/RoleEnum.cs similarity index 86% rename from Azaion.CommonSecurity/DTO/RoleEnum.cs rename to Azaion.Common/DTO/RoleEnum.cs index 548d5e0..558bfff 100644 --- a/Azaion.CommonSecurity/DTO/RoleEnum.cs +++ b/Azaion.Common/DTO/RoleEnum.cs @@ -1,6 +1,4 @@ -using Azaion.Common.Extensions; - -namespace Azaion.CommonSecurity.DTO; +namespace Azaion.Common.DTO; public enum RoleEnum { diff --git a/Azaion.Common/DTO/User.cs b/Azaion.Common/DTO/User.cs new file mode 100644 index 0000000..e0ffb08 --- /dev/null +++ b/Azaion.Common/DTO/User.cs @@ -0,0 +1,21 @@ +namespace Azaion.Common.DTO; + +public class User +{ + 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? QueueOffsets { get; set; } = new(); +} + +public class UserQueueOffsets +{ + 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.Common/Database/Annotation.cs b/Azaion.Common/Database/Annotation.cs index b77e41e..c89f2ab 100644 --- a/Azaion.Common/Database/Annotation.cs +++ b/Azaion.Common/Database/Annotation.cs @@ -2,7 +2,6 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; -using Azaion.CommonSecurity.DTO; using MessagePack; namespace Azaion.Common.Database; @@ -31,6 +30,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; } @@ -56,5 +58,7 @@ public enum AnnotationStatus { None = 0, Created = 10, - Validated = 20 + Edited = 20, + Validated = 30, + Deleted = 40 } \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationName.cs b/Azaion.Common/Database/AnnotationName.cs deleted file mode 100644 index 709066f..0000000 --- a/Azaion.Common/Database/AnnotationName.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Azaion.Common.Database; - -public class AnnotationName -{ - public string Name { get; set; } = null!; -} \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationQueueRecord.cs b/Azaion.Common/Database/AnnotationQueueRecord.cs new file mode 100644 index 0000000..0e1524b --- /dev/null +++ b/Azaion.Common/Database/AnnotationQueueRecord.cs @@ -0,0 +1,9 @@ +namespace Azaion.Common.Database; + +public class AnnotationQueueRecord +{ + public Guid Id { get; set; } + public DateTime DateTime { get; set; } + public AnnotationStatus Operation { get; set; } + public List AnnotationNames { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationsDb.cs b/Azaion.Common/Database/AnnotationsDb.cs index 69ed6ae..c8434d9 100644 --- a/Azaion.Common/Database/AnnotationsDb.cs +++ b/Azaion.Common/Database/AnnotationsDb.cs @@ -7,7 +7,6 @@ namespace Azaion.Common.Database; public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions) { public ITable Annotations => this.GetTable(); - public ITable AnnotationsQueue => this.GetTable(); + public ITable AnnotationsQueueRecords => this.GetTable(); public ITable Detections => this.GetTable(); - public ITable QueueOffsets => this.GetTable(); } \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs index fdce46d..a46f7ff 100644 --- a/Azaion.Common/Database/DbFactory.cs +++ b/Azaion.Common/Database/DbFactory.cs @@ -1,28 +1,28 @@ using System.Data.SQLite; -using System.Diagnostics; using System.IO; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; +using Azaion.Common.Extensions; using LinqToDB; -using LinqToDB.Data; using LinqToDB.DataProvider.SQLite; using LinqToDB.Mapping; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json; namespace Azaion.Common.Database; public interface IDbFactory { Task Run(Func> func); - Task Run(Func func); - void SaveToDisk(); - Task DeleteAnnotations(List annotations, CancellationToken cancellationToken = default); + Task RunWrite(Func func); + Task RunWrite(Func> func); Task DeleteAnnotations(List annotationNames, CancellationToken cancellationToken = default); } public class DbFactory : IDbFactory { + private readonly ILogger _logger; private readonly AnnotationConfig _annConfig; private string MemoryConnStr => "Data Source=:memory:"; @@ -33,8 +33,12 @@ public class DbFactory : IDbFactory private readonly SQLiteConnection _fileConnection; private readonly DataOptions _fileDataOptions; + private static readonly SemaphoreSlim WriteSemaphore = new(1, 1); + private static readonly Guid SaveTaskId = Guid.NewGuid(); + public DbFactory(IOptions annConfig, ILogger logger) { + _logger = logger; _annConfig = annConfig.Value; _memoryConnection = new SQLiteConnection(MemoryConnStr); @@ -53,32 +57,24 @@ public class DbFactory : IDbFactory .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); if (!File.Exists(_annConfig.AnnotationsDbFile)) - CreateDb(); + SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); + RecreateTables(); + _fileConnection.Open(); _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); } - private void CreateDb() + private void RecreateTables() { - SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile); using var db = new AnnotationsDb(_fileDataOptions); - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - db.CreateTable(); - db.QueueOffsets.BulkCopy(new List - { - new() - { - Offset = 0, - QueueName = Constants.MQ_ANNOTATIONS_QUEUE - }, - new() - { - Offset = 0, - QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE - } - }); + var schema = db.DataProvider.GetSchemaProvider().GetSchema(db); + var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet(); + if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME)) + db.CreateTable(); + if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME)) + db.CreateTable(); + if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME)) + db.CreateTable(); } public async Task Run(Func> func) @@ -87,32 +83,63 @@ public class DbFactory : IDbFactory return await func(db); } - public async Task Run(Func func) + public async Task RunWrite(Func func) { - await using var db = new AnnotationsDb(_memoryDataOptions); - await func(db); + await WriteSemaphore.WaitAsync(); + try + { + await using var db = new AnnotationsDb(_memoryDataOptions); + await func(db); + ThrottleExt.Throttle(async () => + { + _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); + await Task.CompletedTask; + }, SaveTaskId, TimeSpan.FromSeconds(5), true); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + finally + { + WriteSemaphore.Release(); + } } - public void SaveToDisk() + public async Task RunWrite(Func> func) { - _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); - } - - public async Task DeleteAnnotations(List annotations, CancellationToken cancellationToken = default) - { - var names = annotations.Select(x => x.Name).ToList(); - await DeleteAnnotations(names, cancellationToken); + await WriteSemaphore.WaitAsync(); + try + { + await using var db = new AnnotationsDb(_memoryDataOptions); + var result = await func(db); + ThrottleExt.Throttle(async () => + { + _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); + await Task.CompletedTask; + }, SaveTaskId, TimeSpan.FromSeconds(5), true); + return result; + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + throw; + } + finally + { + WriteSemaphore.Release(); + } } public async Task DeleteAnnotations(List annotationNames, CancellationToken cancellationToken = default) { - await Run(async db => + await RunWrite(async db => { var detDeleted = await db.Detections.DeleteAsync(x => annotationNames.Contains(x.AnnotationName), token: cancellationToken); var annDeleted = await db.Annotations.DeleteAsync(x => annotationNames.Contains(x.Name), token: cancellationToken); Console.WriteLine($"Deleted {detDeleted} detections, {annDeleted} annotations"); }); - SaveToDisk(); } } @@ -142,8 +169,12 @@ public static class AnnotationsDbSchemaHolder builder.Entity() .HasTableName(Constants.DETECTIONS_TABLENAME); - builder.Entity() - .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME); + builder.Entity() + .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME) + .HasPrimaryKey(x => x.Id) + .Property(x => x.AnnotationNames) + .HasDataType(DataType.NVarChar) + .HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject>(str) ?? new List()); builder.Build(); } diff --git a/Azaion.Common/Database/QueueOffset.cs b/Azaion.Common/Database/QueueOffset.cs deleted file mode 100644 index 1fe3d0f..0000000 --- a/Azaion.Common/Database/QueueOffset.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Azaion.Common.Database; - -public class QueueOffset -{ - public string QueueName { get; set; } = null!; - public ulong Offset { get; set; } -} \ No newline at end of file diff --git a/Azaion.Common/Events/AnnotationsDeletedEvent.cs b/Azaion.Common/Events/AnnotationsDeletedEvent.cs index 1e815f2..c597e2d 100644 --- a/Azaion.Common/Events/AnnotationsDeletedEvent.cs +++ b/Azaion.Common/Events/AnnotationsDeletedEvent.cs @@ -3,7 +3,13 @@ using MediatR; namespace Azaion.Common.Events; -public class AnnotationsDeletedEvent(List annotations) : INotification +public class AnnotationsDeletedEvent(List annotationNames, bool fromQueue = false) : INotification { - public List Annotations { get; set; } = annotations; + public List AnnotationNames { get; set; } = annotationNames; + public bool FromQueue { get; set; } = fromQueue; +} + +public class AnnotationAddedEvent(Annotation annotation) : INotification +{ + public Annotation Annotation { get; set; } = annotation; } \ No newline at end of file diff --git a/Azaion.Common/Events/LoadErrorEvent.cs b/Azaion.Common/Events/LoadErrorEvent.cs new file mode 100644 index 0000000..051827c --- /dev/null +++ b/Azaion.Common/Events/LoadErrorEvent.cs @@ -0,0 +1,8 @@ +using MediatR; + +namespace Azaion.Common.Events; + +public class LoadErrorEvent(string error) : INotification +{ + public string Error { get; set; } = error; +} \ No newline at end of file diff --git a/Azaion.Common/Events/SetStatusTextEvent.cs b/Azaion.Common/Events/SetStatusTextEvent.cs new file mode 100644 index 0000000..7881f77 --- /dev/null +++ b/Azaion.Common/Events/SetStatusTextEvent.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace Azaion.Common.Events; + +public class SetStatusTextEvent(string text, bool isError = false) : INotification +{ + public string Text { get; set; } = text; + public bool IsError { get; set; } = isError; +} \ No newline at end of file diff --git a/Azaion.Common/Exceptions/BusinessException.cs b/Azaion.Common/Exceptions/BusinessException.cs new file mode 100644 index 0000000..51a5a9a --- /dev/null +++ b/Azaion.Common/Exceptions/BusinessException.cs @@ -0,0 +1,3 @@ +namespace Azaion.CommonSecurity.Exceptions; + +public class BusinessException(string message) : Exception(message); \ No newline at end of file diff --git a/Azaion.Common/Extensions/BitmapExtensions.cs b/Azaion.Common/Extensions/BitmapExtensions.cs index 4e13bee..294a8bb 100644 --- a/Azaion.Common/Extensions/BitmapExtensions.cs +++ b/Azaion.Common/Extensions/BitmapExtensions.cs @@ -1,4 +1,5 @@ using System.IO; +using System.Windows.Media; using System.Windows.Media.Imaging; namespace Azaion.Common.Extensions; @@ -22,4 +23,7 @@ public static class BitmapExtensions image.Freeze(); return image; } + + public static Color CreateTransparent(this Color color, byte transparency) => + Color.FromArgb(transparency, color.R, color.G, color.B); } \ No newline at end of file diff --git a/Azaion.Common/Extensions/CancellationTokenExtensions.cs b/Azaion.Common/Extensions/CancellationTokenExtensions.cs new file mode 100644 index 0000000..fbde775 --- /dev/null +++ b/Azaion.Common/Extensions/CancellationTokenExtensions.cs @@ -0,0 +1,30 @@ +namespace Azaion.Common.Extensions; + +public static class CancellationTokenExtensions +{ + public static void WaitForCancel(this CancellationToken token, TimeSpan timeout) + { + try + { + Task.Delay(timeout, token).Wait(token); + } + catch (OperationCanceledException) + { + //Don't need to catch exception, need only return from the waiting + } + } + + public static Task AsTask(this CancellationToken cancellationToken) + { + if (!cancellationToken.CanBeCanceled) + return new TaskCompletionSource().Task; + + if (cancellationToken.IsCancellationRequested) + return Task.FromCanceled(cancellationToken); + + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var registration = cancellationToken.Register(() => tcs.TrySetResult(true)); + tcs.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default); + return tcs.Task; + } +} \ No newline at end of file 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/ResilienceExt.cs b/Azaion.Common/Extensions/ResilienceExt.cs new file mode 100644 index 0000000..fb8f382 --- /dev/null +++ b/Azaion.Common/Extensions/ResilienceExt.cs @@ -0,0 +1,15 @@ +using Polly; + +public static class ResilienceExt +{ + public static void WithRetry(this Action operation, int retryCount = 3, int delayMs = 150) => + Policy.Handle() + .WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs), + (exception, timeSpan) => Console.WriteLine($"Exception: {exception}, TimeSpan: {timeSpan}")) + .Execute(operation); + + public static TResult WithRetry(this Func operation, int retryCount = 3, int delayMs = 150) => + Policy.Handle() + .WaitAndRetry(retryCount, num => TimeSpan.FromMilliseconds(num * delayMs)) + .Execute(operation); +} \ No newline at end of file diff --git a/Azaion.Common/Extensions/ThrottleExtensions.cs b/Azaion.Common/Extensions/ThrottleExtensions.cs index 72cd29e..b0c46c2 100644 --- a/Azaion.Common/Extensions/ThrottleExtensions.cs +++ b/Azaion.Common/Extensions/ThrottleExtensions.cs @@ -4,54 +4,68 @@ namespace Azaion.Common.Extensions; public static class ThrottleExt { - private static ConcurrentDictionary _taskStates = new(); - - public static async Task ThrottleRunFirst(Func func, Guid actionId, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default) + private class ThrottleState(Func action) { - if (_taskStates.ContainsKey(actionId) && _taskStates[actionId]) - return; + public Func Action { get; set; } = action ?? throw new ArgumentNullException(nameof(action)); + public bool IsCoolingDown = false; + public bool CallScheduledDuringCooldown = false; + public Task CooldownTask = Task.CompletedTask; + public readonly object StateLock = new(); + } - _taskStates[actionId] = true; + private static readonly ConcurrentDictionary ThrottlerStates = new(); + + public static void Throttle(Func action, Guid actionId, TimeSpan interval, bool scheduleCallAfterCooldown = false) + { + 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)); + state.Action = action; + + lock (state.StateLock) + { + if (!state.IsCoolingDown) + { + state.IsCoolingDown = true; + state.CooldownTask = ExecuteAndManageCooldownStaticAsync(actionId, interval, state); + } + else + { + if (scheduleCallAfterCooldown) + state.CallScheduledDuringCooldown = true; + } + } + } + + private static async Task ExecuteAndManageCooldownStaticAsync(Guid throttleId, TimeSpan interval, ThrottleState state) + { try { - await func(); + await state.Action(); } - catch (Exception e) + catch (Exception ex) { - Console.WriteLine(e); + Console.WriteLine($"[Throttled Action Error - ID: {throttleId}] {ex.GetType().Name}: {ex.Message}"); } - - _ = Task.Run(async () => + finally { - 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(interval); + lock (state.StateLock) { - await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); - await func(); + if (state.CallScheduledDuringCooldown) + { + state.CallScheduledDuringCooldown = false; + state.CooldownTask = ExecuteAndManageCooldownStaticAsync(throttleId, interval, state); + } + else + { + state.IsCoolingDown = false; + } } - catch (Exception) - { - _taskStates[actionId] = false; - } - finally - { - _taskStates[actionId] = false; - } - - }, cancellationToken); - await Task.CompletedTask; + } } - } \ No newline at end of file diff --git a/Azaion.Common/SecurityConstants.cs b/Azaion.Common/SecurityConstants.cs new file mode 100644 index 0000000..b251dcb --- /dev/null +++ b/Azaion.Common/SecurityConstants.cs @@ -0,0 +1,79 @@ +using System.IO; +using Azaion.Common.DTO; +using Newtonsoft.Json; + +namespace Azaion.Common; + +public class SecurityConstants +{ + public const string CONFIG_PATH = "config.json"; + + private const string DEFAULT_API_URL = "https://api.azaion.com"; + + #region ExternalClientsConfig + + private const string DEFAULT_ZMQ_LOADER_HOST = "127.0.0.1"; + private const int DEFAULT_ZMQ_LOADER_PORT = 5025; + + public const string EXTERNAL_LOADER_PATH = "azaion-loader.exe"; + 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"); + + public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1"; + public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227; + + public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1"; + public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227; + + # region Cache keys + + public const string CURRENT_USER_CACHE_KEY = "CurrentUser"; + public const string HARDWARE_INFO_KEY = "HardwareInfo"; + + # endregion + + public static readonly InitConfig DefaultInitConfig = new() + { + LoaderClientConfig = new LoaderClientConfig + { + ZeroMqHost = DEFAULT_ZMQ_LOADER_HOST, + ZeroMqPort = DEFAULT_ZMQ_LOADER_PORT, + ApiUrl = DEFAULT_API_URL + }, + InferenceClientConfig = new InferenceClientConfig + { + ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST, + ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT, + ApiUrl = DEFAULT_API_URL + }, + GpsDeniedClientConfig = new GpsDeniedClientConfig + { + ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST, + ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT + }, + DirectoriesConfig = new DirectoriesConfig + { + ApiResourcesDirectory = "" + } + }; + #endregion ExternalClientsConfig + + public static InitConfig ReadInitConfig() + { + try + { + if (!File.Exists(CONFIG_PATH)) + throw new FileNotFoundException(CONFIG_PATH); + var configStr = File.ReadAllText(CONFIG_PATH); + var config = JsonConvert.DeserializeObject(configStr); + + return config ?? DefaultInitConfig; + } + catch (Exception e) + { + Console.WriteLine(e); + return DefaultInitConfig; + } + } +} \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs index 5b528bf..9143691 100644 --- a/Azaion.Common/Services/AnnotationService.cs +++ b/Azaion.Common/Services/AnnotationService.cs @@ -1,4 +1,5 @@ -using System.Drawing.Imaging; +using System.Drawing; +using System.Drawing.Imaging; using System.IO; using System.Net; using Azaion.Common.Database; @@ -7,12 +8,11 @@ using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; using Azaion.Common.Events; using Azaion.Common.Extensions; -using Azaion.CommonSecurity.DTO; -using Azaion.CommonSecurity.Services; using LinqToDB; using LinqToDB.Data; using MediatR; using MessagePack; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using RabbitMQ.Stream.Client; @@ -20,45 +20,48 @@ using RabbitMQ.Stream.Client.Reliable; namespace Azaion.Common.Services; -public class AnnotationService : INotificationHandler +// SHOULD BE ONLY ONE INSTANCE OF AnnotationService. Do not add ANY NotificationHandler to it! +public class AnnotationService : IAnnotationService { private readonly IDbFactory _dbFactory; 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 ILogger _logger; private readonly QueueConfig _queueConfig; private Consumer _consumer = null!; private readonly UIConfig _uiConfig; - private static readonly Guid SaveTaskId = Guid.NewGuid(); + private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1); + private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1); + private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid(); + public AnnotationService( - IResourceLoader resourceLoader, IDbFactory dbFactory, FailsafeAnnotationsProducer producer, IOptions queueConfig, IOptions uiConfig, IGalleryService galleryService, IMediator mediator, - IHardwareService hardwareService, - IAuthProvider authProvider) + IAzaionApi api, + ILogger logger) { _dbFactory = dbFactory; _producer = producer; _galleryService = galleryService; _mediator = mediator; - _hardwareService = hardwareService; - _authProvider = authProvider; + _api = api; + _logger = logger; _queueConfig = queueConfig.Value; _uiConfig = uiConfig.Value; - Task.Run(async () => await Init()).Wait(); + Task.Run(async () => await InitQueueConsumer()).Wait(); } - private async Task Init(CancellationToken cancellationToken = default) + private async Task InitQueueConsumer(CancellationToken cancellationToken = default) { - if (!_authProvider.CurrentUser.Role.IsValidator()) + if (!_api.CurrentUser.Role.IsValidator()) return; var consumerSystem = await StreamSystem.Create(new StreamSystemConfig @@ -68,43 +71,63 @@ 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 offsets = _api.CurrentUser.UserConfig?.QueueOffsets ?? new UserQueueOffsets(); _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) { - Reference = _hardwareService.GetHardware().Hash, - OffsetSpec = new OffsetTypeOffset(offset + 1), + Reference = _api.CurrentUser.Email, + OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset), 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.ThrottleRunAfter(() => + await _messageProcessingSemaphore.WaitAsync(cancellationToken); + try { - _dbFactory.SaveToDisk(); - return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), cancellationToken); + var email = (string)message.ApplicationProperties[nameof(User.Email)]!; + if (!Enum.TryParse((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus)) + return; - if (msg.CreatedEmail == _authProvider.CurrentUser.Email) //Don't process messages by yourself - return; - - await SaveAnnotationInner( - msg.CreatedDate, - msg.OriginalMediaName, - msg.Time, - JsonConvert.DeserializeObject>(msg.Detections) ?? [], - msg.Source, - new MemoryStream(msg.Image), - msg.CreatedRole, - msg.CreatedEmail, - generateThumbnail: true, - fromQueue: true, - token: cancellationToken); + if (email != _api.CurrentUser.Email) //Don't process messages by yourself + { + if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited)) + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + await SaveAnnotationInner( + msg.CreatedDate, + msg.OriginalMediaName, + msg.Time, + JsonConvert.DeserializeObject>(msg.Detections) ?? [], + msg.Source, + msg.Image == null ? null : new MemoryStream(msg.Image), + msg.Role, + msg.Email, + context.Offset, + token: cancellationToken); + } + else + { + var msg = MessagePackSerializer.Deserialize(message.Data.Contents); + if (annotationStatus == AnnotationStatus.Validated) + await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken); + if (annotationStatus == AnnotationStatus.Deleted) + await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken); + } + } + + offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message + ThrottleExt.Throttle(() => + { + _api.UpdateOffsets(offsets); + return Task.CompletedTask; + }, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true); + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + } + finally + { + _messageProcessingSemaphore.Release(); + } } }); } @@ -113,51 +136,51 @@ public class AnnotationService : INotificationHandler public async Task SaveAnnotation(AnnotationImage a, CancellationToken ct = default) { 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); + return await SaveAnnotationInner(DateTime.UtcNow, a.OriginalMediaName, a.Time, a.Detections.ToList(), + 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, - _authProvider.CurrentUser.Role, _authProvider.CurrentUser.Email, generateThumbnail: true, token: token); + _api.CurrentUser.Role, _api.CurrentUser.Email, 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); - - // Manual save from Validators -> Validated -> stream: azaion-annotations-confirm - // AI, Manual save from Operators -> Created -> stream: azaion-annotations - private async Task SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List detections, SourceEnum source, Stream? stream, + private async Task SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, + List detections, SourceEnum source, Stream? stream, RoleEnum userRole, string createdEmail, - bool generateThumbnail = false, - bool fromQueue = false, + ulong? offset = null, CancellationToken token = default) { - - AnnotationStatus status; + var status = AnnotationStatus.Created; var fName = originalMediaName.ToTimeName(time); - var annotation = await _dbFactory.Run(async db => + var annotation = await _dbFactory.RunWrite(async db => { - var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token); - status = userRole.IsValidator() && source == SourceEnum.Manual - ? AnnotationStatus.Validated - : AnnotationStatus.Created; + var ann = await db.Annotations + .LoadWith(x => x.Detections) + .FirstOrDefaultAsync(x => x.Name == fName, token: token); await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); - await db.BulkCopyAsync(detections, cancellationToken: token); - if (ann != null) + + if (ann != null) //Annotation is already exists { - await db.Annotations + status = AnnotationStatus.Edited; + + var annotationUpdatable = db.Annotations .Where(x => x.Name == fName) - .Set(x => x.Source, source) + .Set(x => x.Source, source); + + if (userRole.IsValidator() && source == SourceEnum.Manual) + { + annotationUpdatable = annotationUpdatable + .Set(x => x.ValidateDate, createdDate) + .Set(x => x.ValidateEmail, createdEmail); + } + + await annotationUpdatable .Set(x => x.AnnotationStatus, status) - .Set(x => x.CreatedDate, createdDate) - .Set(x => x.CreatedEmail, createdEmail) - .Set(x => x.CreatedRole, userRole) .UpdateAsync(token: token); + ann.Detections = detections; } else @@ -177,43 +200,70 @@ public class AnnotationService : INotificationHandler }; await db.InsertAsync(ann, token: token); } + await db.BulkCopyAsync(detections, cancellationToken: token); return ann; }); - if (stream != null) + //Save image should be done in 1 thread only + await _imageAccessSemaphore.WaitAsync(token); + try { - var img = System.Drawing.Image.FromStream(stream); - 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); + Image image = null!; + if (stream != null) + { + image = Image.FromStream(stream); + if (File.Exists(annotation.ImagePath)) + ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath)); + image.Save(annotation.ImagePath, ImageFormat.Jpeg); + } + + await YoloLabel.WriteToFile(detections, annotation.LabelPath, token); + + await _galleryService.CreateThumbnail(annotation, image, token); if (_uiConfig.GenerateAnnotatedImage) - await _galleryService.CreateAnnotatedImage(annotation, token); + await _galleryService.CreateAnnotatedImage(annotation, image, token); + } + catch (Exception e) + { + _logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}"); + throw; + } + finally + { + _imageAccessSemaphore.Release(); } - - - if (!fromQueue) //Send to queue only if we're not getting from queue already - await _producer.SendToInnerQueue(annotation, token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); - await ThrottleExt.ThrottleRunAfter(() => - { - _dbFactory.SaveToDisk(); - return Task.CompletedTask; - }, SaveTaskId, TimeSpan.FromSeconds(5), token); + + if (!offset.HasValue) //Send to queue only if we're not getting from queue already + await _producer.SendToInnerQueue([annotation.Name], status, token); + return annotation; } - public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) + public async Task ValidateAnnotations(List annotationNames, bool fromQueue = false, CancellationToken token = default) { - await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken); - foreach (var annotation in notification.Annotations) + if (!_api.CurrentUser.Role.IsValidator()) + return; + + var annNames = annotationNames.ToHashSet(); + await _dbFactory.RunWrite(async db => { - File.Delete(annotation.ImagePath); - File.Delete(annotation.LabelPath); - File.Delete(annotation.ThumbPath); - } + 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); + }); + if (!fromQueue) + await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token); } +} + +public interface IAnnotationService +{ + Task SaveAnnotation(AnnotationImage a, CancellationToken ct = default); + Task SaveAnnotation(string originalMediaName, TimeSpan time, List detections, Stream? stream = null, CancellationToken token = default); + Task ValidateAnnotations(List annotationNames, bool fromQueue = false, CancellationToken token = default); } \ No newline at end of file diff --git a/Azaion.Common/Services/AuthProvider.cs b/Azaion.Common/Services/AuthProvider.cs new file mode 100644 index 0000000..841a35e --- /dev/null +++ b/Azaion.Common/Services/AuthProvider.cs @@ -0,0 +1,125 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Azaion.Common.DTO; +using Newtonsoft.Json; + +namespace Azaion.Common.Services; + +public interface IAzaionApi +{ + ApiCredentials Credentials { get; } + User CurrentUser { get; } + void UpdateOffsets(UserQueueOffsets offsets); + //Stream GetResource(string filename, string folder); +} + +public class AzaionApi(HttpClient client, ICache cache, ApiCredentials credentials) : IAzaionApi +{ + private string _jwtToken = null!; + const string APP_JSON = "application/json"; + public ApiCredentials Credentials => credentials; + + public User CurrentUser + { + get + { + var user = cache.GetFromCache(SecurityConstants.CURRENT_USER_CACHE_KEY, + () => Get("currentUser")); + if (user == null) + throw new Exception("Can't get current user"); + return user; + } + } + + public void UpdateOffsets(UserQueueOffsets offsets) + { + Put($"/users/queue-offsets/set", new + { + Email = CurrentUser.Email, + Offsets = offsets + }); + } + + private HttpResponseMessage Send(HttpRequestMessage request) + { + 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}"); + } + + private 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); + } + + private void Put(string url, T obj) + { + Send(new HttpRequestMessage(HttpMethod.Put, url) + { + Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON) + }); + } + + 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; + } + } + + +} \ No newline at end of file diff --git a/Azaion.Common/Services/Cache.cs b/Azaion.Common/Services/Cache.cs new file mode 100644 index 0000000..a1454db --- /dev/null +++ b/Azaion.Common/Services/Cache.cs @@ -0,0 +1,27 @@ +using LazyCache; + +namespace Azaion.Common.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.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs index c46fcc2..8d49bca 100644 --- a/Azaion.Common/Services/FailsafeProducer.cs +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -1,14 +1,17 @@ using System.IO; using System.Net; using Azaion.Common.Database; +using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; +using Azaion.Common.Extensions; using LinqToDB; using MessagePack; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using RabbitMQ.Stream.Client; +using RabbitMQ.Stream.Client.AMQP; using RabbitMQ.Stream.Client.Reliable; namespace Azaion.Common.Services; @@ -17,17 +20,24 @@ public class FailsafeAnnotationsProducer { private readonly ILogger _logger; private readonly IDbFactory _dbFactory; + private readonly IAzaionApi _azaionApi; private readonly QueueConfig _queueConfig; + private readonly UIConfig _uiConfig; private Producer _annotationProducer = null!; - private Producer _annotationConfirmProducer = null!; - public FailsafeAnnotationsProducer(ILogger logger, IDbFactory dbFactory, IOptions queueConfig) + public FailsafeAnnotationsProducer(ILogger logger, + IDbFactory dbFactory, + IOptions queueConfig, + IOptions uiConfig, + IAzaionApi azaionApi) { _logger = logger; _dbFactory = dbFactory; + _azaionApi = azaionApi; _queueConfig = queueConfig.Value; + _uiConfig = uiConfig.Value; Task.Run(async () => await ProcessQueue()); } @@ -41,107 +51,112 @@ public class FailsafeAnnotationsProducer }); } - private async Task Init(CancellationToken cancellationToken = default) + private async Task ProcessQueue(CancellationToken ct = default) { _annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE)); - _annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE)); - } - - private async Task ProcessQueue(CancellationToken cancellationToken = default) - { - await Init(cancellationToken); - while (!cancellationToken.IsCancellationRequested) + while (!ct.IsCancellationRequested) { - var messages = await GetFromInnerQueue(cancellationToken); - foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10 - { - var sent = false; - while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send - { - try - { - var createdMessages = messagesChunk - .Where(x => x.Status == AnnotationStatus.Created) - .Select(x => new Message(MessagePackSerializer.Serialize(x))) - .ToList(); - if (createdMessages.Any()) - await _annotationProducer.Send(createdMessages, CompressionType.Gzip); - - var validatedMessages = messagesChunk - .Where(x => x.Status == AnnotationStatus.Validated) - .Select(x => new Message(MessagePackSerializer.Serialize(x))) - .ToList(); - if (validatedMessages.Any()) - await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip); - - await _dbFactory.Run(async db => - await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken)); - sent = true; - _dbFactory.SaveToDisk(); - } - catch (Exception e) - { - _logger.LogError(e, e.Message); - await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); - } - await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); - } - } - await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken); - } - } - - private async Task> GetFromInnerQueue(CancellationToken cancellationToken = default) - { - return await _dbFactory.Run(async db => - { - var annotations = await db.AnnotationsQueue.Join( - db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a) - .ToListAsync(token: cancellationToken); - - var messages = new List(); - var badImages = new List(); - foreach (var annotation in annotations) + var sent = false; + while (!sent || !ct.IsCancellationRequested) //Waiting for send { try { - var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); - var annCreateMessage = new AnnotationCreatedMessage + var (records, annotationsDict) = await _dbFactory.Run(async db => { - Name = annotation.Name, - OriginalMediaName = annotation.OriginalMediaName, - Time = annotation.Time, - CreatedRole = annotation.CreatedRole, - CreatedEmail = annotation.CreatedEmail, - CreatedDate = annotation.CreatedDate, - Status = annotation.AnnotationStatus, + var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct); + var editedCreatedNames = records + .Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited)) + .Select(x => x.AnnotationNames.FirstOrDefault()) + .ToList(); - ImageExtension = annotation.ImageExtension, - Image = image, - Detections = JsonConvert.SerializeObject(annotation.Detections), - Source = annotation.Source, - }; - messages.Add(annCreateMessage); + var annotationsDict = await db.Annotations.LoadWith(x => x.Detections) + .Where(x => editedCreatedNames.Contains(x.Name)) + .ToDictionaryAsync(a => a.Name, token: ct); + return (records, annotationsDict); + }); + + var messages = new List(); + foreach (var record in records) + { + var appProperties = new ApplicationProperties + { + { nameof(AnnotationStatus), record.Operation.ToString() }, + { nameof(User.Email), _azaionApi.CurrentUser.Email } + }; + + if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted)) + { + var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage + { + AnnotationNames = record.AnnotationNames.ToArray(), + AnnotationStatus = record.Operation, + Email = _azaionApi.CurrentUser.Email, + CreatedDate = record.DateTime + })) { ApplicationProperties = appProperties }; + + messages.Add(message); + } + else + { + var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault()); + if (annotation == null) + continue; + + var image = record.Operation == AnnotationStatus.Created + ? await File.ReadAllBytesAsync(annotation.ImagePath, ct) + : null; + + var annMessage = new AnnotationMessage + { + Name = annotation.Name, + OriginalMediaName = annotation.OriginalMediaName, + Time = annotation.Time, + Role = annotation.CreatedRole, + Email = annotation.CreatedEmail, + CreatedDate = annotation.CreatedDate, + Status = annotation.AnnotationStatus, + + ImageExtension = annotation.ImageExtension, + Image = image, + Detections = JsonConvert.SerializeObject(annotation.Detections), + Source = annotation.Source, + }; + var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties }; + + messages.Add(message); + } + } + + if (messages.Any()) + { + await _annotationProducer.Send(messages, CompressionType.Gzip); + var ids = records.Select(x => x.Id).ToList(); + var removed = await _dbFactory.RunWrite(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct)); + sent = true; + } } catch (Exception e) { _logger.LogError(e, e.Message); - badImages.Add(annotation.Name); + await Task.Delay(TimeSpan.FromSeconds(10), ct); } + await Task.Delay(TimeSpan.FromSeconds(10), ct); } - - if (badImages.Any()) - { - await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken); - _dbFactory.SaveToDisk(); - } - return messages; - }); + } + await Task.Delay(TimeSpan.FromSeconds(5), ct); } - public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default) + public async Task SendToInnerQueue(List annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default) { - await _dbFactory.Run(async db => - await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken)); + if (_uiConfig.SilentDetection) + return; + await _dbFactory.RunWrite(async db => + await db.InsertAsync(new AnnotationQueueRecord + { + Id = Guid.NewGuid(), + DateTime = DateTime.UtcNow, + Operation = status, + AnnotationNames = annotationNames + }, token: cancellationToken)); } } \ No newline at end of file diff --git a/Azaion.Common/Services/GPSMatcherEventHandler.cs b/Azaion.Common/Services/GPSMatcherEventHandler.cs new file mode 100644 index 0000000..4790616 --- /dev/null +++ b/Azaion.Common/Services/GPSMatcherEventHandler.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace Azaion.Common.Services; + +public class GPSMatcherEventHandler(IGpsMatcherService gpsMatcherService) : + INotificationHandler, + INotificationHandler +{ + public async Task Handle(GPSMatcherResultEvent result, CancellationToken cancellationToken) => + await gpsMatcherService.SetGpsResult(result, cancellationToken); + + public async Task Handle(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken) => + await gpsMatcherService.FinishGPS(notification, cancellationToken); +} \ No newline at end of file diff --git a/Azaion.Common/Services/GPSMatcherEvents.cs b/Azaion.Common/Services/GPSMatcherEvents.cs new file mode 100644 index 0000000..ec1cb95 --- /dev/null +++ b/Azaion.Common/Services/GPSMatcherEvents.cs @@ -0,0 +1,18 @@ +using MediatR; + +namespace Azaion.Common.Services; + +public class GPSMatcherResultEvent : INotification +{ + public int Index { get; set; } + public string Image { get; set; } = null!; + public double Latitude { get; set; } + public double Longitude { get; set; } + public int KeyPoints { get; set; } + public int Rotation { get; set; } + public string MatchType { get; set; } = null!; +} + +public class GPSMatcherJobAcceptedEvent : INotification {} + +public class GPSMatcherFinishedEvent : INotification {} \ No newline at end of file diff --git a/Azaion.Common/Services/GPSMatcherService.cs b/Azaion.Common/Services/GPSMatcherService.cs index cd803d6..79aae9d 100644 --- a/Azaion.Common/Services/GPSMatcherService.cs +++ b/Azaion.Common/Services/GPSMatcherService.cs @@ -1,73 +1,97 @@ -using System.Diagnostics; -using System.IO; +using System.IO; using Azaion.Common.DTO; -using Azaion.Common.DTO.Config; -using Azaion.CommonSecurity; using Microsoft.Extensions.Options; namespace Azaion.Common.Services; public interface IGpsMatcherService { - Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func processResult, CancellationToken detectToken = default); + Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default); void StopGpsMatching(); + Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default); + Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken); } public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions dirConfig) : IGpsMatcherService { + private readonly DirectoriesConfig _dirConfig = dirConfig.Value; private const int ZOOM_LEVEL = 18; private const int POINTS_COUNT = 10; private const int DISTANCE_BETWEEN_POINTS_M = 100; private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1); - public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, Func processResult, CancellationToken detectToken = default) + private string _routeDir = ""; + private string _userRouteDir = ""; + private List _allRouteImages = new(); + private Dictionary _currentRouteImages = new(); + private double _currentLat; + private double _currentLon; + private CancellationToken _detectToken; + private int _currentIndex; + + + public async Task RunGpsMatching(string userRouteDir, double initialLatitude, double initialLongitude, CancellationToken detectToken = default) { - var currentLat = initialLatitude; - var currentLon = initialLongitude; + _routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, _dirConfig.GpsRouteDirectory); + _userRouteDir = userRouteDir; - var routeDir = Path.Combine(SecurityConstants.EXTERNAL_GPS_DENIED_FOLDER, dirConfig.Value.GpsRouteDirectory); - if (Directory.Exists(routeDir)) - Directory.Delete(routeDir, true); - Directory.CreateDirectory(routeDir); + _allRouteImages = Directory.GetFiles(userRouteDir) + .OrderBy(x => x).ToList(); - var routeFiles = new List(); - foreach (var file in Directory.GetFiles(userRouteDir)) - { - routeFiles.Add(file); - File.Copy(file, Path.Combine(routeDir, Path.GetFileName(file))); - } + _currentLat = initialLatitude; + _currentLon = initialLongitude; - var indexOffset = 0; - while (routeFiles.Any()) - { - await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken); - gpsMatcherClient.StartMatching(new StartMatchingEvent + _detectToken = detectToken; + await StartMatchingRound(0); + } + + private async Task StartMatchingRound(int startIndex) + { + //empty route dir + if (Directory.Exists(_routeDir)) + Directory.Delete(_routeDir, true); + Directory.CreateDirectory(_routeDir); + + _currentRouteImages = _allRouteImages + .Skip(startIndex) + .Take(POINTS_COUNT) + .Select((fullName, index) => { - ImagesCount = POINTS_COUNT, - Latitude = initialLatitude, - Longitude = initialLongitude, - SatelliteImagesDir = dirConfig.Value.GpsSatDirectory, - RouteDir = dirConfig.Value.GpsRouteDirectory - }); + var filename = Path.GetFileName(fullName); + File.Copy(Path.Combine(_userRouteDir, filename), Path.Combine(_routeDir, filename)); + return new { Filename = Path.GetFileNameWithoutExtension(fullName), Index = startIndex + index }; + }) + .ToDictionary(x => x.Filename, x => x.Index); - while (true) - { - var result = gpsMatcherClient.GetResult(); - if (result == null) - break; - result.Index += indexOffset; - await processResult(result); - currentLat = result.Latitude; - currentLon = result.Longitude; - routeFiles.RemoveAt(0); - } - indexOffset += POINTS_COUNT; - } + await satelliteTileDownloader.GetTiles(_currentLat, _currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, _detectToken); + await gpsMatcherClient.StartMatching(new StartMatchingEvent + { + ImagesCount = POINTS_COUNT, + Latitude = _currentLat, + Longitude = _currentLon, + SatelliteImagesDir = _dirConfig.GpsSatDirectory, + RouteDir = _dirConfig.GpsRouteDirectory + }); } public void StopGpsMatching() { gpsMatcherClient.Stop(); } + + public async Task SetGpsResult(GPSMatcherResultEvent result, CancellationToken detectToken = default) + { + _currentIndex = _currentRouteImages[result.Image]; + _currentRouteImages.Remove(result.Image); + _currentLat = result.Latitude; + _currentLon = result.Longitude; + await Task.CompletedTask; + } + + public async Task FinishGPS(GPSMatcherFinishedEvent notification, CancellationToken cancellationToken) + { + if (_currentRouteImages.Count == 0 && _currentIndex < _allRouteImages.Count) + await StartMatchingRound(_currentIndex); + } } diff --git a/Azaion.Common/Services/GalleryService.cs b/Azaion.Common/Services/GalleryService.cs index 5331e6b..bf15376 100644 --- a/Azaion.Common/Services/GalleryService.cs +++ b/Azaion.Common/Services/GalleryService.cs @@ -9,7 +9,6 @@ using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Queue; using Azaion.Common.Extensions; -using Azaion.CommonSecurity.DTO; using LinqToDB; using LinqToDB.Data; using Microsoft.Extensions.Logging; @@ -61,7 +60,7 @@ public class GalleryService( { foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles()) file.Delete(); - await dbFactory.Run(async db => + await dbFactory.RunWrite(async db => { await db.Detections.DeleteAsync(x => true, token: cancellationToken); await db.Annotations.DeleteAsync(x => true, token: cancellationToken); @@ -73,7 +72,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 +88,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 +135,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,11 +145,18 @@ 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); + await CreateThumbnail(annotation, cancellationToken: cancellationToken); } catch (Exception e) @@ -181,24 +187,33 @@ public class GalleryService( { MaxBatchSize = 50 }; - await dbFactory.Run(async db => + + //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.RunWrite(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(); } } - public async Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default) + public async Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default) { try { var width = (int)_thumbnailConfig.Size.Width; var height = (int)_thumbnailConfig.Size.Height; - var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken))); + originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken))); var bitmap = new Bitmap(width, height); @@ -265,10 +280,9 @@ public class GalleryService( logger.LogError(e, e.Message); } } - - public async Task CreateAnnotatedImage(Annotation annotation, CancellationToken token) + public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default) { - var originalImage = Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token))); + originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token))); using var g = Graphics.FromImage(originalImage); foreach (var detection in annotation.Detections) @@ -282,17 +296,20 @@ public class GalleryService( var label = detection.Confidence >= 0.995 ? detClass.UIName : $"{detClass.UIName}: {detection.Confidence * 100:F0}%"; g.DrawTextBox(label, new PointF((float)(det.X + det.Width / 2.0), (float)(det.Y - 24)), brush, Brushes.Black); } - originalImage.Save(Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg"), ImageFormat.Jpeg); + + var imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg"); + if (File.Exists(imagePath)) + ResilienceExt.WithRetry(() => File.Delete(imagePath)); + + originalImage.Save(imagePath, ImageFormat.Jpeg); } } public interface IGalleryService { event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate; - double ProcessedThumbnailsPercentage { get; set; } - Task CreateThumbnail(Annotation annotation, CancellationToken cancellationToken = default); + Task CreateThumbnail(Annotation annotation, Image? originalImage = null, CancellationToken cancellationToken = default); Task RefreshThumbnails(); Task ClearThumbnails(CancellationToken cancellationToken = default); - - Task CreateAnnotatedImage(Annotation annotation, CancellationToken token); + Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default); } \ No newline at end of file diff --git a/Azaion.Common/Services/GpsMatcherClient.cs b/Azaion.Common/Services/GpsMatcherClient.cs index cb8e225..1ee902a 100644 --- a/Azaion.Common/Services/GpsMatcherClient.cs +++ b/Azaion.Common/Services/GpsMatcherClient.cs @@ -1,18 +1,18 @@ using System.Diagnostics; +using System.IO; using Azaion.Common.DTO; -using Azaion.CommonSecurity; -using Azaion.CommonSecurity.DTO; +using Azaion.Common.Events; +using MediatR; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using NetMQ; using NetMQ.Sockets; namespace Azaion.Common.Services; -public interface IGpsMatcherClient +public interface IGpsMatcherClient : IDisposable { - - void StartMatching(StartMatchingEvent startEvent); - GpsMatchResult? GetResult(int retries = 2, int tryTimeoutSeconds = 5, CancellationToken ct = default); + Task StartMatching(StartMatchingEvent startEvent); void Stop(); } @@ -23,29 +23,28 @@ public class StartMatchingEvent public int ImagesCount { get; set; } public double Latitude { get; set; } public double Longitude { get; set; } - public string ProcessingType { get; set; } = "cuda"; public int Altitude { get; set; } = 400; public double CameraSensorWidth { get; set; } = 23.5; public double CameraFocalLength { get; set; } = 24; public override string ToString() => - $"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}"; + $"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{Altitude},{CameraSensorWidth},{CameraFocalLength}"; } public class GpsMatcherClient : IGpsMatcherClient { - private readonly GpsDeniedClientConfig _gpsDeniedClientConfig; + private readonly IMediator _mediator; + private readonly ILogger _logger; + private readonly string _requestAddress; private readonly RequestSocket _requestSocket = new(); + private readonly string _subscriberAddress; private readonly SubscriberSocket _subscriberSocket = new(); + private readonly NetMQPoller _poller = new(); - public GpsMatcherClient(IOptions gpsDeniedClientConfig) - { - _gpsDeniedClientConfig = gpsDeniedClientConfig.Value; - Start(); - } - - private void Start() + public GpsMatcherClient(IMediator mediator, IOptions gpsConfig, ILogger logger) { + _mediator = mediator; + _logger = logger; try { using var process = new Process(); @@ -61,58 +60,90 @@ public class GpsMatcherClient : IGpsMatcherClient process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; - //process.Start(); + process.Start(); } catch (Exception e) { Console.WriteLine(e); //throw; } - _requestSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqPort}"); - _subscriberSocket.Connect($"tcp://{_gpsDeniedClientConfig.ZeroMqHost}:{_gpsDeniedClientConfig.ZeroMqSubscriberPort}"); + + _requestAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqPort}"; + _requestSocket.Connect(_requestAddress); + + _subscriberAddress = $"tcp://{gpsConfig.Value.ZeroMqHost}:{gpsConfig.Value.ZeroMqReceiverPort}"; + _subscriberSocket.Connect(_subscriberAddress); _subscriberSocket.Subscribe(""); + _subscriberSocket.ReceiveReady += async (sender, e) => await ProcessClientCommand(sender, e); + + _poller.Add(_subscriberSocket); + _poller.RunAsync(); } - public void StartMatching(StartMatchingEvent e) + private async Task ProcessClientCommand(object? sender, NetMQSocketEventArgs e) + { + while (e.Socket.TryReceiveFrameString(TimeSpan.FromMilliseconds(100), out var str)) + { + try + { + if (string.IsNullOrEmpty(str)) + continue; + + switch (str) + { + case "FINISHED": + await _mediator.Publish(new GPSMatcherFinishedEvent()); + break; + case "OK": + await _mediator.Publish(new GPSMatcherJobAcceptedEvent()); + break; + default: + var parts = str.Split(','); + if (parts.Length != 5) + throw new Exception("Matching Result Failed"); + + var filename = Path.GetFileNameWithoutExtension(parts[1]); + await _mediator.Publish(new GPSMatcherResultEvent + { + Index = int.Parse(parts[0]), + Image = filename, + Latitude = double.Parse(parts[2]), + Longitude = double.Parse(parts[3]), + MatchType = parts[4] + }); + break; + } + } + catch (Exception ex) + { + _logger.LogError(ex, ex.Message); + } + } + } + + public async Task StartMatching(StartMatchingEvent e) { _requestSocket.SendFrame(e.ToString()); - var response = _requestSocket.ReceiveFrameString(); + _requestSocket.TryReceiveFrameString(TimeSpan.FromMilliseconds(300), out var response); if (response != "OK") - throw new Exception("Start Matching Failed"); - } - - public GpsMatchResult? GetResult(int retries = 15, int tryTimeoutSeconds = 5, CancellationToken ct = default) - { - var tryNum = 0; - while (!ct.IsCancellationRequested && tryNum++ < retries) { - if (!_subscriberSocket.TryReceiveFrameString(TimeSpan.FromSeconds(tryTimeoutSeconds), out var update)) - continue; - if (update == "FINISHED") - return null; - - var parts = update.Split(','); - if (parts.Length != 5) - throw new Exception("Matching Result Failed"); - - return new GpsMatchResult - { - Index = int.Parse(parts[0]), - Image = parts[1], - Latitude = double.Parse(parts[2]), - Longitude = double.Parse(parts[3]), - MatchType = parts[4] - }; + _logger.LogError(response); + await _mediator.Publish(new SetStatusTextEvent(response ?? "", true)); } - - if (!ct.IsCancellationRequested) - throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each"); - - return null; } - public void Stop() + public void Stop() => _requestSocket.SendFrame("STOP"); + + public void Dispose() { - _requestSocket.SendFrame("STOP"); + _poller.Stop(); + _poller.Dispose(); + + _requestSocket.SendFrame("EXIT"); + _requestSocket.Disconnect(_requestAddress); + _requestSocket.Dispose(); + + _subscriberSocket.Disconnect(_subscriberAddress); + _subscriberSocket.Dispose(); } } \ No newline at end of file diff --git a/Azaion.Common/Services/InferenceClient.cs b/Azaion.Common/Services/InferenceClient.cs new file mode 100644 index 0000000..c0f46ef --- /dev/null +++ b/Azaion.Common/Services/InferenceClient.cs @@ -0,0 +1,109 @@ +using System.Diagnostics; +using System.Text; +using Azaion.Common.DTO; +using MessagePack; +using Microsoft.Extensions.Options; +using NetMQ; +using NetMQ.Sockets; + +namespace Azaion.Common.Services; + +public interface IInferenceClient : IDisposable +{ + event EventHandler? InferenceDataReceived; + event EventHandler? AIAvailabilityReceived; + void Send(RemoteCommand create); + void Stop(); +} + +public class InferenceClient : IInferenceClient +{ + public event EventHandler? BytesReceived; + public event EventHandler? InferenceDataReceived; + public event EventHandler? AIAvailabilityReceived; + + private readonly DealerSocket _dealer = new(); + private readonly NetMQPoller _poller = new(); + private readonly Guid _clientId = Guid.NewGuid(); + private readonly InferenceClientConfig _inferenceClientConfig; + private readonly LoaderClientConfig _loaderClientConfig; + + public InferenceClient(IOptions inferenceConfig, IOptions loaderConfig) + { + _inferenceClientConfig = inferenceConfig.Value; + _loaderClientConfig = loaderConfig.Value; + Start(); + } + + private void Start() + { + try + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo + { + FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH, + Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}", + //RedirectStandardOutput = true, + //RedirectStandardError = true, + //CreateNoWindow = true + }; + + process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; + process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; + process.Start(); + } + catch (Exception e) + { + Console.WriteLine(e); + //throw; + } + + _dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N")); + _dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}"); + + _dealer.ReceiveReady += (_, e) => ProcessClientCommand(e.Socket); + _poller.Add(_dealer); + _ = Task.Run(() => _poller.RunAsync()); + } + + private void ProcessClientCommand(NetMQSocket socket, CancellationToken ct = default) + { + while (socket.TryReceiveFrameBytes(TimeSpan.Zero, out var bytes)) + { + if (bytes?.Length == 0) + continue; + + var remoteCommand = MessagePackSerializer.Deserialize(bytes, cancellationToken: ct); + switch (remoteCommand.CommandType) + { + case CommandType.DataBytes: + BytesReceived?.Invoke(this, remoteCommand); + break; + case CommandType.InferenceData: + InferenceDataReceived?.Invoke(this, remoteCommand); + break; + case CommandType.AIAvailabilityResult: + AIAvailabilityReceived?.Invoke(this, remoteCommand); + break; + } + + } + } + + public void Stop() => + Send(RemoteCommand.Create(CommandType.StopInference)); + + public void Send(RemoteCommand command) => + _dealer.SendFrame(MessagePackSerializer.Serialize(command)); + + public void Dispose() + { + _poller.Stop(); + _poller.Dispose(); + _dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit))); + _dealer.Disconnect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}"); + _dealer.Close(); + _dealer.Dispose(); + } +} \ No newline at end of file diff --git a/Azaion.Common/Services/InferenceService.cs b/Azaion.Common/Services/InferenceService.cs index c66baff..8ae9949 100644 --- a/Azaion.Common/Services/InferenceService.cs +++ b/Azaion.Common/Services/InferenceService.cs @@ -1,11 +1,10 @@ -using System.Text; -using Azaion.Common.Database; +using Azaion.Common.Database; +using Azaion.Common.DTO; using Azaion.Common.DTO.Config; -using Azaion.CommonSecurity; -using Azaion.CommonSecurity.DTO.Commands; -using Azaion.CommonSecurity.Services; +using Azaion.Common.Events; +using Azaion.Common.Extensions; +using MediatR; using MessagePack; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -13,44 +12,71 @@ namespace Azaion.Common.Services; public interface IInferenceService { - Task RunInference(List mediaPaths, Func processAnnotation, CancellationToken detectToken = default); + Task RunInference(List mediaPaths, CancellationToken ct = default); void StopInference(); } -public class InferenceService(ILogger logger, IInferenceClient client, IOptions aiConfigOptions) : IInferenceService +public class InferenceService : IInferenceService { - public async Task RunInference(List mediaPaths, Func processAnnotation, CancellationToken detectToken = default) + private readonly IInferenceClient _client; + private readonly IAzaionApi _azaionApi; + private readonly IOptions _aiConfigOptions; + private readonly IAnnotationService _annotationService; + private readonly IMediator _mediator; + private CancellationTokenSource _inferenceCancelTokenSource = new(); + + public InferenceService( + ILogger logger, + IInferenceClient client, + IAzaionApi azaionApi, + IOptions aiConfigOptions, + IAnnotationService annotationService, + IMediator mediator) { - var aiConfig = aiConfigOptions.Value; + _client = client; + _azaionApi = azaionApi; + _aiConfigOptions = aiConfigOptions; + _annotationService = annotationService; + _mediator = mediator; - aiConfig.Paths = mediaPaths; - client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig)); - - while (!detectToken.IsCancellationRequested) + client.InferenceDataReceived += async (sender, command) => { try { - var bytes = client.GetBytes(ct: detectToken); - if (bytes == null) - throw new Exception("Can't get bytes from inference client"); - - if (bytes.Length == 4 && Encoding.UTF8.GetString(bytes) == "DONE") + if (command.Message == "DONE") + { + _inferenceCancelTokenSource?.Cancel(); return; + } - var annotationImage = MessagePackSerializer.Deserialize(bytes, cancellationToken: detectToken); - - await processAnnotation(annotationImage); + var annImage = MessagePackSerializer.Deserialize(command.Data); + await ProcessDetection(annImage); } catch (Exception e) { logger.LogError(e, e.Message); - break; } - } + }; } - public void StopInference() + private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default) { - client.Send(RemoteCommand.Create(CommandType.StopInference)); + var annotation = await _annotationService.SaveAnnotation(annotationImage, ct); + await _mediator.Publish(new AnnotationAddedEvent(annotation), ct); } + + public async Task RunInference(List mediaPaths, CancellationToken ct = default) + { + _inferenceCancelTokenSource = new CancellationTokenSource(); + _client.Send(RemoteCommand.Create(CommandType.Login, _azaionApi.Credentials)); + + var aiConfig = _aiConfigOptions.Value; + aiConfig.Paths = mediaPaths; + _client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig)); + + using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct, _inferenceCancelTokenSource.Token); + await combinedTokenSource.Token.AsTask(); + } + + public void StopInference() => _client.Stop(); } \ No newline at end of file diff --git a/Azaion.Common/Services/LoaderClient.cs b/Azaion.Common/Services/LoaderClient.cs new file mode 100644 index 0000000..a37e12c --- /dev/null +++ b/Azaion.Common/Services/LoaderClient.cs @@ -0,0 +1,103 @@ +using System.Diagnostics; +using System.IO; +using System.Text; +using Azaion.Common.DTO; +using MessagePack; +using NetMQ; +using NetMQ.Sockets; +using Serilog; +using Exception = System.Exception; + +namespace Azaion.Common.Services; + +public class LoaderClient(LoaderClientConfig config, ILogger logger, CancellationToken ct = default) : IDisposable +{ + private readonly DealerSocket _dealer = new(); + private readonly Guid _clientId = Guid.NewGuid(); + + public void StartClient() + { + try + { + using var process = new Process(); + process.StartInfo = new ProcessStartInfo + { + FileName = SecurityConstants.EXTERNAL_LOADER_PATH, + Arguments = $"--port {config.ZeroMqPort} --api {config.ApiUrl}", + //CreateNoWindow = true + }; + + process.OutputDataReceived += (_, e) => + { + if (e.Data != null) Console.WriteLine(e.Data); + }; + process.ErrorDataReceived += (_, e) => + { + if (e.Data != null) Console.WriteLine(e.Data); + }; + process.Start(); + } + catch (Exception e) + { + logger.Error(e.Message); + throw; + } + } + + public void Connect() + { + _dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N")); + _dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}"); + } + + public void Login(ApiCredentials credentials) + { + var result = SendCommand(RemoteCommand.Create(CommandType.Login, credentials)); + if (result.CommandType != CommandType.Ok) + throw new Exception(result.Message); + } + + public MemoryStream LoadFile(string filename, string folder) + { + var result = SendCommand(RemoteCommand.Create(CommandType.Load, new LoadFileData(filename, folder))); + if (result.Data?.Length == 0) + throw new Exception($"Can't load {filename}. Returns 0 bytes"); + return new MemoryStream(result.Data!); + } + + private RemoteCommand SendCommand(RemoteCommand command, int retryCount = 50, int retryDelayMs = 800) + { + try + { + _dealer.SendFrame(MessagePackSerializer.Serialize(command)); + + var tryNum = 0; + while (!ct.IsCancellationRequested && tryNum++ < retryCount) + { + if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes)) + continue; + var res = MessagePackSerializer.Deserialize(bytes, cancellationToken: ct); + if (res.CommandType == CommandType.Error) + throw new Exception(res.Message); + return res; + } + + throw new Exception($"Sent {command} {retryCount} times, with wait time {retryDelayMs}ms for each call. No response from client."); + } + catch (Exception e) + { + logger.Error(e, e.Message); + throw; + } + } + + public void Stop() + { + _dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit))); + } + + public void Dispose() + { + _dealer.Dispose(); + } +} \ No newline at end of file diff --git a/Azaion.Common/Services/SatelliteDownloader.cs b/Azaion.Common/Services/SatelliteDownloader.cs index 4660820..6be344d 100644 --- a/Azaion.Common/Services/SatelliteDownloader.cs +++ b/Azaion.Common/Services/SatelliteDownloader.cs @@ -5,8 +5,10 @@ using System.Net.Http; using System.Net.Http.Json; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; +using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.CommonSecurity; +using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; @@ -25,7 +27,8 @@ public class SatelliteDownloader( ILogger logger, IOptions mapConfig, IOptions directoriesConfig, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, + IMediator mediator) : ISatelliteDownloader { private const int INPUT_TILE_SIZE = 256; @@ -44,15 +47,19 @@ public class SatelliteDownloader( public async Task GetTiles(double centerLat, double centerLon, double radiusM, int zoomLevel, CancellationToken token = default) { + await mediator.Publish(new SetStatusTextEvent($"Завантажується супутникові зображення по координатах: центр: lat: {centerLat:F3} lon: {centerLon:F3} квадрат {radiusM}м * {radiusM}м, zoom: {zoomLevel}..."), token); //empty Satellite directory if (Directory.Exists(_satDirectory)) Directory.Delete(_satDirectory, true); Directory.CreateDirectory(_satDirectory); var downloadTilesResult = await DownloadTiles(centerLat, centerLon, radiusM, zoomLevel, token); - var image = await ComposeTiles(downloadTilesResult.Tiles, token); - if (image != null) - await SplitToTiles(image, downloadTilesResult, token); + await mediator.Publish(new SetStatusTextEvent("Завершено! Склеюється в 1 зображення..."), token); + var image = ComposeTiles(downloadTilesResult.Tiles, token); + if (image == null) + return; + await mediator.Publish(new SetStatusTextEvent("Розбиття на малі зображення для опрацювання..."), token); + await SplitToTiles(image, downloadTilesResult, token); } private async Task SplitToTiles(Image image, DownloadTilesResult bounds, CancellationToken token = default) @@ -103,52 +110,7 @@ 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) + private Image? ComposeTiles(ConcurrentDictionary<(int x, int y), byte[]> downloadedTiles, CancellationToken token = default) { if (downloadedTiles.IsEmpty) return null; @@ -192,8 +154,6 @@ public class SatelliteDownloader( } }); - // await largeImage.SaveAsync(Path.Combine(_satDirectory, "full_map.tif"), - // new TiffEncoder { Compression = TiffCompression.Deflate }, token); return largeImage; } diff --git a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj deleted file mode 100644 index 4e4f70d..0000000 --- a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - net8.0-windows - enable - enable - - - - - - - - - - - - - diff --git a/Azaion.CommonSecurity/DTO/ApiCredentials.cs b/Azaion.CommonSecurity/DTO/ApiCredentials.cs deleted file mode 100644 index d16dd52..0000000 --- a/Azaion.CommonSecurity/DTO/ApiCredentials.cs +++ /dev/null @@ -1,16 +0,0 @@ -using MessagePack; - -namespace Azaion.CommonSecurity.DTO; - -[MessagePackObject] -public class ApiCredentials(string email, string password) : EventArgs -{ - [Key(nameof(Email))] - public string Email { get; set; } = email; - - [Key(nameof(Password))] - public string Password { get; set; } = password; - - [Key(nameof(Folder))] - public string Folder { get; set; } = null!; -} \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/HardwareInfo.cs b/Azaion.CommonSecurity/DTO/HardwareInfo.cs deleted file mode 100644 index 292c212..0000000 --- a/Azaion.CommonSecurity/DTO/HardwareInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Azaion.CommonSecurity.DTO; - -public class HardwareInfo -{ - public string CPU { get; set; } = null!; - 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 deleted file mode 100644 index 80ea07f..0000000 --- a/Azaion.CommonSecurity/DTO/SecureAppConfig.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Azaion.CommonSecurity.DTO; - -public class SecureAppConfig -{ - public InferenceClientConfig InferenceClientConfig { get; set; } = null!; - public GpsDeniedClientConfig GpsDeniedClientConfig { get; set; } = null!; -} \ No newline at end of file diff --git a/Azaion.CommonSecurity/DTO/User.cs b/Azaion.CommonSecurity/DTO/User.cs deleted file mode 100644 index bb2a2ba..0000000 --- a/Azaion.CommonSecurity/DTO/User.cs +++ /dev/null @@ -1,11 +0,0 @@ -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; } -} \ No newline at end of file diff --git a/Azaion.CommonSecurity/SecurityConstants.cs b/Azaion.CommonSecurity/SecurityConstants.cs deleted file mode 100644 index da0bffc..0000000 --- a/Azaion.CommonSecurity/SecurityConstants.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Azaion.CommonSecurity.DTO; - -namespace Azaion.CommonSecurity; - -public class SecurityConstants -{ - public const string CONFIG_PATH = "config.json"; - - public const string DUMMY_DIR = "dummy"; - - #region ExternalClientsConfig - 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"); - - public const string DEFAULT_ZMQ_INFERENCE_HOST = "127.0.0.1"; - public const int DEFAULT_ZMQ_INFERENCE_PORT = 5227; - - public const string DEFAULT_ZMQ_GPS_DENIED_HOST = "127.0.0.1"; - public const int DEFAULT_ZMQ_GPS_DENIED_PORT = 5227; - - public const int DEFAULT_RETRY_COUNT = 25; - public const int DEFAULT_TIMEOUT_SECONDS = 5; - - public static readonly SecureAppConfig DefaultSecureAppConfig = new() - { - InferenceClientConfig = new InferenceClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_INFERENCE_HOST, - ZeroMqPort = DEFAULT_ZMQ_INFERENCE_PORT, - OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS, - RetryCount = DEFAULT_RETRY_COUNT, - ResourcesFolder = "" - }, - GpsDeniedClientConfig = new GpsDeniedClientConfig - { - ZeroMqHost = DEFAULT_ZMQ_GPS_DENIED_HOST, - ZeroMqPort = DEFAULT_ZMQ_GPS_DENIED_PORT, - OneTryTimeoutSeconds = DEFAULT_TIMEOUT_SECONDS, - RetryCount = DEFAULT_RETRY_COUNT, - } - }; - #endregion ExternalClientsConfig -} \ No newline at end of file diff --git a/Azaion.CommonSecurity/Services/AuthProvider.cs b/Azaion.CommonSecurity/Services/AuthProvider.cs deleted file mode 100644 index ebc5d0d..0000000 --- a/Azaion.CommonSecurity/Services/AuthProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Azaion.CommonSecurity.DTO; -using Azaion.CommonSecurity.DTO.Commands; -using Microsoft.Extensions.DependencyInjection; - -namespace Azaion.CommonSecurity.Services; - -public interface IAuthProvider -{ - void Login(ApiCredentials credentials); - User CurrentUser { get; } -} - -public class AuthProvider(IInferenceClient inferenceClient) : IAuthProvider -{ - public User CurrentUser { get; private set; } = null!; - - public void Login(ApiCredentials credentials) - { - inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); - var user = inferenceClient.Get(); - if (user == null) - throw new Exception("Can't get user from Auth provider"); - - CurrentUser = user; - } -} \ No newline at end of file diff --git a/Azaion.CommonSecurity/Services/HardwareService.cs b/Azaion.CommonSecurity/Services/HardwareService.cs index 3e1a815..ec13a92 100644 --- a/Azaion.CommonSecurity/Services/HardwareService.cs +++ b/Azaion.CommonSecurity/Services/HardwareService.cs @@ -8,105 +8,96 @@ namespace Azaion.CommonSecurity.Services; public interface IHardwareService { - HardwareInfo GetHardware(); + //HardwareInfo GetHardware(); } public class HardwareService : IHardwareService { - private const string WIN32_GET_HARDWARE_COMMAND = - "powershell -Command \"" + - "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " + - "Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " + - "Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output" + - "\""; + // private const string WIN32_GET_HARDWARE_COMMAND = + // "powershell -Command \"" + + // "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " + + // "Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " + + // "Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output" + + // "\""; + // + // private const string UNIX_GET_HARDWARE_COMMAND = + // "/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " + + // "lscpu | grep 'Model name:' | cut -d':' -f2 && " + + // "lspci | grep VGA | cut -d':' -f3\""; - private const string UNIX_GET_HARDWARE_COMMAND = - "/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " + - "lscpu | grep 'Model name:' | cut -d':' -f2 && " + - "lspci | grep VGA | cut -d':' -f3\""; + // public HardwareInfo GetHardware() + // { + // try + // { + // var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT + // ? WIN32_GET_HARDWARE_COMMAND + // : UNIX_GET_HARDWARE_COMMAND); + // + // var lines = output + // .Replace("TotalVisibleMemorySize=", "") + // .Replace("Name=", "") + // .Replace(" ", " ") + // .Trim() + // .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries) + // .Select(x => x.Trim()) + // .ToArray(); + // + // if (lines.Length < 3) + // throw new Exception("Can't get hardware info"); + // + // var hardwareInfo = new HardwareInfo + // { + // CPU = lines[0], + // GPU = lines[1], + // Memory = lines[2], + // MacAddress = GetMacAddress() + // }; + // return hardwareInfo; + // } + // catch (Exception ex) + // { + // Console.WriteLine(ex.Message); + // throw; + // } + // } - public HardwareInfo GetHardware() - { - try - { - var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT - ? WIN32_GET_HARDWARE_COMMAND - : UNIX_GET_HARDWARE_COMMAND); + // private string GetMacAddress() + // { + // var macAddress = NetworkInterface + // .GetAllNetworkInterfaces() + // .Where(nic => nic.OperationalStatus == OperationalStatus.Up) + // .Select(nic => nic.GetPhysicalAddress().ToString()) + // .FirstOrDefault(); + // + // return macAddress ?? string.Empty; + // } + // + // private string RunCommand(string command) + // { + // try + // { + // using var process = new Process(); + // process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe"; + // process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix + // ? $"-c \"{command}\"" + // : $"/c {command}"; + // process.StartInfo.RedirectStandardOutput = true; + // process.StartInfo.UseShellExecute = false; + // process.StartInfo.CreateNoWindow = true; + // + // process.Start(); + // var result = process.StandardOutput.ReadToEnd(); + // process.WaitForExit(); + // + // return result.Trim(); + // } + // catch + // { + // return string.Empty; + // } + // } - var lines = output - .Replace("TotalVisibleMemorySize=", "") - .Replace("Name=", "") - .Replace(" ", " ") - .Trim() - .Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries); - - 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"; - } - - 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 - }; - hardwareInfo.Hash = ToHash($"Az|{hardwareInfo.CPU}|{hardwareInfo.GPU}|{macAddress}"); - return hardwareInfo; - } - catch (Exception ex) - { - Console.WriteLine(ex.Message); - throw; - } - } - - private string MacAddress() - { - var macAddress = NetworkInterface - .GetAllNetworkInterfaces() - .Where(nic => nic.OperationalStatus == OperationalStatus.Up) - .Select(nic => nic.GetPhysicalAddress().ToString()) - .FirstOrDefault(); - - return macAddress ?? string.Empty; - } - - private string RunCommand(string command) - { - try - { - using var process = new Process(); - process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe"; - process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix - ? $"-c \"{command}\"" - : $"/c {command}"; - process.StartInfo.RedirectStandardOutput = true; - process.StartInfo.UseShellExecute = false; - process.StartInfo.CreateNoWindow = true; - - process.Start(); - var result = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); - - return result.Trim(); - } - catch - { - return string.Empty; - } - } - - private static string ToHash(string str) => - Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); + // private static string ToHash(string str) => + // Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str))); } diff --git a/Azaion.CommonSecurity/Services/InferenceClient.cs b/Azaion.CommonSecurity/Services/InferenceClient.cs deleted file mode 100644 index 401243a..0000000 --- a/Azaion.CommonSecurity/Services/InferenceClient.cs +++ /dev/null @@ -1,99 +0,0 @@ -using System.Diagnostics; -using System.Text; -using Azaion.CommonSecurity.DTO; -using Azaion.CommonSecurity.DTO.Commands; -using MessagePack; -using Microsoft.Extensions.Options; -using NetMQ; -using NetMQ.Sockets; - -namespace Azaion.CommonSecurity.Services; - -public interface IInferenceClient -{ - void Send(RemoteCommand create); - T? Get(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class; - byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default); - void Stop(); -} - -public class InferenceClient : IInferenceClient -{ - private readonly DealerSocket _dealer = new(); - private readonly Guid _clientId = Guid.NewGuid(); - private readonly InferenceClientConfig _inferenceClientConfig; - - public InferenceClient(IOptions config) - { - _inferenceClientConfig = config.Value; - Start(); - } - - private void Start() - { - try - { - using var process = new Process(); - process.StartInfo = new ProcessStartInfo - { - FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH, - //Arguments = $"-e {credentials.Email} -p {credentials.Password} -f {apiConfig.ResourcesFolder}", - //RedirectStandardOutput = true, - //RedirectStandardError = true, - //CreateNoWindow = true - }; - - process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; - process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); }; - process.Start(); - } - catch (Exception e) - { - Console.WriteLine(e); - //throw; - } - - _dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N")); - _dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}"); - } - - public void Stop() - { - if (!_dealer.IsDisposed) - { - _dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit))); - _dealer.Close(); - } - } - - public void Send(RemoteCommand command) - { - _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); - return bytes != null ? MessagePackSerializer.Deserialize(bytes, cancellationToken: ct) : null; - } - - public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) - { - var tryNum = 0; - while (!ct.IsCancellationRequested && tryNum++ < retries) - { - if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes)) - continue; - - return bytes; - } - - if (!ct.IsCancellationRequested) - throw new Exception($"Unable to get bytes after {tryNum} retries, {tryTimeoutSeconds} seconds each"); - - return null; - } -} diff --git a/Azaion.CommonSecurity/Services/ResourceLoader.cs b/Azaion.CommonSecurity/Services/ResourceLoader.cs deleted file mode 100644 index 7929ad1..0000000 --- a/Azaion.CommonSecurity/Services/ResourceLoader.cs +++ /dev/null @@ -1,22 +0,0 @@ -using Azaion.CommonSecurity.DTO.Commands; -using Microsoft.Extensions.DependencyInjection; - -namespace Azaion.CommonSecurity.Services; - -public interface IResourceLoader -{ - MemoryStream LoadFile(string fileName, string? folder = null); -} - -public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IInferenceClient inferenceClient) : IResourceLoader -{ - public MemoryStream LoadFile(string fileName, string? folder = null) - { - inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder))); - var bytes = inferenceClient.GetBytes(); - if (bytes == null) - throw new Exception($"Unable to receive {fileName}"); - - return new MemoryStream(bytes); - } -} diff --git a/Azaion.Dataset/Azaion.Dataset.csproj b/Azaion.Dataset/Azaion.Dataset.csproj index f2fdf82..e6564cc 100644 --- a/Azaion.Dataset/Azaion.Dataset.csproj +++ b/Azaion.Dataset/Azaion.Dataset.csproj @@ -7,6 +7,16 @@ true + + $([System.DateTime]::UtcNow.ToString("yyyy.MM.dd")) + $([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes))) + + $(VersionDate).$(VersionSeconds) + $(AssemblyVersion) + $(AssemblyVersion) + Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved. + + MSBuild:Compile @@ -16,8 +26,8 @@ - - + + diff --git a/Azaion.Dataset/Controls/ClassDistribution.xaml b/Azaion.Dataset/Controls/ClassDistribution.xaml new file mode 100644 index 0000000..416470d --- /dev/null +++ b/Azaion.Dataset/Controls/ClassDistribution.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Azaion.Dataset/Controls/ClassDistribution.xaml.cs b/Azaion.Dataset/Controls/ClassDistribution.xaml.cs new file mode 100644 index 0000000..264792a --- /dev/null +++ b/Azaion.Dataset/Controls/ClassDistribution.xaml.cs @@ -0,0 +1,22 @@ +using System.Windows; +using System.Windows.Controls; +using Azaion.Common.DTO; + +namespace Azaion.Dataset.Controls; + +public partial class ClassDistribution : UserControl +{ + public static readonly DependencyProperty ItemsProperty = + DependencyProperty.Register(nameof(Items), typeof(IEnumerable), typeof(ClassDistribution), new PropertyMetadata(null)); + + public IEnumerable Items + { + get => (IEnumerable)GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + public ClassDistribution() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Azaion.Dataset/Controls/ClassDistrtibutionProportionWidthConverter.cs b/Azaion.Dataset/Controls/ClassDistrtibutionProportionWidthConverter.cs new file mode 100644 index 0000000..7a68222 --- /dev/null +++ b/Azaion.Dataset/Controls/ClassDistrtibutionProportionWidthConverter.cs @@ -0,0 +1,31 @@ +using System.Globalization; +using System.Windows.Data; + +namespace Azaion.Dataset.Controls +{ + public class ProportionToWidthConverter : IMultiValueConverter + { + private const double MinPixelBarWidth = 2.0; + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) + { + if (values == null || values.Length < 2 || + !(values[0] is double proportion) || + !(values[1] is double containerActualWidth)) + return MinPixelBarWidth; // Default or fallback width + + if (containerActualWidth <= 0 || !double.IsFinite(containerActualWidth) || double.IsNaN(containerActualWidth)) + return MinPixelBarWidth; // Container not ready or invalid + + double calculatedWidth = proportion * containerActualWidth; + + if (proportion >= 0 && calculatedWidth < MinPixelBarWidth) + return MinPixelBarWidth; + + return Math.Max(0, calculatedWidth); // Ensure width is not negative + } + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => + [value]; + } +} \ No newline at end of file diff --git a/Azaion.Dataset/DatasetExplorer.xaml b/Azaion.Dataset/DatasetExplorer.xaml index 73eeb63..16debd8 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml +++ b/Azaion.Dataset/DatasetExplorer.xaml @@ -4,9 +4,9 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel" - xmlns:scottPlot="clr-namespace:ScottPlot.WPF;assembly=ScottPlot.WPF" xmlns:controls="clr-namespace:Azaion.Common.Controls;assembly=Azaion.Common" xmlns:dto="clr-namespace:Azaion.Common.DTO;assembly=Azaion.Common" + xmlns:controls1="clr-namespace:Azaion.Dataset.Controls" mc:Ignorable="d" Title="Переглядач анотацій" Height="900" Width="1200" WindowState="Maximized"> @@ -30,7 +30,8 @@ - + + + + + + + + + + @@ -92,7 +104,7 @@ - + AnnotationsClasses; + private IAzaionApi _azaionApi; public bool ThumbnailLoading { get; set; } @@ -48,7 +48,8 @@ public partial class DatasetExplorer IGalleryService galleryService, FormState formState, IDbFactory dbFactory, - IMediator mediator) + IMediator mediator, + IAzaionApi azaionApi) { InitializeComponent(); @@ -58,6 +59,7 @@ public partial class DatasetExplorer _galleryService = galleryService; _dbFactory = dbFactory; _mediator = mediator; + _azaionApi = azaionApi; var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast().ToList(); _annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id)) @@ -86,6 +88,7 @@ public partial class DatasetExplorer ThumbnailsView.SelectionChanged += (_, _) => { StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}"; + ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast().Any(x => x.IsSeed) ? Visibility.Visible : Visibility.Hidden; @@ -101,20 +104,6 @@ public partial class DatasetExplorer new List { new() {Id = -1, Name = "All", ShortName = "All"}} .Concat(_annotationConfig.DetectionClasses)); LvClasses.Init(AllDetectionClasses); - - _dbFactory.Run(async db => - { - var allAnnotations = await db.Annotations - .LoadWith(x => x.Detections) - .OrderBy(x => x.AnnotationStatus) - .ThenByDescending(x => x.CreatedDate) - .ToListAsync(); - - foreach (var annotation in allAnnotations) - AddAnnotationToDict(annotation); - }).GetAwaiter().GetResult(); - - DataContext = this; } private async void OnLoaded(object sender, RoutedEventArgs e) @@ -132,8 +121,17 @@ public partial class DatasetExplorer ExplorerEditor.CurrentAnnClass = LvClasses.CurrentDetectionClass ?? _annotationConfig.DetectionClasses.First(); + var allAnnotations = await _dbFactory.Run(async db => + await db.Annotations.LoadWith(x => x.Detections) + .OrderBy(x => x.AnnotationStatus) + .ThenByDescending(x => x.CreatedDate) + .ToListAsync()); + + foreach (var annotation in allAnnotations) + AddAnnotationToDict(annotation); + await ReloadThumbnails(); - await LoadClassDistribution(); + LoadClassDistribution(); DataContext = this; } @@ -145,47 +143,29 @@ public partial class DatasetExplorer _annotationsDict[-1][annotation.Name] = annotation; } - private async Task LoadClassDistribution() + private void LoadClassDistribution() { var data = _annotationsDict .Where(x => x.Key != -1) - .Select(gr => new + .OrderBy(x => x.Key) + .Select(gr => new ClusterDistribution { - gr.Key, - _annotationConfig.DetectionClassesDict[gr.Key].ShortName, - _annotationConfig.DetectionClassesDict[gr.Key].Color, + Label = $"{_annotationConfig.DetectionClassesDict[gr.Key].UIName}: {gr.Value.Count}", + Color = _annotationConfig.DetectionClassesDict[gr.Key].Color, ClassCount = gr.Value.Count }) + .Where(x => x.ClassCount > 0) .ToList(); - var foregroundColor = Color.FromColor(System.Drawing.Color.Black); + var maxClassCount = Math.Max(1, data.Max(x => x.ClassCount)); - var bars = data.Select(x => new Bar + foreach (var cl in data) { - Orientation = Orientation.Horizontal, - Position = -1.5 * x.Key + 1, - Label = x.ClassCount > 200 ? x.ClassCount.ToString() : "", - FillColor = new Color(x.Color.R, x.Color.G, x.Color.B, x.Color.A), - Value = x.ClassCount, - CenterLabel = true, - LabelOffset = 10 - }).ToList(); - - ClassDistribution.Plot.Add.Bars(bars); - - foreach (var x in data) - { - var label = ClassDistribution.Plot.Add.Text(x.ShortName, 50, -1.5 * x.Key + 1.1); - label.LabelFontColor = foregroundColor; - label.LabelFontSize = 18; + cl.Color = cl.Color.CreateTransparent(150); + cl.BarWidth = Math.Clamp(cl.ClassCount / (double)maxClassCount, 0, 1); } - ClassDistribution.Plot.Axes.AutoScale(); - ClassDistribution.Plot.HideAxesAndGrid(); - ClassDistribution.Plot.FigureBackground.Color = new("#888888"); - - ClassDistribution.Refresh(); - await Task.CompletedTask; + ClassDistributionPlot.Items = data; } private async void RefreshThumbnailsBtnClick(object sender, RoutedEventArgs e) @@ -271,10 +251,9 @@ public partial class DatasetExplorer if (result != MessageBoxResult.Yes) return; - var annotations = ThumbnailsView.SelectedItems.Cast().Select(x => x.Annotation) - .ToList(); + var annotationNames = ThumbnailsView.SelectedItems.Cast().Select(x => x.Annotation.Name).ToList(); - await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); + await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames)); ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected); } @@ -282,12 +261,18 @@ public partial class DatasetExplorer { SelectedAnnotations.Clear(); SelectedAnnotationDict.Clear(); - var annotations = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId]; - foreach (var ann in annotations.OrderByDescending(x => x.Value.CreatedDate)) + var annThumbnails = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId] + .Select(x => new AnnotationThumbnail(x.Value, _azaionApi.CurrentUser.Role.IsValidator())) + .OrderBy(x => !x.IsSeed) + .ThenByDescending(x =>x.Annotation.CreatedDate); + + //var dict = annThumbnails.Take(20).ToDictionary(x => x.Annotation.Name, x => x.IsSeed); + + + foreach (var thumb in annThumbnails) { - var annThumb = new AnnotationThumbnail(ann.Value); - SelectedAnnotations.Add(annThumb); - SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); + SelectedAnnotations.Add(thumb); + SelectedAnnotationDict.Add(thumb.Annotation.Name, thumb); } await Task.CompletedTask; } diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 03d69b8..e074623 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -1,17 +1,18 @@ -using System.IO; -using System.Windows; -using System.Windows.Input; +using System.Windows.Input; +using Azaion.Common.Database; using Azaion.Common.DTO; -using Azaion.Common.DTO.Queue; using Azaion.Common.Events; using Azaion.Common.Services; using MediatR; +using Microsoft.Extensions.Logging; namespace Azaion.Dataset; public class DatasetExplorerEventHandler( + ILogger logger, DatasetExplorer datasetExplorer, - AnnotationService annotationService) : + IAnnotationService annotationService, + IAzaionApi azaionApi) : INotificationHandler, INotificationHandler, INotificationHandler, @@ -24,7 +25,9 @@ public class DatasetExplorerEventHandler( { Key.X, PlaybackControlEnum.RemoveAllAnns }, { Key.Escape, PlaybackControlEnum.Close }, { Key.Down, PlaybackControlEnum.Next }, + { Key.PageDown, PlaybackControlEnum.Next }, { Key.Up, PlaybackControlEnum.Previous }, + { Key.PageUp, PlaybackControlEnum.Previous }, { Key.V, PlaybackControlEnum.ValidateAnnotations}, }; @@ -95,47 +98,69 @@ 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.Select(x => x.Name).ToList(), token: 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; } } - public async Task Handle(AnnotationCreatedEvent notification, CancellationToken cancellationToken) + public 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(() => { - 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; + datasetExplorer.AddAnnotationToDict(annotation); + if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) + { + var index = 0; + var annThumb = new AnnotationThumbnail(annotation, azaionApi.CurrentUser.Role.IsValidator()); + 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) + { + index = datasetExplorer.SelectedAnnotations.IndexOf(ann); + datasetExplorer.SelectedAnnotations.Remove(ann); + } + } + + datasetExplorer.SelectedAnnotations.Insert(index, annThumb); + datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); + } + }); + return Task.CompletedTask; } public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) { - var names = notification.Annotations.Select(x => x.Name).ToList(); - var annThumbs = datasetExplorer.SelectedAnnotationDict - .Where(x => names.Contains(x.Key)) - .Select(x => x.Value) - .ToList(); - foreach (var annThumb in annThumbs) + try { - datasetExplorer.SelectedAnnotations.Remove(annThumb); - datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); + datasetExplorer.Dispatcher.Invoke(() => + { + var annThumbs = datasetExplorer.SelectedAnnotationDict + .Where(x => notification.AnnotationNames.Contains(x.Key)) + .Select(x => x.Value) + .ToList(); + foreach (var annThumb in annThumbs) + { + datasetExplorer.SelectedAnnotations.Remove(annThumb); + datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); + } + }); + } + catch (Exception e) + { + logger.LogError(e, e.Message); + throw; } await Task.CompletedTask; } diff --git a/Azaion.Inference/annotation.pxd b/Azaion.Inference/annotation.pxd index e8af677..b8b1b34 100644 --- a/Azaion.Inference/annotation.pxd +++ b/Azaion.Inference/annotation.pxd @@ -14,5 +14,3 @@ cdef class Annotation: cdef format_time(self, ms) cdef bytes serialize(self) - cdef to_str(self, class_names) - diff --git a/Azaion.Inference/annotation.pyx b/Azaion.Inference/annotation.pyx index 4614156..1d4f481 100644 --- a/Azaion.Inference/annotation.pyx +++ b/Azaion.Inference/annotation.pyx @@ -32,12 +32,12 @@ cdef class Annotation: d.annotation_name = self.name self.image = b'' - cdef to_str(self, class_names): + def __str__(self): if not self.detections: return f"{self.name}: No detections" detections_str = ", ".join( - f"{class_names[d.cls]} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})" + f"class: {d.cls} {d.confidence * 100:.1f}% ({d.x:.2f}, {d.y:.2f})" for d in self.detections ) return f"{self.name}: {detections_str}" diff --git a/Azaion.Inference/api_client.pxd b/Azaion.Inference/api_client.pxd deleted file mode 100644 index b53f75b..0000000 --- a/Azaion.Inference/api_client.pxd +++ /dev/null @@ -1,17 +0,0 @@ -from user cimport User -from credentials cimport Credentials - - -cdef class ApiClient: - cdef Credentials credentials - cdef str token, folder, api_url - cdef User user - - cdef set_credentials(self, Credentials credentials) - cdef login(self) - 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=*) diff --git a/Azaion.Inference/api_client.pyx b/Azaion.Inference/api_client.pyx deleted file mode 100644 index 6f3637d..0000000 --- a/Azaion.Inference/api_client.pyx +++ /dev/null @@ -1,132 +0,0 @@ -import json -from http import HTTPStatus -from uuid import UUID -import jwt -import requests -cimport constants -from hardware_service cimport HardwareService, HardwareInfo -from security cimport Security -from io import BytesIO -from user cimport User, RoleEnum - -cdef class ApiClient: - """Handles API authentication and downloading of the AI model.""" - def __init__(self): - self.credentials = None - self.user = None - self.token = None - - cdef set_credentials(self, Credentials credentials): - self.credentials = credentials - - cdef login(self): - response = requests.post(f"{constants.API_URL}/login", - json={"email": self.credentials.email, "password": self.credentials.password}) - response.raise_for_status() - token = response.json()["token"] - self.set_token(token) - - cdef set_token(self, str token): - self.token = token - claims = jwt.decode(token, options={"verify_signature": False}) - - try: - id = str(UUID(claims.get("nameid", ""))) - except ValueError: - raise ValueError("Invalid GUID format in claims") - - email = claims.get("unique_name", "") - - role_str = claims.get("role", "") - if role_str == "ApiAdmin": - role = RoleEnum.ApiAdmin - elif role_str == "Admin": - role = RoleEnum.Admin - elif role_str == "ResourceUploader": - role = RoleEnum.ResourceUploader - elif role_str == "Validator": - role = RoleEnum.Validator - elif role_str == "Operator": - role = RoleEnum.Operator - else: - role = RoleEnum.NONE - self.user = User(id, email, role) - - cdef get_user(self): - if self.user is None: - self.login() - return self.user - - cdef upload_file(self, str filename, str folder=None): - folder = folder or self.credentials.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')) - try: - r = requests.post(url, headers=headers, files=files, allow_redirects=True) - r.raise_for_status() - print(f"Upload success: {r.status_code}") - except Exception as e: - print(f"Upload fail: {e}") - - cdef load_bytes(self, str filename, str folder=None): - folder = folder or self.credentials.folder - - hardware_service = HardwareService() - cdef HardwareInfo hardware = hardware_service.get_hardware_info() - - if self.token is None: - self.login() - url = f"{constants.API_URL}/resources/get/{folder}" - headers = { - "Authorization": f"Bearer {self.token}", - "Content-Type": "application/json" - } - - payload = json.dumps( - { - "password": self.credentials.password, - "hardware": hardware.to_json_object(), - "fileName": filename - }, indent=4) - response = requests.post(url, data=payload, headers=headers, stream=True) - if response.status_code == HTTPStatus.UNAUTHORIZED or response.status_code == HTTPStatus.FORBIDDEN: - self.login() - headers = { - "Authorization": f"Bearer {self.token}", - "Content-Type": "application/json" - } - response = requests.post(url, data=payload, headers=headers, stream=True) - - if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: - print('500!') - - hw_hash = Security.get_hw_hash(hardware) - key = Security.get_api_encryption_key(self.credentials, hw_hash) - - resp_bytes = response.raw.read() - data = Security.decrypt_to(resp_bytes, key) - 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 - - with open(big_file, '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 diff --git a/Azaion.Inference/azaion-inference.spec b/Azaion.Inference/azaion-inference.spec index a01fda6..268f0c2 100644 --- a/Azaion.Inference/azaion-inference.spec +++ b/Azaion.Inference/azaion-inference.spec @@ -1,13 +1,11 @@ # -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_submodules from PyInstaller.utils.hooks import collect_all -datas = [] +datas = [('venv\\Lib\\site-packages\\cv2', 'cv2')] 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('jwt') -datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] -tmp_ret = collect_all('requests') -datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] +hiddenimports = ['constants', 'file_data', 'remote_command', 'remote_command_handler', 'annotation', 'loader_client', 'ai_config', 'tensorrt_engine', 'onnx_engine', 'inference_engine', 'inference', 'main-inf'] +hiddenimports += collect_submodules('cv2') tmp_ret = collect_all('psutil') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('msgpack') @@ -16,7 +14,7 @@ tmp_ret = collect_all('zmq') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('cryptography') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] -tmp_ret = collect_all('cv2') +tmp_ret = collect_all('numpy') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('onnxruntime') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] @@ -24,7 +22,11 @@ tmp_ret = collect_all('tensorrt') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] tmp_ret = collect_all('pycuda') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] -tmp_ret = collect_all('re') +tmp_ret = collect_all('pynvml') +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('loguru') datas += tmp_ret[0]; binaries += tmp_ret[1]; hiddenimports += tmp_ret[2] diff --git a/Azaion.Inference/build.cmd b/Azaion.Inference/build.cmd deleted file mode 100644 index 80d5a8e..0000000 --- a/Azaion.Inference/build.cmd +++ /dev/null @@ -1,28 +0,0 @@ -pyinstaller --name=azaion-inference ^ ---collect-all pyyaml ^ ---collect-all jwt ^ ---collect-all requests ^ ---collect-all psutil ^ ---collect-all msgpack ^ ---collect-all zmq ^ ---collect-all cryptography ^ ---collect-all cv2 ^ ---collect-all onnxruntime ^ ---collect-all tensorrt ^ ---collect-all pycuda ^ ---collect-all re ^ ---hidden-import constants ^ ---hidden-import annotation ^ ---hidden-import credentials ^ ---hidden-import file_data ^ ---hidden-import user ^ ---hidden-import security ^ ---hidden-import secure_model ^ ---hidden-import api_client ^ ---hidden-import hardware_service ^ ---hidden-import remote_command ^ ---hidden-import ai_config ^ ---hidden-import inference_engine ^ ---hidden-import inference ^ ---hidden-import remote_command_handler ^ -start.py \ No newline at end of file diff --git a/Azaion.Inference/build_inference.cmd b/Azaion.Inference/build_inference.cmd new file mode 100644 index 0000000..c11422e --- /dev/null +++ b/Azaion.Inference/build_inference.cmd @@ -0,0 +1,61 @@ +echo Build Cython app +set CURRENT_DIR=%cd% + +REM Change to the parent directory of the current location +cd /d %~dp0 + +echo remove dist folder: +if exist dist rmdir dist /s /q +if exist build rmdir build /s /q + +echo install python and dependencies +if not exist venv ( + python -m venv venv +) + +venv\Scripts\python -m pip install --upgrade pip +venv\Scripts\pip install -r requirements.txt +venv\Scripts\pip install --upgrade pyinstaller pyinstaller-hooks-contrib + +venv\Scripts\python setup.py build_ext --inplace + +echo install azaion-inference +venv\Scripts\pyinstaller --name=azaion-inference ^ +--collect-submodules cv2 ^ +--add-data "venv\Lib\site-packages\cv2;cv2" ^ +--collect-all psutil ^ +--collect-all msgpack ^ +--collect-all zmq ^ +--collect-all cryptography ^ +--collect-all numpy ^ +--collect-all onnxruntime ^ +--collect-all tensorrt ^ +--collect-all pycuda ^ +--collect-all pynvml ^ +--collect-all jwt ^ +--collect-all loguru ^ +--hidden-import constants ^ +--hidden-import file_data ^ +--hidden-import remote_command ^ +--hidden-import remote_command_handler ^ +--hidden-import annotation ^ +--hidden-import loader_client ^ +--hidden-import ai_config ^ +--hidden-import tensorrt_engine ^ +--hidden-import onnx_engine ^ +--hidden-import inference_engine ^ +--hidden-import inference ^ +--hidden-import main-inf ^ +start.py + +robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "ai_config.cp312-win_amd64.pyd" "annotation.cp312-win_amd64.pyd" +robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "constants.cp312-win_amd64.pyd" "file_data.cp312-win_amd64.pyd" +robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "remote_command.cp312-win_amd64.pyd" "remote_command_handler.cp312-win_amd64.pyd" +robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "inference.cp312-win_amd64.pyd" "inference_engine.cp312-win_amd64.pyd" +robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "loader_client.cp312-win_amd64.pyd" "tensorrt_engine.cp312-win_amd64.pyd" +robocopy "dist\azaion-inference\_internal" "..\dist-azaion\_internal" "onnx_engine.cp312-win_amd64.pyd" "main_inference.cp312-win_amd64.pyd" + +robocopy "dist\azaion-inference\_internal" "..\dist-dlls\_internal" /E +robocopy "dist\azaion-inference" "..\dist-azaion" "azaion-inference.exe" + +cd /d %CURRENT_DIR% diff --git a/Azaion.Inference/config.production.yaml b/Azaion.Inference/config.production.yaml deleted file mode 100644 index e575821..0000000 --- a/Azaion.Inference/config.production.yaml +++ /dev/null @@ -1 +0,0 @@ -zmq_port: 5131 \ No newline at end of file diff --git a/Azaion.Inference/config.yaml b/Azaion.Inference/config.yaml deleted file mode 100644 index 20a1884..0000000 --- a/Azaion.Inference/config.yaml +++ /dev/null @@ -1 +0,0 @@ -zmq_port: 5127 \ No newline at end of file diff --git a/Azaion.Inference/constants.pxd b/Azaion.Inference/constants.pxd index ebdae29..19ac2a3 100644 --- a/Azaion.Inference/constants.pxd +++ b/Azaion.Inference/constants.pxd @@ -4,17 +4,14 @@ cdef int QUEUE_MAXSIZE # Maximum size of the command queue cdef str COMMANDS_QUEUE # Name of the commands queue in rabbit 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 - - -cdef log(str log_message, bytes client_id=*) \ No newline at end of file +cdef log(str log_message) +cdef logerror(str error) \ No newline at end of file diff --git a/Azaion.Inference/constants.pyx b/Azaion.Inference/constants.pyx index ec4554a..d552486 100644 --- a/Azaion.Inference/constants.pyx +++ b/Azaion.Inference/constants.pyx @@ -1,21 +1,43 @@ -import time +import sys + +from loguru import logger cdef str CONFIG_FILE = "config.yaml" # Port for the zmq -cdef int QUEUE_MAXSIZE = 1000 # Maximum size of the command queue -cdef str COMMANDS_QUEUE = "azaion-commands" -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 = "azaion.onnx" -cdef str AI_ONNX_MODEL_FILE_BIG = "azaion.onnx.big" -cdef str AI_ONNX_MODEL_FILE_SMALL = "azaion.onnx.small" +cdef str CDN_CONFIG = "cdn.yaml" +cdef str MODELS_FOLDER = "models" -cdef str AI_TENSOR_MODEL_FILE_BIG = "azaion.engine.big" -cdef str AI_TENSOR_MODEL_FILE_SMALL = "azaion.engine.small" +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()) - client_str = '' if client_id is None else f' {client_id}' - print(f'[{local_time}{client_str}]: {log_message}') \ No newline at end of file +logger.remove() +log_format = "[{time:HH:mm:ss} {level}] {message}" +logger.add( + sink="Logs/log_inference_{time:YYYYMMDD}.txt", + level="INFO", + format=log_format, + enqueue=True, + rotation="1 day", + retention="30 days", +) +logger.add( + sys.stdout, + level="DEBUG", + format=log_format, + filter=lambda record: record["level"].name in ("INFO", "DEBUG", "SUCCESS"), + colorize=True +) +logger.add( + sys.stderr, + level="WARNING", + format=log_format, + colorize=True +) + +cdef log(str log_message): + logger.info(log_message) + +cdef logerror(str error): + logger.error(error) \ No newline at end of file diff --git a/Azaion.Inference/file_data.pxd b/Azaion.Inference/file_data.pxd index ad5223e..811155d 100644 --- a/Azaion.Inference/file_data.pxd +++ b/Azaion.Inference/file_data.pxd @@ -4,3 +4,21 @@ cdef class FileData: @staticmethod cdef from_msgpack(bytes data) + + cdef bytes serialize(self) + +cdef class UploadFileData(FileData): + cdef public bytes resource + + @staticmethod + cdef from_msgpack(bytes data) + + cdef bytes serialize(self) + +cdef class FileList: + cdef public list[str] files + + @staticmethod + cdef from_msgpack(bytes data) + + cdef bytes serialize(self) \ No newline at end of file diff --git a/Azaion.Inference/file_data.pyx b/Azaion.Inference/file_data.pyx index 1b13de6..fec6379 100644 --- a/Azaion.Inference/file_data.pyx +++ b/Azaion.Inference/file_data.pyx @@ -1,7 +1,6 @@ -from msgpack import unpackb +from msgpack import unpackb, packb cdef class FileData: - def __init__(self, str folder, str filename): self.folder = folder self.filename = filename @@ -12,3 +11,42 @@ cdef class FileData: return FileData( unpacked.get("Folder"), unpacked.get("Filename")) + + cdef bytes serialize(self): + return packb({ + "Folder": self.folder, + "Filename": self.filename + }) + + +cdef class UploadFileData(FileData): + def __init__(self, bytes resource, str folder, str filename): + super().__init__(folder, filename) + self.resource = resource + + @staticmethod + cdef from_msgpack(bytes data): + unpacked = unpackb(data, strict_map_key=False) + return UploadFileData( + unpacked.get("Resource"), + unpacked.get("Folder"), + unpacked.get("Filename")) + + cdef bytes serialize(self): + return packb({ + "Resource": self.resource, + "Folder": self.folder, + "Filename": self.filename + }) + +cdef class FileList: + def __init__(self, list[str] files): + self.files = files + + @staticmethod + cdef from_msgpack(bytes data): + unpacked = unpackb(data, strict_map_key=False) + return FileList(unpacked.get("files")) + + cdef bytes serialize(self): + return packb({ "files": self.files }) diff --git a/Azaion.Inference/hardware_service.pxd b/Azaion.Inference/hardware_service.pxd deleted file mode 100644 index 2a700a2..0000000 --- a/Azaion.Inference/hardware_service.pxd +++ /dev/null @@ -1,11 +0,0 @@ -cdef class HardwareInfo: - cdef str cpu, gpu, memory, mac_address - cdef to_json_object(self) - -cdef class HardwareService: - cdef bint is_windows - cdef get_mac_address(self, interface=*) - - @staticmethod - cdef has_nvidia_gpu() - cdef HardwareInfo get_hardware_info(self) \ No newline at end of file diff --git a/Azaion.Inference/hardware_service.pyx b/Azaion.Inference/hardware_service.pyx deleted file mode 100644 index 7a1e97c..0000000 --- a/Azaion.Inference/hardware_service.pyx +++ /dev/null @@ -1,84 +0,0 @@ -import re -import subprocess -import psutil - -cdef class HardwareInfo: - def __init__(self, str cpu, str gpu, str memory, str mac_address): - self.cpu = cpu - self.gpu = gpu - self.memory = memory - self.mac_address = mac_address - - cdef to_json_object(self): - return { - "CPU": self.cpu, - "GPU": self.gpu, - "MacAddress": self.mac_address, - "Memory": self.memory - } - - def __str__(self): - return f'CPU: {self.cpu}. GPU: {self.gpu}. Memory: {self.memory}. MAC Address: {self.mac_address}' - -cdef class HardwareService: - """Handles hardware information retrieval and hash generation.""" - - def __init__(self): - try: - res = subprocess.check_output("ver", shell=True).decode('utf-8') - if "Microsoft Windows" in res: - self.is_windows = True - else: - self.is_windows = False - except Exception: - print('Error during os type checking') - self.is_windows = False - - cdef get_mac_address(self, interface="Ethernet"): - addresses = psutil.net_if_addrs() - for interface_name, interface_info in addresses.items(): - if interface_name == interface: - for addr in interface_info: - if addr.family == psutil.AF_LINK: - return addr.address.replace('-', '') - return None - - @staticmethod - cdef has_nvidia_gpu(): - try: - output = subprocess.check_output(['nvidia-smi']).decode() - match = re.search(r'CUDA Version:\s*([\d.]+)', output) - if match: - return float(match.group(1)) > 11 - return False - except Exception as e: - print(e) - return False - - cdef HardwareInfo get_hardware_info(self): - if self.is_windows: - os_command = ( - "powershell -Command \"" - "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " - "Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " - "Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output" - "\"" - ) - else: - os_command = ( - "/bin/bash -c \" lscpu | grep 'Model name:' | cut -d':' -f2 && " - "lspci | grep VGA | cut -d':' -f3 && " - "free -g | grep Mem: | awk '{print $2}' && \"" - ) - # in case of subprocess error do: - # cdef bytes os_command_bytes = os_command.encode('utf-8') - # and use os_command_bytes - result = subprocess.check_output(os_command, shell=True).decode('utf-8') - lines = [line.strip() for line in result.splitlines() if line.strip()] - - cdef str cpu = lines[0].replace("Name=", "").replace(" ", " ") - cdef str gpu = lines[1].replace("Name=", "").replace(" ", " ") - cdef str memory = lines[2].replace("TotalVisibleMemorySize=", "").replace(" ", " ") - cdef str mac_address = self.get_mac_address() - - return HardwareInfo(cpu, gpu, memory, mac_address) diff --git a/Azaion.Inference/inference.pxd b/Azaion.Inference/inference.pxd index c85793c..2a090ab 100644 --- a/Azaion.Inference/inference.pxd +++ b/Azaion.Inference/inference.pxd @@ -1,23 +1,26 @@ from remote_command cimport RemoteCommand from annotation cimport Annotation, Detection from ai_config cimport AIRecognitionConfig -from api_client cimport ApiClient +from loader_client cimport LoaderClient from inference_engine cimport InferenceEngine cdef class Inference: - cdef ApiClient api_client + cdef LoaderClient loader_client cdef InferenceEngine engine cdef object on_annotation cdef Annotation _previous_annotation cdef AIRecognitionConfig ai_config - cdef object class_names cdef bint stop_signal cdef str model_input cdef int model_width cdef int model_height + cdef build_tensor_engine(self, object updater_callback) + 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..858d69d 100644 --- a/Azaion.Inference/inference.pyx +++ b/Azaion.Inference/inference.pyx @@ -1,40 +1,119 @@ -import json import mimetypes -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 +import pynvml + +cdef int tensor_gpu_index + +cdef int check_tensor_gpu_index(): + try: + pynvml.nvmlInit() + deviceCount = pynvml.nvmlDeviceGetCount() + + if deviceCount == 0: + constants.logerror('No NVIDIA GPUs found.') + return -1 + + for i in range(deviceCount): + handle = pynvml.nvmlDeviceGetHandleByIndex(i) + major, minor = pynvml.nvmlDeviceGetCudaComputeCapability(handle) + + if major > 6 or (major == 6 and minor >= 1): + constants.log('found NVIDIA GPU!') + return i + + constants.logerror('NVIDIA GPU doesnt support TensorRT!') + return -1 + + except pynvml.NVMLError: + return -1 + finally: + try: + pynvml.nvmlShutdown() + except: + constants.logerror('Failed to shutdown pynvml cause probably no NVIDIA GPU') + pass + +tensor_gpu_index = check_tensor_gpu_index() +if tensor_gpu_index > -1: + from tensorrt_engine import TensorRTEngine +else: + from onnx_engine import OnnxEngine + cdef class Inference: - def __init__(self, api_client, on_annotation): - self.api_client = api_client + def __init__(self, loader_client, on_annotation): + self.loader_client = loader_client self.on_annotation = on_annotation self.stop_signal = False self.model_input = None self.model_width = 0 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, object updater_callback): + if tensor_gpu_index == -1: + return + + try: + engine_filename = TensorRTEngine.get_engine_filename(0) + models_dir = constants.MODELS_FOLDER + + self.is_building_engine = True + updater_callback('downloading') + + res = self.loader_client.load_big_small_resource(engine_filename, models_dir) + if res.err is None: + constants.log('tensor rt engine is here, no need to build') + self.is_building_engine = False + updater_callback('enabled') + return + + constants.logerror(res.err) + # time.sleep(8) # prevent simultaneously loading dll and models + updater_callback('converting') + constants.log('try to load onnx') + res = self.loader_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir) + if res.err is not None: + updater_callback(f'Error. {res.err}') + model_bytes = TensorRTEngine.convert_from_onnx(res.data) + updater_callback('uploading') + res = self.loader_client.upload_big_small_resource(model_bytes, engine_filename, models_dir) + if res.err is not None: + updater_callback(f'Error. {res.err}') + constants.log(f'uploaded {engine_filename} to CDN and API') + self.is_building_engine = False + updater_callback('enabled') + except Exception as e: + updater_callback(f'Error. {str(e)}') + + cdef init_ai(self): if self.engine is not None: return - is_nvidia = HardwareService.has_nvidia_gpu() - if is_nvidia: - model_bytes = self.api_client.load_ai_model(is_tensor=True) - self.engine = TensorRTEngine(model_bytes, batch_size=4) + models_dir = constants.MODELS_FOLDER + if tensor_gpu_index > -1: + while self.is_building_engine: + time.sleep(1) + engine_filename = TensorRTEngine.get_engine_filename(0) + + res = self.loader_client.load_big_small_resource(engine_filename, models_dir) + if res.err is not None: + raise Exception(res.err) + self.engine = TensorRTEngine(res.data) else: - model_bytes = self.api_client.load_ai_model() - self.engine = OnnxEngine(model_bytes, batch_size=4) + res = self.loader_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir) + if res.err is not None: + raise Exception(res.err) + self.engine = OnnxEngine(res.data) self.model_height, self.model_width = self.engine.get_input_shape() - self.class_names = self.engine.get_class_names() cdef preprocess(self, frames): blobs = [cv2.dnn.blobFromImage(frame, @@ -47,13 +126,11 @@ cdef class Inference: return np.vstack(blobs) cdef postprocess(self, output, ai_config): - print('enter postprocess') cdef list[Detection] detections = [] cdef int ann_index cdef float x1, y1, x2, y2, conf, cx, cy, w, h cdef int class_id cdef list[list[Detection]] results = [] - print('start try: code') try: for ann_index in range(len(output[0])): detections.clear() @@ -127,7 +204,6 @@ cdef class Inference: self.stop_signal = False self.init_ai() - print(ai_config.paths) for m in ai_config.paths: if self.is_video(m): videos.append(m) @@ -135,12 +211,12 @@ 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): - print(f'run inference on {" ".join(chunk)}...') + for chunk in self.split_list_extend(images, self.engine.get_batch_size()): + constants.log(f'run inference on {" ".join(chunk)}...') self._process_images(cmd, ai_config, chunk) if len(videos) > 0: for v in videos: - print(f'run inference on {v}...') + constants.log(f'run inference on {v}...') self._process_video(cmd, ai_config, v) @@ -161,7 +237,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_batch_size(): input_blob = self.preprocess(batch_frames) outputs = self.engine.run(input_blob) @@ -175,10 +251,9 @@ cdef class Inference: annotation.image = image.tobytes() self._previous_annotation = annotation - print(annotation.to_str(self.class_names)) + print(annotation) self.on_annotation(cmd, annotation) - batch_frames.clear() batch_timestamps.clear() v_input.release() @@ -203,7 +278,6 @@ cdef class Inference: annotation = Annotation(image_paths[i], timestamps[i], detections) _, image = cv2.imencode('.jpg', frames[i]) annotation.image = image.tobytes() - print(annotation.to_str(self.class_names)) self.on_annotation(cmd, annotation) diff --git a/Azaion.Inference/inference_engine.pxd b/Azaion.Inference/inference_engine.pxd index 1f76d05..73680d6 100644 --- a/Azaion.Inference/inference_engine.pxd +++ b/Azaion.Inference/inference_engine.pxd @@ -6,24 +6,4 @@ cdef class InferenceEngine: cdef public int batch_size cdef tuple get_input_shape(self) cdef int get_batch_size(self) - cdef get_class_names(self) - cpdef run(self, input_data) - -cdef class OnnxEngine(InferenceEngine): - cdef object session - cdef list model_inputs - cdef str input_name - cdef object input_shape - cdef object class_names - -cdef class TensorRTEngine(InferenceEngine): - cdef object stream - cdef object context - cdef str input_name - cdef str output_name - cdef object d_input - cdef object d_output - cdef object input_shape - cdef object output_shape - cdef object h_output - cdef object class_names \ No newline at end of file + cdef run(self, input_data) diff --git a/Azaion.Inference/inference_engine.pyx b/Azaion.Inference/inference_engine.pyx index 8785a68..9a83be8 100644 --- a/Azaion.Inference/inference_engine.pyx +++ b/Azaion.Inference/inference_engine.pyx @@ -1,13 +1,3 @@ -import json -import struct -from typing import List, Tuple -import numpy as np -import onnxruntime as onnx -import tensorrt as trt -import pycuda.driver as cuda -import pycuda.autoinit # required for automatically initialize CUDA, do not remove. - - cdef class InferenceEngine: def __init__(self, model_bytes: bytes, batch_size: int = 1, **kwargs): self.batch_size = batch_size @@ -18,123 +8,5 @@ cdef class InferenceEngine: cdef int get_batch_size(self): return self.batch_size - cpdef run(self, input_data): + cdef run(self, input_data): raise NotImplementedError("Subclass must implement run") - - cdef get_class_names(self): - raise NotImplementedError("Subclass must implement get_class_names") - - -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] - 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) - self.class_names = eval(model_meta.custom_metadata_map["names"]) - - cdef tuple get_input_shape(self): - shape = self.input_shape - return shape[2], shape[3] - - cdef int get_batch_size(self): - return self.batch_size - - cdef get_class_names(self): - return self.class_names - - cpdef run(self, input_data): - return self.session.run(None, {self.input_name: input_data}) - - -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("'DONE'.encode('utf-8')) + end_inference_command = RemoteCommand(CommandType.INFERENCE_DATA, None, 'DONE') + self.remote_handler.send(command.client_id, end_inference_command.serialize()) except queue.Empty: continue except Exception as e: traceback.print_exc() - print('EXIT!') + constants.log('EXIT!') cdef on_command(self, RemoteCommand command): try: - if command.command_type == CommandType.LOGIN: - self.login(command) - elif command.command_type == CommandType.LOAD: - self.load_file(command) - elif command.command_type == CommandType.INFERENCE: + if command.command_type == CommandType.INFERENCE: self.inference_queue.put(command) + elif command.command_type == CommandType.AI_AVAILABILITY_CHECK: + self.inference.build_tensor_engine(lambda status: self.remote_handler.send(command.client_id, + RemoteCommand(CommandType.AI_AVAILABILITY_RESULT, None, status).serialize())) elif command.command_type == CommandType.STOP_INFERENCE: self.inference.stop() elif command.command_type == CommandType.EXIT: @@ -56,24 +54,14 @@ cdef class CommandProcessor: else: pass except Exception as e: - print(f"Error handling client: {e}") - - cdef login(self, RemoteCommand command): - 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) - response = self.api_client.load_bytes(file_data.filename, file_data.folder) - self.remote_handler.send(command.client_id, response) + constants.logerror(f"Error handling client: {e}") cdef on_annotation(self, RemoteCommand cmd, Annotation annotation): - data = annotation.serialize() - self.remote_handler.send(cmd.client_id, data) + cdef RemoteCommand response = RemoteCommand(CommandType.INFERENCE_DATA, annotation.serialize()) + self.remote_handler.send(cmd.client_id, response.serialize()) def stop(self): self.inference.stop() self.remote_handler.stop() + self.loader_client.stop() self.running = False diff --git a/Azaion.Inference/onnx_engine.pyx b/Azaion.Inference/onnx_engine.pyx new file mode 100644 index 0000000..268a5d6 --- /dev/null +++ b/Azaion.Inference/onnx_engine.pyx @@ -0,0 +1,26 @@ +from inference_engine cimport InferenceEngine +import onnxruntime as onnx +cimport constants + +cdef class OnnxEngine(InferenceEngine): + def __init__(self, model_bytes: bytes, batch_size: int = 1, **kwargs): + super().__init__(model_bytes, 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 + self.batch_size = self.input_shape[0] if self.input_shape[0] != -1 else batch_size + constants.log(f'AI detection model input: {self.model_inputs} {self.input_shape}') + model_meta = self.session.get_modelmeta() + constants.log(f"Metadata: {model_meta.custom_metadata_map}") + + cpdef tuple get_input_shape(self): + shape = self.input_shape + return shape[2], shape[3] + + cpdef int get_batch_size(self): + return self.batch_size + + cpdef run(self, input_data): + return self.session.run(None, {self.input_name: input_data}) \ No newline at end of file diff --git a/Azaion.Inference/remote_command.pxd b/Azaion.Inference/remote_command.pxd index 8817795..2571843 100644 --- a/Azaion.Inference/remote_command.pxd +++ b/Azaion.Inference/remote_command.pxd @@ -1,14 +1,27 @@ cdef enum CommandType: + OK = 3 LOGIN = 10 + LIST_REQUEST = 15 + LIST_FILES = 18 LOAD = 20 + LOAD_BIG_SMALL = 22 + UPLOAD_BIG_SMALL = 24 + DATA_BYTES = 25 INFERENCE = 30 + INFERENCE_DATA = 35 STOP_INFERENCE = 40 + AI_AVAILABILITY_CHECK = 80 + AI_AVAILABILITY_RESULT = 85 + ERROR = 90 EXIT = 100 cdef class RemoteCommand: cdef public bytes client_id cdef CommandType command_type + cdef str message cdef bytes data @staticmethod cdef from_msgpack(bytes data) + + cdef bytes serialize(self) diff --git a/Azaion.Inference/remote_command.pyx b/Azaion.Inference/remote_command.pyx index 5109387..03ff40d 100644 --- a/Azaion.Inference/remote_command.pyx +++ b/Azaion.Inference/remote_command.pyx @@ -1,16 +1,27 @@ import msgpack cdef class RemoteCommand: - def __init__(self, CommandType command_type, bytes data): + def __init__(self, CommandType command_type, bytes data=None, str message=None): self.command_type = command_type self.data = data + self.message = message def __str__(self): command_type_names = { + 3: "OK", 10: "LOGIN", + 15: "LIST_REQUEST", + 18: "LIST_FILES", 20: "LOAD", + 22: "LOAD_BIG_SMALL", + 24: "UPLOAD_BIG_SMALL", + 25: "DATA_BYTES", 30: "INFERENCE", + 35: "INFERENCE_DATA", 40: "STOP_INFERENCE", + 80: "AI_AVAILABILITY_CHECK", + 85: "AI_AVAILABILITY_RESULT", + 90: "ERROR", 100: "EXIT" } data_str = f'{len(self.data)} bytes' if self.data else '' @@ -19,4 +30,11 @@ cdef class RemoteCommand: @staticmethod cdef from_msgpack(bytes data): unpacked = msgpack.unpackb(data, strict_map_key=False) - return RemoteCommand(unpacked.get("CommandType"), unpacked.get("Data")) + return RemoteCommand(unpacked.get("CommandType"), unpacked.get("Data"), unpacked.get("Message")) + + cdef bytes serialize(self): + return msgpack.packb({ + "CommandType": self.command_type, + "Data": self.data, + "Message": self.message + }) diff --git a/Azaion.Inference/remote_command_handler.pyx b/Azaion.Inference/remote_command_handler.pyx index 42c64ce..eee5eee 100644 --- a/Azaion.Inference/remote_command_handler.pyx +++ b/Azaion.Inference/remote_command_handler.pyx @@ -3,19 +3,15 @@ import zmq from threading import Thread, Event from remote_command cimport RemoteCommand cimport constants -import yaml cdef class RemoteCommandHandler: - def __init__(self, object on_command): + def __init__(self, int zmq_port, object on_command): self._on_command = on_command - self._context = zmq.Context.instance() + self._context = zmq.Context() self._router = self._context.socket(zmq.ROUTER) self._router.setsockopt(zmq.LINGER, 0) - with open(constants.CONFIG_FILE, "r") as f: - config = yaml.safe_load(f) - port = config["zmq_port"] - self._router.bind(f'tcp://*:{port}') + self._router.bind(f'tcp://*:{zmq_port}') self._dealer = self._context.socket(zmq.DEALER) self._dealer.setsockopt(zmq.LINGER, 0) @@ -31,7 +27,7 @@ cdef class RemoteCommandHandler: for _ in range(4): # 4 worker threads worker = Thread(target=self._worker_loop, daemon=True) self._workers.append(worker) - print(f'Listening to commands on port {port}...') + constants.log(f'Listening to commands on port {zmq_port}...') cdef start(self): self._proxy_thread.start() @@ -43,7 +39,7 @@ cdef class RemoteCommandHandler: zmq.proxy_steerable(self._router, self._dealer, control=self._control) except zmq.error.ZMQError as e: if self._shutdown_event.is_set(): - print("Shutdown, exit proxy loop.") + constants.log("Shutdown, exit proxy loop.") else: raise @@ -62,21 +58,23 @@ cdef class RemoteCommandHandler: client_id, message = worker_socket.recv_multipart() cmd = RemoteCommand.from_msgpack( message) cmd.client_id = client_id - constants.log(f'{cmd}', client_id) + constants.log(cmd) self._on_command(cmd) except Exception as e: if not self._shutdown_event.is_set(): - print(f"Worker error: {e}") + constants.log(f"Worker error: {e}") import traceback traceback.print_exc() finally: worker_socket.close() cdef send(self, bytes client_id, bytes data): - with self._context.socket(zmq.DEALER) as socket: - socket.connect("inproc://backend") - socket.send_multipart([client_id, data]) - constants.log(f'Sent {len(data)} bytes.', client_id) + self._router.send_multipart([client_id, data]) + + # with self._context.socket(zmq.DEALER) as socket: + # socket.connect("inproc://backend") + # socket.send_multipart([client_id, data]) + # # constants.log(f'Sent {len(data)} bytes.', client_id) cdef stop(self): self._shutdown_event.set() @@ -84,6 +82,7 @@ cdef class RemoteCommandHandler: self._control.send(b"TERMINATE", flags=zmq.DONTWAIT) except zmq.error.ZMQError: pass + self._router.close(linger=0) self._dealer.close(linger=0) self._control.close(linger=0) @@ -91,5 +90,4 @@ cdef class RemoteCommandHandler: self._proxy_thread.join(timeout=2) while any(w.is_alive() for w in self._workers): time.sleep(0.1) - self._context.term() diff --git a/Azaion.Inference/requirements.txt b/Azaion.Inference/requirements.txt index 925baa2..25458a4 100644 --- a/Azaion.Inference/requirements.txt +++ b/Azaion.Inference/requirements.txt @@ -3,7 +3,7 @@ Cython opencv-python==4.10.0.84 numpy onnxruntime-gpu -cryptography +cryptography==44.0.2 psutil msgpack pyjwt @@ -12,3 +12,6 @@ requests pyyaml pycuda tensorrt +pynvml +boto3 +loguru \ No newline at end of file diff --git a/Azaion.Inference/setup.py b/Azaion.Inference/setup.py index 189f946..ba60517 100644 --- a/Azaion.Inference/setup.py +++ b/Azaion.Inference/setup.py @@ -4,19 +4,17 @@ import numpy as np extensions = [ Extension('constants', ['constants.pyx']), - Extension('annotation', ['annotation.pyx']), - Extension('credentials', ['credentials.pyx']), Extension('file_data', ['file_data.pyx']), - Extension('hardware_service', ['hardware_service.pyx'], extra_compile_args=["-g"], extra_link_args=["-g"]), - Extension('security', ['security.pyx']), Extension('remote_command', ['remote_command.pyx']), Extension('remote_command_handler', ['remote_command_handler.pyx']), - Extension('user', ['user.pyx']), - Extension('api_client', ['api_client.pyx']), + Extension('annotation', ['annotation.pyx']), + Extension('loader_client', ['loader_client.pyx']), Extension('ai_config', ['ai_config.pyx']), + Extension('tensorrt_engine', ['tensorrt_engine.pyx'], include_dirs=[np.get_include()]), + Extension('onnx_engine', ['onnx_engine.pyx'], include_dirs=[np.get_include()]), Extension('inference_engine', ['inference_engine.pyx'], include_dirs=[np.get_include()]), Extension('inference', ['inference.pyx'], include_dirs=[np.get_include()]), - Extension('main', ['main.pyx']), + Extension('main_inference', ['main_inference.pyx']), ] setup( diff --git a/Azaion.Inference/start.py b/Azaion.Inference/start.py index 00675fb..97bee35 100644 --- a/Azaion.Inference/start.py +++ b/Azaion.Inference/start.py @@ -1,13 +1,17 @@ -from main import CommandProcessor +from main_inference import CommandProcessor +import argparse -def start(): - processor = CommandProcessor() +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", type=int, default=5127, help="zero mq port") + parser.add_argument("-lh", "--loader-host", type=str, default="127.0.0.1", help="zero mq loader app port") + parser.add_argument("-lp", "--loader-port", type=int, default=5025, help="zero mq loader app port") + parser.add_argument("-a", "--api", type=str, default="https://api.azaion.com", help="api url") + args = parser.parse_args() + + processor = CommandProcessor(args.port, args.loader_host, args.loader_port, args.api) try: processor.start() except KeyboardInterrupt: processor.stop() - - -if __name__ == '__main__': - start() diff --git a/Azaion.Inference/tensorrt_engine.pxd b/Azaion.Inference/tensorrt_engine.pxd new file mode 100644 index 0000000..6fc31bd --- /dev/null +++ b/Azaion.Inference/tensorrt_engine.pxd @@ -0,0 +1,24 @@ +from inference_engine cimport InferenceEngine + + +cdef class TensorRTEngine(InferenceEngine): + + cdef public object context + + cdef public object d_input + cdef public object d_output + cdef str input_name + cdef object input_shape + + cdef object h_output + cdef str output_name + cdef object output_shape + + cdef object stream + + + cpdef tuple get_input_shape(self) + + cpdef int get_batch_size(self) + + cpdef run(self, input_data) diff --git a/Azaion.Inference/tensorrt_engine.pyx b/Azaion.Inference/tensorrt_engine.pyx new file mode 100644 index 0000000..f1c60d2 --- /dev/null +++ b/Azaion.Inference/tensorrt_engine.pyx @@ -0,0 +1,136 @@ +from inference_engine cimport InferenceEngine +import tensorrt as trt +import pycuda.driver as cuda +import pycuda.autoinit # required for automatically initialize CUDA, do not remove. +import pynvml +import numpy as np +cimport constants + + +cdef class TensorRTEngine(InferenceEngine): + def __init__(self, model_bytes: bytes, batch_size: int = 4, **kwargs): + super().__init__(model_bytes, batch_size) + try: + logger = trt.Logger(trt.Logger.WARNING) + + runtime = trt.Runtime(logger) + engine = runtime.deserialize_cuda_engine(model_bytes) + + if engine is None: + raise RuntimeError(f"Failed to load TensorRT engine from bytes") + + self.context = engine.create_execution_context() + + # input + self.input_name = engine.get_tensor_name(0) + engine_input_shape = engine.get_tensor_shape(self.input_name) + if engine_input_shape[0] != -1: + self.batch_size = engine_input_shape[0] + else: + self.batch_size = batch_size + + self.input_shape = [ + self.batch_size, + engine_input_shape[1], # Channels (usually fixed at 3 for RGB) + 1280 if engine_input_shape[2] == -1 else engine_input_shape[2], # Height + 1280 if engine_input_shape[3] == -1 else engine_input_shape[3] # Width + ] + self.context.set_input_shape(self.input_name, self.input_shape) + input_size = trt.volume(self.input_shape) * np.dtype(np.float32).itemsize + self.d_input = cuda.mem_alloc(input_size) + + # output + self.output_name = engine.get_tensor_name(1) + engine_output_shape = tuple(engine.get_tensor_shape(self.output_name)) + self.output_shape = [ + self.batch_size, + 300 if engine_output_shape[1] == -1 else engine_output_shape[1], # max detections number + 6 if engine_output_shape[2] == -1 else engine_output_shape[2] # x1 y1 x2 y2 conf cls + ] + self.h_output = cuda.pagelocked_empty(tuple(self.output_shape), dtype=np.float32) + self.d_output = cuda.mem_alloc(self.h_output.nbytes) + + self.stream = cuda.Stream() + + except Exception as e: + raise RuntimeError(f"Failed to initialize TensorRT engine: {str(e)}") + + @staticmethod + def get_gpu_memory_bytes(int device_id): + total_memory = None + try: + pynvml.nvmlInit() + handle = pynvml.nvmlDeviceGetHandleByIndex(device_id) + mem_info = pynvml.nvmlDeviceGetMemoryInfo(handle) + total_memory = mem_info.total + except pynvml.NVMLError: + total_memory = None + finally: + try: + pynvml.nvmlShutdown() + except pynvml.NVMLError: + pass + return 2 * 1024 * 1024 * 1024 if total_memory is None else total_memory # default 2 Gb + + @staticmethod + def get_engine_filename(int device_id): + try: + device = cuda.Device(device_id) + sm_count = device.multiprocessor_count + cc_major, cc_minor = device.compute_capability() + return f"azaion.cc_{cc_major}.{cc_minor}_sm_{sm_count}.engine" + except Exception: + return None + + @staticmethod + def convert_from_onnx(bytes onnx_model): + workspace_bytes = int(TensorRTEngine.get_gpu_memory_bytes(0) * 0.9) + + explicit_batch_flag = 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) + trt_logger = trt.Logger(trt.Logger.WARNING) + + with trt.Builder(trt_logger) as builder, \ + builder.create_network(explicit_batch_flag) as network, \ + trt.OnnxParser(network, trt_logger) as parser, \ + builder.create_builder_config() as config: + + config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, workspace_bytes) + + if not parser.parse(onnx_model): + return None + + if builder.platform_has_fast_fp16: + constants.log('Converting to supported fp16') + config.set_flag(trt.BuilderFlag.FP16) + else: + constants.log('Converting to supported fp32. (fp16 is not supported)') + plan = builder.build_serialized_network(network, config) + + if plan is None: + constants.logerror('Conversion failed.') + return None + constants.log('conversion done!') + return bytes(plan) + + cpdef tuple get_input_shape(self): + return self.input_shape[2], self.input_shape[3] + + cpdef int get_batch_size(self): + return self.batch_size + + cpdef run(self, input_data): + try: + cuda.memcpy_htod_async(self.d_input, input_data, self.stream) + self.context.set_tensor_address(self.input_name, int(self.d_input)) # input buffer + self.context.set_tensor_address(self.output_name, int(self.d_output)) # output buffer + + self.context.execute_async_v3(stream_handle=self.stream.handle) + self.stream.synchronize() + + # Fix: Remove the stream parameter from memcpy_dtoh + cuda.memcpy_dtoh(self.h_output, self.d_output) + output = self.h_output.reshape(self.output_shape) + return [output] + + except Exception as e: + raise RuntimeError(f"Failed to run TensorRT inference: {str(e)}") diff --git a/Azaion.Inference/test/test_download_large_file.py b/Azaion.Inference/test/test_download_large_file.py index 0b17ecf..39bc576 100644 --- a/Azaion.Inference/test/test_download_large_file.py +++ b/Azaion.Inference/test/test_download_large_file.py @@ -202,7 +202,7 @@ class Api: payload = json.dumps( { "password": self.credentials.password, - "hardware": hardware.to_json_object(), + "hardware": hardware, "fileName": filename }, indent=4) response = requests.post(url, data=payload, headers=headers, stream=True, timeout=20) @@ -215,7 +215,7 @@ class Api: response = requests.post(url, data=payload, headers=headers, stream=True) if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: - print('500!') + print('500!') #test key = self.get_encryption_key(hardware.hash) diff --git a/Azaion.Inference/token b/Azaion.Inference/token deleted file mode 100644 index 34fc187..0000000 --- a/Azaion.Inference/token +++ /dev/null @@ -1 +0,0 @@ -eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1laWQiOiI5N2U5MWI2OC1hNmRlLTQ3YTgtOTgzYi0xOTU3YzViNDQ2MTkiLCJ1bmlxdWVfbmFtZSI6ImFkbWluLXJlbW90ZUBhemFpb24uY29tIiwicm9sZSI6IkFwaUFkbWluIiwibmJmIjoxNzM5MzUxNDEwLCJleHAiOjE3MzkzNjU4MTAsImlhdCI6MTczOTM1MTQxMCwiaXNzIjoiQXphaW9uQXBpIiwiYXVkIjoiQW5ub3RhdG9ycy9PcmFuZ2VQaS9BZG1pbnMifQ.P32xRe6nk-0u2jjBi3rdsd3YwlmGXL0NX_eE2xb7OUI \ No newline at end of file diff --git a/Azaion.Loader/api_client.pxd b/Azaion.Loader/api_client.pxd new file mode 100644 index 0000000..69bfec0 --- /dev/null +++ b/Azaion.Loader/api_client.pxd @@ -0,0 +1,23 @@ +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 + + cdef set_credentials(self, Credentials credentials) + cdef login(self) + cdef set_token(self, str token) + cdef get_user(self) + + cdef request(self, str method, str url, object payload, bint is_stream) + cdef list_files(self, str folder, str search_file) + cdef load_bytes(self, str filename, str folder) + cdef upload_file(self, str filename, bytes resource, str folder) + cdef load_big_file_cdn(self, str folder, str big_part) + cdef load_big_small_resource(self, str resource_name, str folder) + cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder) diff --git a/Azaion.Loader/api_client.pyx b/Azaion.Loader/api_client.pyx new file mode 100644 index 0000000..d3d1f03 --- /dev/null +++ b/Azaion.Loader/api_client.pyx @@ -0,0 +1,195 @@ +import json +import os +from http import HTTPStatus +from os import path +from uuid import UUID +import jwt +import requests +cimport constants +import yaml +from requests import HTTPError +from credentials cimport Credentials +from cdn_manager cimport CDNManager, CDNCredentials +from hardware_service cimport HardwareService +from security cimport Security +from user cimport User, RoleEnum + +cdef class ApiClient: + """Handles API authentication and downloading of the AI model.""" + def __init__(self, str api_url): + self.credentials = None + self.user = None + self.token = None + self.cdn_manager = None + self.api_url = api_url + + cdef set_credentials(self, Credentials credentials): + self.credentials = credentials + if self.cdn_manager is not None: + return + + 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 = None + try: + response = requests.post(f"{self.api_url}/login", + json={"email": self.credentials.email, "password": self.credentials.password}) + response.raise_for_status() + token = response.json()["token"] + self.set_token(token) + except HTTPError as e: + constants.logerror(response.json()) + if response.status_code == HTTPStatus.CONFLICT: + res = response.json() + raise Exception(res['Message']) + + + cdef set_token(self, str token): + self.token = token + claims = jwt.decode(token, options={"verify_signature": False}) + + try: + id = str(UUID(claims.get("nameid", ""))) + except ValueError: + raise ValueError("Invalid GUID format in claims") + + email = claims.get("unique_name", "") + + role_str = claims.get("role", "") + if role_str == "ApiAdmin": + role = RoleEnum.ApiAdmin + elif role_str == "Admin": + role = RoleEnum.Admin + elif role_str == "ResourceUploader": + role = RoleEnum.ResourceUploader + elif role_str == "Validator": + role = RoleEnum.Validator + elif role_str == "Operator": + role = RoleEnum.Operator + else: + role = RoleEnum.NONE + self.user = User(id, email, role) + + cdef get_user(self): + if self.user is None: + self.login() + return self.user + + cdef upload_file(self, str filename, bytes resource, str folder): + if self.token is None: + self.login() + url = f"{self.api_url}/resources/{folder}" + headers = { "Authorization": f"Bearer {self.token}" } + files = {'data': (filename, resource)} + try: + r = requests.post(url, headers=headers, files=files, allow_redirects=True) + r.raise_for_status() + constants.log(f"Uploaded {filename} to {self.api_url}/{folder} successfully: {r.status_code}.") + except Exception as e: + constants.logerror(f"Upload fail: {e}") + + cdef list_files(self, str folder, str search_file): + response = self.request('get', f'{self.api_url}/resources/list/{folder}', { + "search": search_file + }, is_stream=False) + constants.log( f'Get files list by {folder}') + return response.json() + + cdef load_bytes(self, str filename, str folder): + cdef str hardware = HardwareService.get_hardware_info() + hw_hash = Security.get_hw_hash(hardware) + key = Security.get_api_encryption_key(self.credentials, hw_hash) + payload = json.dumps( + { + "password": self.credentials.password, + "hardware": hardware, + "fileName": filename + }, indent=4) + response = self.request('post', f'{self.api_url}/resources/get/{folder}', payload, is_stream=True) + + resp_bytes = response.raw.read() + data = Security.decrypt_to(resp_bytes, key) + constants.log(f'Downloaded file: {filename}, {len(data)} bytes') + return data + + cdef request(self, str method, str url, object payload, bint is_stream): + if self.token is None: + self.login() + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json" + } + + response = requests.request(method, url, data=payload, headers=headers, stream=is_stream) + if response.status_code == HTTPStatus.UNAUTHORIZED or response.status_code == HTTPStatus.FORBIDDEN: + self.login() + headers = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json" + } + response = requests.request(method, url, data=payload, headers=headers, stream=is_stream) + + if response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR: + raise Exception(f'Internal API error! {response.text}') + if response.status_code == HTTPStatus.CONFLICT: + res = response.json() + err_code = res['ErrorCode'] + err_msg = res['Message'] + raise Exception(f"Error {err_code}: {err_msg}") + return response + + cdef load_big_file_cdn(self, str folder, str big_part): + constants.log(f'downloading file {folder}\\{big_part} from cdn...') + if self.cdn_manager.download(folder, big_part): + with open(path.join( folder, big_part), 'rb') as binary_file: + encrypted_bytes_big = binary_file.read() + return encrypted_bytes_big + else: + raise Exception(f'Cannot download file {folder}\\{big_part} from CDN!') + + cdef load_big_small_resource(self, str resource_name, str folder): + cdef str big_part = f'{resource_name}.big' + cdef str small_part = f'{resource_name}.small' + + encrypted_bytes_small = self.load_bytes(small_part, folder) + + key = Security.get_resource_encryption_key() + + constants.log(f'checking on existence for {folder}\\{big_part}') + if os.path.exists(os.path.join( folder, big_part)): + with open(path.join( folder, big_part), 'rb') as binary_file: + local_bytes_big = binary_file.read() + constants.log(f'local file {folder}\\{big_part} is found!') + try: + resource = Security.decrypt_to(encrypted_bytes_small + local_bytes_big, key) + return resource + except Exception as ex: + constants.logerror('Local file {folder}\\{big_part} doesnt match with api file, old version') + + remote_bytes_big = self.load_big_file_cdn(folder, big_part) + return Security.decrypt_to(encrypted_bytes_small + remote_bytes_big, key) + + cdef upload_big_small_resource(self, bytes resource, str resource_name, str folder): + cdef str big_part_name = f'{resource_name}.big' + cdef str small_part_name = f'{resource_name}.small' + key = Security.get_resource_encryption_key() + + 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(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, folder) \ No newline at end of file diff --git a/Azaion.Loader/build_loader.cmd b/Azaion.Loader/build_loader.cmd new file mode 100644 index 0000000..d2cb75c --- /dev/null +++ b/Azaion.Loader/build_loader.cmd @@ -0,0 +1,55 @@ +echo Build Cython app +set CURRENT_DIR=%cd% + +REM Change to the parent directory of the current location +cd /d %~dp0 + +echo remove dist folder: +if exist dist rmdir dist /s /q +if exist build rmdir build /s /q + + +echo install python and dependencies +if not exist venv ( + python -m venv venv +) + +venv\Scripts\python -m pip install --upgrade pip +venv\Scripts\pip install -r requirements.txt +venv\Scripts\pip install --upgrade pyinstaller pyinstaller-hooks-contrib + +venv\Scripts\python setup.py build_ext --inplace + +echo install azaion-loader +venv\Scripts\pyinstaller --name=azaion-loader ^ +--collect-all requests ^ +--collect-all boto3 ^ +--collect-all msgpack ^ +--collect-all zmq ^ +--collect-all jwt ^ +--collect-all boto3 ^ +--collect-all cryptography ^ +--collect-all yaml ^ +--collect-all loguru ^ +--hidden-import constants ^ +--hidden-import file_data ^ +--hidden-import remote_command ^ +--hidden-import remote_command_handler ^ +--hidden-import user ^ +--hidden-import security ^ +--hidden-import cdn_manager ^ +--hidden-import credentials ^ +--hidden-import api_client ^ +--hidden-import hardware_service ^ +start.py + + +robocopy "dist\azaion-loader\_internal" "..\dist-azaion\_internal" "security.cp312-win_amd64.pyd" "cdn_manager.cp312-win_amd64.pyd" +robocopy "dist\azaion-loader\_internal" "..\dist-azaion\_internal" "credentials.cp312-win_amd64.pyd" "api_client.cp312-win_amd64.pyd" +robocopy "dist\azaion-loader\_internal" "..\dist-azaion\_internal" "hardware_service.cp312-win_amd64.pyd" "user.cp312-win_amd64.pyd" +robocopy "dist\azaion-loader\_internal" "..\dist-azaion\_internal" "main_loader.cp312-win_amd64.pyd" + +robocopy "dist\azaion-loader\_internal" "..\dist-dlls\_internal" /E +robocopy "dist\azaion-loader" "..\dist-azaion" "azaion-loader.exe" + +cd /d %CURRENT_DIR% \ No newline at end of file diff --git a/Azaion.Loader/cdn_manager.pxd b/Azaion.Loader/cdn_manager.pxd new file mode 100644 index 0000000..028c26d --- /dev/null +++ b/Azaion.Loader/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.Loader/cdn_manager.pyx b/Azaion.Loader/cdn_manager.pyx new file mode 100644 index 0000000..5a5c02f --- /dev/null +++ b/Azaion.Loader/cdn_manager.pyx @@ -0,0 +1,44 @@ +import io +import os +cimport constants +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) + constants.log(f'uploaded {filename} ({len(file_bytes)} bytes) to the {bucket}') + return True + except Exception as e: + constants.logerror(e) + return False + + cdef download(self, str folder, str filename): + try: + os.makedirs(folder, exist_ok=True) + self.download_client.download_file(folder, filename, f'{folder}\\{filename}') + constants.log(f'downloaded {filename} from the {folder}') + return True + except Exception as e: + constants.logerror(e) + return False diff --git a/Azaion.Loader/constants.pxd b/Azaion.Loader/constants.pxd new file mode 100644 index 0000000..fd0224f --- /dev/null +++ b/Azaion.Loader/constants.pxd @@ -0,0 +1,18 @@ +cdef str CONFIG_FILE # Port for the zmq + +cdef int QUEUE_MAXSIZE # Maximum size of the command queue +cdef str COMMANDS_QUEUE # Name of the commands queue in rabbit +cdef str ANNOTATIONS_QUEUE # Name of the annotations queue in rabbit + +cdef str QUEUE_CONFIG_FILENAME # queue config filename to load from api + +cdef str AI_ONNX_MODEL_FILE + +cdef str CDN_CONFIG +cdef str MODELS_FOLDER + +cdef int SMALL_SIZE_KB + + +cdef log(str log_message) +cdef logerror(str error) \ No newline at end of file diff --git a/Azaion.Loader/constants.pyx b/Azaion.Loader/constants.pyx new file mode 100644 index 0000000..7fdfd9e --- /dev/null +++ b/Azaion.Loader/constants.pyx @@ -0,0 +1,45 @@ +import sys +import time +from loguru import logger + +cdef str CONFIG_FILE = "config.yaml" # Port for the zmq + +cdef str QUEUE_CONFIG_FILENAME = "secured-config.json" +cdef str AI_ONNX_MODEL_FILE = "azaion.onnx" + +cdef str CDN_CONFIG = "cdn.yaml" +cdef str MODELS_FOLDER = "models" + +cdef int SMALL_SIZE_KB = 3 + +cdef int ALIGNMENT_WIDTH = 32 + +logger.remove() +log_format = "[{time:HH:mm:ss} {level}] {message}" +logger.add( + sink="Logs/log_loader_{time:YYYYMMDD}.txt", + level="INFO", + format=log_format, + enqueue=True, + rotation="1 day", + retention="30 days", +) +logger.add( + sys.stdout, + level="DEBUG", + format=log_format, + filter=lambda record: record["level"].name in ("INFO", "DEBUG", "SUCCESS"), + colorize=True +) +logger.add( + sys.stderr, + level="WARNING", + format=log_format, + colorize=True +) + +cdef log(str log_message): + logger.info(log_message) + +cdef logerror(str error): + logger.error(error) \ No newline at end of file diff --git a/Azaion.Inference/credentials.pxd b/Azaion.Loader/credentials.pxd similarity index 82% rename from Azaion.Inference/credentials.pxd rename to Azaion.Loader/credentials.pxd index cd6c090..74bc016 100644 --- a/Azaion.Inference/credentials.pxd +++ b/Azaion.Loader/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.Loader/credentials.pyx similarity index 63% rename from Azaion.Inference/credentials.pyx rename to Azaion.Loader/credentials.pyx index 2eb020c..bb07e5f 100644 --- a/Azaion.Inference/credentials.pyx +++ b/Azaion.Loader/credentials.pyx @@ -2,16 +2,17 @@ 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")) + + def __str__(self): + return f'{self.email}: {self.password}' diff --git a/Azaion.Loader/file_data.pxd b/Azaion.Loader/file_data.pxd new file mode 100644 index 0000000..811155d --- /dev/null +++ b/Azaion.Loader/file_data.pxd @@ -0,0 +1,24 @@ +cdef class FileData: + cdef public str folder + cdef public str filename + + @staticmethod + cdef from_msgpack(bytes data) + + cdef bytes serialize(self) + +cdef class UploadFileData(FileData): + cdef public bytes resource + + @staticmethod + cdef from_msgpack(bytes data) + + cdef bytes serialize(self) + +cdef class FileList: + cdef public list[str] files + + @staticmethod + cdef from_msgpack(bytes data) + + cdef bytes serialize(self) \ No newline at end of file diff --git a/Azaion.Loader/file_data.pyx b/Azaion.Loader/file_data.pyx new file mode 100644 index 0000000..fec6379 --- /dev/null +++ b/Azaion.Loader/file_data.pyx @@ -0,0 +1,52 @@ +from msgpack import unpackb, packb + +cdef class FileData: + def __init__(self, str folder, str filename): + self.folder = folder + self.filename = filename + + @staticmethod + cdef from_msgpack(bytes data): + unpacked = unpackb(data, strict_map_key=False) + return FileData( + unpacked.get("Folder"), + unpacked.get("Filename")) + + cdef bytes serialize(self): + return packb({ + "Folder": self.folder, + "Filename": self.filename + }) + + +cdef class UploadFileData(FileData): + def __init__(self, bytes resource, str folder, str filename): + super().__init__(folder, filename) + self.resource = resource + + @staticmethod + cdef from_msgpack(bytes data): + unpacked = unpackb(data, strict_map_key=False) + return UploadFileData( + unpacked.get("Resource"), + unpacked.get("Folder"), + unpacked.get("Filename")) + + cdef bytes serialize(self): + return packb({ + "Resource": self.resource, + "Folder": self.folder, + "Filename": self.filename + }) + +cdef class FileList: + def __init__(self, list[str] files): + self.files = files + + @staticmethod + cdef from_msgpack(bytes data): + unpacked = unpackb(data, strict_map_key=False) + return FileList(unpacked.get("files")) + + cdef bytes serialize(self): + return packb({ "files": self.files }) diff --git a/Azaion.Loader/hardware_service.pxd b/Azaion.Loader/hardware_service.pxd new file mode 100644 index 0000000..9cf2ffe --- /dev/null +++ b/Azaion.Loader/hardware_service.pxd @@ -0,0 +1,4 @@ +cdef class HardwareService: + + @staticmethod + cdef str get_hardware_info() \ No newline at end of file diff --git a/Azaion.Loader/hardware_service.pyx b/Azaion.Loader/hardware_service.pyx new file mode 100644 index 0000000..5907ed1 --- /dev/null +++ b/Azaion.Loader/hardware_service.pyx @@ -0,0 +1,36 @@ +import os +import subprocess +cdef class HardwareService: + + @staticmethod + cdef str get_hardware_info(): + if os.name == 'nt': # windows + os_command = ( + "powershell -Command \"" + "Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty Name | Write-Output; " + "Get-CimInstance -ClassName Win32_VideoController | Select-Object -ExpandProperty Name | Write-Output; " + "Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty TotalVisibleMemorySize | Write-Output; " + "(Get-Disk | Where-Object {$_.IsSystem -eq $true}).SerialNumber" + "\"" + ) + else: + os_command = ( + "lscpu | grep 'Model name:' | cut -d':' -f2 && " + "lspci | grep VGA | cut -d':' -f3 && " + "free -k | awk '/^Mem:/ {print $2}' && " + "cat /sys/block/sda/device/vpd_pg80 2>/dev/null || cat /sys/block/sda/device/serial 2>/dev/null" + ) + + result = subprocess.check_output(os_command, shell=True).decode('utf-8', errors='ignore') + lines = [line.replace(" ", " ").replace("Name=", "").strip('\x00\x14 \t\n\r\v\f') for line in result.splitlines() if line.strip()] + + cdef str cpu = lines[0] + cdef str gpu = lines[1] + # could be multiple gpus + + len_lines = len(lines) + cdef str memory = lines[len_lines-2].replace("TotalVisibleMemorySize=", "").replace(" ", " ") + cdef str drive_serial = lines[len_lines-1] + + cdef str res = f'CPU: {cpu}. GPU: {gpu}. Memory: {memory}. DriveSerial: {drive_serial}' + return res diff --git a/Azaion.Loader/main_loader.pyx b/Azaion.Loader/main_loader.pyx new file mode 100644 index 0000000..8746480 --- /dev/null +++ b/Azaion.Loader/main_loader.pyx @@ -0,0 +1,77 @@ +import threading +from threading import Thread +import traceback +cimport constants +from credentials cimport Credentials +from remote_command cimport RemoteCommand, CommandType +from remote_command_handler cimport RemoteCommandHandler +from file_data cimport FileData, UploadFileData, FileList +from api_client cimport ApiClient + +cdef class CommandProcessor: + cdef RemoteCommandHandler remote_handler + cdef ApiClient api_client + cdef bint running + cdef object shutdown_event + cdef RemoteCommand ok_response + + def __init__(self, int zmq_port, str api_url): + self.api_client = ApiClient(api_url) + self.shutdown_event = threading.Event() + self.remote_handler = RemoteCommandHandler(zmq_port, self.on_command) + self.remote_handler.start() + self.running = True + self.ok_response = RemoteCommand(CommandType.OK) + + def start(self): + while self.running: + try: + while not self.shutdown_event.is_set(): + self.shutdown_event.wait(timeout=1.0) + except Exception as e: + traceback.print_exc() + constants.log('EXIT!') + + + cdef on_command(self, RemoteCommand command): + try: + if command.command_type == CommandType.EXIT: + self.remote_handler.send(command.client_id, self.ok_response.serialize()) + t = Thread(target=self.stop) # non-block worker: + t.start() + return + + if command.command_type == CommandType.LOGIN: + self.api_client.set_credentials(Credentials.from_msgpack(command.data)) + self.remote_handler.send(command.client_id, self.ok_response.serialize()) + elif command.command_type == CommandType.LOAD: + file_data = FileData.from_msgpack(command.data) + file_bytes = self.api_client.load_bytes(file_data.filename, file_data.folder) + self.remote_handler.send(command.client_id, RemoteCommand(CommandType.DATA_BYTES, file_bytes).serialize()) + elif command.command_type == CommandType.LIST_REQUEST: + search_data = FileData.from_msgpack(command.data) + list_files = self.api_client.load_bytes(search_data.folder, search_data.filename) + file_list_bytes = FileList(list_files).serialize() + self.remote_handler.send(command.client_id, RemoteCommand(CommandType.LIST_FILES, file_list_bytes).serialize()) + elif command.command_type == CommandType.LOAD_BIG_SMALL: + data = FileData.from_msgpack(command.data) + file_bytes = self.api_client.load_big_small_resource(data.filename, data.folder) + self.remote_handler.send(command.client_id, RemoteCommand(CommandType.DATA_BYTES, file_bytes).serialize()) + elif command.command_type == CommandType.UPLOAD_BIG_SMALL: + data = UploadFileData.from_msgpack(command.data) + file_bytes = self.api_client.upload_big_small_resource(data.resource, data.filename, data.folder) + self.remote_handler.send(command.client_id, RemoteCommand(CommandType.OK).serialize()) + elif command.command_type == CommandType.EXIT: + t = Thread(target=self.stop) # non-block worker: + t.start() + else: + pass + except Exception as e: + constants.logerror(f"Error handling client: {e}") + err_command = RemoteCommand(CommandType.ERROR, None, str(e)) + self.remote_handler.send(command.client_id, err_command.serialize()) + + def stop(self): + self.shutdown_event.set() + self.remote_handler.stop() + self.running = False diff --git a/Azaion.Loader/remote_command.pxd b/Azaion.Loader/remote_command.pxd new file mode 100644 index 0000000..2571843 --- /dev/null +++ b/Azaion.Loader/remote_command.pxd @@ -0,0 +1,27 @@ +cdef enum CommandType: + OK = 3 + LOGIN = 10 + LIST_REQUEST = 15 + LIST_FILES = 18 + LOAD = 20 + LOAD_BIG_SMALL = 22 + UPLOAD_BIG_SMALL = 24 + DATA_BYTES = 25 + INFERENCE = 30 + INFERENCE_DATA = 35 + STOP_INFERENCE = 40 + AI_AVAILABILITY_CHECK = 80 + AI_AVAILABILITY_RESULT = 85 + ERROR = 90 + EXIT = 100 + +cdef class RemoteCommand: + cdef public bytes client_id + cdef CommandType command_type + cdef str message + cdef bytes data + + @staticmethod + cdef from_msgpack(bytes data) + + cdef bytes serialize(self) diff --git a/Azaion.Loader/remote_command.pyx b/Azaion.Loader/remote_command.pyx new file mode 100644 index 0000000..03ff40d --- /dev/null +++ b/Azaion.Loader/remote_command.pyx @@ -0,0 +1,40 @@ +import msgpack + +cdef class RemoteCommand: + def __init__(self, CommandType command_type, bytes data=None, str message=None): + self.command_type = command_type + self.data = data + self.message = message + + def __str__(self): + command_type_names = { + 3: "OK", + 10: "LOGIN", + 15: "LIST_REQUEST", + 18: "LIST_FILES", + 20: "LOAD", + 22: "LOAD_BIG_SMALL", + 24: "UPLOAD_BIG_SMALL", + 25: "DATA_BYTES", + 30: "INFERENCE", + 35: "INFERENCE_DATA", + 40: "STOP_INFERENCE", + 80: "AI_AVAILABILITY_CHECK", + 85: "AI_AVAILABILITY_RESULT", + 90: "ERROR", + 100: "EXIT" + } + data_str = f'{len(self.data)} bytes' if self.data else '' + return f'{command_type_names[self.command_type]} ({data_str})' + + @staticmethod + cdef from_msgpack(bytes data): + unpacked = msgpack.unpackb(data, strict_map_key=False) + return RemoteCommand(unpacked.get("CommandType"), unpacked.get("Data"), unpacked.get("Message")) + + cdef bytes serialize(self): + return msgpack.packb({ + "CommandType": self.command_type, + "Data": self.data, + "Message": self.message + }) diff --git a/Azaion.Loader/remote_command_handler.pxd b/Azaion.Loader/remote_command_handler.pxd new file mode 100644 index 0000000..b4aaba9 --- /dev/null +++ b/Azaion.Loader/remote_command_handler.pxd @@ -0,0 +1,16 @@ +cdef class RemoteCommandHandler: + cdef object _context + cdef object _router + cdef object _dealer + cdef object _control + cdef object _shutdown_event + cdef object _on_command + + cdef object _proxy_thread + cdef object _workers + + cdef start(self) + cdef _proxy_loop(self) + cdef _worker_loop(self) + cdef send(self, bytes client_id, bytes data) + cdef stop(self) diff --git a/Azaion.Loader/remote_command_handler.pyx b/Azaion.Loader/remote_command_handler.pyx new file mode 100644 index 0000000..97b1a3c --- /dev/null +++ b/Azaion.Loader/remote_command_handler.pyx @@ -0,0 +1,89 @@ +import time +import zmq +from threading import Thread, Event +from remote_command cimport RemoteCommand +cimport constants + + +cdef class RemoteCommandHandler: + def __init__(self, int zmq_port, object on_command): + self._on_command = on_command + self._context = zmq.Context.instance() + + self._router = self._context.socket(zmq.ROUTER) + self._router.setsockopt(zmq.LINGER, 0) + self._router.bind(f'tcp://*:{zmq_port}') + + self._dealer = self._context.socket(zmq.DEALER) + self._dealer.setsockopt(zmq.LINGER, 0) + self._dealer.bind("inproc://backend") + + self._control = self._context.socket(zmq.PAIR) + self._control.bind("inproc://control") + self._shutdown_event = Event() + + self._proxy_thread = Thread(target=self._proxy_loop, daemon=True) + + self._workers = [] + for _ in range(4): # 4 worker threads + worker = Thread(target=self._worker_loop, daemon=True) + self._workers.append(worker) + constants.log(f'Listening to commands on port {zmq_port}...') + + cdef start(self): + self._proxy_thread.start() + for worker in self._workers: + worker.start() + + cdef _proxy_loop(self): + try: + zmq.proxy_steerable(self._router, self._dealer, control=self._control) + except zmq.error.ZMQError as e: + if self._shutdown_event.is_set(): + constants.log("Shutdown, exit proxy loop.") + else: + raise + + cdef _worker_loop(self): + worker_socket = self._context.socket(zmq.DEALER) + worker_socket.setsockopt(zmq.LINGER, 0) + worker_socket.connect("inproc://backend") + poller = zmq.Poller() + poller.register(worker_socket, zmq.POLLIN) + try: + + while not self._shutdown_event.is_set(): + try: + socks = dict(poller.poll(500)) + if worker_socket in socks: + client_id, message = worker_socket.recv_multipart() + cmd = RemoteCommand.from_msgpack( message) + cmd.client_id = client_id + constants.log(f'{cmd}') + self._on_command(cmd) + except Exception as e: + if not self._shutdown_event.is_set(): + constants.logerror(f"Worker error: {e}") + import traceback + traceback.print_exc() + finally: + worker_socket.close() + + cdef send(self, bytes client_id, bytes data): + self._router.send_multipart([client_id, data]) + + cdef stop(self): + self._shutdown_event.set() + try: + self._control.send(b"TERMINATE", flags=zmq.DONTWAIT) + except zmq.error.ZMQError: + pass + self._router.close(linger=0) + self._dealer.close(linger=0) + self._control.close(linger=0) + + self._proxy_thread.join(timeout=2) + while any(w.is_alive() for w in self._workers): + time.sleep(0.1) + + self._context.term() diff --git a/Azaion.Loader/requirements.txt b/Azaion.Loader/requirements.txt new file mode 100644 index 0000000..77008ff --- /dev/null +++ b/Azaion.Loader/requirements.txt @@ -0,0 +1,11 @@ +pyinstaller +Cython +psutil +msgpack +pyjwt +zmq +requests +pyyaml +boto3 +loguru +cryptography==44.0.2 \ No newline at end of file diff --git a/Azaion.Inference/security.pxd b/Azaion.Loader/security.pxd similarity index 74% rename from Azaion.Inference/security.pxd rename to Azaion.Loader/security.pxd index ba6f4d8..e0e92ed 100644 --- a/Azaion.Inference/security.pxd +++ b/Azaion.Loader/security.pxd @@ -1,5 +1,4 @@ from credentials cimport Credentials -from hardware_service cimport HardwareInfo cdef class Security: @staticmethod @@ -9,13 +8,13 @@ cdef class Security: cdef decrypt_to(input_bytes, key) @staticmethod - cdef get_hw_hash(HardwareInfo hardware) + cdef get_hw_hash(str hardware) @staticmethod cdef get_api_encryption_key(Credentials credentials, str hardware_hash) @staticmethod - cdef get_model_encryption_key() + cdef get_resource_encryption_key() @staticmethod cdef calc_hash(str key) \ No newline at end of file diff --git a/Azaion.Inference/security.pyx b/Azaion.Loader/security.pyx similarity index 92% rename from Azaion.Inference/security.pyx rename to Azaion.Loader/security.pyx index aabc5e2..a7771c8 100644 --- a/Azaion.Inference/security.pyx +++ b/Azaion.Loader/security.pyx @@ -3,8 +3,6 @@ import hashlib import os from hashlib import sha384 from credentials cimport Credentials -from hardware_service cimport HardwareInfo - from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.primitives import padding @@ -48,8 +46,8 @@ cdef class Security: return bytes(plaintext_bytes) @staticmethod - cdef get_hw_hash(HardwareInfo hardware): - cdef str key = f'Azaion_{hardware.mac_address}_{hardware.cpu}_{hardware.gpu}' + cdef get_hw_hash(str hardware): + cdef str key = f'Azaion_{hardware}_%$$$)0_' return Security.calc_hash(key) @staticmethod @@ -58,7 +56,7 @@ cdef class Security: return Security.calc_hash(key) @staticmethod - cdef get_model_encryption_key(): + cdef get_resource_encryption_key(): cdef str key = '-#%@AzaionKey@%#---234sdfklgvhjbnn' return Security.calc_hash(key) diff --git a/Azaion.Loader/setup.py b/Azaion.Loader/setup.py new file mode 100644 index 0000000..f6fe08c --- /dev/null +++ b/Azaion.Loader/setup.py @@ -0,0 +1,31 @@ +from setuptools import setup, Extension +from Cython.Build import cythonize + +extensions = [ + Extension('constants', ['constants.pyx']), + Extension('file_data', ['file_data.pyx']), + Extension('remote_command', ['remote_command.pyx']), + Extension('remote_command_handler', ['remote_command_handler.pyx']), + Extension('credentials', ['credentials.pyx']), + Extension('hardware_service', ['hardware_service.pyx'], extra_compile_args=["-g"], extra_link_args=["-g"]), + Extension('security', ['security.pyx']), + Extension('user', ['user.pyx']), + Extension('cdn_manager', ['cdn_manager.pyx']), + Extension('api_client', ['api_client.pyx']), + Extension('main_loader', ['main_loader.pyx']), +] + +setup( + name="azaion.loader", + ext_modules=cythonize( + extensions, + compiler_directives={ + "language_level": 3, + "emit_code_comments": False, + "binding": True, + 'boundscheck': False, + 'wraparound': False + } + ), + zip_safe=False +) \ No newline at end of file diff --git a/Azaion.Loader/start.py b/Azaion.Loader/start.py new file mode 100644 index 0000000..5187220 --- /dev/null +++ b/Azaion.Loader/start.py @@ -0,0 +1,15 @@ +from main_loader import CommandProcessor +import argparse + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument("-p", "--port", type=str, default="5025", help="zero mq port") + parser.add_argument("-a", "--api", type=str, default="https://api.azaion.com", help="api url") + args = parser.parse_args() + + processor = CommandProcessor(int(args.port), args.api) + try: + processor.start() + except KeyboardInterrupt: + processor.stop() diff --git a/Azaion.Inference/user.pxd b/Azaion.Loader/user.pxd similarity index 100% rename from Azaion.Inference/user.pxd rename to Azaion.Loader/user.pxd diff --git a/Azaion.Inference/user.pyx b/Azaion.Loader/user.pyx similarity index 100% rename from Azaion.Inference/user.pyx rename to Azaion.Loader/user.pyx diff --git a/Azaion.LoaderUI/ApiCredentials.cs b/Azaion.LoaderUI/ApiCredentials.cs new file mode 100644 index 0000000..360b3c6 --- /dev/null +++ b/Azaion.LoaderUI/ApiCredentials.cs @@ -0,0 +1,10 @@ +namespace Azaion.LoaderUI; + +public class ApiCredentials(string email, string pw) +{ + public string Email { get; set; } = email; + public string Password { get; set; } = pw; + + public bool IsValid() => + !string.IsNullOrWhiteSpace(Email) && !string.IsNullOrWhiteSpace(Password); +} \ No newline at end of file diff --git a/Azaion.LoaderUI/App.xaml b/Azaion.LoaderUI/App.xaml new file mode 100644 index 0000000..db0e726 --- /dev/null +++ b/Azaion.LoaderUI/App.xaml @@ -0,0 +1,7 @@ + + + + + diff --git a/Azaion.LoaderUI/App.xaml.cs b/Azaion.LoaderUI/App.xaml.cs new file mode 100644 index 0000000..d879953 --- /dev/null +++ b/Azaion.LoaderUI/App.xaml.cs @@ -0,0 +1,56 @@ +using System.Windows; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Serilog; + +namespace Azaion.LoaderUI; + +public partial class App +{ + protected override void OnStartup(StartupEventArgs e) + { + base.OnStartup(e); + Start(); + } + + private void Start() + { + Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .MinimumLevel.Information() + .WriteTo.Console() + .WriteTo.File( + path: "Logs/log.txt", + rollingInterval: RollingInterval.Day) + .CreateLogger(); + + var host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((_, config) => config + .AddCommandLine(Environment.GetCommandLineArgs()) + .AddJsonFile(Constants.CONFIG_JSON_FILE, optional: true)) + .UseSerilog() + .ConfigureServices((context, services) => + { + services.AddSingleton(); + services.Configure(context.Configuration.GetSection(nameof(DirectoriesConfig))); + services.AddHttpClient((sp, client) => + { + client.BaseAddress = new Uri(Constants.API_URL); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.DefaultRequestHeaders.Add("User-Agent", "Azaion.LoaderUI"); + }); + }) + .Build(); + host.Start(); + + host.Services.GetRequiredService().Show(); + } + + + + //AFter: + //_loaderClient.Login(credentials); + //_loaderClient.Dispose(); +} + diff --git a/Azaion.LoaderUI/AssemblyInfo.cs b/Azaion.LoaderUI/AssemblyInfo.cs new file mode 100644 index 0000000..b0ec827 --- /dev/null +++ b/Azaion.LoaderUI/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Azaion.LoaderUI/Azaion.LoaderUI.csproj b/Azaion.LoaderUI/Azaion.LoaderUI.csproj new file mode 100644 index 0000000..74c4c88 --- /dev/null +++ b/Azaion.LoaderUI/Azaion.LoaderUI.csproj @@ -0,0 +1,28 @@ + + + + WinExe + net8.0-windows + enable + enable + true + ..\logo.ico + + + + + + + + + + + + + + + + + + + diff --git a/Azaion.LoaderUI/AzaionApi.cs b/Azaion.LoaderUI/AzaionApi.cs new file mode 100644 index 0000000..effc478 --- /dev/null +++ b/Azaion.LoaderUI/AzaionApi.cs @@ -0,0 +1,118 @@ +using System.IO; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using Newtonsoft.Json; + +namespace Azaion.LoaderUI; + +public interface IAzaionApi +{ + void Login(ApiCredentials credentials); + Task GetLastInstallerName(string folder); + Task<(string name, Stream stream)> DownloadInstaller(string folder); +} + +public class AzaionApi(HttpClient client) : IAzaionApi +{ + private string _jwtToken = null!; + const string APP_JSON = "application/json"; + private ApiCredentials _credentials = null!; + + public void Login(ApiCredentials credentials) + { + _credentials = credentials; + } + + public async Task GetLastInstallerName(string folder) + { + var res = await Get>($"/resources/list/{folder}"); + return res?.FirstOrDefault() ?? ""; + } + + public async Task<(string name, Stream stream)> DownloadInstaller(string folder) + { + var response = await Send(new HttpRequestMessage(HttpMethod.Get, $"resources/get-installer/{folder}")); + var fileStream = await response.Content.ReadAsStreamAsync(); + var fileName = response.Content.Headers.ContentDisposition?.FileName?.Trim('"') ?? "installer.exe"; + return (fileName, fileStream); + } + + private async Task Send(HttpRequestMessage request) + { + if (string.IsNullOrEmpty(_jwtToken)) + await Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken); + var response = await client.SendAsync(request); + + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + await Authorize(); + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _jwtToken); + response = await client.SendAsync(request); + } + + if (response.IsSuccessStatusCode) + return response; + + var stream = await response.Content.ReadAsStreamAsync(); + var content = await new StreamReader(stream).ReadToEndAsync(); + 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}"); + } + + private async Task Get(string url) + { + var response = await Send(new HttpRequestMessage(HttpMethod.Get, url)); + var stream = await response.Content.ReadAsStreamAsync(); + var json = await new StreamReader(stream).ReadToEndAsync(); + return JsonConvert.DeserializeObject(json); + } + + private async Task Put(string url, T obj) + { + await Send(new HttpRequestMessage(HttpMethod.Put, url) + { + Content = new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, APP_JSON) + }); + } + + private async Task Authorize() + { + try + { + if (string.IsNullOrEmpty(_credentials.Email) || _credentials.Password.Length == 0) + throw new Exception("Email or password is empty! Please do EnterCredentials first!"); + + var content = new StringContent(JsonConvert.SerializeObject(new + { + email = _credentials.Email, + password = _credentials.Password + }), Encoding.UTF8, APP_JSON); + var message = new HttpRequestMessage(HttpMethod.Post, "login") { Content = content }; + var response = await client.SendAsync(message); + + if (!response.IsSuccessStatusCode) + throw new Exception($"EnterCredentials failed: {response.StatusCode}"); + + var stream = await response.Content.ReadAsStreamAsync(); + var json = await new StreamReader(stream).ReadToEndAsync(); + 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; + } + } +} \ No newline at end of file diff --git a/Azaion.LoaderUI/BusinessExceptionDto.cs b/Azaion.LoaderUI/BusinessExceptionDto.cs new file mode 100644 index 0000000..bb9afee --- /dev/null +++ b/Azaion.LoaderUI/BusinessExceptionDto.cs @@ -0,0 +1,7 @@ +namespace Azaion.LoaderUI; + +public class BusinessExceptionDto +{ + public int ErrorCode { get; set; } + public string Message { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Azaion.LoaderUI/Constants.cs b/Azaion.LoaderUI/Constants.cs new file mode 100644 index 0000000..acdc649 --- /dev/null +++ b/Azaion.LoaderUI/Constants.cs @@ -0,0 +1,9 @@ +namespace Azaion.LoaderUI; + +public static class Constants +{ + public const string CONFIG_JSON_FILE = "loaderconfig.json"; + public const string API_URL = "https://api.azaion.com"; + public const string AZAION_SUITE_EXE = "Azaion.Suite.exe"; + public const string SUITE_FOLDER = "suite"; +} \ No newline at end of file diff --git a/Azaion.LoaderUI/LoaderConfig.cs b/Azaion.LoaderUI/LoaderConfig.cs new file mode 100644 index 0000000..169e376 --- /dev/null +++ b/Azaion.LoaderUI/LoaderConfig.cs @@ -0,0 +1,6 @@ +namespace Azaion.LoaderUI; + +public class DirectoriesConfig +{ + public string SuiteInstallerDirectory {get;set;} = null!; +} \ No newline at end of file diff --git a/Azaion.Suite/Login.xaml b/Azaion.LoaderUI/Login.xaml similarity index 66% rename from Azaion.Suite/Login.xaml rename to Azaion.LoaderUI/Login.xaml index 8b419d6..2398b7f 100644 --- a/Azaion.Suite/Login.xaml +++ b/Azaion.LoaderUI/Login.xaml @@ -1,4 +1,4 @@ - + Margin="15"> + - diff --git a/Azaion.LoaderUI/Login.xaml.cs b/Azaion.LoaderUI/Login.xaml.cs new file mode 100644 index 0000000..431f3e7 --- /dev/null +++ b/Azaion.LoaderUI/Login.xaml.cs @@ -0,0 +1,98 @@ +using System.Diagnostics; +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 ILogger _logger; + private readonly DirectoriesConfig? _dirConfig; + + public Login(IAzaionApi azaionApi, IOptions directoriesConfig, ILogger logger) + { + _azaionApi = azaionApi; + _logger = logger; + _dirConfig = directoriesConfig.Value; + InitializeComponent(); + } + + private async void LoginClick(object sender, RoutedEventArgs e) + { + var creds = new ApiCredentials(TbEmail.Text, TbPassword.Password); + if (!creds.IsValid()) + return; + + LoginBtn.Cursor = Cursors.Wait; + Cursor = Cursors.Wait; + + _azaionApi.Login(creds); + + 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 async Task DownloadAndRunInstaller() + { + var (installerName, stream) = await _azaionApi.DownloadInstaller(_dirConfig?.SuiteInstallerDirectory ?? ""); + var localFileStream = new FileStream(installerName, FileMode.Create, FileAccess.Write); + await stream.CopyToAsync(localFileStream); + localFileStream.Close(); + stream.Close(); + var processInfo = new ProcessStartInfo(installerName) + { + UseShellExecute = true, + Arguments = "/VERYSILENT" + }; + + var process = Process.Start(processInfo); + await process!.WaitForExitAsync(); + } + + private async Task GetInstallerVer() + { + 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", ""); + return new Version(version); + } + + private Version GetLocalVer() + { + var localFileInfo = FileVersionInfo.GetVersionInfo(Constants.AZAION_SUITE_EXE); + if (string.IsNullOrWhiteSpace(localFileInfo.ProductVersion)) + throw new Exception($"Can't find {Constants.AZAION_SUITE_EXE} and its version"); + return new Version(localFileInfo.FileVersion!); + } + + private void CloseClick(object sender, RoutedEventArgs e) => Close(); + + private void MainMouseMove(object sender, MouseEventArgs e) + { + if (e.OriginalSource is Button || e.OriginalSource is TextBox) + return; + + if (e.LeftButton == MouseButtonState.Pressed) + DragMove(); + } +} diff --git a/Azaion.LoaderUI/LoginResponse.cs b/Azaion.LoaderUI/LoginResponse.cs new file mode 100644 index 0000000..9e36fca --- /dev/null +++ b/Azaion.LoaderUI/LoginResponse.cs @@ -0,0 +1,6 @@ +namespace Azaion.LoaderUI; + +public class LoginResponse +{ + public string Token { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Suite.sln b/Azaion.Suite.sln index 73c9f06..515cc63 100644 --- a/Azaion.Suite.sln +++ b/Azaion.Suite.sln @@ -1,5 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36127.28 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Azaion.Annotator\Azaion.Annotator.csproj", "{8E0809AF-2920-4267-B14D-84BAB334A46F}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Test", "Azaion.Test\Azaion.Test.csproj", "{85359558-FB59-4542-A597-FD9E1B04C8E7}" @@ -10,28 +13,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Common", "Azaion.Com EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Azaion.Dataset\Azaion.Dataset.csproj", "{01A5CA37-A62E-4EF3-8678-D72CD9525677}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Dummy", "Dummy", "{C307BE2E-FFCC-4BD7-AD89-C82D40B65D03}" - ProjectSection(SolutionItems) = preProject - Dummy\Azaion.Annotator.dll = Dummy\Azaion.Annotator.dll - Dummy\Azaion.Dataset.dll = Dummy\Azaion.Dataset.dll - EndProjectSection -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Dummy\Azaion.Annotator\Azaion.Annotator.csproj", "{32C4747F-F700-44FD-B4ED-21B4A66B5FAB}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Dummy\Azaion.Dataset\Azaion.Dataset.csproj", "{A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.CommonSecurity", "Azaion.CommonSecurity\Azaion.CommonSecurity.csproj", "{E0C7176D-2E91-4928-B3C1-55CC91C8F77D}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CF141A48-8002-4006-81CF-6B85AE5B0B5F}" ProjectSection(SolutionItems) = preProject - build\publish.cmd = build\publish.cmd - build\requirements.txt = build\requirements.txt - build\build_downloader.cmd = build\build_downloader.cmd - build\installer.iss = build\installer.iss + build\build_cdn_manager.cmd = build\build_cdn_manager.cmd + build\build_dotnet.cmd = build\build_dotnet.cmd build\cdn_manager.py = build\cdn_manager.py build\downloader_config.yaml = build\downloader_config.yaml + build\download_models.cmd = build\download_models.cmd + build\init.cmd = build\init.cmd + build\installer.full.iss = build\installer.full.iss + build\installer.iterative.iss = build\installer.iterative.iss + build\publish.cmd = build\publish.cmd + build\requirements.txt = build\requirements.txt + build\upload.cmd = build\upload.cmd EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.LoaderUI", "Azaion.LoaderUI\Azaion.LoaderUI.csproj", "{C96C142E-3ED3-4455-9C22-93A12022B8A9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -63,22 +61,17 @@ Global {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.ActiveCfg = Release|Any CPU {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.Build.0 = Release|Any CPU {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.Deploy.0 = Release|Any CPU - {32C4747F-F700-44FD-B4ED-21B4A66B5FAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {32C4747F-F700-44FD-B4ED-21B4A66B5FAB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {32C4747F-F700-44FD-B4ED-21B4A66B5FAB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {32C4747F-F700-44FD-B4ED-21B4A66B5FAB}.Release|Any CPU.Build.0 = Release|Any CPU - {A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E}.Release|Any CPU.Build.0 = Release|Any CPU - {E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Release|Any CPU.Build.0 = Release|Any CPU - {E0C7176D-2E91-4928-B3C1-55CC91C8F77D}.Release|Any CPU.Deploy.0 = Release|Any CPU + {C96C142E-3ED3-4455-9C22-93A12022B8A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C96C142E-3ED3-4455-9C22-93A12022B8A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C96C142E-3ED3-4455-9C22-93A12022B8A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C96C142E-3ED3-4455-9C22-93A12022B8A9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {32C4747F-F700-44FD-B4ED-21B4A66B5FAB} = {C307BE2E-FFCC-4BD7-AD89-C82D40B65D03} - {A2E3D3AE-5DB7-4342-BE20-88A9D1B0C05E} = {C307BE2E-FFCC-4BD7-AD89-C82D40B65D03} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {788BD4AD-E4EC-43A1-85A0-AEC644BD8D48} EndGlobalSection EndGlobal diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index b86517e..ca9f56e 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -1,24 +1,23 @@ using System.IO; -using System.Reflection; +using System.Net.Http; +using System.Text; 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; using Azaion.Common.Events; using Azaion.Common.Extensions; using Azaion.Common.Services; -using Azaion.CommonSecurity; -using Azaion.CommonSecurity.DTO; -using Azaion.CommonSecurity.Services; using Azaion.Dataset; +using CommandLine; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Newtonsoft.Json; using Serilog; @@ -28,113 +27,24 @@ namespace Azaion.Suite; public partial class App { - private IHost _host = null!; - private ILogger _logger = null!; private IMediator _mediator = null!; private FormState _formState = null!; - - private IInferenceClient _inferenceClient = null!; - private IResourceLoader _resourceLoader = null!; - private IAuthProvider _authProvider = null!; - - private Stream _securedConfig = null!; - private Stream _systemConfig = null!; + private IHost _host = null!; private static readonly Guid KeyPressTaskId = Guid.NewGuid(); + private LoaderClient _loaderClient = null!; + + private readonly ICache _cache = new MemoryCache(); + private readonly CancellationTokenSource _mainCTokenSource = new(); private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { - _logger.LogError(e.Exception, e.Exception.Message); + Log.Logger.Error(e.Exception, "Unhandled exception"); e.Handled = true; } protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); - StartLogin(); - } - - private readonly List _encryptedResources = - [ - "Azaion.Annotator", - "Azaion.Dataset" - ]; - - private static SecureAppConfig ReadSecureAppConfig() - { - try - { - if (!File.Exists(SecurityConstants.CONFIG_PATH)) - throw new FileNotFoundException(SecurityConstants.CONFIG_PATH); - var configStr = File.ReadAllText(SecurityConstants.CONFIG_PATH); - var config = JsonConvert.DeserializeObject(configStr); - - return config ?? SecurityConstants.DefaultSecureAppConfig; - } - catch (Exception e) - { - Console.WriteLine(e); - return SecurityConstants.DefaultSecureAppConfig; - } - } - - private void StartLogin() - { - new ConfigUpdater().CheckConfig(); - 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) => - { - credentials.Folder = secureAppConfig.InferenceClientConfig.ResourcesFolder; - _authProvider.Login(credentials); - _securedConfig = _resourceLoader.LoadFile("config.secured.json"); - _systemConfig = _resourceLoader.LoadFile("config.system.json"); - - AppDomain.CurrentDomain.AssemblyResolve += (_, a) => - { - var assemblyName = a.Name.Split(',').First(); - if (_encryptedResources.Contains(assemblyName)) - { - try - { - var stream = _resourceLoader.LoadFile($"{assemblyName}.dll"); - return Assembly.Load(stream.ToArray()); - } - catch (Exception e) - { - Console.WriteLine(e); - var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; - var dllPath = Path.Combine(currentLocation, SecurityConstants.DUMMY_DIR, $"{assemblyName}.dll"); - return Assembly.LoadFile(dllPath); - } - } - - var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies() - .FirstOrDefault(a => a.GetName().Name == assemblyName); - - return loadedAssembly; - }; - - StartMain(); - _host.Start(); - EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick)); - _host.Services.GetRequiredService().Show(); - }; - - login.ShowDialog(); - } - - private void StartMain() - { Log.Logger = new LoggerConfiguration() .Enrich.FromLogContext() .MinimumLevel.Information() @@ -144,86 +54,170 @@ public partial class App rollingInterval: RollingInterval.Day) .CreateLogger(); - _host = Host.CreateDefaultBuilder() - .ConfigureAppConfiguration((context, config) => config - .AddCommandLine(Environment.GetCommandLineArgs()) - .AddJsonFile(SecurityConstants.CONFIG_PATH, optional: true, reloadOnChange: true) - .AddJsonStream(_securedConfig) - .AddJsonStream(_systemConfig)) - .UseSerilog() - .ConfigureServices((context, services) => - { - services.AddSingleton(); - services.AddSingleton(); - - services.Configure(context.Configuration); - services.ConfigureSection(context.Configuration); - services.ConfigureSection(context.Configuration); - services.ConfigureSection(context.Configuration); - services.ConfigureSection(context.Configuration); - services.ConfigureSection(context.Configuration); - services.ConfigureSection(context.Configuration); - - #region External Services - - services.ConfigureSection(context.Configuration); - services.ConfigureSection(context.Configuration); - services.AddSingleton(_inferenceClient); - services.AddSingleton(); - services.AddSingleton(_resourceLoader); - services.AddSingleton(_authProvider); - services.AddSingleton(); - - #endregion - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddMediatR(c => c.RegisterServicesFromAssemblies( - typeof(Annotator.Annotator).Assembly, - typeof(DatasetExplorer).Assembly, - typeof(AnnotationService).Assembly)); - services.AddSingleton(_ => new LibVLC()); - services.AddSingleton(); - services.AddSingleton(sp => - { - var libVLC = sp.GetRequiredService(); - return new MediaPlayer(libVLC); - }); - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - - services.AddSingleton(); - services.AddSingleton(); - }) - .Build(); - - Annotation.InitializeDirs(_host.Services.GetRequiredService>().Value); - - _mediator = _host.Services.GetRequiredService(); - _logger = _host.Services.GetRequiredService>(); - _formState = _host.Services.GetRequiredService(); - DispatcherUnhandledException += OnDispatcherUnhandledException; + Parser.Default.ParseArguments(e.Args) + .WithParsed(Start) + .WithNotParsed(ErrorHandling); } - private void GlobalClick(object sender, RoutedEventArgs e) + private void ErrorHandling(IEnumerable obj) + { + Log.Fatal($"Error happened: {string.Join(",", obj.Select(x => + { + if (x is MissingRequiredOptionError err) + return $"{err.Tag} {err.NameInfo.NameText}"; + return x.Tag.ToString(); + } ))}"); + Current.Shutdown(); + } + + private Stream GetSystemConfig(LoaderClient loaderClient, string apiDir) + { + try + { + return loaderClient.LoadFile("config.system.json", apiDir); + } + catch (Exception e) + { + Log.Logger.Error(e, e.Message); + return new MemoryStream(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new + { + AnnotationConfig = Constants.DefaultAnnotationConfig, + AIRecognitionConfig = Constants.DefaultAIRecognitionConfig, + ThumbnailConfig = Constants.DefaultThumbnailConfig, + }))); + } + } + + private Stream GetSecuredConfig(LoaderClient loaderClient, string apiDir) + { + try + { + return loaderClient.LoadFile("config.secured.json", apiDir); + } + catch (Exception e) + { + Log.Logger.Error(e, e.Message); + throw; + } + } + + private void Start(ApiCredentials credentials) + { + try + { + new ConfigUpdater().CheckConfig(); + var initConfig = SecurityConstants.ReadInitConfig(); + var apiDir = initConfig.DirectoriesConfig.ApiResourcesDirectory; + _loaderClient = new LoaderClient(initConfig.LoaderClientConfig, Log.Logger, _mainCTokenSource.Token); + + _loaderClient.StartClient(); + _loaderClient.Connect(); //Client app should be already started by LoaderUI + _loaderClient.Login(credentials); + + var azaionApi = new AzaionApi(new HttpClient { BaseAddress = new Uri(initConfig.InferenceClientConfig.ApiUrl) }, _cache, credentials); + + _host = Host.CreateDefaultBuilder() + .ConfigureAppConfiguration((_, config) => config + .AddCommandLine(Environment.GetCommandLineArgs()) + .AddJsonFile(SecurityConstants.CONFIG_PATH, optional: true, reloadOnChange: true) + .AddJsonStream(GetSystemConfig(_loaderClient, apiDir)) + .AddJsonStream(GetSecuredConfig(_loaderClient, apiDir))) + .UseSerilog() + .ConfigureServices((context, services) => + { + services.AddSingleton(); + + services.Configure(context.Configuration); + services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); + services.ConfigureSection(context.Configuration); + + #region External Services + + services.ConfigureSection(context.Configuration); + services.AddSingleton(_loaderClient); + + services.ConfigureSection(context.Configuration); + services.AddSingleton(); + services.AddSingleton(); + + services.ConfigureSection(context.Configuration); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddHttpClient(); + services.AddSingleton(azaionApi); + #endregion + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddMediatR(c => c.RegisterServicesFromAssemblies( + typeof(Annotator.Annotator).Assembly, + typeof(DatasetExplorer).Assembly, + typeof(AnnotationService).Assembly)); + services.AddSingleton(_ => new LibVLC()); + services.AddSingleton(); + services.AddSingleton(sp => + { + var libVLC = sp.GetRequiredService(); + return new MediaPlayer(libVLC); + }); + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); + }) + .Build(); + + Annotation.InitializeDirs(_host.Services.GetRequiredService>().Value); + _host.Services.GetRequiredService(); + // datasetExplorer.Show(); + // datasetExplorer.Hide(); + + _mediator = _host.Services.GetRequiredService(); + + _formState = _host.Services.GetRequiredService(); + DispatcherUnhandledException += OnDispatcherUnhandledException; + + _host.Start(); + EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new RoutedEventHandler(GlobalKeyHandler)); + _host.Services.GetRequiredService().Show(); + } + catch (Exception e) + { + Log.Logger.Error(e, e.Message); + throw; + } + } + + private void GlobalKeyHandler(object sender, RoutedEventArgs e) { 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, _mainCTokenSource.Token), KeyPressTaskId, TimeSpan.FromMilliseconds(50)); + //e.Handled = true; } protected override async void OnExit(ExitEventArgs e) { base.OnExit(e); + _loaderClient.Stop(); + _loaderClient.Dispose(); await _host.StopAsync(); } } \ No newline at end of file diff --git a/Azaion.Suite/Azaion.Suite.csproj b/Azaion.Suite/Azaion.Suite.csproj index cde203f..dae6fd6 100644 --- a/Azaion.Suite/Azaion.Suite.csproj +++ b/Azaion.Suite/Azaion.Suite.csproj @@ -11,20 +11,33 @@ true + + $([System.DateTime]::UtcNow.ToString("yyyy.MM.dd")) + $([System.Convert]::ToInt32($([System.DateTime]::UtcNow.TimeOfDay.TotalMinutes))) + + $(VersionDate).$(VersionSeconds) + $(AssemblyVersion) + $(AssemblyVersion) + Copyright @ $([System.DateTime]::UtcNow.ToString("yyyy")) Azaion LLC. All rights reserved. + + + + - - - - + + + + - - + + + - + @@ -33,8 +46,8 @@ - - + + @@ -52,19 +65,20 @@ - - - MSBuild:Compile - Wpf - Designer - - - - - - - + + + $(SolutionDir)Azaion.LoaderUI\bin\$(Configuration)\$(TargetFramework)\ + + + + + + + + + + diff --git a/Azaion.Suite/Login.xaml.cs b/Azaion.Suite/Login.xaml.cs deleted file mode 100644 index 6da38db..0000000 --- a/Azaion.Suite/Login.xaml.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Windows; -using System.Windows.Controls; -using System.Windows.Input; -using Azaion.Common.DTO; -using Azaion.CommonSecurity.DTO; - -namespace Azaion.Suite; - -public partial class Login -{ - public bool MainSuiteOpened { get; set; } = false; - - public Login() - { - InitializeComponent(); - } - - public event EventHandler? CredentialsEntered; - - private void LoginClick(object sender, RoutedEventArgs e) - { - LoginBtn.Cursor = Cursors.Wait; - Cursor = Cursors.Wait; - CredentialsEntered?.Invoke(this, new ApiCredentials(TbEmail.Text, TbPassword.Password)); - MainSuiteOpened = true; - Close(); - } - - private void CloseClick(object sender, RoutedEventArgs e) => Close(); - - private void MainMouseMove(object sender, MouseEventArgs e) - { - if (e.OriginalSource is Button || e.OriginalSource is TextBox) - return; - - if (e.LeftButton == MouseButtonState.Pressed) - DragMove(); - } -} diff --git a/Azaion.Suite/MainSuite.xaml.cs b/Azaion.Suite/MainSuite.xaml.cs index c3854c1..80c0be2 100644 --- a/Azaion.Suite/MainSuite.xaml.cs +++ b/Azaion.Suite/MainSuite.xaml.cs @@ -8,8 +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; using SharpVectors.Converters; @@ -25,7 +23,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 +33,6 @@ public partial class MainSuite IServiceProvider sp, IGalleryService galleryService, IDbFactory dbFactory, - IResourceLoader resourceLoader, IInferenceClient inferenceClient, IGpsMatcherClient gpsMatcherClient) { @@ -45,18 +41,17 @@ public partial class MainSuite _sp = sp; _galleryService = galleryService; _dbFactory = dbFactory; - _resourceLoader = resourceLoader; _inferenceClient = inferenceClient; _gpsMatcherClient = gpsMatcherClient; - _appConfig = appConfig.Value; + InitializeComponent(); Loaded += OnLoaded; Closed += OnFormClosed; - SizeChanged += async (_, _) => await SaveUserSettings(); - LocationChanged += async (_, _) => await SaveUserSettings(); - StateChanged += async (_, _) => await SaveUserSettings(); + SizeChanged += (_, _) => SaveUserSettings(); + LocationChanged += (_, _) => SaveUserSettings(); + StateChanged += (_, _) => SaveUserSettings(); Left = (SystemParameters.WorkArea.Width - Width) / 2; } @@ -123,9 +118,6 @@ public partial class MainSuite if (_openedWindows.Any()) return; - _inferenceClient.Stop(); - _gpsMatcherClient.Stop(); - Close(); }; window.Show(); @@ -133,9 +125,10 @@ public partial class MainSuite } } - private async Task SaveUserSettings() + + private void SaveUserSettings() { - await ThrottleExt.ThrottleRunFirst(() => + ThrottleExt.Throttle(() => { _configUpdater.Save(_appConfig); return Task.CompletedTask; @@ -145,9 +138,11 @@ public partial class MainSuite private void OnFormClosed(object? sender, EventArgs e) { _configUpdater.Save(_appConfig); - _dbFactory.SaveToDisk(); foreach (var window in _openedWindows) window.Value.Close(); + + _inferenceClient.Dispose(); + _gpsMatcherClient.Dispose(); Application.Current.Shutdown(); } diff --git a/Azaion.Suite/build_loader_inf.cmd b/Azaion.Suite/build_loader_inf.cmd new file mode 100644 index 0000000..be08122 --- /dev/null +++ b/Azaion.Suite/build_loader_inf.cmd @@ -0,0 +1,3 @@ +call ..\Azaion.Inference\build_inference +call ..\Azaion.Loader\build_loader +call copy_loader_inf \ No newline at end of file diff --git a/Azaion.Suite/config.json b/Azaion.Suite/config.json index da36fa7..422921b 100644 --- a/Azaion.Suite/config.json +++ b/Azaion.Suite/config.json @@ -1,28 +1,33 @@ { + "LoaderClientConfig": { + "ZeroMqHost": "127.0.0.1", + "ZeroMqPort": 5025, + "ApiUrl": "https://api.azaion.com" + }, "InferenceClientConfig": { "ZeroMqHost": "127.0.0.1", "ZeroMqPort": 5127, - "RetryCount": 25, - "TimeoutSeconds": 5, - "ResourcesFolder": "stage" + "ApiUrl": "https://api.azaion.com" }, "GpsDeniedClientConfig": { "ZeroMqHost": "127.0.0.1", "ZeroMqPort": 5555, - "ZeroMqReceiverPort": 5556, - "RetryCount": 25, - "TimeoutSeconds": 5 + "ZeroMqReceiverPort": 5556 }, "DirectoriesConfig": { + "ApiResourcesDirectory": "stage", "VideosDirectory": "E:\\Azaion6", "LabelsDirectory": "E:\\labels", "ImagesDirectory": "E:\\images", "ResultsDirectory": "E:\\results", - "ThumbnailsDirectory": "E:\\thumbnails" + "ThumbnailsDirectory": "E:\\thumbnails", + "GpsSatDirectory": "satellitesDir", + "GpsRouteDirectory": "routeDir" }, "UIConfig": { "LeftPanelWidth": 220.0, "RightPanelWidth": 230.0, - "GenerateAnnotatedImage": true + "GenerateAnnotatedImage": true, + "SilentDetection": false } } \ No newline at end of file diff --git a/Azaion.Suite/config.production.json b/Azaion.Suite/config.production.json index dd879a7..71dfe6f 100644 --- a/Azaion.Suite/config.production.json +++ b/Azaion.Suite/config.production.json @@ -1,27 +1,33 @@ { + "LoaderClientConfig": { + "ZeroMqHost": "127.0.0.1", + "ZeroMqPort": 5025, + "ApiUrl": "https://api.azaion.com" + }, "InferenceClientConfig": { "ZeroMqHost": "127.0.0.1", - "ZeroMqPort": 5131, - "RetryCount": 25, - "TimeoutSeconds": 5, - "ResourcesFolder": "" + "ZeroMqPort": 5127, + "ApiUrl": "https://api.azaion.com" }, "GpsDeniedClientConfig": { "ZeroMqHost": "127.0.0.1", "ZeroMqPort": 5555, - "ZeroMqReceiverPort": 5556, - "RetryCount": 25, - "TimeoutSeconds": 5 + "ZeroMqReceiverPort": 5556 }, "DirectoriesConfig": { + "ApiResourcesDirectory": "", "VideosDirectory": "videos", "LabelsDirectory": "labels", "ImagesDirectory": "images", "ResultsDirectory": "results", - "ThumbnailsDirectory": "thumbnails" + "ThumbnailsDirectory": "thumbnails", + "GpsSatDirectory": "satellitesDir", + "GpsRouteDirectory": "routeDir" }, "UIConfig": { "LeftPanelWidth": 170.0, - "RightPanelWidth": 120.0 + "RightPanelWidth": 120.0, + "GenerateAnnotatedImage": true, + "SilentDetection": false } } \ No newline at end of file diff --git a/Azaion.Suite/config.system.json b/Azaion.Suite/config.system.json index bc78f06..e3a280c 100644 --- a/Azaion.Suite/config.system.json +++ b/Azaion.Suite/config.system.json @@ -1,22 +1,23 @@ { "AnnotationConfig": { "DetectionClasses": [ - { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#FF0000" }, - { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00FF00" }, - { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000FF" }, - { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#FFFF00" }, - { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#FF00FF" }, - { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00FFFF" }, - { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021" }, - { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000" }, - { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000" }, - { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, - { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#000080" }, - { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, - { "Id": 12, "Name": "CamouflageNnet", "ShortName": "Сітка", "Color": "#800080" }, - { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" }, - { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" }, - { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" } + { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#ff0000" }, + { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00ff00" }, + { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000ff" }, + { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#ffff00" }, + { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#ff00ff" }, + { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00ffff" }, + { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021" }, + { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000" }, + { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000" }, + { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, + { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#a52a2a" }, + { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, + { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#87ceeb" }, + { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" }, + { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" }, + { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" }, + { "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500" } ], "VideoFormats": [ ".mp4", ".mov", ".avi" ], "ImageFormats": [ ".jpg", ".jpeg", ".png", ".bmp" ], diff --git a/Azaion.Suite/postbuild.cmd b/Azaion.Suite/copy_gps_denied.cmd similarity index 65% rename from Azaion.Suite/postbuild.cmd rename to Azaion.Suite/copy_gps_denied.cmd index da1b574..cead2c9 100644 --- a/Azaion.Suite/postbuild.cmd +++ b/Azaion.Suite/copy_gps_denied.cmd @@ -1,15 +1,4 @@ -@echo off - -set CONFIG=%1 -set RESOURCES_FOLDER=%2 - -set FILE1_TO_UPLOAD=%cd%\..\Azaion.Annotator\bin\%CONFIG%\net8.0-windows\Azaion.Annotator.dll -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 DESTINATION=%SUITE_FOLDER%\gps-denied set GPS_DENIED=%cd%\..\..\gps-denied\ rmdir %DESTINATION% /s /q diff --git a/Azaion.Suite/copy_loader_inf.cmd b/Azaion.Suite/copy_loader_inf.cmd new file mode 100644 index 0000000..9c712f2 --- /dev/null +++ b/Azaion.Suite/copy_loader_inf.cmd @@ -0,0 +1,13 @@ +echo Build Cython app +set CURRENT_DIR=%cd% + +REM Change to the parent directory of the current location +cd /d %~dp0 + + +robocopy "..\dist-azaion\_internal" "bin\Debug\net8.0-windows\_internal" /E +robocopy "..\dist-dlls\_internal" "bin\Debug\net8.0-windows\_internal" /E + +robocopy "..\dist-azaion" "bin\Debug\net8.0-windows" "azaion-inference.exe" "azaion-loader.exe" + +cd /d %CURRENT_DIR% \ No newline at end of file diff --git a/Azaion.Suite/upload-file.cmd b/Azaion.Suite/upload-file.cmd deleted file mode 100644 index b32d6e5..0000000 --- a/Azaion.Suite/upload-file.cmd +++ /dev/null @@ -1,28 +0,0 @@ -setlocal enabledelayedexpansion - -set API_URL=https://api.azaion.com -set SOURCE_FILE=%1 -set DESTINATION=%2 - -set "SOURCE_FILE=%SOURCE_FILE:\=/%" - -set EMAIL=uploader@azaion.com -set PASSWORD=Az@1on_10Upl0@der - -for /f "tokens=*" %%i in ('curl -s -X POST -H "Content-Type: application/json" ^ - -d "{\"email\":\"%EMAIL%\",\"password\":\"%PASSWORD%\"}" %API_URL%/login') do set RESPONSE=%%i - -for /f "tokens=2 delims=:" %%a in ('echo %RESPONSE% ^| findstr /i "token"') do ( - set "TOKEN=%%a" - set "TOKEN=!TOKEN:~1,-1!" - set "TOKEN=!TOKEN:~0,-2!" -) - -set UPLOAD_URL=%API_URL%/resources/%DESTINATION% - -echo Uploading %SOURCE_FILE% to %UPLOAD_URL%... -curl --location %UPLOAD_URL% ^ - -H "Authorization: Bearer %TOKEN%" ^ - -H "Content-Type: multipart/form-data" ^ - --form "data=@%SOURCE_FILE%" -echo Upload complete! diff --git a/Azaion.Test/Azaion.Test.csproj b/Azaion.Test/Azaion.Test.csproj index e0cc5b7..6c13800 100644 --- a/Azaion.Test/Azaion.Test.csproj +++ b/Azaion.Test/Azaion.Test.csproj @@ -13,7 +13,7 @@ - + diff --git a/Azaion.Test/GetTilesTest.cs b/Azaion.Test/GetTilesTest.cs index d27e8d3..b33a7fc 100644 --- a/Azaion.Test/GetTilesTest.cs +++ b/Azaion.Test/GetTilesTest.cs @@ -1,5 +1,7 @@ -using Azaion.Common.DTO.Config; +using Azaion.Common.DTO; +using Azaion.Common.DTO.Config; using Azaion.Common.Services; +using MediatR; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -10,6 +12,13 @@ namespace Azaion.Annotator.Test; public class GetTilesTestClass { + [Fact] + public void TestPath() + { + var filename = "./input/images-scaled\\AD000010.tif"; + filename = Path.GetFileName(filename); + } + [Fact] public async Task GetTilesTest() { @@ -25,7 +34,7 @@ public class GetTilesTestClass }), new OptionsWrapper(new DirectoriesConfig { GpsSatDirectory = "satelliteMaps" - }), httpClientFactory); + }), httpClientFactory, Mock.Of()); await satelliteDownloader.GetTiles(48.2748909, 37.3834877, 600, 18); } diff --git a/Azaion.Test/HardwareServiceTest.cs b/Azaion.Test/HardwareServiceTest.cs deleted file mode 100644 index a7e5688..0000000 --- a/Azaion.Test/HardwareServiceTest.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Azaion.Common.Services; -using Azaion.CommonSecurity.Services; -using Xunit; - -namespace Azaion.Annotator.Test; - -public class HardwareServiceTest -{ - [Fact] - public void GetHardware_Test() - { - var hardwareService = new HardwareService(); - var hw = hardwareService.GetHardware(); - Console.WriteLine(hw); - } -} \ No newline at end of file diff --git a/Azaion.Test/ThrottleTest.cs b/Azaion.Test/ThrottleTest.cs new file mode 100644 index 0000000..6a5b746 --- /dev/null +++ b/Azaion.Test/ThrottleTest.cs @@ -0,0 +1,57 @@ +using Azaion.Common.Extensions; +using FluentAssertions; +using Xunit; + +namespace Azaion.Annotator.Test; + +public class ThrottleTest +{ + private readonly Guid _testTaskId = Guid.NewGuid(); + + [Fact] + public async Task TestScheduleAfterCooldown() + { + var calls = new List(); + + Console.WriteLine($"Start time: {DateTime.Now}"); + for (int i = 0; i < 10; i++) + { + ThrottleExt.Throttle(() => + { + calls.Add(DateTime.Now); + return Task.CompletedTask; + }, _testTaskId, TimeSpan.FromSeconds(1), scheduleCallAfterCooldown: true); + } + + await Task.Delay(TimeSpan.FromSeconds(2)); + Console.WriteLine(string.Join(',', calls)); + + calls.Count.Should().Be(2); + } + + [Fact] + public async Task TestScheduleAfterCooldown2() + { + var calls = new List(); + + Console.WriteLine($"Start time: {DateTime.Now}"); + ThrottleExt.Throttle(() => + { + calls.Add(DateTime.Now); + return Task.CompletedTask; + }, _testTaskId, TimeSpan.FromSeconds(1), scheduleCallAfterCooldown: true); + await Task.Delay(TimeSpan.FromSeconds(2)); + + ThrottleExt.Throttle(() => + { + calls.Add(DateTime.Now); + return Task.CompletedTask; + }, _testTaskId, TimeSpan.FromSeconds(1), scheduleCallAfterCooldown: true); + + + await Task.Delay(TimeSpan.FromSeconds(2)); + Console.WriteLine(string.Join(',', calls)); + + calls.Count.Should().Be(2); + } +} \ No newline at end of file diff --git a/Dummy/Azaion.Annotator/Annotator.xaml b/Dummy/Azaion.Annotator/Annotator.xaml deleted file mode 100644 index acd521e..0000000 --- a/Dummy/Azaion.Annotator/Annotator.xaml +++ /dev/null @@ -1,25 +0,0 @@ - - - - Будь ласка перевірте правильність email чи паролю! - Також зауважте, що запуск можливий лише з одного конкретного компьютера, копіювання заборонене! - Для подальшого вирішення проблеми ви моежете зв'язатися з нами: hi@azaion.com - - Please check your email or password! - The program is restricted to start only from particular hardware, copying is forbidden! - For the further guidance, please feel free to contact us: hi@azaion.com - - diff --git a/Dummy/Azaion.Annotator/Annotator.xaml.cs b/Dummy/Azaion.Annotator/Annotator.xaml.cs deleted file mode 100644 index 067b45b..0000000 --- a/Dummy/Azaion.Annotator/Annotator.xaml.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Azaion.Annotator; - -public partial class Annotator -{ - public Annotator() - { - InitializeComponent(); - } -} diff --git a/Dummy/Azaion.Annotator/AnnotatorEventHandler.cs b/Dummy/Azaion.Annotator/AnnotatorEventHandler.cs deleted file mode 100644 index d27500f..0000000 --- a/Dummy/Azaion.Annotator/AnnotatorEventHandler.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Azaion.Annotator; - -public class AnnotatorEventHandler; diff --git a/Dummy/Azaion.Annotator/AnnotatorModule.cs b/Dummy/Azaion.Annotator/AnnotatorModule.cs deleted file mode 100644 index 2df179a..0000000 --- a/Dummy/Azaion.Annotator/AnnotatorModule.cs +++ /dev/null @@ -1,54 +0,0 @@ -using Azaion.Common.DTO; - -namespace Azaion.Annotator; - -public class AnnotatorModule : IAzaionModule -{ - public string Name => "Анотатор"; - - public string SvgIcon => -@" - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -"; - - public Type MainWindowType => typeof(Annotator); - - public WindowEnum WindowEnum => WindowEnum.Annotator; -} \ No newline at end of file diff --git a/Dummy/Azaion.Annotator/AssemblyInfo.cs b/Dummy/Azaion.Annotator/AssemblyInfo.cs deleted file mode 100644 index 4a05c7d..0000000 --- a/Dummy/Azaion.Annotator/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] \ No newline at end of file diff --git a/Dummy/Azaion.Annotator/Azaion.Annotator.csproj b/Dummy/Azaion.Annotator/Azaion.Annotator.csproj deleted file mode 100644 index 0005c54..0000000 --- a/Dummy/Azaion.Annotator/Azaion.Annotator.csproj +++ /dev/null @@ -1,40 +0,0 @@ - - - - enable - enable - true - net8.0-windows - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Dummy/Azaion.Annotator/DTO/FormState.cs b/Dummy/Azaion.Annotator/DTO/FormState.cs deleted file mode 100644 index ae14d69..0000000 --- a/Dummy/Azaion.Annotator/DTO/FormState.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Azaion.Annotator.DTO; - -public class FormState; \ No newline at end of file diff --git a/Dummy/Azaion.Annotator/Extensions/VLCFrameExtractor.cs b/Dummy/Azaion.Annotator/Extensions/VLCFrameExtractor.cs deleted file mode 100644 index 7887ba9..0000000 --- a/Dummy/Azaion.Annotator/Extensions/VLCFrameExtractor.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Azaion.Annotator.Extensions; - -public class VLCFrameExtractor; \ No newline at end of file diff --git a/Dummy/Azaion.Annotator/HelpWindow.xaml b/Dummy/Azaion.Annotator/HelpWindow.xaml deleted file mode 100644 index a15a573..0000000 --- a/Dummy/Azaion.Annotator/HelpWindow.xaml +++ /dev/null @@ -1,11 +0,0 @@ - - diff --git a/Dummy/Azaion.Annotator/HelpWindow.xaml.cs b/Dummy/Azaion.Annotator/HelpWindow.xaml.cs deleted file mode 100644 index 4031078..0000000 --- a/Dummy/Azaion.Annotator/HelpWindow.xaml.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Azaion.Annotator; - -public partial class HelpWindow; \ No newline at end of file diff --git a/Dummy/Azaion.Dataset/AssemblyInfo.cs b/Dummy/Azaion.Dataset/AssemblyInfo.cs deleted file mode 100644 index 4a05c7d..0000000 --- a/Dummy/Azaion.Dataset/AssemblyInfo.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System.Windows; - -[assembly: ThemeInfo( - ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located - //(used if a resource is not found in the page, - // or application resource dictionaries) - ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located - //(used if a resource is not found in the page, - // app, or any theme specific resource dictionaries) -)] \ No newline at end of file diff --git a/Dummy/Azaion.Dataset/Azaion.Dataset.csproj b/Dummy/Azaion.Dataset/Azaion.Dataset.csproj deleted file mode 100644 index 85c6517..0000000 --- a/Dummy/Azaion.Dataset/Azaion.Dataset.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net8.0-windows - enable - enable - true - - - - - MSBuild:Compile - Wpf - Designer - - - - - - - - - - - - - - diff --git a/Dummy/Azaion.Dataset/BitmapExtensions.cs b/Dummy/Azaion.Dataset/BitmapExtensions.cs deleted file mode 100644 index cb54e6a..0000000 --- a/Dummy/Azaion.Dataset/BitmapExtensions.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.IO; -using System.Windows.Media.Imaging; - -namespace Azaion.Dataset; - -public static class BitmapExtensions -{ - public static async Task OpenImage(this string imagePath) - { - var image = new BitmapImage(); - await using var stream = File.OpenRead(imagePath); - image.BeginInit(); - image.CacheOption = BitmapCacheOption.OnLoad; - image.StreamSource = stream; - image.EndInit(); - image.Freeze(); - return image; - } -} \ No newline at end of file diff --git a/Dummy/Azaion.Dataset/DatasetExplorer.xaml b/Dummy/Azaion.Dataset/DatasetExplorer.xaml deleted file mode 100644 index 9b78e59..0000000 --- a/Dummy/Azaion.Dataset/DatasetExplorer.xaml +++ /dev/null @@ -1,24 +0,0 @@ - - - - Будь ласка перевірте правильність email чи паролю! - Також зауважте, що запуск можливий лише з одного конкретного компьютера, копіювання заборонене! - Для подальшого вирішення проблеми ви моежете зв'язатися з нами: hi@azaion.com - - Please check your email or password! - The program is restricted to start only from particular hardware, copying is forbidden! - For the further guidance, please feel free to contact us: hi@azaion.com - - diff --git a/Dummy/Azaion.Dataset/DatasetExplorer.xaml.cs b/Dummy/Azaion.Dataset/DatasetExplorer.xaml.cs deleted file mode 100644 index c7fc94e..0000000 --- a/Dummy/Azaion.Dataset/DatasetExplorer.xaml.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Azaion.Dataset; - -public partial class DatasetExplorer; \ No newline at end of file diff --git a/Dummy/Azaion.Dataset/DatasetExplorerModule.cs b/Dummy/Azaion.Dataset/DatasetExplorerModule.cs deleted file mode 100644 index c907ec5..0000000 --- a/Dummy/Azaion.Dataset/DatasetExplorerModule.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Azaion.Common.DTO; - -namespace Azaion.Dataset; - -public class DatasetExplorerModule : IAzaionModule -{ - public string Name => "Переглядач"; - - public string SvgIcon => - @" - - - - - - - - - -"; - - public Type MainWindowType => typeof(DatasetExplorer); - - public WindowEnum WindowEnum => WindowEnum.DatasetExplorer; -} \ No newline at end of file diff --git a/Dummy/Azaion.Dataset/GalleryManager.cs b/Dummy/Azaion.Dataset/GalleryManager.cs deleted file mode 100644 index 7aeb993..0000000 --- a/Dummy/Azaion.Dataset/GalleryManager.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Collections.Concurrent; -using System.Drawing; -using System.IO; -using Azaion.Annotator.Extensions; -using Azaion.Common; -using Azaion.Common.DTO; -using Azaion.Common.Extensions; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Newtonsoft.Json; -using Color = System.Drawing.Color; -using ParallelOptions = Azaion.Annotator.Extensions.ParallelOptions; -using Size = System.Windows.Size; -using System.Drawing.Imaging; -using System.Drawing.Drawing2D; -using Azaion.Common.DTO.Config; - -namespace Azaion.Dataset; - -public delegate void ThumbnailsUpdatedEventHandler(double thumbnailsPercentage); - -public class GalleryManager : IGalleryManager; - -public interface IGalleryManager; \ No newline at end of file diff --git a/build/build_cdn_manager.cmd b/build/build_cdn_manager.cmd new file mode 100644 index 0000000..94b0cd6 --- /dev/null +++ b/build/build_cdn_manager.cmd @@ -0,0 +1,7 @@ +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 + diff --git a/build/build_dotnet.cmd b/build/build_dotnet.cmd new file mode 100644 index 0000000..f83c3f2 --- /dev/null +++ b/build/build_dotnet.cmd @@ -0,0 +1,26 @@ +echo Build .net app +set CURRENT_DIR=%cd% +cd /d %~dp0.. + +cd Azaion.Suite +dotnet publish -c Release -r win-x64 -p:SatelliteResourceLanguages="en" -p:DebugSymbols=false --self-contained true +cd ..\Azaion.LoaderUI +dotnet publish -c Release -r win-x64 -p:SatelliteResourceLanguages="en" -p:DebugSymbols=false --self-contained true +cd .. + +xcopy Azaion.Suite\bin\Release\net8.0-windows\win-x64\publish dist\ /s /e /q +del dist\config.json +move dist\config.production.json dist\config.json + +robocopy "dist" "dist-azaion" "Azaion.Annotator.dll" "Azaion.Dataset.dll" "Azaion.Common.dll" "Azaion.CommonSecurity.dll" /MOV +robocopy "dist" "dist-azaion" "Azaion.Suite.dll" "Azaion.Suite.exe" "Azaion.Suite.runtimeconfig.json" "Azaion.Suite.deps.json" "config.json" "logo.png" /MOV +robocopy "Azaion.LoaderUI\bin\Release\net8.0-windows\win-x64\publish" "dist-dlls" "Azaion.LoaderUI.dll" "Azaion.LoaderUI.exe" "Azaion.LoaderUI.runtimeconfig.json" ^ + "Azaion.LoaderUI.deps.json" "loaderconfig.json" + +if exist dist\libvlc\win-x86 rmdir dist\libvlc\win-x86 /s /q +robocopy "dist" "dist-dlls" /E /MOVE + +echo Copy ico +copy logo.ico dist-azaion\ + +cd /d %CURRENT_DIR% \ No newline at end of file diff --git a/build/build_downloader.cmd b/build/build_downloader.cmd deleted file mode 100644 index d8a0e03..0000000 --- a/build/build_downloader.cmd +++ /dev/null @@ -1,5 +0,0 @@ -pyinstaller --onefile --collect-all boto3 cdn_manager.py -move dist\cdn_manager.exe .\cdn_manager.exe -rmdir /s /q dist -rmdir /s /q build - diff --git a/build/cdn_manager.py b/build/cdn_manager.py index 60e820b..e3ed75b 100644 --- a/build/cdn_manager.py +++ b/build/cdn_manager.py @@ -34,20 +34,27 @@ class CDNManager: print(e) return False - def upload(self, bucket: str, filename: str, file_bytes: bytearray): + def download(self, folder: str, filename: str): 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 + if filename is not None: + self.download_client.download_file(folder, filename, f'{folder}\\{filename}') + print(f'downloaded {filename} from the {folder} to current folder') + return True + else: + response = self.download_client.list_objects_v2(Bucket=folder) + if 'Contents' in response: + for obj in response['Contents']: + object_key = obj['Key'] + local_filepath = os.path.join(folder, object_key) + local_dir = os.path.dirname(local_filepath) + if local_dir: + os.makedirs(local_dir, exist_ok=True) - def download(self, bucket: str, filename: str): - try: - self.download_client.download_file(bucket, filename, filename) - print(f'downloaded {filename} from the {bucket} to current folder') + if not object_key.endswith('/'): + try: + self.download_client.download_file(folder, object_key, local_filepath) + except Exception as e_file: + all_successful = False # Mark as failed if any file fails return True except Exception as e: print(e) @@ -68,7 +75,7 @@ cdn_manager = CDNManager(CDNCredentials( input_action = sys.argv[1] input_bucket = sys.argv[2] -input_filename = sys.argv[3] +input_filename = sys.argv[3] if len(sys.argv) > 3 else None if len(sys.argv) > 4: # 0 is this script's path, hence 5 args is max input_path = sys.argv[4] diff --git a/build/download_models.cmd b/build/download_models.cmd new file mode 100644 index 0000000..9063a61 --- /dev/null +++ b/build/download_models.cmd @@ -0,0 +1,14 @@ +echo Download onnx model +set CURRENT_DIR=%cd% + +REM Change to the parent directory of the current location +cd /d %~dp0 + +if not exist cdn_manager.exe ( + echo Install cdn_manager + call build_cdn_manager +) +call cdn_manager.exe download models +move models ..\dist-dlls\ + +cd /d %CURRENT_DIR% diff --git a/build/gdrive_retention.ps1 b/build/gdrive_retention.ps1 new file mode 100644 index 0000000..29de8d3 --- /dev/null +++ b/build/gdrive_retention.ps1 @@ -0,0 +1,107 @@ +# Configuration +$rcloneRemote = "AzaionGoogleDrive:AzaionSuiteBuilds" +$rclone = "rclone" # Assuming rclone is available in the PATH. + +function Write-Header($text) { + Write-Host "`n=== $text ===`n" -ForegroundColor Cyan +} + +function Write-Info($text) { + Write-Host "[INFO] $text" -ForegroundColor Green +} + +function Write-WarningMsg($text) { + Write-Host "[WARNING] $text" -ForegroundColor Yellow +} + +function Write-ErrorMsg($text) { + Write-Host "[ERROR] $text" -ForegroundColor Red +} + +Write-Header "Starting cleanup of older Full and Iterative installers on Google Drive" + +Write-Info "Listing all files in: $rcloneRemote" +$allFilesRaw = & $rclone lsf --files-only $rcloneRemote + +# Check if the command succeeded +if ($LASTEXITCODE -ne 0) { + Write-ErrorMsg "Failed to list files from Google Drive using rclone." + exit 1 +} + +$allFiles = $allFilesRaw -split "`n" | Where-Object { $_ -ne "" } + +if ($allFiles.Count -eq 0) { + Write-WarningMsg "No files found in the remote folder." + exit 0 +} +Write-Info "Files found on Google Drive:" +foreach ($file in $allFiles) { + Write-Host " $file" +} + +# --- Full Installers --- +Write-Header "Checking for old Full installers (AzaionSuite.Full.*.zip)" +$fullFiles = $allFiles | Where-Object { $_ -like "AzaionSuite.Full.*.zip" } | Sort-Object -Descending + +if ($fullFiles.Count -eq 0) { + Write-WarningMsg "No Full installer files found." +} else { + Write-Info "Matching Full installer files:" + foreach ($file in $fullFiles) { + Write-Host " $file" + } + + if ($fullFiles.Count -le 5) { + Write-Info "Less than or equal to 5 Full installers found — nothing to delete." + } else { + $fullToDelete = $fullFiles | Select-Object -Skip 5 + Write-WarningMsg "Full installers to delete:" + foreach ($file in $fullToDelete) { + Write-Host " $file" + } + + foreach ($file in $fullToDelete) { + $res = & $rclone deletefile "$rcloneRemote/$file" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Info "Deleted Full installer: $file" + } else { + Write-ErrorMsg "Failed to delete Full installer: $file. Error: $res" + } + } + } +} + +# --- Iterative Installers --- +Write-Header "Checking for old Iterative installers (AzaionSuite.Iterative.*.exe)" +$iterativeFiles = $allFiles | Where-Object { $_ -like "AzaionSuite.Iterative.*.exe" } | Sort-Object -Descending + +if ($iterativeFiles.Count -eq 0) { + Write-WarningMsg "No Iterative installer files found." +} else { + Write-Info "Matching Iterative installer files:" + foreach ($file in $iterativeFiles) { + Write-Host " $file" + } + + if ($iterativeFiles.Count -le 5) { + Write-Info "Less than or equal to 5 Iterative installers found — nothing to delete." + } else { + $iterativeToDelete = $iterativeFiles | Select-Object -Skip 5 + Write-WarningMsg "Iterative installers to delete:" + foreach ($file in $iterativeToDelete) { + Write-Host " $file" + } + + foreach ($file in $iterativeToDelete) { + $res = & $rclone deletefile "$rcloneRemote/$file" 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Info "Deleted Iterative installer: $file" + } else { + Write-ErrorMsg "Failed to delete Iterative installer: $file. Error: $res" + } + } + } +} + +Write-Header "Cleanup script completed" diff --git a/build/gdrive_upload_full.ps1 b/build/gdrive_upload_full.ps1 new file mode 100644 index 0000000..d6f5164 --- /dev/null +++ b/build/gdrive_upload_full.ps1 @@ -0,0 +1,31 @@ +param ( + [string]$Path = "." +) + +Write-Output "`n=== Starting Upload: Full Installer ZIP ===" + +$uploadFolder = "AzaionSuiteBuilds" +Write-Output "[INFO] Target Google Drive folder: $uploadFolder" +Write-Output "[INFO] Looking for latest .zip matching 'AzaionSuite.Full.*.zip' in folder: $Path..." + +$fullZip = Get-ChildItem -Path $Path -Filter "AzaionSuite.Full.*.zip" | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + +if ($fullZip) { + Write-Output "[FOUND] Full installer ZIP: $($fullZip.Name)" + Write-Output "[INFO] Full path: $($fullZip.FullName)" + Write-Output "[ACTION] Uploading file to Google Drive using rclone..." + + rclone copy "$($fullZip.FullName)" "AzaionGoogleDrive:$uploadFolder" --progress + + if ($LASTEXITCODE -eq 0) { + Write-Output "[SUCCESS] Full installer ZIP uploaded successfully." + } else { + Write-Output "[ERROR] Upload failed with exit code $LASTEXITCODE." + } +} else { + Write-Output "[WARNING] No matching Full installer ZIP found in folder: $Path." +} + +Write-Output "=== Upload Complete: Full Installer ZIP ===`n" diff --git a/build/gdrive_upload_iterative.ps1 b/build/gdrive_upload_iterative.ps1 new file mode 100644 index 0000000..221e4f9 --- /dev/null +++ b/build/gdrive_upload_iterative.ps1 @@ -0,0 +1,31 @@ +param ( + [string]$Path = "." +) + +Write-Output "`n=== Starting Upload: Iterative Installer ===" + +$uploadFolder = "AzaionSuiteBuilds" +Write-Output "[INFO] Target Google Drive folder: $uploadFolder" +Write-Output "[INFO] Looking for latest .exe matching 'AzaionSuite.Iterative.*.exe' in folder: $Path..." + +$iterativeFile = Get-ChildItem -Path $Path -Filter "AzaionSuite.Iterative.*.exe" | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + +if ($iterativeFile) { + Write-Output "[FOUND] Iterative installer: $($iterativeFile.Name)" + Write-Output "[INFO] Full path: $($iterativeFile.FullName)" + Write-Output "[ACTION] Uploading file to Google Drive using rclone..." + + rclone copy "$($iterativeFile.FullName)" "AzaionGoogleDrive:$uploadFolder" --progress + + if ($LASTEXITCODE -eq 0) { + Write-Output "[SUCCESS] Iterative installer uploaded successfully." + } else { + Write-Output "[ERROR] Upload failed with exit code $LASTEXITCODE." + } +} else { + Write-Output "[WARNING] No matching Iterative installer found in folder: $Path." +} + +Write-Output "=== Upload Complete: Iterative Installer ===`n" diff --git a/build/init.cmd b/build/init.cmd new file mode 100644 index 0000000..cb3e2ca --- /dev/null +++ b/build/init.cmd @@ -0,0 +1,19 @@ +@echo off +echo Make dirs, copy init dlls +set CURRENT_DIR=%cd% +cd /d %~dp0.. + +if exist dist-dlls rmdir dist-dlls /s /q +mkdir dist-dlls +if exist dist-azaion rmdir dist-azaion /s /q +mkdir dist-azaion +if exist dist rmdir dist /s /q +mkdir dist +del "AzaionSuite.Iterative.*" + +echo Copying shared libs + +robocopy "C:\\share" "dist-dlls" "*" + +cd /d %CURRENT_DIR% +echo Init script completed. diff --git a/build/installer.full.iss b/build/installer.full.iss new file mode 100644 index 0000000..f67a5d0 --- /dev/null +++ b/build/installer.full.iss @@ -0,0 +1,34 @@ +#define MyAppVersion GetFileVersion("..\dist-azaion\Azaion.Suite.exe") + +[Setup] +AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} +AppName=Azaion Suite +AppVersion={#MyAppVersion} +VersionInfoVersion={#MyAppVersion} +AppPublisher=Azaion LLC +DefaultDirName={localappdata}\Azaion\Azaion Suite +DefaultGroupName=Azaion Suite +OutputDir=..\ +OutputBaseFilename=AzaionSuite.Full.{#MyAppVersion} +SetupIconFile=..\dist-azaion\logo.ico +UninstallDisplayName=Azaion Suite +UninstallDisplayIcon={app}\Azaion.Suite.exe +Compression=lzma2/fast +SolidCompression=yes +DiskSpanning=yes + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + +[Files] +Source: "..\dist-dlls\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; +Source: "..\dist-azaion\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; + +[Icons] +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.iss b/build/installer.iterative.iss similarity index 50% rename from build/installer.iss rename to build/installer.iterative.iss index bbd2c70..a6029b6 100644 --- a/build/installer.iss +++ b/build/installer.iterative.iss @@ -1,13 +1,16 @@ +#define MyAppVersion GetFileVersion("..\dist-azaion\Azaion.Suite.exe") + [Setup] AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} AppName=Azaion Suite -AppVersion=1.4.2 -AppPublisher=Azaion Ukraine +AppVersion={#MyAppVersion} +VersionInfoVersion={#MyAppVersion} +AppPublisher=Azaion LLC DefaultDirName={localappdata}\Azaion\Azaion Suite DefaultGroupName=Azaion Suite OutputDir=..\ -OutputBaseFilename=AzaionSuite1.4.2 -SetupIconFile=..\dist\logo.ico +OutputBaseFilename=AzaionSuite.Iterative.{#MyAppVersion} +SetupIconFile=..\dist-azaion\logo.ico UninstallDisplayName=Azaion Suite UninstallDisplayIcon={app}\Azaion.Suite.exe Compression=lzma2/fast @@ -20,10 +23,10 @@ Name: "english"; MessagesFile: "compiler:Default.isl" Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked [Files] -Source: "..\dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; +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-full.cmd b/build/publish-full.cmd new file mode 100644 index 0000000..315b8c3 --- /dev/null +++ b/build/publish-full.cmd @@ -0,0 +1,17 @@ +@echo off +pushd %~dp0.. + +call build\init + +call build\build_dotnet + +call Azaion.Inference\build_inference + +call ..\gps-denied\image-matcher\build_gps + +call build\download_models + +echo building installer... +iscc build\installer.full.iss + +popd \ No newline at end of file diff --git a/build/publish.cmd b/build/publish.cmd index c53bd8c..b672b92 100644 --- a/build/publish.cmd +++ b/build/publish.cmd @@ -1,107 +1,26 @@ +setlocal enabledelayedexpansion @echo off +set CURRENT_DIR=%cd% +cd /d %~dp0.. -cd %~dp0.. -echo Build .net app -dotnet build -c Release +call build\init -cd Azaion.Suite -call postbuild.cmd Release -echo %cd% -call upload-file %cd%\config.secured.json -call upload-file %cd%\config.system.json +call build\build_dotnet -dotnet publish -r win-x64 -p:SatelliteResourceLanguages="en" -p:DebugSymbols=false -p:ForPublish=true --self-contained true -cd .. -rmdir dist /s /q -xcopy Azaion.Suite\bin\Release\net8.0-windows\win-x64\publish dist\ /s /e /q -del dist\config.json -move dist\config.production.json dist\config.json +call Azaion.Loader\build_loader -mkdir dist\dummy -move dist\Azaion.Annotator.dll dist\dummy\ -move dist\Azaion.Dataset.dll dist\dummy\ +call Azaion.Inference\build_inference -echo Build Cython app -cd Azaion.Inference -echo remove dist folder: -rmdir dist /s /q +call ..\gps-denied\image-matcher\build_gps -echo install python and dependencies -python -m venv venv -venv\Scripts\pip install -r requirements.txt -venv\Scripts\python setup.py build_ext --inplace +call build\download_models -echo install azaion-inference -venv\Scripts\pyinstaller --name=azaion-inference ^ ---collect-all jwt ^ ---collect-all requests ^ ---collect-all psutil ^ ---collect-all msgpack ^ ---collect-all zmq ^ ---collect-all cryptography ^ ---collect-all cv2 ^ ---collect-all onnxruntime ^ ---collect-all tensorrt ^ ---collect-all pycuda ^ ---collect-all re ^ ---hidden-import constants ^ ---hidden-import annotation ^ ---hidden-import credentials ^ ---hidden-import file_data ^ ---hidden-import user ^ ---hidden-import security ^ ---hidden-import secure_model ^ ---hidden-import api_client ^ ---hidden-import hardware_service ^ ---hidden-import remote_command ^ ---hidden-import ai_config ^ ---hidden-import inference_engine ^ ---hidden-import inference ^ ---hidden-import remote_command_handler ^ -start.py +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 -xcopy /E dist\azaion-inference ..\dist\ -copy venv\Lib\site-packages\tensorrt_libs\nvinfer_10.dll ..\dist -copy venv\Lib\site-packages\tensorrt_libs\nvinfer_plugin_10.dll ..\dist -copy venv\Lib\site-packages\tensorrt_libs\nvonnxparser_10.dll ..\dist - -copy config.production.yaml ..\dist\config.yaml -cd.. - -echo Download onnx model -cd build -call cdn_manager.exe download models azaion.onnx.big -call cdn_manager.exe download models azaion.engine.big - -move azaion.* ..\dist\ -cd.. - -echo Copy ico -copy logo.ico dist\ - -echo Copying cudnn files -set cudnn-folder="C:\Program Files\NVIDIA\CUDNN\v9.4\bin\12.6" -copy %cudnn-folder%\* dist\* -@REM don't need -del dist\cudnn_adv64_9.dll - - -set DESTINATION=dist\gps-denied\ - -rd /S /Q %DESTINATION% -mkdir %DESTINATION% -copy ..\gps-denied\image-matcher\build\Desktop_Qt_6_9_0_MSVC2022_64bit-Release\release\image-matcher.exe %DESTINATION% - -copy ..\gps-denied\.libs\libzmq\build\dist\bin\libzmq-v143-mt-4_3_6.dll %DESTINATION% -copy ..\gps-denied\.libs\onnxruntime\lib\onnxruntime.dll %DESTINATION% -copy ..\gps-denied\.libs\onnxruntime\lib\onnxruntime_providers_cuda.dll %DESTINATION% -copy ..\gps-denied\.libs\onnxruntime\lib\onnxruntime_providers_shared.dll %DESTINATION% -copy ..\gps-denied\.libs\opencv\build\x64\vc16\bin\opencv_world4110.dll %DESTINATION% -copy C:\Qt\6.9.0\msvc2022_64\bin\Qt6Core.dll %DESTINATION% - -mkdir %DESTINATION%\models -copy ..\gps-denied-work\models\* %DESTINATION%\models - -@REM echo building installer... -@REM iscc build\installer.iss +cd /d %CURRENT_DIR% +echo Done! \ No newline at end of file diff --git a/build/upload.cmd b/build/upload.cmd new file mode 100644 index 0000000..f379be3 --- /dev/null +++ b/build/upload.cmd @@ -0,0 +1,43 @@ +setlocal enabledelayedexpansion +@echo off +set CURRENT_DIR=%cd% +cd /d %~dp0.. + +set API_URL=https://api.azaion.com +set RESOURCES_FOLDER=%1 +set EMAIL=uploader@azaion.com +set PASSWORD=Az@1on_10Upl0@der + +set "UPLOAD_FILE=" +for /f "delims=" %%i in ('dir /b /a-d "AzaionSuite.Iterative*"') do ( + if not defined UPLOAD_FILE set "UPLOAD_FILE=%%i" +) + +if not defined UPLOAD_FILE ( + echo No matching file found. + exit /b +) + +echo Logging in and retrieving token... +for /f "tokens=*" %%i in ('curl -s -X POST -H "Content-Type: application/json" ^ + -d "{\"email\":\"%EMAIL%\",\"password\":\"%PASSWORD%\"}" %API_URL%/login') do set RESPONSE=%%i + +for /f "tokens=2 delims=:" %%a in ('echo %RESPONSE% ^| findstr /i "token"') do ( + set "TOKEN=%%a" + set "TOKEN=!TOKEN:~1,-1!" + set "TOKEN=!TOKEN:~0,-2!" +) + +echo clear suite folder + +curl -X POST %API_URL%/resources/clear/%RESOURCES_FOLDER% -d {} -H "Authorization: Bearer %TOKEN%" + +echo Uploading files to resources... + +curl --location %API_URL%/resources/%RESOURCES_FOLDER% ^ + -H "Authorization: Bearer %TOKEN%" ^ + -H "Content-Type: multipart/form-data" ^ + --form "data=@%UPLOAD_FILE%" + +cd /d %CURRENT_DIR% +echo Done! diff --git a/build/zip_full.ps1 b/build/zip_full.ps1 new file mode 100644 index 0000000..0a54b95 --- /dev/null +++ b/build/zip_full.ps1 @@ -0,0 +1,68 @@ +$host.UI.RawUI.WindowTitle = "Full Installer Zipper" +Write-Host "`n===== [AZAION SUITE FULL PACKAGER] =====" -ForegroundColor Cyan + +function Show-ProgressBar { + param ( + [string]$Activity, + [int]$Steps = 20, + [int]$Delay = 30 + ) + + for ($i = 1; $i -le $Steps; $i++) { + $percent = [math]::Round(($i / $Steps) * 100) + $bar = ("#" * $i).PadRight($Steps) + Write-Host -NoNewline "`r[PROGRESS] $Activity [$bar] $percent% " + Start-Sleep -Milliseconds $Delay + } + Write-Host "" +} + +Write-Host "[INFO] Preparing to package Full installer..." -ForegroundColor Yellow + +Set-Location -Path (Split-Path -Parent $MyInvocation.MyCommand.Path) +Set-Location .. + +$currentDir = Get-Location +Write-Host "[INFO] Working directory: $currentDir" -ForegroundColor Gray + +# Locate latest Full EXE by LastWriteTime +$fullExe = Get-ChildItem -Filter "AzaionSuite.Full.*.exe" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + +if (-not $fullExe) { + Write-Host "[ERROR] No Full build EXE found (AzaionSuite.Full.*.exe)" -ForegroundColor Red + exit 1 +} + +Write-Host "[OK] Found Full EXE: $($fullExe.Name)" -ForegroundColor Green + +# Match BIN files with same base name +$baseName = [System.IO.Path]::GetFileNameWithoutExtension($fullExe.Name) +$binFiles = Get-ChildItem -Filter "$baseName-*.bin" + +if ($binFiles.Count -gt 0) { + Write-Host "[INFO] Matching BIN files found:" -ForegroundColor Cyan + $binFiles | ForEach-Object { Write-Host " - $_" -ForegroundColor DarkCyan } +} else { + Write-Host "[INFO] No BIN files found. Creating ZIP with only EXE." -ForegroundColor Yellow +} + +# Compose final ZIP path +$zipName = "$baseName.zip" +$zipPath = Join-Path -Path $currentDir -ChildPath $zipName + +# Collect files to zip +$toZip = @($fullExe.FullName) +$toZip += $binFiles.FullName + +Write-Host "[INFO] Creating ZIP file: $zipName" -ForegroundColor Yellow +Show-ProgressBar -Activity "Compressing files..." -Steps 25 -Delay 25 + +try { + Compress-Archive -Force -Path $toZip -DestinationPath $zipPath -Verbose + Write-Host "[SUCCESS] ZIP archive created successfully: $zipName" -ForegroundColor Green +} catch { + Write-Host "[ERROR] Failed to create ZIP archive: $_" -ForegroundColor Red + exit 1 +} + +Write-Host "`n[FINISHED] Full installer packaging complete." -ForegroundColor Cyan