diff --git a/.gitignore b/.gitignore index 10391fc..566dd17 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ obj .vs *.DotSettings* *.user -log*.txt \ No newline at end of file +log*.txt +secured-config \ No newline at end of file diff --git a/Azaion.Annotator/Annotator.xaml.cs b/Azaion.Annotator/Annotator.xaml.cs index 738674d..74b5002 100644 --- a/Azaion.Annotator/Annotator.xaml.cs +++ b/Azaion.Annotator/Annotator.xaml.cs @@ -91,8 +91,8 @@ public partial class Annotator _suspendLayout = true; - MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.AnnotatorWindowConfig.LeftPanelWidth); - MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotatorWindowConfig.RightPanelWidth); + MainGrid.ColumnDefinitions.FirstOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.LeftPanelWidth); + MainGrid.ColumnDefinitions.LastOrDefault()!.Width = new GridLength(_appConfig.AnnotationConfig.RightPanelWidth); _suspendLayout = false; @@ -192,27 +192,13 @@ public partial class Annotator if (result != MessageBoxResult.OK) return; - // var allWindows = Application.Current.Windows.Cast(); - // try - // { - // foreach (var window in allWindows) - // window.IsEnabled = false; - // - // } - // finally - // { - // foreach (var window in allWindows) - // { - // window.IsEnabled = true; - // } - // } - var res = DgAnnotations.SelectedItems.Cast().ToList(); foreach (var annotationResult in res) { var imgName = Path.GetFileNameWithoutExtension(annotationResult.Image); var thumbnailPath = Path.Combine(_appConfig.DirectoriesConfig.ThumbnailsDirectory, $"{imgName}{Constants.THUMBNAIL_PREFIX}.jpg"); File.Delete(annotationResult.Image); + File.Delete(Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{imgName}.txt")); File.Delete(thumbnailPath); _formState.AnnotationResults.Remove(annotationResult); @@ -246,8 +232,8 @@ public partial class Annotator if (_suspendLayout) return; - _appConfig.AnnotatorWindowConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; - _appConfig.AnnotatorWindowConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; + _appConfig.AnnotationConfig.LeftPanelWidth = MainGrid.ColumnDefinitions.FirstOrDefault()!.Width.Value; + _appConfig.AnnotationConfig.RightPanelWidth = MainGrid.ColumnDefinitions.LastOrDefault()!.Width.Value; await ThrottleExt.Throttle(() => { @@ -710,9 +696,6 @@ public partial class Annotator var fName = _formState.GetTimeName(timeframe.Time); var imgPath = Path.Combine(_appConfig.DirectoriesConfig.ImagesDirectory, $"{fName}.jpg"); - var img = System.Drawing.Image.FromStream(timeframe.Stream); - img.Save(imgPath, ImageFormat.Jpeg); - await YoloLabel.WriteToFile(detections, Path.Combine(_appConfig.DirectoriesConfig.LabelsDirectory, $"{fName}.txt"), token); Editor.Background = new ImageBrush { ImageSource = await imgPath.OpenImage() }; Editor.RemoveAllAnns(); diff --git a/Azaion.Annotator/AnnotatorEventHandler.cs b/Azaion.Annotator/AnnotatorEventHandler.cs index 07c26fb..f68ab58 100644 --- a/Azaion.Annotator/AnnotatorEventHandler.cs +++ b/Azaion.Annotator/AnnotatorEventHandler.cs @@ -1,11 +1,12 @@ using System.IO; using System.Windows; -using System.Windows.Controls; using System.Windows.Input; -using System.Windows.Media; using Azaion.Annotator.DTO; +using Azaion.Common; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; +using Azaion.Common.DTO.Queue; +using Azaion.Common.Services; using LibVLCSharp.Shared; using MediatR; using Microsoft.Extensions.Logging; @@ -19,16 +20,16 @@ public class AnnotatorEventHandler( MediaPlayer mediaPlayer, Annotator mainWindow, FormState formState, - IOptions directoriesConfig, + AnnotationService annotationService, IMediator mediator, - ILogger logger) + ILogger logger, + IOptions dirConfig) : INotificationHandler, INotificationHandler, INotificationHandler, INotificationHandler { - private readonly DirectoriesConfig _directoriesConfig = directoriesConfig.Value; private const int STEP = 20; private const int LARGE_STEP = 5000; private const int RESULT_WIDTH = 1280; @@ -59,7 +60,7 @@ public class AnnotatorEventHandler( mainWindow.LvClasses.SelectedIndex = annClass.Id; } - public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken) + public async Task Handle(KeyEvent keyEvent, CancellationToken cancellationToken = default) { if (keyEvent.WindowEnum != WindowEnum.Annotator) return; @@ -75,23 +76,19 @@ public class AnnotatorEventHandler( SelectClass((AnnotationClass)mainWindow.LvClasses.Items[keyNumber.Value]!); if (_keysControlEnumDict.TryGetValue(key, out var value)) - await ControlPlayback(value); + await ControlPlayback(value, cancellationToken); if (key == Key.A) mainWindow.AutoDetect(null!, null!); - await VolumeControl(key); - } - - private async Task VolumeControl(Key key) - { + #region Volume switch (key) { case Key.VolumeMute when mediaPlayer.Volume == 0: - await ControlPlayback(PlaybackControlEnum.TurnOnVolume); + await ControlPlayback(PlaybackControlEnum.TurnOnVolume, cancellationToken); break; case Key.VolumeMute: - await ControlPlayback(PlaybackControlEnum.TurnOffVolume); + await ControlPlayback(PlaybackControlEnum.TurnOffVolume, cancellationToken); break; case Key.Up: case Key.VolumeUp: @@ -106,15 +103,16 @@ public class AnnotatorEventHandler( mainWindow.Volume.Value = vDown; break; } + #endregion } - public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken) + public async Task Handle(PlaybackControlEvent notification, CancellationToken cancellationToken = default) { - await ControlPlayback(notification.PlaybackControl); + await ControlPlayback(notification.PlaybackControl, cancellationToken); mainWindow.VideoView.Focus(); } - private async Task ControlPlayback(PlaybackControlEnum controlEnum) + private async Task ControlPlayback(PlaybackControlEnum controlEnum, CancellationToken cancellationToken = default) { try { @@ -222,10 +220,9 @@ public class AnnotatorEventHandler( mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); } - private async Task SaveAnnotations() + //SAVE: MANUAL + private async Task SaveAnnotations(CancellationToken cancellationToken = default) { - var annGridSelectedIndex = mainWindow.DgAnnotations.SelectedIndex; - if (formState.CurrentMedia == null) return; @@ -236,16 +233,15 @@ public class AnnotatorEventHandler( .Select(x => new YoloLabel(x.Info, mainWindow.Editor.RenderSize, formState.BackgroundTime.HasValue ? mainWindow.Editor.RenderSize : formState.CurrentVideoSize)) .ToList(); - await YoloLabel.WriteToFile(currentAnns, Path.Combine(_directoriesConfig.LabelsDirectory, $"{fName}.txt")); - await mainWindow.AddAnnotations(time, currentAnns); + await mainWindow.AddAnnotations(time, currentAnns, cancellationToken); formState.CurrentMedia.HasAnnotations = mainWindow.Annotations.Count != 0; mainWindow.LvFiles.Items.Refresh(); + mainWindow.Editor.RemoveAllAnns(); var isVideo = formState.CurrentMedia.MediaType == MediaTypes.Video; - var destinationPath = Path.Combine(_directoriesConfig.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}"); + var imgPath = Path.Combine(dirConfig.Value.ImagesDirectory, $"{fName}{(isVideo ? ".jpg" : Path.GetExtension(formState.CurrentMedia.Path))}"); - mainWindow.Editor.RemoveAllAnns(); if (isVideo) { if (formState.BackgroundTime.HasValue) @@ -256,22 +252,23 @@ public class AnnotatorEventHandler( //next item var annGrid = mainWindow.DgAnnotations; - annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGridSelectedIndex + 1); + annGrid.SelectedIndex = Math.Min(annGrid.Items.Count, annGrid.SelectedIndex + 1); mainWindow.OpenAnnotationResult((AnnotationResult)annGrid.SelectedItem); } else { var resultHeight = (uint)Math.Round(RESULT_WIDTH / formState.CurrentVideoSize.Width * formState.CurrentVideoSize.Height); - mediaPlayer.TakeSnapshot(0, destinationPath, RESULT_WIDTH, resultHeight); + mediaPlayer.TakeSnapshot(0, imgPath, RESULT_WIDTH, resultHeight); mediaPlayer.Play(); } } else { - File.Copy(formState.CurrentMedia.Path, destinationPath, overwrite: true); + File.Copy(formState.CurrentMedia.Path, imgPath, overwrite: true); NextMedia(); } + await annotationService.SaveAnnotation(fName, currentAnns, SourceEnum.Manual, token: cancellationToken); - await mediator.Publish(new ImageCreatedEvent(destinationPath)); + await mediator.Publish(new ImageCreatedEvent(imgPath), cancellationToken); } } diff --git a/Azaion.Annotator/HelpWindow.xaml b/Azaion.Annotator/HelpWindow.xaml index be3dc04..3bae289 100644 --- a/Azaion.Annotator/HelpWindow.xaml +++ b/Azaion.Annotator/HelpWindow.xaml @@ -51,8 +51,5 @@ Тоді будь яка самохідна артилерія на гусеницях, хоч вона являє собою артилерію, мусить бути анотована як "Броньована техніка", оскільки візуально вона значно більш схожа на танк ніж на міномет. - - Показувати при запуску - diff --git a/Azaion.Annotator/HelpWindow.xaml.cs b/Azaion.Annotator/HelpWindow.xaml.cs index f5e7cc3..d945bde 100644 --- a/Azaion.Annotator/HelpWindow.xaml.cs +++ b/Azaion.Annotator/HelpWindow.xaml.cs @@ -6,12 +6,8 @@ namespace Azaion.Annotator; public partial class HelpWindow : Window { - private readonly AnnotatorWindowConfig _annotatorWindowConfig; - - public HelpWindow(IOptions windowConfig) + public HelpWindow() { - _annotatorWindowConfig = windowConfig.Value; - Loaded += (_, _) => CbShowHelp.IsChecked = _annotatorWindowConfig.ShowHelpOnStart; Closing += (sender, args) => { args.Cancel = true; @@ -20,7 +16,4 @@ public partial class HelpWindow : Window InitializeComponent(); } - private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _annotatorWindowConfig.ShowHelpOnStart = true; - private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _annotatorWindowConfig.ShowHelpOnStart = false; - } \ No newline at end of file diff --git a/Azaion.Annotator/YOLODetector.cs b/Azaion.Annotator/YOLODetector.cs index 4ef77b0..fafe424 100644 --- a/Azaion.Annotator/YOLODetector.cs +++ b/Azaion.Annotator/YOLODetector.cs @@ -4,6 +4,7 @@ using Azaion.Annotator.Extensions; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Services; +using Azaion.CommonSecurity.Services; using Compunet.YoloV8; using Microsoft.Extensions.Options; using SixLabors.ImageSharp; diff --git a/Azaion.Common/Azaion.Common.csproj b/Azaion.Common/Azaion.Common.csproj index 04c0f62..845f5e2 100644 --- a/Azaion.Common/Azaion.Common.csproj +++ b/Azaion.Common/Azaion.Common.csproj @@ -7,12 +7,19 @@ + + - + + + + + + diff --git a/Azaion.Common/Constants.cs b/Azaion.Common/Constants.cs index cf99f65..6f42d35 100644 --- a/Azaion.Common/Constants.cs +++ b/Azaion.Common/Constants.cs @@ -5,20 +5,7 @@ namespace Azaion.Common; public class Constants { - public const string CONFIG_PATH = "config.json"; - public const string DEFAULT_DLL_CACHE_DIR = "DllCache"; - - #region ApiConfig - - public const string DEFAULT_API_URL = "https://api.azaion.com/"; - public const int DEFAULT_API_RETRY_COUNT = 3; - public const int DEFAULT_API_TIMEOUT_SECONDS = 40; - - public const string CLAIM_NAME_ID = "nameid"; - public const string CLAIM_EMAIL = "unique_name"; - public const string CLAIM_ROLE = "role"; - - #endregion ApiConfig + public const string SECURE_RESOURCE_CACHE = "SecureResourceCache"; #region DirectoriesConfig @@ -44,12 +31,18 @@ public class Constants new() { Id = 7, Name = "Накати", ShortName = "Накати" }, new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" }, new() { Id = 9, Name = "Дим", ShortName = "Дим" }, - new() { Id = 10, Name = "Літак", ShortName = "Літак" } + new() { Id = 10, Name = "Літак", ShortName = "Літак" }, + new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" } ]; public static readonly List DefaultVideoFormats = ["mp4", "mov", "avi"]; public static readonly List DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; + public static int DEFAULT_LEFT_PANEL_WIDTH = 250; + public static int DEFAULT_RIGHT_PANEL_WIDTH = 250; + + public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db"; + # endregion AnnotatorConfig # region AIRecognitionConfig @@ -62,13 +55,6 @@ public class Constants # endregion AIRecognitionConfig - # region AnnotatorWindowConfig - - public static int DEFAULT_LEFT_PANEL_WIDTH = 250; - public static int DEFAULT_RIGHT_PANEL_WIDTH = 250; - - #endregion - #region Thumbnails public static readonly Size DefaultThumbnailSize = new(240, 135); @@ -98,4 +84,15 @@ public class Constants return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100); } + #region Queue + + public const string MQ_DIRECT_TYPE = "direct"; + public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; + public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm"; + public const string ANNOTATION_PRODUCER = "AnnotationsProducer"; + public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer"; + + + #endregion + } \ No newline at end of file diff --git a/Azaion.Common/DTO/Annotation.cs b/Azaion.Common/DTO/Annotation.cs new file mode 100644 index 0000000..fdb0f0c --- /dev/null +++ b/Azaion.Common/DTO/Annotation.cs @@ -0,0 +1,42 @@ +using System.IO; +using Azaion.Common.DTO.Config; +using Azaion.Common.DTO.Queue; +using Azaion.CommonSecurity.DTO; + +namespace Azaion.Common.DTO; + +public class Annotation +{ + private static string _labelsDir = null!; + private static string _imagesDir = null!; + + public static void InitializeDirs(DirectoriesConfig config) + { + _labelsDir = config.LabelsDirectory; + _imagesDir = config.ImagesDirectory; + } + + public string Name { get; set; } = null!; + public DateTime CreatedDate { get; set; } + public List Classes { get; set; } = null!; + public string CreatedEmail { get; set; } = null!; + public RoleEnum CreatedRole { get; set; } + public SourceEnum Source { get; set; } + public AnnotationStatus AnnotationStatus { get; set; } + + public string ImagePath => Path.Combine(_imagesDir, $"{Name}.jpg"); + public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt"); + +} + +public enum AnnotationStatus +{ + None = 0, + Created = 10, + Validated = 20 +} + +public class AnnotationName +{ + public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/AnnotationConfig.cs b/Azaion.Common/DTO/Config/AnnotationConfig.cs index 192ddff..71d0e8e 100644 --- a/Azaion.Common/DTO/Config/AnnotationConfig.cs +++ b/Azaion.Common/DTO/Config/AnnotationConfig.cs @@ -15,4 +15,9 @@ public class AnnotationConfig public List VideoFormats { get; set; } = null!; public List ImageFormats { get; set; } = null!; + + public string AnnotationsDbFile { get; set; } = null!; + + public double LeftPanelWidth { get; set; } + public double RightPanelWidth { get; set; } } \ No newline at end of file diff --git a/Azaion.Common/DTO/Config/AnnotatorWindowConfig.cs b/Azaion.Common/DTO/Config/AnnotatorWindowConfig.cs deleted file mode 100644 index 6c7d8aa..0000000 --- a/Azaion.Common/DTO/Config/AnnotatorWindowConfig.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Azaion.Common.DTO.Config; - -public class AnnotatorWindowConfig -{ - public double LeftPanelWidth { get; set; } - public double RightPanelWidth { get; set; } - public bool ShowHelpOnStart { 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 da0d3be..aa65490 100644 --- a/Azaion.Common/DTO/Config/AppConfig.cs +++ b/Azaion.Common/DTO/Config/AppConfig.cs @@ -1,5 +1,7 @@ using System.IO; using System.Text; +using Azaion.CommonSecurity; +using Azaion.CommonSecurity.DTO; using Newtonsoft.Json; namespace Azaion.Common.DTO.Config; @@ -8,12 +10,12 @@ public class AppConfig { public ApiConfig ApiConfig { get; set; } = null!; + public QueueConfig QueueConfig { get; set; } = null!; + public DirectoriesConfig DirectoriesConfig { get; set; } = null!; public AnnotationConfig AnnotationConfig { get; set; } = null!; - public AnnotatorWindowConfig AnnotatorWindowConfig { get; set; } = null!; - public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!; public ThumbnailConfig ThumbnailConfig { get; set; } = null!; @@ -30,7 +32,7 @@ public class ConfigUpdater : IConfigUpdater public void CheckConfig() { var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!; - var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH); + var configFilePath = Path.Combine(exePath, SecurityConstants.CONFIG_PATH); if (File.Exists(configFilePath)) return; @@ -39,9 +41,9 @@ public class ConfigUpdater : IConfigUpdater { ApiConfig = new ApiConfig { - Url = Constants.DEFAULT_API_URL, - RetryCount = Constants.DEFAULT_API_RETRY_COUNT, - TimeoutSeconds = Constants.DEFAULT_API_TIMEOUT_SECONDS + Url = SecurityConstants.DEFAULT_API_URL, + RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT, + TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS }, AnnotationConfig = new AnnotationConfig @@ -49,12 +51,11 @@ public class ConfigUpdater : IConfigUpdater AnnotationClasses = Constants.DefaultAnnotationClasses, VideoFormats = Constants.DefaultVideoFormats, ImageFormats = Constants.DefaultImageFormats, - }, - AnnotatorWindowConfig = new AnnotatorWindowConfig - { LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH, - RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH + RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH, + + AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE }, DirectoriesConfig = new DirectoriesConfig @@ -86,6 +87,6 @@ public class ConfigUpdater : IConfigUpdater public void Save(AppConfig config) { - File.WriteAllText(Constants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8); + File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8); } } diff --git a/Azaion.Common/DTO/Config/QueueConfig.cs b/Azaion.Common/DTO/Config/QueueConfig.cs new file mode 100644 index 0000000..c772cc9 --- /dev/null +++ b/Azaion.Common/DTO/Config/QueueConfig.cs @@ -0,0 +1,14 @@ +namespace Azaion.Common.DTO.Config; + +public class QueueConfig +{ + public string Host { get; set; } = null!; + public int Port { get; set; } + + public string ProducerUsername { get; set; } = null!; + public string ProducerPassword { get; set; } = null!; + + public string ConsumerUsername { get; set; } = null!; + public string ConsumerPassword { get; set; } = null!; + +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Label.cs b/Azaion.Common/DTO/Label.cs index 9c90b3d..e044674 100644 --- a/Azaion.Common/DTO/Label.cs +++ b/Azaion.Common/DTO/Label.cs @@ -158,19 +158,25 @@ public class YoloLabel : Label public static async Task> ReadFromFile(string filename, CancellationToken cancellationToken = default) { var str = await File.ReadAllTextAsync(filename, cancellationToken); - - return str.Split('\n') - .Select(Parse) - .Where(ann => ann != null) - .ToList()!; + return Deserialize(str); } public static async Task WriteToFile(IEnumerable labels, string filename, CancellationToken cancellationToken = default) { - var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString())); + var labelsStr = Serialize(labels); await File.WriteAllTextAsync(filename, labelsStr, cancellationToken); } + public static string Serialize(IEnumerable labels) => + string.Join(Environment.NewLine, labels.Select(x => x.ToString())); + + public static List Deserialize(string str) => + str.Split('\n') + .Select(Parse) + .Where(ann => ann != null) + .ToList()!; + + public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.'); } diff --git a/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs new file mode 100644 index 0000000..1b0f07c --- /dev/null +++ b/Azaion.Common/DTO/Queue/AnnotationCreatedMessage.cs @@ -0,0 +1,23 @@ +using Azaion.CommonSecurity.DTO; + +namespace Azaion.Common.DTO.Queue; +using MessagePack; + +[MessagePackObject] +public class AnnotationCreatedMessage +{ + [Key(0)] public DateTime CreatedDate { get; set; } + [Key(1)] public string Name { get; set; } = null!; + [Key(2)] public string Label { get; set; } = null!; + [Key(3)] public byte[] Image { get; set; } = null!; + [Key(4)] public RoleEnum CreatedRole { get; set; } + [Key(5)] public string CreatedEmail { get; set; } = null!; + [Key(6)] public SourceEnum Source { get; set; } + [Key(7)] public AnnotationStatus Status { get; set; } +} + +[MessagePackObject] +public class AnnotationValidatedMessage +{ + [Key(0)] public string Name { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/Queue/SourceEnum.cs b/Azaion.Common/DTO/Queue/SourceEnum.cs new file mode 100644 index 0000000..0d6273b --- /dev/null +++ b/Azaion.Common/DTO/Queue/SourceEnum.cs @@ -0,0 +1,7 @@ +namespace Azaion.Common.DTO.Queue; + +public enum SourceEnum +{ + AI, + Manual +} \ No newline at end of file diff --git a/Azaion.Common/Database/AnnotationsDb.cs b/Azaion.Common/Database/AnnotationsDb.cs new file mode 100644 index 0000000..0cb064e --- /dev/null +++ b/Azaion.Common/Database/AnnotationsDb.cs @@ -0,0 +1,11 @@ +using Azaion.Common.DTO; +using LinqToDB; +using LinqToDB.Data; + +namespace Azaion.Common.Database; + +public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions) +{ + public ITable Annotations => this.GetTable(); + public ITable AnnotationsQueue => this.GetTable(); +} \ No newline at end of file diff --git a/Azaion.Common/Database/DbFactory.cs b/Azaion.Common/Database/DbFactory.cs new file mode 100644 index 0000000..b0e861a --- /dev/null +++ b/Azaion.Common/Database/DbFactory.cs @@ -0,0 +1,65 @@ +using System.Diagnostics; +using Azaion.Common.DTO; +using Azaion.Common.DTO.Config; +using LinqToDB; +using LinqToDB.Mapping; +using Microsoft.Extensions.Options; + +namespace Azaion.Common.Database; + +public interface IDbFactory +{ + Task Run(Func> func); + Task Run(Func func); +} + +public class DbFactory : IDbFactory +{ + private readonly DataOptions _dataOptions; + + public DbFactory(IOptions annConfig) + { + _dataOptions = LoadOptions(annConfig.Value.AnnotationsDbFile); + } + + private DataOptions LoadOptions(string dbFile) + { + if (string.IsNullOrEmpty(dbFile)) + throw new ArgumentException($"Empty AnnotationsDbFile in config!"); + + var dataOptions = new DataOptions() + .UseSQLiteOfficial($"Data Source={dbFile}") + .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); + + _ = dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText)); + return dataOptions; + } + + + public async Task Run(Func> func) + { + await using var db = new AnnotationsDb(_dataOptions); + return await func(db); + } + + public async Task Run(Func func) + { + await using var db = new AnnotationsDb(_dataOptions); + await func(db); + } +} + +public static class AnnotationsDbSchemaHolder +{ + public static readonly MappingSchema MappingSchema; + + static AnnotationsDbSchemaHolder() + { + MappingSchema = new MappingSchema(); + var builder = new FluentMappingBuilder(MappingSchema); + + builder.Entity().HasTableName("annotations_queue"); + + builder.Build(); + } +} diff --git a/Azaion.Common/Extensions/IEnumerableExtensions.cs b/Azaion.Common/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..df3cf07 --- /dev/null +++ b/Azaion.Common/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,7 @@ +namespace Azaion.Common.Extensions; + +public static class EnumerableExtensions +{ + public static bool In(this T obj, params T[] objects) => + objects.Contains(obj); +} \ No newline at end of file diff --git a/Azaion.Common/Extensions/ThrottleExtensions.cs b/Azaion.Common/Extensions/ThrottleExtensions.cs index 9cd9520..06f4f50 100644 --- a/Azaion.Common/Extensions/ThrottleExtensions.cs +++ b/Azaion.Common/Extensions/ThrottleExtensions.cs @@ -1,9 +1,9 @@ -namespace Azaion.Annotator.Extensions; +namespace Azaion.Common.Extensions; public static class ThrottleExt { private static bool _throttleOn; - public static async Task Throttle(Func func, TimeSpan? throttleTime = null) + public static async Task Throttle(this Func func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default) { if (_throttleOn) return; @@ -12,8 +12,8 @@ public static class ThrottleExt await func(); _ = Task.Run(async () => { - await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500)); + await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken); _throttleOn = false; - }); + }, cancellationToken); } } \ No newline at end of file diff --git a/Azaion.Common/Services/AnnotationService.cs b/Azaion.Common/Services/AnnotationService.cs new file mode 100644 index 0000000..47aefb5 --- /dev/null +++ b/Azaion.Common/Services/AnnotationService.cs @@ -0,0 +1,132 @@ +using System.Drawing.Imaging; +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.CommonSecurity.DTO; +using Azaion.CommonSecurity.Services; +using LinqToDB; +using MessagePack; +using Microsoft.Extensions.Options; +using RabbitMQ.Stream.Client; +using RabbitMQ.Stream.Client.Reliable; + +namespace Azaion.Common.Services; + +public class AnnotationService +{ + private readonly AzaionApiClient _apiClient; + private readonly IDbFactory _dbFactory; + private readonly FailsafeAnnotationsProducer _producer; + private readonly QueueConfig _queueConfig; + private Consumer _consumer = null!; + + public AnnotationService(AzaionApiClient apiClient, + IDbFactory dbFactory, + FailsafeAnnotationsProducer producer, + IOptions queueConfig) + { + _apiClient = apiClient; + _dbFactory = dbFactory; + _producer = producer; + _queueConfig = queueConfig.Value; + + Task.Run(async () => await Init()).Wait(); + } + + private async Task Init() + { + var consumerSystem = await StreamSystem.Create(new StreamSystemConfig + { + Endpoints = new List{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)}, + UserName = _queueConfig.ConsumerUsername, + Password = _queueConfig.ConsumerPassword + }); + _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) + { + OffsetSpec = new OffsetTypeFirst(), + MessageHandler = async (stream, _, _, message) => + await Consume(MessagePackSerializer.Deserialize(message.Data.Contents)), + }); + } + + //AI / Manual + public async Task SaveAnnotation(string fName, List? labels, SourceEnum source, MemoryStream? stream = null, CancellationToken token = default) => + await SaveAnnotationInner(DateTime.UtcNow, fName, labels, source, stream, _apiClient.User.Role, _apiClient.User.Email, token); + + //Queue (only from operators) + public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default) + { + if (message.CreatedRole == RoleEnum.Validator) //Don't proceed our own messages (or from another Validator) + return; + + await SaveAnnotationInner( + message.CreatedDate, + message.Name, + YoloLabel.Deserialize(message.Label), + message.Source, + new MemoryStream(message.Image), + message.CreatedRole, + message.CreatedEmail, + cancellationToken); + } + + private async Task SaveAnnotationInner(DateTime createdDate, string fName, List? labels, SourceEnum source, MemoryStream? stream, + RoleEnum createdRole, + string createdEmail, + CancellationToken token = default) + { + //Flow for roles: + // Operator: + // sourceEnum: (manual, ai) + // Validator: + // sourceEnum: (manual) if was in received.json then else + // sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json + + var classes = labels?.Select(x => x.ClassNumber).Distinct().ToList() ?? []; + AnnotationStatus status; + + var annotation = await _dbFactory.Run(async db => + { + var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token); + status = ann?.AnnotationStatus == AnnotationStatus.Created && createdRole == RoleEnum.Validator + ? AnnotationStatus.Validated + : AnnotationStatus.Created; + + if (ann != null) + await db.Annotations + .Where(x => x.Name == fName) + .Set(x => x.Classes, classes) + .Set(x => x.Source, source) + .Set(x => x.AnnotationStatus, status) + .UpdateAsync(token: token); + else + { + ann = new Annotation + { + CreatedDate = createdDate, + Name = fName, + Classes = classes, + CreatedEmail = createdEmail, + CreatedRole = createdRole, + AnnotationStatus = status, + Source = source + }; + await db.InsertAsync(ann, token: token); + } + return ann; + }); + + if (stream != null) + { + var img = System.Drawing.Image.FromStream(stream); + img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue + } + if (labels != null) + await YoloLabel.WriteToFile(labels, annotation.LabelPath, token); + + await _producer.SendToQueue(annotation, token); + } +} \ No newline at end of file diff --git a/Azaion.Common/Services/FailsafeProducer.cs b/Azaion.Common/Services/FailsafeProducer.cs new file mode 100644 index 0000000..32fcf11 --- /dev/null +++ b/Azaion.Common/Services/FailsafeProducer.cs @@ -0,0 +1,125 @@ +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 LinqToDB; +using MessagePack; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using RabbitMQ.Stream.Client; +using RabbitMQ.Stream.Client.Reliable; + +namespace Azaion.Common.Services; + +public class FailsafeAnnotationsProducer +{ + private readonly ILogger _logger; + private readonly IDbFactory _dbFactory; + private readonly QueueConfig _queueConfig; + + private Producer _annotationProducer = null!; + private Producer _annotationConfirmProducer = null!; + + + public FailsafeAnnotationsProducer(ILogger logger, IDbFactory dbFactory, IOptions queueConfig) + { + _logger = logger; + _dbFactory = dbFactory; + _queueConfig = queueConfig.Value; + Task.Run(async () => await ProcessQueue()).Wait(); + } + + private async Task GetProducerQueueConfig() + { + return await StreamSystem.Create(new StreamSystemConfig + { + + Endpoints = new List { new IPEndPoint(IPAddress.Parse(_queueConfig.Host), _queueConfig.Port) }, + UserName = _queueConfig.ProducerUsername, + Password = _queueConfig.ProducerPassword + }); + } + + private async Task Init(CancellationToken cancellationToken = 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) + { + var messages = await GetFromQueue(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(); + await _annotationProducer.Send(createdMessages, CompressionType.Gzip); + + var validatedMessages = messagesChunk + .Where(x => x.Status == AnnotationStatus.Validated) + .Select(x => new Message(MessagePackSerializer.Serialize(x))) + .ToList(); + 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; + } + catch (Exception e) + { + _logger.LogError(e, e.Message); + await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken); + } + } + } + } + } + + private async Task> GetFromQueue(CancellationToken cancellationToken = default) + { + return await _dbFactory.Run(async db => + { + var annotations = await db.AnnotationsQueue.Join(db.Annotations, aq => aq.Name, a => a.Name, (aq, a) => a) + .ToListAsync(token: cancellationToken); + + var messages = new List(); + foreach (var annotation in annotations) + { + var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); + var label = await File.ReadAllTextAsync(annotation.LabelPath, cancellationToken); + var annCreateMessage = new AnnotationCreatedMessage + { + Name = annotation.Name, + + CreatedRole = annotation.CreatedRole, + CreatedEmail = annotation.CreatedEmail, + CreatedDate = annotation.CreatedDate, + + Image = image, + Label = label, + Source = annotation.Source + }; + messages.Add(annCreateMessage); + } + return messages; + }); + } + + public async Task SendToQueue(Annotation annotation, CancellationToken cancellationToken = default) + { + await _dbFactory.Run(async db => + await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken)); + } +} \ No newline at end of file diff --git a/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj new file mode 100644 index 0000000..f9007e4 --- /dev/null +++ b/Azaion.CommonSecurity/Azaion.CommonSecurity.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + + + + + + + + diff --git a/Azaion.Common/DTO/Config/ApiConfig.cs b/Azaion.CommonSecurity/DTO/ApiConfig.cs similarity index 79% rename from Azaion.Common/DTO/Config/ApiConfig.cs rename to Azaion.CommonSecurity/DTO/ApiConfig.cs index 607c68d..849371f 100644 --- a/Azaion.Common/DTO/Config/ApiConfig.cs +++ b/Azaion.CommonSecurity/DTO/ApiConfig.cs @@ -1,4 +1,4 @@ -namespace Azaion.Common.DTO.Config; +namespace Azaion.CommonSecurity.DTO; public class ApiConfig { diff --git a/Azaion.Common/DTO/ApiCredentials.cs b/Azaion.CommonSecurity/DTO/ApiCredentials.cs similarity index 81% rename from Azaion.Common/DTO/ApiCredentials.cs rename to Azaion.CommonSecurity/DTO/ApiCredentials.cs index 493b310..7280c17 100644 --- a/Azaion.Common/DTO/ApiCredentials.cs +++ b/Azaion.CommonSecurity/DTO/ApiCredentials.cs @@ -1,4 +1,4 @@ -namespace Azaion.Common.DTO; +namespace Azaion.CommonSecurity.DTO; public class ApiCredentials(string email, string password) : EventArgs { diff --git a/Azaion.Common/DTO/HardwareInfo.cs b/Azaion.CommonSecurity/DTO/HardwareInfo.cs similarity index 87% rename from Azaion.Common/DTO/HardwareInfo.cs rename to Azaion.CommonSecurity/DTO/HardwareInfo.cs index b219de8..292c212 100644 --- a/Azaion.Common/DTO/HardwareInfo.cs +++ b/Azaion.CommonSecurity/DTO/HardwareInfo.cs @@ -1,4 +1,4 @@ -namespace Azaion.Common.DTO; +namespace Azaion.CommonSecurity.DTO; public class HardwareInfo { diff --git a/Azaion.Common/DTO/LoginResponse.cs b/Azaion.CommonSecurity/DTO/LoginResponse.cs similarity index 67% rename from Azaion.Common/DTO/LoginResponse.cs rename to Azaion.CommonSecurity/DTO/LoginResponse.cs index b2a027b..a9d070f 100644 --- a/Azaion.Common/DTO/LoginResponse.cs +++ b/Azaion.CommonSecurity/DTO/LoginResponse.cs @@ -1,4 +1,4 @@ -namespace Azaion.Common.DTO; +namespace Azaion.CommonSecurity.DTO; public class LoginResponse { diff --git a/Azaion.Common/DTO/RoleEnum.cs b/Azaion.CommonSecurity/DTO/RoleEnum.cs similarity index 90% rename from Azaion.Common/DTO/RoleEnum.cs rename to Azaion.CommonSecurity/DTO/RoleEnum.cs index 3eac6d8..b1b518e 100644 --- a/Azaion.Common/DTO/RoleEnum.cs +++ b/Azaion.CommonSecurity/DTO/RoleEnum.cs @@ -1,4 +1,4 @@ -namespace Azaion.Common.DTO; +namespace Azaion.CommonSecurity.DTO; public enum RoleEnum { diff --git a/Azaion.CommonSecurity/DTO/SecureAppConfig.cs b/Azaion.CommonSecurity/DTO/SecureAppConfig.cs new file mode 100644 index 0000000..cd4e723 --- /dev/null +++ b/Azaion.CommonSecurity/DTO/SecureAppConfig.cs @@ -0,0 +1,6 @@ +namespace Azaion.CommonSecurity.DTO; + +public class SecureAppConfig +{ + public ApiConfig ApiConfig { get; set; } = null!; +} \ No newline at end of file diff --git a/Azaion.Common/DTO/User.cs b/Azaion.CommonSecurity/DTO/User.cs similarity index 57% rename from Azaion.Common/DTO/User.cs rename to Azaion.CommonSecurity/DTO/User.cs index 2d9b90a..b6f9ee4 100644 --- a/Azaion.Common/DTO/User.cs +++ b/Azaion.CommonSecurity/DTO/User.cs @@ -1,6 +1,6 @@ using System.Security.Claims; -namespace Azaion.Common.DTO; +namespace Azaion.CommonSecurity.DTO; public class User { @@ -12,9 +12,9 @@ public class User { var claimDict = claims.ToDictionary(x => x.Type, x => x.Value); - Id = Guid.Parse(claimDict[Constants.CLAIM_NAME_ID]); - Email = claimDict[Constants.CLAIM_EMAIL]; - if (!Enum.TryParse(claimDict[Constants.CLAIM_ROLE], out RoleEnum role)) + Id = Guid.Parse(claimDict[SecurityConstants.CLAIM_NAME_ID]); + Email = claimDict[SecurityConstants.CLAIM_EMAIL]; + if (!Enum.TryParse(claimDict[SecurityConstants.CLAIM_ROLE], out RoleEnum role)) role = RoleEnum.None; Role = role; } diff --git a/Azaion.CommonSecurity/SecurityConstants.cs b/Azaion.CommonSecurity/SecurityConstants.cs new file mode 100644 index 0000000..9f2bba3 --- /dev/null +++ b/Azaion.CommonSecurity/SecurityConstants.cs @@ -0,0 +1,18 @@ +namespace Azaion.CommonSecurity; + +public class SecurityConstants +{ + public const string CONFIG_PATH = "config.json"; + + #region ApiConfig + + public const string DEFAULT_API_URL = "https://api.azaion.com/"; + public const int DEFAULT_API_RETRY_COUNT = 3; + public const int DEFAULT_API_TIMEOUT_SECONDS = 40; + + public const string CLAIM_NAME_ID = "nameid"; + public const string CLAIM_EMAIL = "unique_name"; + public const string CLAIM_ROLE = "role"; + + #endregion ApiConfig +} \ No newline at end of file diff --git a/Azaion.Common/Services/AzaionApiClient.cs b/Azaion.CommonSecurity/Services/AzaionApiClient.cs similarity index 72% rename from Azaion.Common/Services/AzaionApiClient.cs rename to Azaion.CommonSecurity/Services/AzaionApiClient.cs index 28f352f..fda2007 100644 --- a/Azaion.Common/Services/AzaionApiClient.cs +++ b/Azaion.CommonSecurity/Services/AzaionApiClient.cs @@ -1,14 +1,12 @@ -using System.IO; +using System.IdentityModel.Tokens.Jwt; using System.Net; -using System.Net.Http; using System.Net.Http.Headers; using System.Security; using System.Text; -using Azaion.Common.DTO; +using Azaion.CommonSecurity.DTO; using Newtonsoft.Json; -using System.IdentityModel.Tokens.Jwt; -namespace Azaion.Common.Services; +namespace Azaion.CommonSecurity.Services; public class AzaionApiClient(HttpClient httpClient) : IDisposable { @@ -20,6 +18,37 @@ public class AzaionApiClient(HttpClient httpClient) : IDisposable private string JwtToken { get; set; } = null!; public User User { get; set; } = null!; + public static AzaionApiClient Create(ApiCredentials credentials) + { + ApiConfig apiConfig; + try + { + if (!File.Exists(SecurityConstants.CONFIG_PATH)) + throw new FileNotFoundException(SecurityConstants.CONFIG_PATH); + var configStr = File.ReadAllText(SecurityConstants.CONFIG_PATH); + apiConfig = JsonConvert.DeserializeObject(configStr)!.ApiConfig; + } + catch (Exception e) + { + Console.WriteLine(e); + apiConfig = new ApiConfig + { + Url = SecurityConstants.DEFAULT_API_URL, + RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT , + TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS + }; + } + + var api = new AzaionApiClient(new HttpClient + { + BaseAddress = new Uri(apiConfig.Url), + Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds) + }); + + api.EnterCredentials(credentials); + return api; + } + public void EnterCredentials(ApiCredentials credentials) { if (string.IsNullOrWhiteSpace(credentials.Email) || string.IsNullOrWhiteSpace(credentials.Password)) diff --git a/Azaion.Common/Services/HardwareService.cs b/Azaion.CommonSecurity/Services/HardwareService.cs similarity index 97% rename from Azaion.Common/Services/HardwareService.cs rename to Azaion.CommonSecurity/Services/HardwareService.cs index d03e78f..f2b369d 100644 --- a/Azaion.Common/Services/HardwareService.cs +++ b/Azaion.CommonSecurity/Services/HardwareService.cs @@ -2,9 +2,9 @@ using System.Net.NetworkInformation; using System.Security.Cryptography; using System.Text; -using Azaion.Common.DTO; +using Azaion.CommonSecurity.DTO; -namespace Azaion.Common.Services; +namespace Azaion.CommonSecurity.Services; public interface IHardwareService { diff --git a/Azaion.Common/Services/ResourceLoader.cs b/Azaion.CommonSecurity/Services/ResourceLoader.cs similarity index 92% rename from Azaion.Common/Services/ResourceLoader.cs rename to Azaion.CommonSecurity/Services/ResourceLoader.cs index c180ccc..d390b4f 100644 --- a/Azaion.Common/Services/ResourceLoader.cs +++ b/Azaion.CommonSecurity/Services/ResourceLoader.cs @@ -1,8 +1,7 @@ -using System.IO; -using System.Reflection; -using Azaion.Common.DTO; +using System.Reflection; +using Azaion.CommonSecurity.DTO; -namespace Azaion.Common.Services; +namespace Azaion.CommonSecurity.Services; public interface IResourceLoader { @@ -53,6 +52,7 @@ public class ResourceLoader(AzaionApiClient api, ApiCredentials credentials) : I var key = Security.MakeEncryptionKey(credentials.Email, credentials.Password, hardwareInfo.Hash); var stream = new MemoryStream(); await encryptedStream.DecryptTo(stream, key, cancellationToken); + stream.Seek(0, SeekOrigin.Begin); return stream; } } \ No newline at end of file diff --git a/Azaion.Common/Services/Security.cs b/Azaion.CommonSecurity/Services/Security.cs similarity index 97% rename from Azaion.Common/Services/Security.cs rename to Azaion.CommonSecurity/Services/Security.cs index cc0ec33..a1f8dfb 100644 --- a/Azaion.Common/Services/Security.cs +++ b/Azaion.CommonSecurity/Services/Security.cs @@ -1,10 +1,9 @@ -using System.IO; -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; using System.Security; using System.Security.Cryptography; using System.Text; -namespace Azaion.Common.Services; +namespace Azaion.CommonSecurity.Services; public static class Security { diff --git a/Azaion.Dataset/DatasetExplorer.xaml.cs b/Azaion.Dataset/DatasetExplorer.xaml.cs index 031f141..1b6aa32 100644 --- a/Azaion.Dataset/DatasetExplorer.xaml.cs +++ b/Azaion.Dataset/DatasetExplorer.xaml.cs @@ -307,7 +307,7 @@ public partial class DatasetExplorer return null; } - var classes = (await YoloLabel.ReadFromFile(labelPath)) + var classes = (await YoloLabel.ReadFromFile(labelPath)) .Select(x => x.ClassNumber) .Distinct() .ToList(); diff --git a/Azaion.Dataset/DatasetExplorerEventHandler.cs b/Azaion.Dataset/DatasetExplorerEventHandler.cs index 6db0ab0..2b370cb 100644 --- a/Azaion.Dataset/DatasetExplorerEventHandler.cs +++ b/Azaion.Dataset/DatasetExplorerEventHandler.cs @@ -75,5 +75,6 @@ public class DatasetExplorerEventHandler(DatasetExplorer datasetExplorer, IGalle var (thumbnailDto, detections) = await galleryManager.CreateThumbnail(imageCreatedEvent.ImagePath, cancellationToken); if (thumbnailDto != null && detections != null) datasetExplorer.AddThumbnail(thumbnailDto, detections); + await galleryManager.SaveLabelsCache(); } } diff --git a/Azaion.Suite.sln b/Azaion.Suite.sln index 3cc2933..6989ff1 100644 --- a/Azaion.Suite.sln +++ b/Azaion.Suite.sln @@ -20,6 +20,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Annotator", "Dummy\A 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 Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -54,6 +56,10 @@ Global {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 EndGlobalSection GlobalSection(NestedProjects) = preSolution {32C4747F-F700-44FD-B4ED-21B4A66B5FAB} = {C307BE2E-FFCC-4BD7-AD89-C82D40B65D03} diff --git a/Azaion.Suite/App.xaml.cs b/Azaion.Suite/App.xaml.cs index 2f9aa5f..92fb502 100644 --- a/Azaion.Suite/App.xaml.cs +++ b/Azaion.Suite/App.xaml.cs @@ -1,14 +1,16 @@ using System.IO; -using System.Net.Http; using System.Windows; using System.Windows.Threading; using Azaion.Annotator; using Azaion.Annotator.Extensions; -using Azaion.Common; +using Azaion.Common.Database; using Azaion.Common.DTO; using Azaion.Common.DTO.Config; using Azaion.Common.Extensions; using Azaion.Common.Services; +using Azaion.CommonSecurity; +using Azaion.CommonSecurity.DTO; +using Azaion.CommonSecurity.Services; using Azaion.Dataset; using LibVLCSharp.Shared; using MediatR; @@ -17,7 +19,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using Serilog; using KeyEventArgs = System.Windows.Input.KeyEventArgs; @@ -32,38 +33,7 @@ public partial class App private AzaionApiClient _apiClient = null!; private IResourceLoader _resourceLoader = null!; - - private static AzaionApiClient CreateApiClient(ApiCredentials credentials) - { - ApiConfig apiConfig; - try - { - if (!File.Exists(Constants.CONFIG_PATH)) - throw new FileNotFoundException(Constants.CONFIG_PATH); - var configStr = File.ReadAllText(Constants.CONFIG_PATH); - apiConfig = JsonConvert.DeserializeObject(configStr)!.ApiConfig; - } - catch (Exception e) - { - Console.WriteLine(e); - apiConfig = new ApiConfig - { - Url = "https://api.azaion.com", - RetryCount = 3, - TimeoutSeconds = 40 - }; - } - - var api = new AzaionApiClient(new HttpClient - { - BaseAddress = new Uri(apiConfig.Url), - Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds) - }); - - api.EnterCredentials(credentials); - return api; - } - + private Stream _securedConfig = null!; private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) { @@ -83,8 +53,9 @@ public partial class App var login = new Login(); login.CredentialsEntered += async (s, args) => { - _apiClient = CreateApiClient(args); + _apiClient = AzaionApiClient.Create(args); _resourceLoader = new ResourceLoader(_apiClient, args); + _securedConfig = await _resourceLoader.Load("secured-config.json"); AppDomain.CurrentDomain.AssemblyResolve += (_, a) => _resourceLoader.LoadAssembly(a.Name); StartMain(); @@ -110,7 +81,8 @@ public partial class App _host = Host.CreateDefaultBuilder() .ConfigureAppConfiguration((context, config) => config .AddCommandLine(Environment.GetCommandLineArgs()) - .AddJsonFile(Constants.CONFIG_PATH, optional: true, reloadOnChange: true)) + .AddJsonFile(SecurityConstants.CONFIG_PATH, optional: true, reloadOnChange: true) + .AddJsonStream(_securedConfig)) .ConfigureServices((context, services) => { services.AddSingleton(); @@ -121,9 +93,9 @@ public partial class App 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); @@ -144,6 +116,7 @@ public partial class App }); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddHttpClient((sp, client) => { @@ -152,6 +125,10 @@ public partial class App client.Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds); }); + services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); @@ -159,6 +136,9 @@ public partial class App services.AddSingleton(); }) .Build(); + + Annotation.InitializeDirs(_host.Services.GetRequiredService>().Value); + _mediator = _host.Services.GetRequiredService(); _logger = _host.Services.GetRequiredService>(); _formState = _host.Services.GetRequiredService(); diff --git a/Azaion.Suite/Azaion.Suite.csproj b/Azaion.Suite/Azaion.Suite.csproj index cd6667d..55dba53 100644 --- a/Azaion.Suite/Azaion.Suite.csproj +++ b/Azaion.Suite/Azaion.Suite.csproj @@ -17,6 +17,7 @@ + @@ -37,6 +38,9 @@ PreserveNewest + + PreserveNewest + diff --git a/Azaion.Suite/Login.xaml b/Azaion.Suite/Login.xaml index 0978c07..64b3bce 100644 --- a/Azaion.Suite/Login.xaml +++ b/Azaion.Suite/Login.xaml @@ -74,6 +74,7 @@ BorderBrush="DimGray" BorderThickness="0,0,0,1" HorizontalAlignment="Left" + Text="admin@azaion.com" /> + HorizontalAlignment="Left" + Password="Az@1on1000Odm$n"/>