rework to Azaion.Suite, show tabs with annotator and dataset explorer

This commit is contained in:
Alex Bezdieniezhnykh
2024-11-23 08:53:12 +02:00
parent 490e90f239
commit 3b40bd601e
40 changed files with 374 additions and 284 deletions
+1 -5
View File
@@ -102,9 +102,6 @@ public partial class Annotator
if (LvFiles.Items.IsEmpty) if (LvFiles.Items.IsEmpty)
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]); BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
if (_appConfig.WindowConfig.ShowHelpOnStart)
_helpWindow.Show();
} }
public void BlinkHelp(string helpText, int times = 2) public void BlinkHelp(string helpText, int times = 2)
@@ -541,7 +538,6 @@ public partial class Annotator
_ = Task.Run(async () => _ = Task.Run(async () =>
{ {
using var detector = new YOLODetector(_appConfig.AIRecognitionConfig);
Dispatcher.Invoke(() => _autoDetectDialog.Log("Ініціалізація AI...")); Dispatcher.Invoke(() => _autoDetectDialog.Log("Ініціалізація AI..."));
var prevSeekTime = 0.0; var prevSeekTime = 0.0;
@@ -549,7 +545,7 @@ public partial class Annotator
{ {
try try
{ {
var detections = _aiDetector.Detect(timeframe.Stream); var detections = await _aiDetector.Detect(timeframe.Stream, token);
if (timeframe.Time.TotalSeconds > prevSeekTime + 1) if (timeframe.Time.TotalSeconds > prevSeekTime + 1)
{ {
Dispatcher.Invoke(() => SeekTo(timeframe.Time)); Dispatcher.Invoke(() => SeekTo(timeframe.Time));
@@ -5,6 +5,7 @@ using System.Windows.Input;
using System.Windows.Media; using System.Windows.Media;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
+3 -6
View File
@@ -10,8 +10,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="libc.translation" Version="7.1.1" /> <PackageReference Include="libc.translation" Version="7.1.1" />
<PackageReference Include="LibVLCSharp" Version="3.8.2" /> <PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.8.2" /> <PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
@@ -26,16 +26,13 @@
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SkiaSharp" Version="2.88.9" /> <PackageReference Include="SkiaSharp" Version="2.88.9" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.20" /> <PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
<PackageReference Include="WindowsAPICodePack" Version="7.0.4" /> <PackageReference Include="WindowsAPICodePack" Version="7.0.4" />
<PackageReference Include="YoloV8.Gpu" Version="5.0.4" /> <PackageReference Include="YoloV8.Gpu" Version="5.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="logo.ico" /> <None Remove="logo.ico" />
<None Update="config.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
-3
View File
@@ -1,8 +1,5 @@
using System.Windows.Media; using System.Windows.Media;
using Azaion.Annotator.Extensions;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Annotator.DTO; namespace Azaion.Annotator.DTO;
+10 -4
View File
@@ -1,5 +1,6 @@
using System.Windows; using System.Windows;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Microsoft.Extensions.Options;
namespace Azaion.Annotator; namespace Azaion.Annotator;
@@ -7,14 +8,19 @@ public partial class HelpWindow : Window
{ {
private readonly WindowConfig _windowConfig; private readonly WindowConfig _windowConfig;
public HelpWindow(WindowConfig windowConfig) public HelpWindow(IOptions<WindowConfig> windowConfig)
{ {
_windowConfig = windowConfig; _windowConfig = windowConfig.Value;
Loaded += (_, _) => CbShowHelp.IsChecked = windowConfig.ShowHelpOnStart; Loaded += (_, _) => CbShowHelp.IsChecked = _windowConfig.ShowHelpOnStart;
Closing += (sender, args) =>
{
args.Cancel = true;
Visibility = Visibility.Hidden;
};
InitializeComponent(); InitializeComponent();
} }
private void Close(object sender, RoutedEventArgs e) => Close();
private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = true; private void CbShowHelp_OnChecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = true;
private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = false; private void CbShowHelp_OnUnchecked(object sender, RoutedEventArgs e) => _windowConfig.ShowHelpOnStart = false;
} }
+20 -9
View File
@@ -2,6 +2,8 @@
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Services;
using Compunet.YoloV8; using Compunet.YoloV8;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
@@ -12,18 +14,27 @@ namespace Azaion.Annotator;
public interface IAIDetector public interface IAIDetector
{ {
List<Detection> Detect(Stream stream); Task<List<Detection>> Detect(Stream imageStream, CancellationToken cancellationToken = default);
} }
public class YOLODetector(AIRecognitionConfig recognitionConfig) : IAIDetector, IDisposable public class YOLODetector(IOptions<AIRecognitionConfig> recognitionConfig, IResourceLoader resourceLoader) : IAIDetector, IDisposable
{ {
private readonly YoloPredictor _predictor = new(recognitionConfig.AIModelPath); private readonly AIRecognitionConfig _recognitionConfig = recognitionConfig.Value;
private YoloPredictor? _predictor;
private const string YOLO_MODEL = "azaion.onnx";
public List<Detection> Detect(Stream stream)
public async Task<List<Detection>> Detect(Stream imageStream, CancellationToken cancellationToken)
{ {
stream.Seek(0, SeekOrigin.Begin); if (_predictor == null)
var image = Image.Load<Rgb24>(stream); {
var result = _predictor.Detect(image); await using var stream = await resourceLoader.Load(YOLO_MODEL, cancellationToken);
_predictor = new YoloPredictor(stream.ToArray());
}
imageStream.Seek(0, SeekOrigin.Begin);
var image = Image.Load<Rgb24>(imageStream);
var result = await _predictor.DetectAsync(image);
var imageSize = new System.Windows.Size(image.Width, image.Height); var imageSize = new System.Windows.Size(image.Width, image.Height);
@@ -38,7 +49,7 @@ public class YOLODetector(AIRecognitionConfig recognitionConfig) : IAIDetector,
private List<Detection> FilterOverlapping(List<Detection> detections) private List<Detection> FilterOverlapping(List<Detection> detections)
{ {
var k = recognitionConfig.TrackingIntersectionThreshold; var k = _recognitionConfig.TrackingIntersectionThreshold;
var filteredDetections = new List<Detection>(); var filteredDetections = new List<Detection>();
for (var i = 0; i < detections.Count; i++) for (var i = 0; i < detections.Count; i++)
{ {
@@ -73,5 +84,5 @@ public class YOLODetector(AIRecognitionConfig recognitionConfig) : IAIDetector,
return filteredDetections; return filteredDetections;
} }
public void Dispose() => _predictor.Dispose(); public void Dispose() => _predictor?.Dispose();
} }
-47
View File
@@ -1,47 +0,0 @@
{
"VideosDirectory": "E:\\Azaion1\\Videos",
"LabelsDirectory": "E:\\labels",
"ImagesDirectory": "E:\\images",
"ThumbnailsDirectory": "E:\\thumbnails",
"ResultsDirectory": "E:\\results",
"UnknownImages": "E:\\unknown",
"AnnotationClasses": [
{ "Id": 0, "Name": "Броньована техніка", "ShortName": "Бронь" },
{ "Id": 1, "Name": "Вантажівка", "ShortName": "Вантаж" },
{ "Id": 2, "Name": "Машина легкова", "ShortName": "Машина" },
{ "Id": 3, "Name": "Артилерія", "ShortName": "Арта" },
{ "Id": 4, "Name": "Тінь від техніки", "ShortName": "Тінь" },
{ "Id": 5, "Name": "Окопи", "ShortName": "Окопи" },
{ "Id": 6, "Name": "Військовий", "ShortName": "Військов" },
{ "Id": 7, "Name": "Накати", "ShortName": "Накати" },
{ "Id": 8, "Name": "Танк з захистом", "ShortName": "Танк захист" },
{ "Id": 9, "Name": "Дим", "ShortName": "Дим" },
{ "Id": 10, "Name": "Літак", "ShortName": "Літак" }
],
"MainWindowConfig": {
"WindowSize": "1920,1080",
"WindowLocation": "50,50",
"FullScreen": true
},
"DatasetExplorerConfig": {
"WindowSize": "1920,1080",
"WindowLocation": "50,50",
"FullScreen": true
},
"ThumbnailConfig": {
"Size": "480,270",
"Border": 10
},
"LeftPanelWidth": 300,
"RightPanelWidth": 300,
"ShowHelpOnStart": false,
"VideoFormats": ["mov", "mp4"],
"ImageFormats": ["jpg", "jpeg", "png", "bmp", "gif"],
"AIRecognitionConfig": {
"AIModelPath": "azaion.onnx",
"FrameRecognitionSeconds": 2,
"TrackingDistanceConfidence": 0.15,
"TrackingProbabilityIncrease": 15,
"TrackingIntersectionThreshold": 0.8
}
}
+1
View File
@@ -10,6 +10,7 @@
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup> </ItemGroup>
+39 -18
View File
@@ -5,9 +5,17 @@ namespace Azaion.Common;
public class Constants public class Constants
{ {
#region DefaultConfig
public const string CONFIG_PATH = "config.json"; 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;
#endregion ApiConfig
#region DirectoriesConfig
public const string DEFAULT_VIDEO_DIR = "video"; public const string DEFAULT_VIDEO_DIR = "video";
public const string DEFAULT_LABELS_DIR = "labels"; public const string DEFAULT_LABELS_DIR = "labels";
@@ -15,24 +23,9 @@ public class Constants
public const string DEFAULT_RESULTS_DIR = "results"; public const string DEFAULT_RESULTS_DIR = "results";
public const string DEFAULT_THUMBNAILS_DIR = "thumbnails"; public const string DEFAULT_THUMBNAILS_DIR = "thumbnails";
public const int DEFAULT_THUMBNAIL_BORDER = 10;
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public static readonly Size DefaultWindowSize = new(1280, 720);
public static readonly Point DefaultWindowLocation = new(100, 100);
public static readonly Size DefaultThumbnailSize = new(240, 135);
#endregion #endregion
#region Thumbnails #region AnnotatorConfig
public const string THUMBNAIL_PREFIX = "_thumb";
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
#endregion
public static readonly List<AnnotationClass> DefaultAnnotationClasses = public static readonly List<AnnotationClass> DefaultAnnotationClasses =
[ [
@@ -52,6 +45,34 @@ public class Constants
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"]; public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"]; public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
# endregion AnnotatorConfig
# region AIRecognitionConfig
public const double DEFAULT_FRAME_RECOGNITION_SECONDS = 2;
public const double TRACKING_DISTANCE_CONFIDENCE = 0.15;
public const double TRACKING_PROBABILITY_INCREASE = 15;
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
# endregion AIRecognitionConfig
# region WindowConfig
public static readonly Size DefaultWindowSize = new(1280, 720);
public static readonly Point DefaultWindowLocation = new(100, 100);
public static readonly Size DefaultThumbnailSize = new(240, 135);
#endregion
#region Thumbnails
public const int DEFAULT_THUMBNAIL_BORDER = 10;
public const string THUMBNAIL_PREFIX = "_thumb";
public const string THUMBNAILS_CACHE_FILE = "thumbnails.cache";
#endregion
public static TimeSpan? GetTime(string imagePath) public static TimeSpan? GetTime(string imagePath)
{ {
var timeStr = imagePath.Split("_").LastOrDefault(); var timeStr = imagePath.Split("_").LastOrDefault();
+2 -2
View File
@@ -1,6 +1,6 @@
using System.Text.Json.Serialization; using System.Windows.Media;
using System.Windows.Media;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Newtonsoft.Json;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
@@ -1,8 +1,7 @@
namespace Azaion.Annotator.DTO; namespace Azaion.Common.DTO.Config;
public class AIRecognitionConfig public class AIRecognitionConfig
{ {
public string AIModelPath { get; set; } = null!;
public double FrameRecognitionSeconds { get; set; } public double FrameRecognitionSeconds { get; set; }
public double TrackingDistanceConfidence { get; set; } public double TrackingDistanceConfidence { get; set; }
public double TrackingProbabilityIncrease { get; set; } public double TrackingProbabilityIncrease { get; set; }
+1 -6
View File
@@ -1,4 +1,4 @@
namespace Azaion.Suite.Services.DTO; namespace Azaion.Common.DTO.Config;
public class ApiConfig public class ApiConfig
{ {
@@ -6,8 +6,3 @@ public class ApiConfig
public int RetryCount {get;set;} public int RetryCount {get;set;}
public double TimeoutSeconds { get; set; } public double TimeoutSeconds { get; set; }
} }
public class LocalFilesConfig
{
public string DllPath { get; set; } = null!;
}
+9 -7
View File
@@ -1,8 +1,5 @@
using System.IO; using System.IO;
using System.Text; using System.Text;
using Azaion.Annotator.DTO;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config; namespace Azaion.Common.DTO.Config;
@@ -11,14 +8,14 @@ public class AppConfig
{ {
public ApiConfig ApiConfig { get; set; } = null!; public ApiConfig ApiConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
public AnnotationConfig AnnotationConfig { get; set; } = null!; public AnnotationConfig AnnotationConfig { get; set; } = null!;
public WindowConfig WindowConfig { get; set; } = null!; public WindowConfig WindowConfig { get; set; } = null!;
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!; public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
public ThumbnailConfig ThumbnailConfig { get; set; } = null!; public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
} }
@@ -40,6 +37,13 @@ public class ConfigUpdater : IConfigUpdater
var appConfig = new AppConfig var appConfig = new AppConfig
{ {
ApiConfig = new ApiConfig
{
Url = Constants.DEFAULT_API_URL,
RetryCount = Constants.DEFAULT_API_RETRY_COUNT,
TimeoutSeconds = Constants.DEFAULT_API_TIMEOUT_SECONDS
},
AnnotationConfig = new AnnotationConfig AnnotationConfig = new AnnotationConfig
{ {
AnnotationClasses = Constants.DefaultAnnotationClasses, AnnotationClasses = Constants.DefaultAnnotationClasses,
@@ -51,7 +55,6 @@ public class ConfigUpdater : IConfigUpdater
{ {
WindowSize = Constants.DefaultWindowSize, WindowSize = Constants.DefaultWindowSize,
WindowLocation = Constants.DefaultWindowLocation, WindowLocation = Constants.DefaultWindowLocation,
ShowHelpOnStart = true,
FullScreen = true, FullScreen = true,
LeftPanelWidth = 250, LeftPanelWidth = 250,
RightPanelWidth = 250, RightPanelWidth = 250,
@@ -74,7 +77,6 @@ public class ConfigUpdater : IConfigUpdater
AIRecognitionConfig = new AIRecognitionConfig AIRecognitionConfig = new AIRecognitionConfig
{ {
AIModelPath = "azaion.onnx",
FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS, FrameRecognitionSeconds = Constants.DEFAULT_FRAME_RECOGNITION_SECONDS,
TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE, TrackingDistanceConfidence = Constants.TRACKING_DISTANCE_CONFIDENCE,
TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE, TrackingProbabilityIncrease = Constants.TRACKING_PROBABILITY_INCREASE,
@@ -1,4 +1,4 @@
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO.Config;
public class DirectoriesConfig public class DirectoriesConfig
{ {
@@ -1,9 +1,10 @@
namespace Azaion.Suite; namespace Azaion.Common.DTO;
public class HardwareInfo public class HardwareInfo
{ {
public string CPU { get; set; } = null!; public string CPU { get; set; } = null!;
public string GPU { get; set; } = null!; public string GPU { get; set; } = null!;
public string MacAddress { get; set; } = null!;
public string Memory { get; set; } = null!; public string Memory { get; set; } = null!;
public string Hash { get; set; } = null!; public string Hash { get; set; } = null!;
@@ -0,0 +1,10 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace Azaion.Common.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection ConfigureSection<T>(this IServiceCollection services, IConfiguration config) where T: class =>
services.Configure<T>(config.GetSection(typeof(T).Name));
}
@@ -4,12 +4,12 @@ using System.Net.Http;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Security; using System.Security;
using System.Text; using System.Text;
using Azaion.Suite.Services.DTO; using Azaion.Common.DTO;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Azaion.Suite.Services; namespace Azaion.Common.Services;
public class AzaionApiClient(HttpClient httpClient) public class AzaionApiClient(HttpClient httpClient) : IDisposable
{ {
const string JSON_MEDIA = "application/json"; const string JSON_MEDIA = "application/json";
@@ -26,11 +26,11 @@ public class AzaionApiClient(HttpClient httpClient)
Password = password.ToSecureString(); Password = password.ToSecureString();
} }
public async Task<Stream> GetResource(string password, HardwareInfo hardware, ResourceEnum resourceEnum) public async Task<Stream> GetResource(string fileName, string password, HardwareInfo hardware)
{ {
var response = await Send(httpClient, new HttpRequestMessage(HttpMethod.Post, "/resources/get") var response = await Send(httpClient, new HttpRequestMessage(HttpMethod.Post, "/resources/get")
{ {
Content = new StringContent(JsonConvert.SerializeObject(new { password, hardware, resourceEnum }), Encoding.UTF8, JSON_MEDIA) Content = new StringContent(JsonConvert.SerializeObject(new { fileName, password, hardware }), Encoding.UTF8, JSON_MEDIA)
}); });
return await response.Content.ReadAsStreamAsync(); return await response.Content.ReadAsStreamAsync();
} }
@@ -82,4 +82,10 @@ public class AzaionApiClient(HttpClient httpClient)
var result = await response.Content.ReadAsStringAsync(); var result = await response.Content.ReadAsStringAsync();
throw new Exception($"Failed: {response.StatusCode}! Result: {result}"); throw new Exception($"Failed: {response.StatusCode}! Result: {result}");
} }
public void Dispose()
{
httpClient.Dispose();
Password.Dispose();
}
} }
@@ -2,8 +2,9 @@
using System.Net.NetworkInformation; using System.Net.NetworkInformation;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using Azaion.Common.DTO;
namespace Azaion.Suite.Services; namespace Azaion.Common.Services;
public interface IHardwareService public interface IHardwareService
{ {
+22
View File
@@ -0,0 +1,22 @@
using System.IO;
namespace Azaion.Common.Services;
public interface IResourceLoader
{
Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default);
}
public class ResourceLoader(string email, string password, AzaionApiClient api, IHardwareService hardwareService) : IResourceLoader
{
public async Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default)
{
var hardwareInfo = await hardwareService.GetHardware();
var encryptedStream = await api.GetResource(fileName, password, hardwareInfo);
var key = Security.MakeEncryptionKey(email, password, hardwareInfo.Hash);
var stream = new MemoryStream();
await encryptedStream.DecryptTo(stream, key, cancellationToken);
return stream;
}
}
@@ -4,7 +4,7 @@ using System.Security;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
namespace Azaion.Suite.Services; namespace Azaion.Common.Services;
public static class Security public static class Security
{ {
@@ -1,6 +1,7 @@
using System.IO; using System.IO;
using System.Windows.Input; using System.Windows.Input;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using MediatR; using MediatR;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
+8
View File
@@ -0,0 +1,8 @@
<Application x:Class="Azaion.Launcher.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="Loader.xaml">
<Application.Resources>
</Application.Resources>
</Application>
+3
View File
@@ -0,0 +1,3 @@
namespace Azaion.Launcher;
public partial class App;
+10
View File
@@ -0,0 +1,10 @@
using System.Windows;
[assembly: ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
+19
View File
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<UseWPF>true</UseWPF>
</PropertyGroup>
<ItemGroup>
<Page Update="Loader.xaml">
<Generator>MSBuild:Compile</Generator>
<XamlRuntime>Wpf</XamlRuntime>
<SubType>Designer</SubType>
</Page>
</ItemGroup>
</Project>
@@ -1,4 +1,4 @@
<Window x:Class="Azaion.Suite.Loader" <Window x:Class="Azaion.Launcher.Loader"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+30
View File
@@ -0,0 +1,30 @@
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Azaion.Launcher;
public partial class Loader : Window
{
public Loader()
{
InitializeComponent();
}
private void RunClick(object sender, RoutedEventArgs e)
{
Process.Start("Azaion.Suite.exe", $"-e {TbEmail.Text} -p {TbPassword.Password}");
}
private void CloseClick(object sender, RoutedEventArgs e) => Close();
private void MainMouseMove(object sender, MouseEventArgs e)
{
if (e.OriginalSource is Button || e.OriginalSource is TextBox)
return;
if (e.LeftButton == MouseButtonState.Pressed)
DragMove();
}
}
@@ -10,6 +10,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Common", "Azaion.Com
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Azaion.Dataset\Azaion.Dataset.csproj", "{01A5CA37-A62E-4EF3-8678-D72CD9525677}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Dataset", "Azaion.Dataset\Azaion.Dataset.csproj", "{01A5CA37-A62E-4EF3-8678-D72CD9525677}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azaion.Launcher", "Azaion.Launcher\Azaion.Launcher.csproj", "{00CC9AFE-2952-4943-BCBA-976AE03DE841}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -36,5 +38,9 @@ Global
{01A5CA37-A62E-4EF3-8678-D72CD9525677}.Debug|Any CPU.Build.0 = Debug|Any CPU {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.ActiveCfg = Release|Any CPU {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.Build.0 = Release|Any CPU {01A5CA37-A62E-4EF3-8678-D72CD9525677}.Release|Any CPU.Build.0 = Release|Any CPU
{00CC9AFE-2952-4943-BCBA-976AE03DE841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00CC9AFE-2952-4943-BCBA-976AE03DE841}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00CC9AFE-2952-4943-BCBA-976AE03DE841}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00CC9AFE-2952-4943-BCBA-976AE03DE841}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+60 -9
View File
@@ -1,16 +1,21 @@
using System.Reflection; using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading; using System.Windows.Threading;
using Azaion.Annotator; using Azaion.Annotator;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services;
using Azaion.Suite.Services; using Azaion.Suite.Services;
using Azaion.Suite.Services.DTO;
using Azaion.Dataset; using Azaion.Dataset;
using Azaion.Suite.Services.DTO;
using CommandLine;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -18,16 +23,53 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Serilog; using Serilog;
namespace Azaion.Suite; namespace Azaion.Suite;
public partial class App : Application public partial class App
{ {
private readonly IHost _host; private readonly IHost _host;
private readonly ILogger<App> _logger; private readonly ILogger<App> _logger;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private static readonly List<string> EncryptedResources =
[
"Azaion.Annotator.dll",
"Azaion.Dataset.dll"
];
private static readonly IResourceLoader? ResourceLoader;
static App()
{
var result = Parser.Default.ParseArguments<SuiteCommandLineOptions>(Environment.GetCommandLineArgs());
if (result.Errors.Any())
return;
var configStr = File.ReadAllText(Constants.CONFIG_PATH);
var apiConfig = JsonConvert.DeserializeObject<AppConfig>(configStr)!.ApiConfig;
var api = new AzaionApiClient(new HttpClient
{
BaseAddress = new Uri(apiConfig.Url),
Timeout = TimeSpan.FromSeconds(apiConfig.TimeoutSeconds)
});
var email = result.Value.Email;
var password = result.Value.Password;
api.Login(email, password);
ResourceLoader = new ResourceLoader(email, password, api, new HardwareService());
foreach (var resource in EncryptedResources)
{
var stream = ResourceLoader.Load(resource).GetAwaiter().GetResult();
Assembly.Load(stream.ToArray());
}
new ConfigUpdater().CheckConfig();
}
public App() public App()
{ {
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
@@ -42,21 +84,30 @@ public partial class App : Application
_host = Host.CreateDefaultBuilder() _host = Host.CreateDefaultBuilder()
.ConfigureAppConfiguration((context, config) => config .ConfigureAppConfiguration((context, config) => config
.AddCommandLine(Environment.GetCommandLineArgs()) .AddCommandLine(Environment.GetCommandLineArgs())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)) .AddJsonFile(Constants.CONFIG_PATH, optional: true, reloadOnChange: true))
.ConfigureServices((context, services) => .ConfigureServices((context, services) =>
{ {
services.AddSingleton<Loader>(); services.AddSingleton<MainSuite>();
services.AddSingleton<IHardwareService, HardwareService>(); services.AddSingleton<IHardwareService, HardwareService>();
services.AddSingleton<IResourceLoader, ResourceLoader>(); services.AddSingleton<IResourceLoader>(ResourceLoader!);
services.Configure<AppConfig>(context.Configuration);
services.ConfigureSection<ApiConfig>(context.Configuration);
services.ConfigureSection<DirectoriesConfig>(context.Configuration);
services.ConfigureSection<AnnotationConfig>(context.Configuration);
services.ConfigureSection<WindowConfig>(context.Configuration);
services.ConfigureSection<AIRecognitionConfig>(context.Configuration);
services.ConfigureSection<ThumbnailConfig>(context.Configuration);
services.Configure<ApiConfig>(context.Configuration.GetSection(nameof(ApiConfig)));
services.AddSingleton<IConfigUpdater, ConfigUpdater>(); services.AddSingleton<IConfigUpdater, ConfigUpdater>();
services.AddSingleton<Annotator.Annotator>(); services.AddSingleton<Annotator.Annotator>();
services.AddSingleton<DatasetExplorer>(); services.AddSingleton<DatasetExplorer>();
services.AddSingleton<HelpWindow>(); services.AddSingleton<HelpWindow>();
services.AddSingleton<IAIDetector, YOLODetector>(); services.AddSingleton<IAIDetector, YOLODetector>();
services.AddMediatR(c => c.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddMediatR(c => c.RegisterServicesFromAssemblies(
typeof(Annotator.Annotator).Assembly,
typeof(DatasetExplorer).Assembly));
services.AddSingleton<LibVLC>(_ => new LibVLC()); services.AddSingleton<LibVLC>(_ => new LibVLC());
services.AddSingleton<FormState>(); services.AddSingleton<FormState>();
services.AddSingleton<MediaPlayer>(sp => services.AddSingleton<MediaPlayer>(sp =>
@@ -93,7 +144,7 @@ public partial class App : Application
{ {
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick)); EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick));
await _host.StartAsync(); await _host.StartAsync();
_host.Services.GetRequiredService<Loader>().Show(); _host.Services.GetRequiredService<MainSuite>().Show();
base.OnStartup(e); base.OnStartup(e);
} }
+4 -7
View File
@@ -10,6 +10,9 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="CommandLineParser" Version="2.9.1" />
<PackageReference Include="LibVLCSharp" Version="3.9.1" />
<PackageReference Include="LibVLCSharp.WPF" Version="3.9.1" />
<PackageReference Include="MediatR" Version="12.4.1" /> <PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="8.0.0" />
@@ -19,13 +22,7 @@
<PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" /> <PackageReference Include="Serilog.Settings.Configuration" Version="8.0.4" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" /> <PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
</ItemGroup> <PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.21" />
<ItemGroup>
<None Remove="appsettings.json" />
<Content Include="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
-29
View File
@@ -1,29 +0,0 @@
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Suite;
public class DynamicAssemblyLoader(IOptions<LocalFilesConfig> localFilesConfig) : AssemblyLoadContext
{
private static readonly Dictionary<string?, Assembly> LoadedAssemblies = new();
static DynamicAssemblyLoader()
{
LoadedAssemblies = Default.Assemblies.ToDictionary(a => a.GetName().Name, a => a);
}
protected override Assembly Load(AssemblyName assemblyName)
{
var assembly = LoadedAssemblies.GetValueOrDefault(assemblyName.Name);
if (assembly != null)
return assembly;
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var asm = Assembly.LoadFile(Path.Combine(currentLocation, localFilesConfig.Value.DllPath, $"{assemblyName.Name!}.dll"));
return asm;
}
}
-58
View File
@@ -1,58 +0,0 @@
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Suite;
public partial class Loader : Window
{
private readonly IResourceLoader _resourceLoader;
private readonly IOptions<LocalFilesConfig> _localFilesConfig;
public Loader(IResourceLoader resourceLoader, IOptions<LocalFilesConfig> localFilesConfig)
{
_resourceLoader = resourceLoader;
_localFilesConfig = localFilesConfig;
InitializeComponent();
}
private async void RunClick(object sender, RoutedEventArgs e)
{
var stream = new MemoryStream();
await _resourceLoader.LoadAnnotator(TbEmail.Text, TbPassword.Password, stream);
stream.Seek(0, SeekOrigin.Begin);
var loader = new AssemblyLoadContext("DynamicContext", isCollectible: true);
var annotatorAssembly = loader.LoadFromStream(stream);
var appType = annotatorAssembly.GetType("Azaion.Annotator.App");
var appInstance = Activator.CreateInstance(appType);
var runMethod = appType.GetMethod("Run", BindingFlags.Public | BindingFlags.Instance);
if (runMethod != null)
{
runMethod.Invoke(appInstance, null);
}
// var entryPoint = annotatorAssembly.EntryPoint;
// if (entryPoint == null)
// return;
//
// var o = annotatorAssembly.CreateInstance(entryPoint.Name);
// entryPoint.Invoke(o, null);
}
private void CloseClick(object sender, RoutedEventArgs e) => Close();
private void MainMouseMove(object sender, MouseEventArgs e)
{
if (e.OriginalSource is Button || e.OriginalSource is TextBox)
return;
if (e.LeftButton == MouseButtonState.Pressed)
DragMove();
}
}
+5 -2
View File
@@ -3,10 +3,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Azaion.Suite"
mc:Ignorable="d" mc:Ignorable="d"
Title="MainSuite" Height="450" Width="800"> Title="MainSuite" Height="450" Width="800">
<Grid> <Grid>
<TabControl Name="MainTabControl"
TabStripPlacement="Left"
Margin="0, 0, 0, 10"
Background="Black">
</TabControl>
</Grid> </Grid>
</Window> </Window>
+19 -2
View File
@@ -1,19 +1,25 @@
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using Azaion.Annotator.Extensions; using Azaion.Annotator.Extensions;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Dataset;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Azaion.Suite; namespace Azaion.Suite;
public partial class MainSuite : Window public partial class MainSuite
{ {
private readonly AppConfig _appConfig; private readonly AppConfig _appConfig;
private readonly IConfigUpdater _configUpdater; private readonly IConfigUpdater _configUpdater;
private readonly Annotator.Annotator _annotator;
private readonly DatasetExplorer _datasetExplorer;
public MainSuite(IOptions<AppConfig> appConfig, IConfigUpdater configUpdater) public MainSuite(IOptions<AppConfig> appConfig, IConfigUpdater configUpdater, Annotator.Annotator annotator, DatasetExplorer datasetExplorer)
{ {
_configUpdater = configUpdater; _configUpdater = configUpdater;
_annotator = annotator;
_datasetExplorer = datasetExplorer;
_appConfig = appConfig.Value; _appConfig = appConfig.Value;
InitializeComponent(); InitializeComponent();
Loaded += OnLoaded; Loaded += OnLoaded;
@@ -41,6 +47,17 @@ public partial class MainSuite : Window
if (_appConfig.WindowConfig.FullScreen) if (_appConfig.WindowConfig.FullScreen)
WindowState = WindowState.Maximized; WindowState = WindowState.Maximized;
MainTabControl.Items.Add(new TabItem
{
Header = "Annotator",
Content = _annotator.Content
});
MainTabControl.Items.Add(new TabItem
{
Header = "Dataset Explorer",
Content = _datasetExplorer.Content
});
} }
private async Task SaveUserSettings() private async Task SaveUserSettings()
-38
View File
@@ -1,38 +0,0 @@
using System.IO;
using System.Reflection;
using Azaion.Suite.Services;
using Azaion.Suite.Services.DTO;
using Microsoft.Extensions.Options;
namespace Azaion.Suite;
public interface IResourceLoader
{
Task LoadAnnotator(string email, string password, Stream outStream, CancellationToken cancellationToken = default);
Assembly LoadAssembly(string name, CancellationToken cancellationToken = default);
}
public class ResourceLoader(AzaionApiClient azaionApi, IHardwareService hardwareService, IOptions<LocalFilesConfig> localFilesConfig) : IResourceLoader
{
public async Task LoadAnnotator(string email, string password, Stream outStream, CancellationToken cancellationToken = default)
{
var hardwareInfo = await hardwareService.GetHardware();
azaionApi.Login(email, password);
var key = Security.MakeEncryptionKey(email, password, hardwareInfo.Hash);
var encryptedStream = await azaionApi.GetResource(password, hardwareInfo, ResourceEnum.AnnotatorDll);
await encryptedStream.DecryptTo(outStream, key, cancellationToken);
//return Assembly.Load(stream.ToArray());
}
public Assembly LoadAssembly(string name, CancellationToken cancellationToken = default)
{
var dllValues = name.Split(",");
var dllName = $"{dllValues[0]}.dll";
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var asm = Assembly.LoadFile(Path.Combine(currentLocation, localFilesConfig.Value.DllPath, dllName));
return asm;
}
}
@@ -1,9 +0,0 @@
namespace Azaion.Suite.Services.DTO;
public enum ResourceEnum
{
None = 0,
AnnotatorDll = 10,
AIModelRKNN = 20,
AIModelONNX = 30,
}
+12
View File
@@ -0,0 +1,12 @@
using CommandLine;
namespace Azaion.Suite.Services.DTO;
public class SuiteCommandLineOptions
{
[Option('e', "email", Required = true, HelpText = "The email for authorization.")]
public string Email { get; set; } = null!;
[Option('p', "password", Required = true, HelpText = "The password for authorization.")]
public string Password { get; set; } = null!;
}
-10
View File
@@ -1,10 +0,0 @@
{
"ApiConfig": {
"Url": "https://api.azaion.com",
"TimeoutSeconds": 20,
"RetryCount": 3
},
"LocalFilesConfig": {
"DllPath": "AzaionSuite"
}
}
+57
View File
@@ -0,0 +1,57 @@
{
"ApiConfig": {
"Url": "https://api.azaion.com",
"TimeoutSeconds": 20,
"RetryCount": 3
},
"DirectoriesConfig": {
"VideosDirectory" : "E:\\Azaion1\\Videos",
"LabelsDirectory" : "E:\\labels",
"ImagesDirectory" : "E:\\images",
"ResultsDirectory" : "E:\\results",
"ThumbnailsDirectory" : "E:\\thumbnails",
"DllCacheDirectory" : "Cache"
},
"AnnotationConfig" : {
"AnnotationClasses": [
{ "Id": 0, "Name": "Броньована техніка", "ShortName": "Бронь" },
{ "Id": 1, "Name": "Вантажівка", "ShortName": "Вантаж" },
{ "Id": 2, "Name": "Машина легкова", "ShortName": "Машина" },
{ "Id": 3, "Name": "Артилерія", "ShortName": "Арта" },
{ "Id": 4, "Name": "Тінь від техніки", "ShortName": "Тінь" },
{ "Id": 5, "Name": "Окопи", "ShortName": "Окопи" },
{ "Id": 6, "Name": "Військовий", "ShortName": "Військов" },
{ "Id": 7, "Name": "Накати", "ShortName": "Накати" },
{ "Id": 8, "Name": "Танк з захистом", "ShortName": "Танк захист" },
{ "Id": 9, "Name": "Дим", "ShortName": "Дим" },
{ "Id": 10, "Name": "Літак", "ShortName": "Літак" }
],
"LastSelectedExplorerClass": 1,
"VideoFormats": ["mov", "mp4"],
"ImageFormats": ["jpg", "jpeg", "png", "bmp", "gif"]
},
"WindowConfig": {
"WindowSize": "1920,1080",
"WindowLocation": "50,50",
"FullScreen": true,
"LeftPanelWidth": 220,
"RightPanelWidth": 220,
"ShowHelpOnStart": false
},
"AIRecognitionConfig": {
"FrameRecognitionSeconds" : 2,
"TrackingDistanceConfidence" : 0.15,
"TrackingProbabilityIncrease" : 15,
"TrackingIntersectionThreshold" : 0.8
},
"ThumbnailConfig": {
"Size" : "240,135",
"Border" : 10
}
}
+2 -1
View File
@@ -1,4 +1,5 @@
using Azaion.Suite; using Azaion.Common.Services;
using Azaion.Suite;
using Azaion.Suite.Services; using Azaion.Suite.Services;
using Xunit; using Xunit;