mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 18:26:31 +00:00
big refactoring. get rid of static properties and coupled architecture. prepare system for integration tests
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user