big refactoring. get rid of static properties and coupled architecture. prepare system for integration tests

This commit is contained in:
Oleksandr Bezdieniezhnykh
2025-11-17 13:14:05 +02:00
parent 22529c26ec
commit e7ea5a8ded
38 changed files with 808 additions and 157 deletions
+1 -1
View File
@@ -94,7 +94,7 @@ public class DetectionControl : Border
_detectionLabelPanel = new DetectionLabelPanel
{
Confidence = canvasLabel.Confidence,
DetectionClass = Annotation.DetectionClassesDict[canvasLabel.ClassNumber]
DetectionClass = detectionClass
};
DetectionLabelContainer.Children.Add(_detectionLabelPanel);
+7 -3
View File
@@ -7,10 +7,12 @@ using Azaion.Common.Extensions;
namespace Azaion.Common.DTO;
public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged
public class AnnotationThumbnail(Annotation annotation, bool isValidator, string imagePath, string thumbPath) : INotifyPropertyChanged
{
public Annotation Annotation { get; set; } = annotation;
public bool IsValidator { get; set; } = isValidator;
private readonly string _imagePath = imagePath;
private readonly string _thumbPath = thumbPath;
private BitmapImage? _thumbnail;
public BitmapImage? Thumbnail
@@ -18,7 +20,9 @@ public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INot
get
{
if (_thumbnail == null)
Task.Run(async () => Thumbnail = await Annotation.ThumbPath.OpenImage());
{
Task.Run(async () => Thumbnail = await _thumbPath.OpenImage());
}
return _thumbnail;
}
private set
@@ -28,7 +32,7 @@ public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INot
}
}
public string ImageName => Path.GetFileName(Annotation.ImagePath);
public string ImageName => Path.GetFileName(_imagePath);
public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
public string CreatedEmail => Annotation.CreatedEmail;
public bool IsSeed => IsValidator &&
+12 -16
View File
@@ -1,6 +1,7 @@
using System.IO;
using System.Text;
using Azaion.Common.Extensions;
using Azaion.Common.Services;
using Newtonsoft.Json;
namespace Azaion.Common.DTO.Config;
@@ -40,7 +41,16 @@ public interface IConfigUpdater
public class ConfigUpdater : IConfigUpdater
{
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
private readonly IConfigurationStore _configStore;
public ConfigUpdater(IConfigurationStore configStore)
{
_configStore = configStore;
}
public ConfigUpdater() : this(new FileConfigurationStore(Constants.CONFIG_PATH, new PhysicalFileSystem()))
{
}
public void CheckConfig()
{
@@ -55,20 +65,6 @@ public class ConfigUpdater : IConfigUpdater
public void Save(AppConfig config)
{
ThrottleExt.Throttle(async () =>
{
var publicConfig = new
{
config.LoaderClientConfig,
config.InferenceClientConfig,
config.GpsDeniedClientConfig,
config.DirectoriesConfig,
config.UIConfig,
config.CameraConfig
};
await File.WriteAllTextAsync(Constants.CONFIG_PATH, JsonConvert.SerializeObject(publicConfig, Formatting.Indented), Encoding.UTF8);
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
_ = _configStore.SaveAsync(config);
}
}
-39
View File
@@ -9,19 +9,6 @@ namespace Azaion.Common.Database;
[MessagePackObject]
public class Annotation
{
private static string _labelsDir = null!;
private static string _imagesDir = null!;
private static string _thumbDir = null!;
public static Dictionary<int, DetectionClass> DetectionClassesDict = null!;
public static void Init(DirectoriesConfig config, Dictionary<int, DetectionClass> detectionClassesDict)
{
_labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory;
DetectionClassesDict = detectionClassesDict;
}
[Key("n")] public string Name { get; set; } = null!;
[Key("hash")] public string MediaHash { get; set; } = null!;
[Key("mn")] public string OriginalMediaName { get; set; } = null!;
@@ -44,9 +31,6 @@ public class Annotation
#region Calculated
[IgnoreMember] public List<int> Classes => Detections.Select(x => x.ClassNumber).ToList();
[IgnoreMember] public string ImagePath => Path.Combine(_imagesDir, $"{Name}{ImageExtension}");
[IgnoreMember] public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
[IgnoreMember] public string ThumbPath => Path.Combine(_thumbDir, $"{Name}{Constants.THUMBNAIL_PREFIX}.jpg");
[IgnoreMember] public bool IsSplit => Name.Contains(Constants.SPLIT_SUFFIX);
private CanvasLabel? _splitTile;
@@ -73,30 +57,7 @@ public class Annotation
}
[IgnoreMember] public string TimeStr => $"{Time:h\\:mm\\:ss}";
private List<(Color Color, double Confidence)>? _colors;
[IgnoreMember] public List<(Color Color, double Confidence)> Colors => _colors ??= Detections
.Select(d => (DetectionClassesDict[d.ClassNumber].Color, d.Confidence))
.ToList();
private string? _className;
[IgnoreMember] public string ClassName
{
get
{
if (string.IsNullOrEmpty(_className))
{
var detectionClasses = Detections.Select(x => x.ClassNumber).Distinct().ToList();
_className = detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => DetectionClassesDict[x].UIName))
: DetectionClassesDict[detectionClasses.FirstOrDefault()].UIName;
}
return _className;
}
}
#endregion Calculated
public override string ToString() => $"Annotation: {Name}{TimeStr}: {ClassName}";
}
[MessagePackObject]
@@ -0,0 +1,48 @@
using LinqToDB;
namespace Azaion.Common.Database;
public class AnnotationRepository : IAnnotationRepository
{
private readonly IDbFactory _dbFactory;
public AnnotationRepository(IDbFactory dbFactory)
{
_dbFactory = dbFactory;
}
public async Task<List<Annotation>> GetByMediaHashAsync(string hash, CancellationToken ct = default)
{
return await _dbFactory.Run(async db =>
await db.GetTable<Annotation>()
.Where(x => x.MediaHash == hash)
.ToListAsync(ct));
}
public async Task<Annotation?> GetByNameAsync(string name, CancellationToken ct = default)
{
return await _dbFactory.Run(async db =>
await db.GetTable<Annotation>()
.FirstOrDefaultAsync(x => x.Name == name, ct));
}
public async Task<List<Annotation>> GetAllAsync(CancellationToken ct = default)
{
return await _dbFactory.Run(async db =>
await db.GetTable<Annotation>().ToListAsync(ct));
}
public async Task SaveAsync(Annotation annotation, CancellationToken ct = default)
{
await _dbFactory.RunWrite(async db =>
{
await db.InsertOrReplaceAsync(annotation, token: ct);
});
}
public async Task DeleteAsync(List<string> names, CancellationToken ct = default)
{
await _dbFactory.DeleteAnnotations(names, ct);
}
}
@@ -24,11 +24,6 @@ public static class AnnotationsDbSchemaHolder
annotationBuilder
.Ignore(x => x.Milliseconds)
.Ignore(x => x.Classes)
.Ignore(x => x.ImagePath)
.Ignore(x => x.LabelPath)
.Ignore(x => x.ThumbPath)
.Ignore(x => x.ClassName)
.Ignore(x => x.Colors)
.Ignore(x => x.SplitTile)
.Ignore(x => x.IsSplit)
.Ignore(x => x.TimeStr);
@@ -0,0 +1,11 @@
namespace Azaion.Common.Database;
public interface IAnnotationRepository
{
Task<List<Annotation>> GetByMediaHashAsync(string hash, CancellationToken ct = default);
Task<Annotation?> GetByNameAsync(string name, CancellationToken ct = default);
Task<List<Annotation>> GetAllAsync(CancellationToken ct = default);
Task SaveAsync(Annotation annotation, CancellationToken ct = default);
Task DeleteAsync(List<string> names, CancellationToken ct = default);
}
@@ -0,0 +1,45 @@
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Services;
using Azaion.Common.Services.Inference;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Infrastructure;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddAzaionInfrastructure(this IServiceCollection services)
{
services.AddSingleton<IFileSystem, PhysicalFileSystem>();
services.AddSingleton<IProcessLauncher, ProcessLauncher>();
services.AddSingleton<IAnnotationPathResolver>(sp =>
new AnnotationPathResolver(sp.GetRequiredService<IOptions<DirectoriesConfig>>().Value));
services.AddSingleton<IDetectionClassProvider, DetectionClassProvider>();
services.AddSingleton<IAnnotationRepository, AnnotationRepository>();
return services;
}
public static IServiceCollection AddAzaionConfiguration(this IServiceCollection services)
{
services.AddSingleton<IConfigurationStore>(sp =>
new FileConfigurationStore(Constants.CONFIG_PATH, sp.GetRequiredService<IFileSystem>()));
services.AddSingleton<IConfigUpdater, ConfigUpdater>();
return services;
}
public static IServiceCollection AddAzaionServices(this IServiceCollection services)
{
services.AddSingleton<IDbFactory, DbFactory>();
services.AddSingleton<FailsafeAnnotationsProducer>();
services.AddSingleton<IAnnotationService, AnnotationService>();
services.AddSingleton<IGalleryService, GalleryService>();
services.AddSingleton<IInferenceService, InferenceService>();
return services;
}
}
@@ -0,0 +1,29 @@
using System.IO;
using Azaion.Common.Database;
using Azaion.Common.DTO;
namespace Azaion.Common.Services;
public class AnnotationPathResolver : IAnnotationPathResolver
{
private readonly string _labelsDir;
private readonly string _imagesDir;
private readonly string _thumbDir;
public AnnotationPathResolver(DirectoriesConfig config)
{
_labelsDir = config.LabelsDirectory;
_imagesDir = config.ImagesDirectory;
_thumbDir = config.ThumbnailsDirectory;
}
public string GetImagePath(Annotation annotation) =>
Path.Combine(_imagesDir, $"{annotation.Name}{annotation.ImageExtension}");
public string GetLabelPath(Annotation annotation) =>
Path.Combine(_labelsDir, $"{annotation.Name}.txt");
public string GetThumbPath(Annotation annotation) =>
Path.Combine(_thumbDir, $"{annotation.Name}{Constants.THUMBNAIL_PREFIX}.jpg");
}
+18 -7
View File
@@ -33,6 +33,8 @@ public class AnnotationService : IAnnotationService
private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!;
private readonly UIConfig _uiConfig;
private readonly IAnnotationPathResolver _pathResolver;
private readonly IFileSystem _fileSystem;
private readonly SemaphoreSlim _imageAccessSemaphore = new(1, 1);
private readonly SemaphoreSlim _messageProcessingSemaphore = new(1, 1);
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
@@ -46,7 +48,9 @@ public class AnnotationService : IAnnotationService
IGalleryService galleryService,
IMediator mediator,
IAzaionApi api,
ILogger<AnnotationService> logger)
ILogger<AnnotationService> logger,
IAnnotationPathResolver pathResolver,
IFileSystem fileSystem)
{
_dbFactory = dbFactory;
_producer = producer;
@@ -56,8 +60,13 @@ public class AnnotationService : IAnnotationService
_logger = logger;
_queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
_pathResolver = pathResolver;
_fileSystem = fileSystem;
}
Task.Run(async () => await InitQueueConsumer()).Wait();
public async Task StartQueueConsumerAsync(CancellationToken token = default)
{
await InitQueueConsumer(token);
}
private async Task InitQueueConsumer(CancellationToken token = default)
@@ -221,13 +230,14 @@ public class AnnotationService : IAnnotationService
Image image = null!;
if (stream != null)
{
var imagePath = _pathResolver.GetImagePath(annotation);
image = Image.FromStream(stream);
if (File.Exists(annotation.ImagePath))
ResilienceExt.WithRetry(() => File.Delete(annotation.ImagePath));
image.Save(annotation.ImagePath, ImageFormat.Jpeg);
if (_fileSystem.FileExists(imagePath))
ResilienceExt.WithRetry(() => _fileSystem.DeleteFile(imagePath));
image.Save(imagePath, ImageFormat.Jpeg);
}
await YoloLabel.WriteToFile(detections, annotation.LabelPath, token);
await YoloLabel.WriteToFile(detections, _pathResolver.GetLabelPath(annotation), token);
await _galleryService.CreateThumbnail(annotation, image, token);
if (_uiConfig.GenerateAnnotatedImage)
@@ -235,7 +245,7 @@ public class AnnotationService : IAnnotationService
}
catch (Exception e)
{
_logger.LogError(e, $"Try to save {annotation.ImagePath}, Error: {e.Message}");
_logger.LogError(e, $"Try to save {_pathResolver.GetImagePath(annotation)}, Error: {e.Message}");
throw;
}
finally
@@ -274,6 +284,7 @@ public class AnnotationService : IAnnotationService
public interface IAnnotationService
{
Task StartQueueConsumerAsync(CancellationToken token = default);
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
Task<Annotation> SaveAnnotation(string mediaHash, string originalMediaName, string annotationName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
@@ -0,0 +1,35 @@
using System.Windows.Media;
using Azaion.Common.Database;
using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Microsoft.Extensions.Options;
namespace Azaion.Common.Services;
public class DetectionClassProvider : IDetectionClassProvider
{
private readonly Dictionary<int, DetectionClass> _detectionClasses;
public DetectionClassProvider(IOptions<AnnotationConfig> annotationConfig)
{
_detectionClasses = annotationConfig.Value.DetectionClassesDict;
}
public Dictionary<int, DetectionClass> GetDetectionClasses() => _detectionClasses;
public List<(Color Color, double Confidence)> GetColors(Annotation annotation)
{
return annotation.Detections
.Select(d => (_detectionClasses[d.ClassNumber].Color, d.Confidence))
.ToList();
}
public string GetClassName(Annotation annotation)
{
var detectionClasses = annotation.Detections.Select(x => x.ClassNumber).Distinct().ToList();
return detectionClasses.Count > 1
? string.Join(", ", detectionClasses.Select(x => _detectionClasses[x].UIName))
: _detectionClasses[detectionClasses.FirstOrDefault()].UIName;
}
}
+14 -3
View File
@@ -23,6 +23,8 @@ public class FailsafeAnnotationsProducer
private readonly IAzaionApi _azaionApi;
private readonly QueueConfig _queueConfig;
private readonly UIConfig _uiConfig;
private readonly IAnnotationPathResolver _pathResolver;
private readonly IFileSystem _fileSystem;
private Producer _annotationProducer = null!;
@@ -31,14 +33,23 @@ public class FailsafeAnnotationsProducer
IDbFactory dbFactory,
IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig,
IAzaionApi azaionApi)
IAzaionApi azaionApi,
IAnnotationPathResolver pathResolver,
IFileSystem fileSystem)
{
_logger = logger;
_dbFactory = dbFactory;
_azaionApi = azaionApi;
_queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await ProcessQueue());
_pathResolver = pathResolver;
_fileSystem = fileSystem;
}
public async Task StartAsync(CancellationToken ct = default)
{
_ = Task.Run(async () => await ProcessQueue(ct), ct);
await Task.CompletedTask;
}
private async Task<StreamSystem> GetProducerQueueConfig()
@@ -104,7 +115,7 @@ public class FailsafeAnnotationsProducer
continue;
var image = record.Operation == AnnotationStatus.Created
? await File.ReadAllBytesAsync(annotation.ImagePath, ct)
? await _fileSystem.ReadAllBytesAsync(_pathResolver.GetImagePath(annotation), ct)
: null;
var annMessage = new AnnotationMessage
@@ -0,0 +1,49 @@
using System.Text;
using Azaion.Common.DTO.Config;
using Azaion.Common.Extensions;
using Newtonsoft.Json;
namespace Azaion.Common.Services;
public class FileConfigurationStore : IConfigurationStore
{
private readonly string _configPath;
private readonly IFileSystem _fileSystem;
private static readonly Guid SaveConfigTaskId = Guid.NewGuid();
public FileConfigurationStore(string configPath, IFileSystem fileSystem)
{
_configPath = configPath;
_fileSystem = fileSystem;
}
public async Task<AppConfig> LoadAsync(CancellationToken ct = default)
{
if (!_fileSystem.FileExists(_configPath))
return new AppConfig();
var json = Encoding.UTF8.GetString(await _fileSystem.ReadAllBytesAsync(_configPath, ct));
return JsonConvert.DeserializeObject<AppConfig>(json) ?? new AppConfig();
}
public Task SaveAsync(AppConfig config, CancellationToken ct = default)
{
ThrottleExt.Throttle(async () =>
{
var publicConfig = new
{
config.LoaderClientConfig,
config.InferenceClientConfig,
config.GpsDeniedClientConfig,
config.DirectoriesConfig,
config.UIConfig,
config.CameraConfig
};
var json = JsonConvert.SerializeObject(publicConfig, Formatting.Indented);
await _fileSystem.WriteAllBytesAsync(_configPath, Encoding.UTF8.GetBytes(json), ct);
}, SaveConfigTaskId, TimeSpan.FromSeconds(5));
return Task.CompletedTask;
}
}
+19 -13
View File
@@ -26,11 +26,15 @@ public class GalleryService(
IOptions<ThumbnailConfig> thumbnailConfig,
IOptions<AnnotationConfig> annotationConfig,
ILogger<GalleryService> logger,
IDbFactory dbFactory) : IGalleryService
IDbFactory dbFactory,
IAnnotationPathResolver pathResolver,
IFileSystem fileSystem) : IGalleryService
{
private readonly DirectoriesConfig _dirConfig = directoriesConfig.Value;
private readonly ThumbnailConfig _thumbnailConfig = thumbnailConfig.Value;
private readonly AnnotationConfig _annotationConfig = annotationConfig.Value;
private readonly IAnnotationPathResolver _pathResolver = pathResolver;
private readonly IFileSystem _fileSystem = fileSystem;
public event ThumbnailsUpdatedEventHandler? ThumbnailsUpdate;
@@ -58,8 +62,9 @@ public class GalleryService(
public async Task ClearThumbnails(CancellationToken cancellationToken = default)
{
foreach(var file in new DirectoryInfo(_dirConfig.ThumbnailsDirectory).GetFiles())
file.Delete();
var thumbDir = _fileSystem.GetDirectoryInfo(_dirConfig.ThumbnailsDirectory);
foreach(var file in thumbDir.GetFiles())
_fileSystem.DeleteFile(file.FullName);
await dbFactory.RunWrite(async db =>
{
await db.Detections.DeleteAsync(x => true, token: cancellationToken);
@@ -83,7 +88,8 @@ public class GalleryService(
.Select(gr => gr.Key)
.ToHashSet();
var files = new DirectoryInfo(_dirConfig.ImagesDirectory).GetFiles();
var imagesDir = _fileSystem.GetDirectoryInfo(_dirConfig.ImagesDirectory);
var files = imagesDir.GetFiles();
var imagesCount = files.Length;
await ParallelExt.ForEachAsync(files, async (file, cancellationToken) =>
@@ -92,9 +98,9 @@ public class GalleryService(
try
{
var labelName = Path.Combine(_dirConfig.LabelsDirectory, $"{fName}.txt");
if (!File.Exists(labelName))
if (!_fileSystem.FileExists(labelName))
{
File.Delete(file.FullName);
_fileSystem.DeleteFile(file.FullName);
logger.LogInformation($"No labels found for image {file.FullName}! Image deleted!");
await dbFactory.DeleteAnnotations([fName], cancellationToken);
return;
@@ -213,7 +219,7 @@ public class GalleryService(
var width = (int)_thumbnailConfig.Size.Width;
var height = (int)_thumbnailConfig.Size.Height;
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken)));
originalImage ??= Image.FromStream(new MemoryStream(await _fileSystem.ReadAllBytesAsync(_pathResolver.GetImagePath(annotation), cancellationToken)));
var bitmap = new Bitmap(width, height);
@@ -273,7 +279,7 @@ public class GalleryService(
g.DrawRectangle(new Pen(brush, width: 3), (float)((label.Left - frameX) / scale), (float)((label.Top - frameY) / scale), (float)(label.Width / scale), (float)(label.Height / scale));
}
bitmap.Save(annotation.ThumbPath, ImageFormat.Jpeg);
bitmap.Save(_pathResolver.GetThumbPath(annotation), ImageFormat.Jpeg);
}
catch (Exception e)
{
@@ -282,7 +288,7 @@ public class GalleryService(
}
public async Task CreateAnnotatedImage(Annotation annotation, Image? originalImage = null, CancellationToken token = default)
{
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(annotation.ImagePath, token)));
originalImage ??= Image.FromStream(new MemoryStream(await File.ReadAllBytesAsync(_pathResolver.GetImagePath(annotation), token)));
using var g = Graphics.FromImage(originalImage);
foreach (var detection in annotation.Detections)
@@ -297,11 +303,11 @@ public class GalleryService(
g.DrawTextBox(label, new PointF((float)(det.Left + det.Width / 2.0), (float)(det.Top - 24)), brush, Brushes.Black);
}
var imagePath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
if (File.Exists(imagePath))
ResilienceExt.WithRetry(() => File.Delete(imagePath));
var resultPath = Path.Combine(_dirConfig.ResultsDirectory, $"{annotation.Name}{Constants.RESULT_PREFIX}.jpg");
if (_fileSystem.FileExists(resultPath))
ResilienceExt.WithRetry(() => _fileSystem.DeleteFile(resultPath));
originalImage.Save(imagePath, ImageFormat.Jpeg);
originalImage.Save(resultPath, ImageFormat.Jpeg);
}
}
@@ -34,27 +34,24 @@ public class GpsMatcherClient : IGpsMatcherClient
{
private readonly IMediator _mediator;
private readonly ILogger<GpsMatcherClient> _logger;
private readonly IProcessLauncher _processLauncher;
private readonly string _requestAddress;
private readonly RequestSocket _requestSocket = new();
private readonly string _subscriberAddress;
private readonly SubscriberSocket _subscriberSocket = new();
private readonly NetMQPoller _poller = new();
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger)
public GpsMatcherClient(IMediator mediator, IOptions<GpsDeniedClientConfig> gpsConfig, ILogger<GpsMatcherClient> logger, IProcessLauncher processLauncher)
{
_mediator = mediator;
_logger = logger;
_processLauncher = processLauncher;
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.ExternalGpsDeniedPath,
Arguments = $"zeromq --rep {gpsConfig.Value.ZeroMqPort} --pub {gpsConfig.Value.ZeroMqReceiverPort}",
WorkingDirectory = Constants.EXTERNAL_GPS_DENIED_FOLDER,
CreateNoWindow = true
};
process.Start();
_processLauncher.Launch(
Constants.ExternalGpsDeniedPath,
$"zeromq --rep {gpsConfig.Value.ZeroMqPort} --pub {gpsConfig.Value.ZeroMqReceiverPort}",
Constants.EXTERNAL_GPS_DENIED_FOLDER);
}
catch (Exception e)
{
@@ -0,0 +1,11 @@
using Azaion.Common.Database;
namespace Azaion.Common.Services;
public interface IAnnotationPathResolver
{
string GetImagePath(Annotation annotation);
string GetLabelPath(Annotation annotation);
string GetThumbPath(Annotation annotation);
}
@@ -0,0 +1,10 @@
using Azaion.Common.DTO.Config;
namespace Azaion.Common.Services;
public interface IConfigurationStore
{
Task<AppConfig> LoadAsync(CancellationToken ct = default);
Task SaveAsync(AppConfig config, CancellationToken ct = default);
}
@@ -0,0 +1,13 @@
using System.Windows.Media;
using Azaion.Common.Database;
using Azaion.Common.DTO;
namespace Azaion.Common.Services;
public interface IDetectionClassProvider
{
Dictionary<int, DetectionClass> GetDetectionClasses();
List<(Color Color, double Confidence)> GetColors(Annotation annotation);
string GetClassName(Annotation annotation);
}
+17
View File
@@ -0,0 +1,17 @@
using System.IO;
namespace Azaion.Common.Services;
public interface IFileSystem
{
Task<byte[]> ReadAllBytesAsync(string path, CancellationToken ct = default);
Task WriteAllBytesAsync(string path, byte[] content, CancellationToken ct = default);
bool FileExists(string path);
void DeleteFile(string path);
IEnumerable<string> GetFiles(string directory, string searchPattern);
IEnumerable<FileInfo> GetFileInfos(string directory, string[] searchPatterns);
DirectoryInfo GetDirectoryInfo(string path);
bool DirectoryExists(string path);
void CreateDirectory(string path);
}
@@ -0,0 +1,22 @@
namespace Azaion.Common.Services;
public interface IMediaPlayerService
{
void Play();
void Pause();
void Stop();
long Time { get; set; }
float Position { get; set; }
int Volume { get; set; }
bool IsPlaying { get; }
long Length { get; }
void SetMedia(string mediaPath);
void TakeSnapshot(uint num, string path, uint width, uint height);
event EventHandler? Playing;
event EventHandler? Paused;
event EventHandler? Stopped;
event EventHandler? PositionChanged;
event EventHandler? LengthChanged;
}
@@ -0,0 +1,7 @@
namespace Azaion.Common.Services;
public interface IProcessLauncher
{
void Launch(string fileName, string arguments, string? workingDirectory = null);
}
@@ -0,0 +1,9 @@
namespace Azaion.Common.Services;
public interface IUICommandDispatcher
{
Task ExecuteAsync(Action action);
Task<T> ExecuteAsync<T>(Func<T> func);
void Execute(Action action);
}
@@ -20,6 +20,8 @@ public interface IInferenceClient : IDisposable
public class InferenceClient : IInferenceClient
{
private readonly ILogger<InferenceClient> _logger;
private readonly IProcessLauncher _processLauncher;
private readonly IDetectionClassProvider _classProvider;
private readonly DealerSocket _dealer = new();
private readonly NetMQPoller _poller = new();
@@ -30,12 +32,16 @@ public class InferenceClient : IInferenceClient
public InferenceClient(ILogger<InferenceClient> logger, IOptions<InferenceClientConfig> inferenceConfig,
IMediator mediator,
IOptions<LoaderClientConfig> loaderConfig)
IOptions<LoaderClientConfig> loaderConfig,
IProcessLauncher processLauncher,
IDetectionClassProvider classProvider)
{
_logger = logger;
_inferenceClientConfig = inferenceConfig.Value;
_loaderClientConfig = loaderConfig.Value;
_mediator = mediator;
_processLauncher = processLauncher;
_classProvider = classProvider;
Start();
}
@@ -43,14 +49,9 @@ public class InferenceClient : IInferenceClient
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.EXTERNAL_INFERENCE_PATH,
Arguments = $"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}",
CreateNoWindow = true
};
process.Start();
_processLauncher.Launch(
Constants.EXTERNAL_INFERENCE_PATH,
$"-p {_inferenceClientConfig.ZeroMqPort} -lp {_loaderClientConfig.ZeroMqPort} -a {_inferenceClientConfig.ApiUrl}");
}
catch (Exception e)
{
@@ -77,7 +78,9 @@ public class InferenceClient : IInferenceClient
{
case CommandType.InferenceData:
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(remoteCommand.Data, cancellationToken: ct);
_logger.LogInformation("Received command: {AnnotationImage}", annotationImage.ToString());
var className = _classProvider.GetClassName(annotationImage);
_logger.LogInformation("Received command: Annotation {Name} {Time} - {ClassName}",
annotationImage.Name, annotationImage.TimeStr, className);
await _mediator.Publish(new InferenceDataEvent(annotationImage), ct);
break;
case CommandType.InferenceStatus:
@@ -7,6 +7,7 @@ namespace Azaion.Common.Services.Inference;
public interface IInferenceService
{
Task StartAsync();
Task RunInference(List<string> mediaPaths, CameraConfig cameraConfig, CancellationToken ct = default);
CancellationTokenSource InferenceCancelTokenSource { get; set; }
CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; }
@@ -28,12 +29,17 @@ public class InferenceService : IInferenceService
_client = client;
_azaionApi = azaionApi;
_aiConfigOptions = aiConfigOptions;
_ = Task.Run(async () => await CheckAIAvailabilityStatus());
}
public CancellationTokenSource InferenceCancelTokenSource { get; set; } = new();
public CancellationTokenSource CheckAIAvailabilityTokenSource { get; set; } = new();
public async Task StartAsync()
{
_ = Task.Run(async () => await CheckAIAvailabilityStatus());
await Task.CompletedTask;
}
private async Task CheckAIAvailabilityStatus()
{
CheckAIAvailabilityTokenSource = new CancellationTokenSource();
+21 -14
View File
@@ -10,27 +10,34 @@ using Exception = System.Exception;
namespace Azaion.Common.Services;
public class LoaderClient(LoaderClientConfig config, ILogger logger, CancellationToken ct = default) : IDisposable
public class LoaderClient : IDisposable
{
private readonly LoaderClientConfig _config;
private readonly ILogger _logger;
private readonly CancellationToken _ct;
private readonly IProcessLauncher _processLauncher;
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
public LoaderClient(LoaderClientConfig config, ILogger logger, IProcessLauncher processLauncher, CancellationToken ct = default)
{
_config = config;
_logger = logger;
_processLauncher = processLauncher;
_ct = ct;
}
public void StartClient()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = Constants.EXTERNAL_LOADER_PATH,
Arguments = $"--port {config.ZeroMqPort} --api {config.ApiUrl}",
CreateNoWindow = true
};
process.Start();
_processLauncher.Launch(
Constants.EXTERNAL_LOADER_PATH,
$"--port {_config.ZeroMqPort} --api {_config.ApiUrl}");
}
catch (Exception e)
{
logger.Error(e, e.Message);
_logger.Error(e, e.Message);
throw;
}
}
@@ -38,7 +45,7 @@ public class LoaderClient(LoaderClientConfig config, ILogger logger, Cancellatio
public void Connect()
{
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{config.ZeroMqHost}:{config.ZeroMqPort}");
_dealer.Connect($"tcp://{_config.ZeroMqHost}:{_config.ZeroMqPort}");
}
public void Login(ApiCredentials credentials)
@@ -63,11 +70,11 @@ public class LoaderClient(LoaderClientConfig config, ILogger logger, Cancellatio
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum++ < retryCount)
while (!_ct.IsCancellationRequested && tryNum++ < retryCount)
{
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromMilliseconds(retryDelayMs), out var bytes))
continue;
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
var res = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: _ct);
if (res.CommandType == CommandType.Error)
throw new Exception(res.Message);
return res;
@@ -77,7 +84,7 @@ public class LoaderClient(LoaderClientConfig config, ILogger logger, Cancellatio
}
catch (Exception e)
{
logger.Error(e, e.Message);
_logger.Error(e, e.Message);
throw;
}
}
@@ -0,0 +1,9 @@
namespace Azaion.Common.Services;
public class NoOpProcessLauncher : IProcessLauncher
{
public void Launch(string fileName, string arguments, string? workingDirectory = null)
{
}
}
@@ -0,0 +1,56 @@
using System.IO;
namespace Azaion.Common.Services;
public class PhysicalFileSystem : IFileSystem
{
public Task<byte[]> ReadAllBytesAsync(string path, CancellationToken ct = default)
{
return File.ReadAllBytesAsync(path, ct);
}
public Task WriteAllBytesAsync(string path, byte[] content, CancellationToken ct = default)
{
return File.WriteAllBytesAsync(path, content, ct);
}
public bool FileExists(string path)
{
return File.Exists(path);
}
public void DeleteFile(string path)
{
File.Delete(path);
}
public IEnumerable<string> GetFiles(string directory, string searchPattern)
{
return Directory.GetFiles(directory, searchPattern);
}
public IEnumerable<FileInfo> GetFileInfos(string directory, string[] searchPatterns)
{
var dir = new DirectoryInfo(directory);
if (!dir.Exists)
return Enumerable.Empty<FileInfo>();
return searchPatterns.SelectMany(pattern => dir.GetFiles(pattern));
}
public DirectoryInfo GetDirectoryInfo(string path)
{
return new DirectoryInfo(path);
}
public bool DirectoryExists(string path)
{
return Directory.Exists(path);
}
public void CreateDirectory(string path)
{
Directory.CreateDirectory(path);
}
}
+20
View File
@@ -0,0 +1,20 @@
using System.Diagnostics;
namespace Azaion.Common.Services;
public class ProcessLauncher : IProcessLauncher
{
public void Launch(string fileName, string arguments, string? workingDirectory = null)
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
WorkingDirectory = workingDirectory ?? "",
CreateNoWindow = true
};
process.Start();
}
}
@@ -0,0 +1,29 @@
using System.Windows.Threading;
namespace Azaion.Common.Services;
public class WpfUICommandDispatcher : IUICommandDispatcher
{
private readonly Dispatcher _dispatcher;
public WpfUICommandDispatcher(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public Task ExecuteAsync(Action action)
{
return _dispatcher.InvokeAsync(action).Task;
}
public Task<T> ExecuteAsync<T>(Func<T> func)
{
return _dispatcher.InvokeAsync(func).Task;
}
public void Execute(Action action)
{
_dispatcher.Invoke(action);
}
}