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"/>