Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
dzaitsev
2025-05-21 14:13:06 +03:00
57 changed files with 996 additions and 633 deletions
+3
View File
@@ -1,6 +1,9 @@
.idea .idea
bin bin
obj obj
*.dll
*.exe
*.log
.vs .vs
*.DotSettings* *.DotSettings*
*.user *.user
+1 -1
View File
@@ -500,7 +500,7 @@
Padding="2" Width="25" Padding="2" Width="25"
Height="25" Height="25"
ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black" ToolTip="Розпізнати за допомогою AI. Клавіша: [R]" Background="Black" BorderBrush="Black"
Click="AutoDetect"> Click="AIDetectBtn_OnClick">
<Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477 <Path Stretch="Fill" Fill="LightGray" Data="M144.317 85.269h223.368c15.381 0 29.391 6.325 39.567 16.494l.025-.024c10.163 10.164 16.477 24.193 16.477
39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4 39.599v189.728c0 15.401-6.326 29.425-16.485 39.584-10.159 10.159-24.183 16.484-39.584 16.484H144.317c-15.4
0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139 0-29.437-6.313-39.601-16.476-10.152-10.152-16.47-24.167-16.47-39.592V141.338c0-15.374 6.306-29.379 16.463-39.558l.078-.078c10.178-10.139
+60 -109
View File
@@ -14,6 +14,7 @@ using Azaion.Common.DTO.Config;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO.Commands;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.WindowsAPICodePack.Dialogs; using Microsoft.WindowsAPICodePack.Dialogs;
@@ -37,9 +38,9 @@ public partial class Annotator
private readonly IConfigUpdater _configUpdater; private readonly IConfigUpdater _configUpdater;
private readonly HelpWindow _helpWindow; private readonly HelpWindow _helpWindow;
private readonly ILogger<Annotator> _logger; private readonly ILogger<Annotator> _logger;
private readonly AnnotationService _annotationService;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly IInferenceService _inferenceService; private readonly IInferenceService _inferenceService;
private readonly IInferenceClient _inferenceClient;
private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new(); private ObservableCollection<DetectionClass> AnnotationClasses { get; set; } = new();
private bool _suspendLayout; private bool _suspendLayout;
@@ -47,7 +48,6 @@ public partial class Annotator
public readonly CancellationTokenSource MainCancellationSource = new(); public readonly CancellationTokenSource MainCancellationSource = new();
public CancellationTokenSource DetectionCancellationSource = new(); public CancellationTokenSource DetectionCancellationSource = new();
public bool FollowAI = false;
public bool IsInferenceNow = false; public bool IsInferenceNow = false;
private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50); private readonly TimeSpan _thresholdBefore = TimeSpan.FromMilliseconds(50);
@@ -57,6 +57,7 @@ public partial class Annotator
public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new(); public ObservableCollection<MediaFileInfo> AllMediaFiles { get; set; } = new();
public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new(); public ObservableCollection<MediaFileInfo> FilteredMediaFiles { get; set; } = new();
public Dictionary<string, MediaFileInfo> MediaFilesDict = new();
public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new(); public IntervalTree<TimeSpan, Annotation> TimedAnnotations { get; set; } = new();
@@ -69,9 +70,9 @@ public partial class Annotator
FormState formState, FormState formState,
HelpWindow helpWindow, HelpWindow helpWindow,
ILogger<Annotator> logger, ILogger<Annotator> logger,
AnnotationService annotationService,
IDbFactory dbFactory, IDbFactory dbFactory,
IInferenceService inferenceService, IInferenceService inferenceService,
IInferenceClient inferenceClient,
IGpsMatcherService gpsMatcherService) IGpsMatcherService gpsMatcherService)
{ {
InitializeComponent(); InitializeComponent();
@@ -84,9 +85,9 @@ public partial class Annotator
_formState = formState; _formState = formState;
_helpWindow = helpWindow; _helpWindow = helpWindow;
_logger = logger; _logger = logger;
_annotationService = annotationService;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_inferenceService = inferenceService; _inferenceService = inferenceService;
_inferenceClient = inferenceClient;
_gpsMatcherService = gpsMatcherService; _gpsMatcherService = gpsMatcherService;
Loaded += OnLoaded; Loaded += OnLoaded;
@@ -107,6 +108,28 @@ public partial class Annotator
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
} }
}; };
_inferenceClient.AIAvailabilityReceived += (_, command) =>
{
Dispatcher.Invoke(() =>
{
_logger.LogInformation(command.Message);
var aiEnabled = command.Message == "enabled";
AIDetectBtn.IsEnabled = aiEnabled;
var aiDisabledText = "Будь ласка, зачекайте, наразі розпізнавання AI недоступне";
var messagesDict = new Dictionary<string, string>
{
{ "disabled", aiDisabledText },
{ "downloading", "Будь ласка зачекайте, йде завантаження AI для Вашої відеокарти" },
{ "converting", "Будь ласка зачекайте, йде налаштування AI під Ваше залізо. (5-12 хвилин в залежності від моделі відеокарти, до 50 хв на старих GTX1650)" },
{ "uploading", "Будь ласка зачекайте, йде зберігання" },
{ "enabled", "AI готовий для розпізнавання" }
};
StatusHelp.Text = messagesDict!.GetValueOrDefault(command.Message, aiDisabledText);
if (aiEnabled)
StatusHelp.Foreground = aiEnabled ? Brushes.White : Brushes.Red;
});
};
_inferenceClient.Send(RemoteCommand.Create(CommandType.AIAvailabilityCheck));
Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time); Editor.GetTimeFunc = () => TimeSpan.FromMilliseconds(_mediaPlayer.Time);
MapMatcherComponent.Init(_appConfig, _gpsMatcherService); MapMatcherComponent.Init(_appConfig, _gpsMatcherService);
@@ -126,9 +149,6 @@ public partial class Annotator
TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory; TbFolder.Text = _appConfig.DirectoriesConfig.VideosDirectory;
LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses); LvClasses.Init(_appConfig.AnnotationConfig.DetectionClasses);
if (LvFiles.Items.IsEmpty)
BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.Initial]);
} }
public void BlinkHelp(string helpText, int times = 2) public void BlinkHelp(string helpText, int times = 2)
@@ -175,8 +195,6 @@ public partial class Annotator
LvFiles.MouseDoubleClick += async (_, _) => LvFiles.MouseDoubleClick += async (_, _) =>
{ {
if (IsInferenceNow)
FollowAI = false;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play)); await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play));
}; };
@@ -225,9 +243,9 @@ public partial class Annotator
return; return;
var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList(); var res = DgAnnotations.SelectedItems.Cast<AnnotationResult>().ToList();
var annotations = res.Select(x => x.Annotation).ToList(); var annotationNames = res.Select(x => x.Annotation.Name).ToList();
await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
break; break;
} }
}; };
@@ -238,8 +256,6 @@ public partial class Annotator
public void OpenAnnotationResult(AnnotationResult res) public void OpenAnnotationResult(AnnotationResult res)
{ {
if (IsInferenceNow)
FollowAI = false;
_mediaPlayer.SetPause(true); _mediaPlayer.SetPause(true);
Editor.RemoveAllAnns(); Editor.RemoveAllAnns();
_mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds; _mediaPlayer.Time = (long)res.Annotation.Time.TotalMilliseconds;
@@ -325,6 +341,10 @@ public partial class Annotator
//Add manually //Add manually
public void AddAnnotation(Annotation annotation) public void AddAnnotation(Annotation annotation)
{ {
var mediaInfo = (MediaFileInfo)LvFiles.SelectedItem;
if ((mediaInfo?.FName ?? "") != annotation.OriginalMediaName)
return;
var time = annotation.Time; var time = annotation.Time;
var previousAnnotations = TimedAnnotations.Query(time); var previousAnnotations = TimedAnnotations.Query(time);
TimedAnnotations.Remove(previousAnnotations); TimedAnnotations.Remove(previousAnnotations);
@@ -342,10 +362,8 @@ public partial class Annotator
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
throw; throw;
} }
} }
var dict = _formState.AnnotationResults var dict = _formState.AnnotationResults
.Select((x, i) => new { x.Annotation.Time, Index = i }) .Select((x, i) => new { x.Annotation.Time, Index = i })
.ToDictionary(x => x.Time, x => x.Index); .ToDictionary(x => x.Time, x => x.Index);
@@ -399,11 +417,9 @@ public partial class Annotator
mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName); mediaFile.HasAnnotations = labelsDict.ContainsKey(mediaFile.FName);
AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles); AllMediaFiles = new ObservableCollection<MediaFileInfo>(allFiles);
MediaFilesDict = AllMediaFiles.GroupBy(x => x.Name)
.ToDictionary(gr => gr.Key, gr => gr.First());
LvFiles.ItemsSource = AllMediaFiles; LvFiles.ItemsSource = AllMediaFiles;
BlinkHelp(AllMediaFiles.Count == 0
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
: HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]);
DataContext = this; DataContext = this;
} }
@@ -448,7 +464,7 @@ public partial class Annotator
{ {
Title = "Open Video folder", Title = "Open Video folder",
IsFolderPicker = true, IsFolderPicker = true,
InitialDirectory = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory) InitialDirectory = Path.GetDirectoryName(_appConfig.DirectoriesConfig.VideosDirectory)
}; };
var dialogResult = dlg.ShowDialog(); var dialogResult = dlg.ShowDialog();
@@ -463,14 +479,13 @@ public partial class Annotator
private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e) private void TbFilter_OnTextChanged(object sender, TextChangedEventArgs e)
{ {
FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList()); FilteredMediaFiles = new ObservableCollection<MediaFileInfo>(AllMediaFiles.Where(x => x.Name.ToLower().Contains(TbFilter.Text.ToLower())).ToList());
MediaFilesDict = FilteredMediaFiles.ToDictionary(x => x.FName);
LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles;
LvFiles.ItemsSource = FilteredMediaFiles; LvFiles.ItemsSource = FilteredMediaFiles;
} }
private void PlayClick(object sender, RoutedEventArgs e) private void PlayClick(object sender, RoutedEventArgs e)
{ {
if (IsInferenceNow)
FollowAI = false;
_mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play)); _mediator.Publish(new AnnotatorControlEvent(_mediaPlayer.CanPause ? PlaybackControlEnum.Pause : PlaybackControlEnum.Play));
} }
@@ -501,13 +516,22 @@ public partial class Annotator
LvFilesContextMenu.DataContext = listItem!.DataContext; LvFilesContextMenu.DataContext = listItem!.DataContext;
} }
public void AutoDetect(object sender, RoutedEventArgs e) private async void AIDetectBtn_OnClick(object sender, RoutedEventArgs e)
{
try
{
await AutoDetect();
}
catch (Exception ex)
{
_logger.LogError(ex, ex.Message);
}
}
public async Task AutoDetect()
{ {
if (IsInferenceNow) if (IsInferenceNow)
{
FollowAI = true;
return; return;
}
if (LvFiles.Items.IsEmpty) if (LvFiles.Items.IsEmpty)
return; return;
@@ -517,96 +541,23 @@ public partial class Annotator
Dispatcher.Invoke(() => Editor.ResetBackground()); Dispatcher.Invoke(() => Editor.ResetBackground());
IsInferenceNow = true; IsInferenceNow = true;
FollowAI = true; AIDetectBtn.IsEnabled = false;
DetectionCancellationSource = new CancellationTokenSource(); DetectionCancellationSource = new CancellationTokenSource();
var detectToken = DetectionCancellationSource.Token;
_ = Task.Run(async () => var files = (FilteredMediaFiles.Count == 0 ? AllMediaFiles : FilteredMediaFiles)
{ .Skip(LvFiles.SelectedIndex)
while (!detectToken.IsCancellationRequested)
{
var files = new List<string>();
await Dispatcher.Invoke(async () =>
{
//Take all medias
files = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?.Skip(LvFiles.SelectedIndex)
//.Where(x => !x.HasAnnotations)
.Take(Constants.DETECTION_BATCH_SIZE)
.Select(x => x.Path) .Select(x => x.Path)
.ToList() ?? []; .ToList();
if (files.Count != 0)
{
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), detectToken);
await ReloadAnnotations();
}
});
if (files.Count == 0) if (files.Count == 0)
break; return;
await _inferenceService.RunInference(files, async annotationImage => await ProcessDetection(annotationImage, detectToken), detectToken); await _inferenceService.RunInference(files, DetectionCancellationSource.Token);
Dispatcher.Invoke(() =>
{
if (LvFiles.SelectedIndex + files.Count >= LvFiles.Items.Count)
DetectionCancellationSource.Cancel();
LvFiles.SelectedIndex += files.Count;
});
}
Dispatcher.Invoke(() =>
{
LvFiles.Items.Refresh(); LvFiles.Items.Refresh();
IsInferenceNow = false; IsInferenceNow = false;
FollowAI = false; StatusHelp.Text = "Розпізнавання зваершено";
}); AIDetectBtn.IsEnabled = true;
});
}
private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
{
await Dispatcher.Invoke(async () =>
{
try
{
var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
if (annotation.OriginalMediaName != _formState.CurrentMedia?.FName)
{
var nextFile = (LvFiles.ItemsSource as IEnumerable<MediaFileInfo>)?
.Select((info, i) => new
{
MediaInfo = info,
Index = i
})
.FirstOrDefault(x => x.MediaInfo.FName == annotation.OriginalMediaName);
if (nextFile != null)
{
LvFiles.SelectedIndex = nextFile.Index;
await _mediator.Publish(new AnnotatorControlEvent(PlaybackControlEnum.Play), ct);
}
}
AddAnnotation(annotation);
if (FollowAI)
SeekTo(annotationImage.Milliseconds, false);
var log = string.Join(Environment.NewLine, annotation.Detections.Select(det =>
$"{_appConfig.AnnotationConfig.DetectionClassesDict[det.ClassNumber].Name}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"size=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
Dispatcher.Invoke(() =>
{
if (_formState.CurrentMedia != null)
_formState.CurrentMedia.HasAnnotations = true;
LvFiles.Items.Refresh();
StatusHelp.Text = log;
});
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
});
} }
private void SwitchGpsPanel(object sender, RoutedEventArgs e) private void SwitchGpsPanel(object sender, RoutedEventArgs e)
+54 -17
View File
@@ -1,9 +1,11 @@
using System.IO; using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Threading;
using Azaion.Annotator.DTO; using Azaion.Annotator.DTO;
using Azaion.Common; using Azaion.Common;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Config;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.Common.Services; using Azaion.Common.Services;
@@ -21,16 +23,18 @@ public class AnnotatorEventHandler(
MediaPlayer mediaPlayer, MediaPlayer mediaPlayer,
Annotator mainWindow, Annotator mainWindow,
FormState formState, FormState formState,
AnnotationService annotationService, IAnnotationService annotationService,
ILogger<AnnotatorEventHandler> logger, ILogger<AnnotatorEventHandler> logger,
IOptions<DirectoriesConfig> dirConfig, IOptions<DirectoriesConfig> dirConfig,
IOptions<AnnotationConfig> annotationConfig,
IInferenceService inferenceService) IInferenceService inferenceService)
: :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<AnnClassSelectedEvent>, INotificationHandler<AnnClassSelectedEvent>,
INotificationHandler<AnnotatorControlEvent>, INotificationHandler<AnnotatorControlEvent>,
INotificationHandler<VolumeChangedEvent>, INotificationHandler<VolumeChangedEvent>,
INotificationHandler<AnnotationsDeletedEvent> INotificationHandler<AnnotationsDeletedEvent>,
INotificationHandler<AnnotationAddedEvent>
{ {
private const int STEP = 20; private const int STEP = 20;
private const int LARGE_STEP = 5000; private const int LARGE_STEP = 5000;
@@ -81,7 +85,7 @@ public class AnnotatorEventHandler(
await ControlPlayback(value, cancellationToken); await ControlPlayback(value, cancellationToken);
if (key == Key.R) if (key == Key.R)
mainWindow.AutoDetect(null!, null!); await mainWindow.AutoDetect();
#region Volume #region Volume
switch (key) switch (key)
@@ -128,10 +132,6 @@ public class AnnotatorEventHandler(
break; break;
case PlaybackControlEnum.Pause: case PlaybackControlEnum.Pause:
mediaPlayer.Pause(); mediaPlayer.Pause();
if (mainWindow.IsInferenceNow)
mainWindow.FollowAI = false;
if (!mediaPlayer.IsPlaying)
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.AnnotationHelp]);
if (formState.BackgroundTime.HasValue) if (formState.BackgroundTime.HasValue)
{ {
@@ -225,7 +225,6 @@ public class AnnotatorEventHandler(
await Task.Delay(100, ct); await Task.Delay(100, ct);
mediaPlayer.Stop(); mediaPlayer.Stop();
mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}"; mainWindow.Title = $"Azaion Annotator - {mediaInfo.Name}";
mainWindow.BlinkHelp(HelpTexts.HelpTextsDict[HelpTextEnum.PauseForAnnotations]);
mediaPlayer.Play(new Media(libVLC, mediaInfo.Path)); mediaPlayer.Play(new Media(libVLC, mediaInfo.Path));
if (formState.CurrentMedia.MediaType == MediaTypes.Image) if (formState.CurrentMedia.MediaType == MediaTypes.Image)
mediaPlayer.SetPause(true); mediaPlayer.SetPause(true);
@@ -278,17 +277,24 @@ public class AnnotatorEventHandler(
mainWindow.AddAnnotation(annotation); mainWindow.AddAnnotation(annotation);
} }
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) public Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{ {
var annResDict = formState.AnnotationResults.ToDictionary(x => x.Annotation.Name, x => x); try
foreach (var ann in notification.Annotations)
{ {
if (!annResDict.TryGetValue(ann.Name, out var value)) mainWindow.Dispatcher.Invoke(() =>
continue; {
var namesSet = notification.AnnotationNames.ToHashSet();
formState.AnnotationResults.Remove(value); var remainAnnotations = formState.AnnotationResults
mainWindow.TimedAnnotations.Remove(ann); .Where(x => !namesSet.Contains(x.Annotation?.Name ?? "")).ToList();
} formState.AnnotationResults.Clear();
foreach (var ann in remainAnnotations)
formState.AnnotationResults.Add(ann);
var timedAnnsToRemove = mainWindow.TimedAnnotations
.Where(x => namesSet.Contains(x.Value.Name))
.Select(x => x.Value).ToList();
mainWindow.TimedAnnotations.Remove(timedAnnsToRemove);
if (formState.AnnotationResults.Count == 0) if (formState.AnnotationResults.Count == 0)
{ {
@@ -299,6 +305,37 @@ public class AnnotatorEventHandler(
mainWindow.LvFiles.Items.Refresh(); mainWindow.LvFiles.Items.Refresh();
} }
} }
await Task.CompletedTask; });
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
return Task.CompletedTask;
}
public Task Handle(AnnotationAddedEvent e, CancellationToken cancellationToken)
{
mainWindow.Dispatcher.Invoke(() =>
{
mainWindow.AddAnnotation(e.Annotation);
var log = string.Join(Environment.NewLine, e.Annotation.Detections.Select(det =>
$"Розпізнавання {e.Annotation.OriginalMediaName}: {annotationConfig.Value.DetectionClassesDict[det.ClassNumber].ShortName}: " +
$"xy=({det.CenterX:F2},{det.CenterY:F2}), " +
$"розмір=({det.Width:F2}, {det.Height:F2}), " +
$"conf: {det.Confidence*100:F0}%"));
mainWindow.LvFiles.Items.Refresh();
var media = mainWindow.MediaFilesDict.GetValueOrDefault(e.Annotation.OriginalMediaName);
if (media != null)
media.HasAnnotations = true;
mainWindow.LvFiles.Items.Refresh();
mainWindow.StatusHelp.Text = log;
});
return Task.CompletedTask;
} }
} }
+1 -9
View File
@@ -1,6 +1,4 @@
using System.Windows; using System.Windows;
using System.Windows.Media;
using Azaion.Common.Database;
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;
@@ -10,7 +8,7 @@ namespace Azaion.Common;
public class Constants public class Constants
{ {
public const string JPG_EXT = ".jpg"; public const string JPG_EXT = ".jpg";
public const string TXT_EXT = ".txt";
#region DirectoriesConfig #region DirectoriesConfig
public const string DEFAULT_VIDEO_DIR = "video"; public const string DEFAULT_VIDEO_DIR = "video";
@@ -80,7 +78,6 @@ public class Constants
public const double TRACKING_INTERSECTION_THRESHOLD = 0.8; public const double TRACKING_INTERSECTION_THRESHOLD = 0.8;
public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4; public const int DEFAULT_FRAME_PERIOD_RECOGNITION = 4;
public const int DETECTION_BATCH_SIZE = 4;
# endregion AIRecognitionConfig # endregion AIRecognitionConfig
#region Thumbnails #region Thumbnails
@@ -100,12 +97,7 @@ public class Constants
#endregion #endregion
#region Queue
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations"; public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
#endregion
#region Database #region Database
@@ -52,6 +52,7 @@
CanUserResizeColumns="False" CanUserResizeColumns="False"
SelectionChanged="DetectionDataGrid_SelectionChanged" SelectionChanged="DetectionDataGrid_SelectionChanged"
x:FieldModifier="public" x:FieldModifier="public"
PreviewKeyDown="OnKeyBanActivity"
> >
<DataGrid.Columns> <DataGrid.Columns>
<DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False"> <DataGridTemplateColumn Width="50" Header="Клавіша" CanUserSort="False">
@@ -1,8 +1,10 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Input;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
namespace Azaion.Common.Controls; namespace Azaion.Common.Controls;
@@ -86,4 +88,11 @@ public partial class DetectionClasses
{ {
DetectionDataGrid.SelectedIndex = keyNumber; DetectionDataGrid.SelectedIndex = keyNumber;
} }
private void OnKeyBanActivity(object sender, KeyEventArgs e)
{
if (e.Key.In(Key.Enter, Key.Down, Key.Up, Key.PageDown, Key.PageUp))
e.Handled = true;
}
} }
+8 -2
View File
@@ -4,12 +4,14 @@ using System.Runtime.CompilerServices;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.Extensions; using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
namespace Azaion.Common.DTO; namespace Azaion.Common.DTO;
public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged public class AnnotationThumbnail(Annotation annotation, bool isValidator) : INotifyPropertyChanged
{ {
public Annotation Annotation { get; set; } = annotation; public Annotation Annotation { get; set; } = annotation;
public bool IsValidator { get; set; } = isValidator;
private BitmapImage? _thumbnail; private BitmapImage? _thumbnail;
public BitmapImage? Thumbnail public BitmapImage? Thumbnail
@@ -28,7 +30,11 @@ public class AnnotationThumbnail(Annotation annotation) : INotifyPropertyChanged
} }
public string ImageName => Path.GetFileName(Annotation.ImagePath); public string ImageName => Path.GetFileName(Annotation.ImagePath);
public bool IsSeed => Annotation.AnnotationStatus == AnnotationStatus.Created; public string CreatedDate => $"{Annotation.CreatedDate:dd.MM.yyyy HH:mm:ss}";
public string CreatedEmail => Annotation.CreatedEmail;
public bool IsSeed => IsValidator &&
Annotation.AnnotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited) &&
!Annotation.CreatedRole.IsValidator();
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
@@ -5,7 +5,7 @@ namespace Azaion.Common.DTO.Queue;
using MessagePack; using MessagePack;
[MessagePackObject] [MessagePackObject]
public class AnnotationCreatedMessage public class AnnotationMessage
{ {
[Key(0)] public DateTime CreatedDate { get; set; } [Key(0)] public DateTime CreatedDate { get; set; }
[Key(1)] public string Name { get; set; } = null!; [Key(1)] public string Name { get; set; } = null!;
@@ -13,15 +13,15 @@ public class AnnotationCreatedMessage
[Key(3)] public TimeSpan Time { get; set; } [Key(3)] public TimeSpan Time { get; set; }
[Key(4)] public string ImageExtension { get; set; } = null!; [Key(4)] public string ImageExtension { get; set; } = null!;
[Key(5)] public string Detections { get; set; } = null!; [Key(5)] public string Detections { get; set; } = null!;
[Key(6)] public byte[] Image { get; set; } = null!; [Key(6)] public byte[]? Image { get; set; } = null!;
[Key(7)] public RoleEnum CreatedRole { get; set; } [Key(7)] public RoleEnum Role { get; set; }
[Key(8)] public string CreatedEmail { get; set; } = null!; [Key(8)] public string Email { get; set; } = null!;
[Key(9)] public SourceEnum Source { get; set; } [Key(9)] public SourceEnum Source { get; set; }
[Key(10)] public AnnotationStatus Status { get; set; } [Key(10)] public AnnotationStatus Status { get; set; }
} }
[MessagePackObject] [MessagePackObject]
public class AnnotationValidatedMessage public class AnnotationBulkMessage
{ {
[Key(0)] public string Name { get; set; } = null!; [Key(0)] public string[] AnnotationNames { get; set; } = null!;
} }
+3 -1
View File
@@ -59,5 +59,7 @@ public enum AnnotationStatus
{ {
None = 0, None = 0,
Created = 10, Created = 10,
Validated = 20 Edited = 20,
Validated = 30,
Deleted = 40
} }
-6
View File
@@ -1,6 +0,0 @@
namespace Azaion.Common.Database;
public class AnnotationName
{
public string Name { get; set; } = null!;
}
@@ -0,0 +1,9 @@
namespace Azaion.Common.Database;
public class AnnotationQueueRecord
{
public Guid Id { get; set; }
public DateTime DateTime { get; set; }
public AnnotationStatus Operation { get; set; }
public List<string> AnnotationNames { get; set; } = null!;
}
+1 -2
View File
@@ -7,7 +7,6 @@ namespace Azaion.Common.Database;
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions) public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
{ {
public ITable<Annotation> Annotations => this.GetTable<Annotation>(); public ITable<Annotation> Annotations => this.GetTable<Annotation>();
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>(); public ITable<AnnotationQueueRecord> AnnotationsQueueRecords => this.GetTable<AnnotationQueueRecord>();
public ITable<Detection> Detections => this.GetTable<Detection>(); public ITable<Detection> Detections => this.GetTable<Detection>();
public ITable<QueueOffset> QueueOffsets => this.GetTable<QueueOffset>();
} }
+17 -27
View File
@@ -9,6 +9,7 @@ using LinqToDB.DataProvider.SQLite;
using LinqToDB.Mapping; using LinqToDB.Mapping;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json;
namespace Azaion.Common.Database; namespace Azaion.Common.Database;
@@ -17,7 +18,6 @@ public interface IDbFactory
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func); Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
Task Run(Func<AnnotationsDb, Task> func); Task Run(Func<AnnotationsDb, Task> func);
void SaveToDisk(); void SaveToDisk();
Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default);
Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default); Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default);
} }
@@ -53,32 +53,24 @@ public class DbFactory : IDbFactory
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema); .UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
if (!File.Exists(_annConfig.AnnotationsDbFile)) if (!File.Exists(_annConfig.AnnotationsDbFile))
CreateDb(); SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
RecreateTables();
_fileConnection.Open(); _fileConnection.Open();
_fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1); _fileConnection.BackupDatabase(_memoryConnection, "main", "main", -1, null, -1);
} }
private void CreateDb() private void RecreateTables()
{ {
SQLiteConnection.CreateFile(_annConfig.AnnotationsDbFile);
using var db = new AnnotationsDb(_fileDataOptions); using var db = new AnnotationsDb(_fileDataOptions);
var schema = db.DataProvider.GetSchemaProvider().GetSchema(db);
var existingTables = schema.Tables.Select(x => x.TableName).ToHashSet();
if (!existingTables.Contains(Constants.ANNOTATIONS_TABLENAME))
db.CreateTable<Annotation>(); db.CreateTable<Annotation>();
db.CreateTable<AnnotationName>(); if (!existingTables.Contains(Constants.DETECTIONS_TABLENAME))
db.CreateTable<Detection>(); db.CreateTable<Detection>();
db.CreateTable<QueueOffset>(); if (!existingTables.Contains(Constants.ANNOTATIONS_QUEUE_TABLENAME))
db.QueueOffsets.BulkCopy(new List<QueueOffset> db.CreateTable<AnnotationQueueRecord>();
{
new()
{
Offset = 0,
QueueName = Constants.MQ_ANNOTATIONS_QUEUE
},
new()
{
Offset = 0,
QueueName = Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE
}
});
} }
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func) public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
@@ -98,12 +90,6 @@ public class DbFactory : IDbFactory
_memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1); _memoryConnection.BackupDatabase(_fileConnection, "main", "main", -1, null, -1);
} }
public async Task DeleteAnnotations(List<Annotation> annotations, CancellationToken cancellationToken = default)
{
var names = annotations.Select(x => x.Name).ToList();
await DeleteAnnotations(names, cancellationToken);
}
public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default) public async Task DeleteAnnotations(List<string> annotationNames, CancellationToken cancellationToken = default)
{ {
await Run(async db => await Run(async db =>
@@ -142,8 +128,12 @@ public static class AnnotationsDbSchemaHolder
builder.Entity<Detection>() builder.Entity<Detection>()
.HasTableName(Constants.DETECTIONS_TABLENAME); .HasTableName(Constants.DETECTIONS_TABLENAME);
builder.Entity<AnnotationName>() builder.Entity<AnnotationQueueRecord>()
.HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME); .HasTableName(Constants.ANNOTATIONS_QUEUE_TABLENAME)
.HasPrimaryKey(x => x.Id)
.Property(x => x.AnnotationNames)
.HasDataType(DataType.NVarChar)
.HasConversion(list => JsonConvert.SerializeObject(list), str => JsonConvert.DeserializeObject<List<string>>(str) ?? new List<string>());
builder.Build(); builder.Build();
} }
-7
View File
@@ -1,7 +0,0 @@
namespace Azaion.Common.Database;
public class QueueOffset
{
public string QueueName { get; set; } = null!;
public ulong Offset { get; set; }
}
@@ -3,7 +3,13 @@ using MediatR;
namespace Azaion.Common.Events; namespace Azaion.Common.Events;
public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotification public class AnnotationsDeletedEvent(List<string> annotationNames, bool fromQueue = false) : INotification
{ {
public List<Annotation> Annotations { get; set; } = annotations; public List<string> AnnotationNames { get; set; } = annotationNames;
public bool FromQueue { get; set; } = fromQueue;
}
public class AnnotationAddedEvent(Annotation annotation) : INotification
{
public Annotation Annotation { get; set; } = annotation;
} }
+8
View File
@@ -0,0 +1,8 @@
using MediatR;
namespace Azaion.Common.Events;
public class LoadErrorEvent(string error) : INotification
{
public string Error { get; set; } = error;
}
@@ -0,0 +1,30 @@
namespace Azaion.Common.Extensions;
public static class CancellationTokenExtensions
{
public static void WaitForCancel(this CancellationToken token, TimeSpan timeout)
{
try
{
Task.Delay(timeout, token).Wait(token);
}
catch (OperationCanceledException)
{
//Don't need to catch exception, need only return from the waiting
}
}
public static Task AsTask(this CancellationToken cancellationToken)
{
if (!cancellationToken.CanBeCanceled)
return new TaskCompletionSource<bool>().Task;
if (cancellationToken.IsCancellationRequested)
return Task.FromCanceled(cancellationToken);
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
var registration = cancellationToken.Register(() => tcs.TrySetResult(true));
tcs.Task.ContinueWith(_ => registration.Dispose(), TaskScheduler.Default);
return tcs.Task;
}
}
@@ -54,7 +54,6 @@ public static class ThrottleExt
finally finally
{ {
await Task.Delay(interval); await Task.Delay(interval);
lock (state.StateLock) lock (state.StateLock)
{ {
if (state.CallScheduledDuringCooldown) if (state.CallScheduledDuringCooldown)
+100 -42
View File
@@ -13,6 +13,7 @@ using LinqToDB;
using LinqToDB.Data; using LinqToDB.Data;
using MediatR; using MediatR;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
@@ -20,39 +21,46 @@ using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent> public class AnnotationService : IAnnotationService, INotificationHandler<AnnotationsDeletedEvent>
{ {
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly FailsafeAnnotationsProducer _producer; private readonly FailsafeAnnotationsProducer _producer;
private readonly IGalleryService _galleryService; private readonly IGalleryService _galleryService;
private readonly IMediator _mediator; private readonly IMediator _mediator;
private readonly IAzaionApi _api; private readonly IAzaionApi _api;
private readonly ILogger<AnnotationService> _logger;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private Consumer _consumer = null!; private Consumer _consumer = null!;
private readonly UIConfig _uiConfig; private readonly UIConfig _uiConfig;
private readonly DirectoriesConfig _dirConfig;
private static readonly Guid SaveTaskId = Guid.NewGuid(); private static readonly Guid SaveTaskId = Guid.NewGuid();
private static readonly Guid SaveQueueOffsetTaskId = Guid.NewGuid();
public AnnotationService( public AnnotationService(
IDbFactory dbFactory, IDbFactory dbFactory,
FailsafeAnnotationsProducer producer, FailsafeAnnotationsProducer producer,
IOptions<QueueConfig> queueConfig, IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig, IOptions<UIConfig> uiConfig,
IOptions<DirectoriesConfig> directoriesConfig,
IGalleryService galleryService, IGalleryService galleryService,
IMediator mediator, IMediator mediator,
IAzaionApi api) IAzaionApi api,
ILogger<AnnotationService> logger)
{ {
_dbFactory = dbFactory; _dbFactory = dbFactory;
_producer = producer; _producer = producer;
_galleryService = galleryService; _galleryService = galleryService;
_mediator = mediator; _mediator = mediator;
_api = api; _api = api;
_logger = logger;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value; _uiConfig = uiConfig.Value;
_dirConfig = directoriesConfig.Value;
Task.Run(async () => await Init()).Wait(); Task.Run(async () => await InitQueueConsumer()).Wait();
} }
private async Task Init(CancellationToken cancellationToken = default) private async Task InitQueueConsumer(CancellationToken cancellationToken = default)
{ {
if (!_api.CurrentUser.Role.IsValidator()) if (!_api.CurrentUser.Role.IsValidator())
return; return;
@@ -69,33 +77,54 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE) _consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
{ {
Reference = _api.CurrentUser.Email, Reference = _api.CurrentUser.Email,
OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset + 1), OffsetSpec = new OffsetTypeOffset(offsets.AnnotationsOffset),
MessageHandler = async (_, _, context, message) => MessageHandler = async (_, _, context, message) =>
{ {
var msg = MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents); try
offsets.AnnotationsOffset = context.Offset;
ThrottleExt.Throttle(() =>
{ {
_api.UpdateOffsets(offsets); var email = (string)message.ApplicationProperties[nameof(User.Email)]!;
return Task.CompletedTask; if (!Enum.TryParse<AnnotationStatus>((string)message.ApplicationProperties[nameof(AnnotationStatus)], out var annotationStatus))
}, SaveTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
if (msg.CreatedEmail == _api.CurrentUser.Email) //Don't process messages by yourself
return; return;
if (email != _api.CurrentUser.Email) //Don't process messages by yourself
{
if (annotationStatus.In(AnnotationStatus.Created, AnnotationStatus.Edited))
{
var msg = MessagePackSerializer.Deserialize<AnnotationMessage>(message.Data.Contents);
await SaveAnnotationInner( await SaveAnnotationInner(
msg.CreatedDate, msg.CreatedDate,
msg.OriginalMediaName, msg.OriginalMediaName,
msg.Time, msg.Time,
JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [], JsonConvert.DeserializeObject<List<Detection>>(msg.Detections) ?? [],
msg.Source, msg.Source,
new MemoryStream(msg.Image), msg.Image == null ? null : new MemoryStream(msg.Image),
msg.CreatedRole, msg.Role,
msg.CreatedEmail, msg.Email,
fromQueue: true, fromQueue: true,
token: cancellationToken); token: cancellationToken);
} }
else
{
var msg = MessagePackSerializer.Deserialize<AnnotationBulkMessage>(message.Data.Contents);
if (annotationStatus == AnnotationStatus.Validated)
await ValidateAnnotations(msg.AnnotationNames.ToList(), true, cancellationToken);
if (annotationStatus == AnnotationStatus.Deleted)
await _mediator.Publish(new AnnotationsDeletedEvent(msg.AnnotationNames.ToList(), fromQueue:true), cancellationToken);
}
}
offsets.AnnotationsOffset = context.Offset + 1; //to consume on the next launch from the next message
ThrottleExt.Throttle(() =>
{
_api.UpdateOffsets(offsets);
return Task.CompletedTask;
}, SaveQueueOffsetTaskId, TimeSpan.FromSeconds(10), scheduleCallAfterCooldown: true);
}
catch (Exception e)
{
_logger.LogError(e, e.Message);
}
}
}); });
} }
@@ -112,35 +141,42 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream, await SaveAnnotationInner(DateTime.UtcNow, originalMediaName, time, detections, SourceEnum.Manual, stream,
_api.CurrentUser.Role, _api.CurrentUser.Email, token: token); _api.CurrentUser.Role, _api.CurrentUser.Email, token: token);
// Manual save from Validators -> Validated -> stream: azaion-annotations-confirm private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time,
// AI, Manual save from Operators -> Created -> stream: azaion-annotations List<Detection> detections, SourceEnum source, Stream? stream,
private async Task<Annotation> SaveAnnotationInner(DateTime createdDate, string originalMediaName, TimeSpan time, List<Detection> detections, SourceEnum source, Stream? stream,
RoleEnum userRole, RoleEnum userRole,
string createdEmail, string createdEmail,
bool fromQueue = false, bool fromQueue = false,
CancellationToken token = default) CancellationToken token = default)
{ {
AnnotationStatus status; var status = AnnotationStatus.Created;
var fName = originalMediaName.ToTimeName(time); var fName = originalMediaName.ToTimeName(time);
var annotation = await _dbFactory.Run(async db => var annotation = await _dbFactory.Run(async db =>
{ {
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token); var ann = await db.Annotations
status = userRole.IsValidator() && source == SourceEnum.Manual .LoadWith(x => x.Detections)
? AnnotationStatus.Validated .FirstOrDefaultAsync(x => x.Name == fName, token: token);
: AnnotationStatus.Created;
await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token); await db.Detections.DeleteAsync(x => x.AnnotationName == fName, token: token);
if (ann != null) if (ann != null) //Annotation is already exists
{ {
await db.Annotations status = AnnotationStatus.Edited;
var annotationUpdatable = db.Annotations
.Where(x => x.Name == fName) .Where(x => x.Name == fName)
.Set(x => x.Source, source) .Set(x => x.Source, source);
if (userRole.IsValidator() && source == SourceEnum.Manual)
{
annotationUpdatable = annotationUpdatable
.Set(x => x.ValidateDate, createdDate)
.Set(x => x.ValidateEmail, createdEmail);
}
await annotationUpdatable
.Set(x => x.AnnotationStatus, status) .Set(x => x.AnnotationStatus, status)
.Set(x => x.CreatedDate, createdDate)
.Set(x => x.CreatedEmail, createdEmail)
.Set(x => x.CreatedRole, userRole)
.UpdateAsync(token: token); .UpdateAsync(token: token);
ann.Detections = detections; ann.Detections = detections;
} }
else else
@@ -175,10 +211,11 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
if (_uiConfig.GenerateAnnotatedImage) if (_uiConfig.GenerateAnnotatedImage)
await _galleryService.CreateAnnotatedImage(annotation, token); await _galleryService.CreateAnnotatedImage(annotation, token);
if (!fromQueue && !_uiConfig.SilentDetection) //Send to queue only if we're not getting from queue already
await _producer.SendToInnerQueue(annotation, token);
await _mediator.Publish(new AnnotationCreatedEvent(annotation), token); await _mediator.Publish(new AnnotationCreatedEvent(annotation), token);
if (!fromQueue) //Send to queue only if we're not getting from queue already
await _producer.SendToInnerQueue([annotation.Name], status, token);
ThrottleExt.Throttle(async () => ThrottleExt.Throttle(async () =>
{ {
_dbFactory.SaveToDisk(); _dbFactory.SaveToDisk();
@@ -187,12 +224,12 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
return annotation; return annotation;
} }
public async Task ValidateAnnotations(List<Annotation> annotations, CancellationToken token = default) public async Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default)
{ {
if (!_api.CurrentUser.Role.IsValidator()) if (!_api.CurrentUser.Role.IsValidator())
return; return;
var annNames = annotations.Select(x => x.Name).ToHashSet(); var annNames = annotationNames.ToHashSet();
await _dbFactory.Run(async db => await _dbFactory.Run(async db =>
{ {
await db.Annotations await db.Annotations
@@ -202,6 +239,9 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
.Set(x => x.ValidateEmail, _api.CurrentUser.Email) .Set(x => x.ValidateEmail, _api.CurrentUser.Email)
.UpdateAsync(token: token); .UpdateAsync(token: token);
}); });
if (!fromQueue)
await _producer.SendToInnerQueue(annotationNames, AnnotationStatus.Validated, token);
ThrottleExt.Throttle(async () => ThrottleExt.Throttle(async () =>
{ {
_dbFactory.SaveToDisk(); _dbFactory.SaveToDisk();
@@ -209,14 +249,32 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
}, SaveTaskId, TimeSpan.FromSeconds(5), true); }, SaveTaskId, TimeSpan.FromSeconds(5), true);
} }
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken ct)
{ {
await _dbFactory.DeleteAnnotations(notification.Annotations, cancellationToken); await _dbFactory.DeleteAnnotations(notification.AnnotationNames, ct);
foreach (var annotation in notification.Annotations) foreach (var name in notification.AnnotationNames)
{ {
File.Delete(annotation.ImagePath); File.Delete(Path.Combine(_dirConfig.ImagesDirectory, $"{name}{Constants.JPG_EXT}"));
File.Delete(annotation.LabelPath); File.Delete(Path.Combine(_dirConfig.LabelsDirectory, $"{name}{Constants.TXT_EXT}"));
File.Delete(annotation.ThumbPath); File.Delete(Path.Combine(_dirConfig.ThumbnailsDirectory, $"{name}{Constants.THUMBNAIL_PREFIX}{Constants.JPG_EXT}"));
File.Delete(Path.Combine(_dirConfig.ResultsDirectory, $"{name}{Constants.RESULT_PREFIX}{Constants.JPG_EXT}"));
}
//Only validators can send Delete to the queue
if (!notification.FromQueue && _api.CurrentUser.Role.IsValidator())
await _producer.SendToInnerQueue(notification.AnnotationNames, AnnotationStatus.Deleted, ct);
ThrottleExt.Throttle(async () =>
{
_dbFactory.SaveToDisk();
await Task.CompletedTask;
}, SaveTaskId, TimeSpan.FromSeconds(5), true);
} }
} }
public interface IAnnotationService
{
Task<Annotation> SaveAnnotation(AnnotationImage a, CancellationToken ct = default);
Task<Annotation> SaveAnnotation(string originalMediaName, TimeSpan time, List<Detection> detections, Stream? stream = null, CancellationToken token = default);
Task ValidateAnnotations(List<string> annotationNames, bool fromQueue = false, CancellationToken token = default);
} }
+82 -67
View File
@@ -3,12 +3,16 @@ using System.Net;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.Common.DTO.Queue; using Azaion.Common.DTO.Queue;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services;
using LinqToDB; using LinqToDB;
using MessagePack; using MessagePack;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Newtonsoft.Json; using Newtonsoft.Json;
using RabbitMQ.Stream.Client; using RabbitMQ.Stream.Client;
using RabbitMQ.Stream.Client.AMQP;
using RabbitMQ.Stream.Client.Reliable; using RabbitMQ.Stream.Client.Reliable;
namespace Azaion.Common.Services; namespace Azaion.Common.Services;
@@ -17,17 +21,24 @@ public class FailsafeAnnotationsProducer
{ {
private readonly ILogger<FailsafeAnnotationsProducer> _logger; private readonly ILogger<FailsafeAnnotationsProducer> _logger;
private readonly IDbFactory _dbFactory; private readonly IDbFactory _dbFactory;
private readonly IAzaionApi _azaionApi;
private readonly QueueConfig _queueConfig; private readonly QueueConfig _queueConfig;
private readonly UIConfig _uiConfig;
private Producer _annotationProducer = null!; private Producer _annotationProducer = null!;
private Producer _annotationConfirmProducer = null!;
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger, IDbFactory dbFactory, IOptions<QueueConfig> queueConfig) public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger,
IDbFactory dbFactory,
IOptions<QueueConfig> queueConfig,
IOptions<UIConfig> uiConfig,
IAzaionApi azaionApi)
{ {
_logger = logger; _logger = logger;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_azaionApi = azaionApi;
_queueConfig = queueConfig.Value; _queueConfig = queueConfig.Value;
_uiConfig = uiConfig.Value;
Task.Run(async () => await ProcessQueue()); Task.Run(async () => await ProcessQueue());
} }
@@ -41,78 +52,63 @@ public class FailsafeAnnotationsProducer
}); });
} }
private async Task Init(CancellationToken cancellationToken = default) private async Task ProcessQueue(CancellationToken ct = default)
{ {
_annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE)); _annotationProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_QUEUE));
_annotationConfirmProducer = await Producer.Create(new ProducerConfig(await GetProducerQueueConfig(), Constants.MQ_ANNOTATIONS_CONFIRM_QUEUE)); while (!ct.IsCancellationRequested)
}
private async Task ProcessQueue(CancellationToken cancellationToken = default)
{
await Init(cancellationToken);
while (!cancellationToken.IsCancellationRequested)
{
var messages = await GetFromInnerQueue(cancellationToken);
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
{ {
var sent = false; var sent = false;
while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send while (!sent || !ct.IsCancellationRequested) //Waiting for send
{ {
try try
{ {
var createdMessages = messagesChunk var result = await _dbFactory.Run(async db =>
.Where(x => x.Status == AnnotationStatus.Created) {
.Select(x => new Message(MessagePackSerializer.Serialize(x))) var records = await db.AnnotationsQueueRecords.OrderBy(x => x.DateTime).ToListAsync(token: ct);
var editedCreatedNames = records
.Where(x => x.Operation.In(AnnotationStatus.Created, AnnotationStatus.Edited))
.Select(x => x.AnnotationNames.FirstOrDefault())
.ToList(); .ToList();
if (createdMessages.Any())
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
var validatedMessages = messagesChunk var annotationsDict = await db.Annotations.LoadWith(x => x.Detections)
.Where(x => x.Status == AnnotationStatus.Validated) .Where(x => editedCreatedNames.Contains(x.Name))
.Select(x => new Message(MessagePackSerializer.Serialize(x))) .ToDictionaryAsync(a => a.Name, token: ct);
.ToList();
if (validatedMessages.Any())
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
await _dbFactory.Run(async db => var messages = new List<Message>();
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken)); foreach (var record in records)
sent = true;
_dbFactory.SaveToDisk();
}
catch (Exception e)
{ {
_logger.LogError(e, e.Message); var appProperties = new ApplicationProperties
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); {
} { nameof(AnnotationStatus), record.Operation.ToString() },
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken); { nameof(User.Email), _azaionApi.CurrentUser.Email }
} };
}
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
}
}
private async Task<List<AnnotationCreatedMessage>> GetFromInnerQueue(CancellationToken cancellationToken = default) if (record.Operation.In(AnnotationStatus.Validated, AnnotationStatus.Deleted))
{ {
return await _dbFactory.Run(async db => var message = new Message(MessagePackSerializer.Serialize(new AnnotationBulkMessage
{ {
var annotations = await db.AnnotationsQueue.Join( AnnotationNames = record.AnnotationNames.ToArray()
db.Annotations.LoadWith(x => x.Detections), aq => aq.Name, a => a.Name, (aq, a) => a) })) { ApplicationProperties = appProperties };
.ToListAsync(token: cancellationToken);
var messages = new List<AnnotationCreatedMessage>(); messages.Add(message);
var badImages = new List<string>(); }
foreach (var annotation in annotations) else
{ {
try var annotation = annotationsDict!.GetValueOrDefault(record.AnnotationNames.FirstOrDefault());
{ if (annotation == null)
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken); continue;
var annCreateMessage = new AnnotationCreatedMessage
var image = record.Operation == AnnotationStatus.Created
? await File.ReadAllBytesAsync(annotation.ImagePath, ct)
: null;
var annMessage = new AnnotationMessage
{ {
Name = annotation.Name, Name = annotation.Name,
OriginalMediaName = annotation.OriginalMediaName, OriginalMediaName = annotation.OriginalMediaName,
Time = annotation.Time, Time = annotation.Time,
CreatedRole = annotation.CreatedRole, Role = annotation.CreatedRole,
CreatedEmail = annotation.CreatedEmail, Email = annotation.CreatedEmail,
CreatedDate = annotation.CreatedDate, CreatedDate = annotation.CreatedDate,
Status = annotation.AnnotationStatus, Status = annotation.AnnotationStatus,
@@ -121,27 +117,46 @@ public class FailsafeAnnotationsProducer
Detections = JsonConvert.SerializeObject(annotation.Detections), Detections = JsonConvert.SerializeObject(annotation.Detections),
Source = annotation.Source, Source = annotation.Source,
}; };
messages.Add(annCreateMessage); var message = new Message(MessagePackSerializer.Serialize(annMessage)) { ApplicationProperties = appProperties };
messages.Add(message);
}
}
return (messages, records);
});
if (result.messages.Any())
{
await _annotationProducer.Send(result.messages, CompressionType.Gzip);
var ids = result.records.Select(x => x.Id).ToList();
var removed = await _dbFactory.Run(async db => await db.AnnotationsQueueRecords.DeleteAsync(x => ids.Contains(x.Id), token: ct));
sent = true;
_dbFactory.SaveToDisk();
}
} }
catch (Exception e) catch (Exception e)
{ {
_logger.LogError(e, e.Message); _logger.LogError(e, e.Message);
badImages.Add(annotation.Name); await Task.Delay(TimeSpan.FromSeconds(10), ct);
} }
await Task.Delay(TimeSpan.FromSeconds(10), ct);
}
}
await Task.Delay(TimeSpan.FromSeconds(5), ct);
} }
if (badImages.Any()) public async Task SendToInnerQueue(List<string> annotationNames, AnnotationStatus status, CancellationToken cancellationToken = default)
{
await db.AnnotationsQueue.Where(x => badImages.Contains(x.Name)).DeleteAsync(token: cancellationToken);
_dbFactory.SaveToDisk();
}
return messages;
});
}
public async Task SendToInnerQueue(Annotation annotation, CancellationToken cancellationToken = default)
{ {
if (_uiConfig.SilentDetection)
return;
await _dbFactory.Run(async db => await _dbFactory.Run(async db =>
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken)); await db.InsertAsync(new AnnotationQueueRecord
{
Id = Guid.NewGuid(),
DateTime = DateTime.UtcNow,
Operation = status,
AnnotationNames = annotationNames
}, token: cancellationToken));
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ public interface IGpsMatcherService
public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDownloader satelliteTileDownloader, IOptions<DirectoriesConfig> dirConfig) : IGpsMatcherService
{ {
private const int ZOOM_LEVEL = 18; private const int ZOOM_LEVEL = 18;
private const int POINTS_COUNT = 5; private const int POINTS_COUNT = 10;
private const int DISTANCE_BETWEEN_POINTS_M = 100; private const int DISTANCE_BETWEEN_POINTS_M = 100;
private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1); private const double SATELLITE_RADIUS_M = DISTANCE_BETWEEN_POINTS_M * (POINTS_COUNT + 1);
+1 -2
View File
@@ -23,13 +23,12 @@ public class StartMatchingEvent
public int ImagesCount { get; set; } public int ImagesCount { get; set; }
public double Latitude { get; set; } public double Latitude { get; set; }
public double Longitude { get; set; } public double Longitude { get; set; }
public string ProcessingType { get; set; } = "cuda";
public int Altitude { get; set; } = 400; public int Altitude { get; set; } = 400;
public double CameraSensorWidth { get; set; } = 23.5; public double CameraSensorWidth { get; set; } = 23.5;
public double CameraFocalLength { get; set; } = 24; public double CameraFocalLength { get; set; } = 24;
public override string ToString() => public override string ToString() =>
$"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{ProcessingType},{Altitude},{CameraSensorWidth},{CameraFocalLength}"; $"{RouteDir},{SatelliteImagesDir},{ImagesCount},{Latitude},{Longitude},{Altitude},{CameraSensorWidth},{CameraFocalLength}";
} }
public class GpsMatcherClient : IGpsMatcherClient public class GpsMatcherClient : IGpsMatcherClient
+150
View File
@@ -0,0 +1,150 @@
using System.Diagnostics;
using System.IO;
using System.Text;
using Azaion.Common.Database;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands;
using Azaion.CommonSecurity.Exceptions;
using Azaion.CommonSecurity.Services;
using MessagePack;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.Common.Services;
public interface IInferenceClient : IDisposable
{
event EventHandler<RemoteCommand> BytesReceived;
event EventHandler<RemoteCommand>? InferenceDataReceived;
event EventHandler<RemoteCommand>? AIAvailabilityReceived;
void Send(RemoteCommand create);
void Stop();
}
public class InferenceClient : IInferenceClient, IResourceLoader
{
private CancellationTokenSource _waitFileCancelSource = new();
public event EventHandler<RemoteCommand>? BytesReceived;
public event EventHandler<RemoteCommand>? InferenceDataReceived;
public event EventHandler<RemoteCommand>? AIAvailabilityReceived;
private readonly DealerSocket _dealer = new();
private readonly NetMQPoller _poller = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
public InferenceClient(IOptions<InferenceClientConfig> config, CancellationToken ct)
{
_inferenceClientConfig = config.Value;
Start(ct);
}
private void Start(CancellationToken ct = default)
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
Arguments = $"--port {_inferenceClientConfig.ZeroMqPort} --api {_inferenceClientConfig.ApiUrl}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.ReceiveReady += (_, e) => ProcessClientCommand(e.Socket, ct);
_poller.Add(_dealer);
_ = Task.Run(() => _poller.RunAsync(), ct);
}
private void ProcessClientCommand(NetMQSocket socket, CancellationToken ct = default)
{
while (socket.TryReceiveFrameBytes(TimeSpan.Zero, out var bytes))
{
if (bytes?.Length == 0)
continue;
var remoteCommand = MessagePackSerializer.Deserialize<RemoteCommand>(bytes, cancellationToken: ct);
switch (remoteCommand.CommandType)
{
case CommandType.DataBytes:
BytesReceived?.Invoke(this, remoteCommand);
break;
case CommandType.InferenceData:
InferenceDataReceived?.Invoke(this, remoteCommand);
break;
case CommandType.AIAvailabilityResult:
AIAvailabilityReceived?.Invoke(this, remoteCommand);
break;
}
}
}
public void Stop()
{
}
public void Send(RemoteCommand command)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
}
public MemoryStream LoadFile(string fileName, string? folder = null, TimeSpan? timeout = null)
{
//TODO: Bad solution, look for better implementation
byte[] bytes = [];
Exception? exception = null;
_waitFileCancelSource = new CancellationTokenSource();
Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder)));
BytesReceived += OnBytesReceived;
void OnBytesReceived(object? sender, RemoteCommand command)
{
if (command.Data is null)
{
exception = new BusinessException(command.Message ?? "File is empty");
_waitFileCancelSource.Cancel();
}
bytes = command.Data!;
_waitFileCancelSource.Cancel();
}
_waitFileCancelSource.Token.WaitForCancel(timeout ?? TimeSpan.FromSeconds(15));
BytesReceived -= OnBytesReceived;
if (exception != null)
throw exception;
return new MemoryStream(bytes);
}
public void Dispose()
{
_waitFileCancelSource.Dispose();
_poller.Stop();
_poller.Dispose();
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Disconnect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
_dealer.Close();
_dealer.Dispose();
}
}
+52 -23
View File
@@ -1,11 +1,11 @@
using System.Text; using Azaion.Common.Database;
using Azaion.Common.Database;
using Azaion.Common.DTO.Config; using Azaion.Common.DTO.Config;
using Azaion.CommonSecurity; using Azaion.Common.Events;
using Azaion.Common.Extensions;
using Azaion.CommonSecurity.DTO.Commands; using Azaion.CommonSecurity.DTO.Commands;
using Azaion.CommonSecurity.Services; using Azaion.CommonSecurity.Services;
using MediatR;
using MessagePack; using MessagePack;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
@@ -13,45 +13,74 @@ namespace Azaion.Common.Services;
public interface IInferenceService public interface IInferenceService
{ {
Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default); Task RunInference(List<string> mediaPaths, CancellationToken ct = default);
void StopInference(); void StopInference();
} }
public class InferenceService(ILogger<InferenceService> logger, IInferenceClient client, IAzaionApi azaionApi, IOptions<AIRecognitionConfig> aiConfigOptions) : IInferenceService public class InferenceService : IInferenceService
{ {
public async Task RunInference(List<string> mediaPaths, Func<AnnotationImage, Task> processAnnotation, CancellationToken detectToken = default) private readonly IInferenceClient _client;
private readonly IAzaionApi _azaionApi;
private readonly IOptions<AIRecognitionConfig> _aiConfigOptions;
private readonly IAnnotationService _annotationService;
private readonly IMediator _mediator;
private CancellationTokenSource _inferenceCancelTokenSource = new();
public InferenceService(
ILogger<InferenceService> logger,
IInferenceClient client,
IAzaionApi azaionApi,
IOptions<AIRecognitionConfig> aiConfigOptions,
IAnnotationService annotationService,
IMediator mediator)
{ {
client.Send(RemoteCommand.Create(CommandType.Login, azaionApi.Credentials)); _client = client;
var aiConfig = aiConfigOptions.Value; _azaionApi = azaionApi;
_aiConfigOptions = aiConfigOptions;
_annotationService = annotationService;
_mediator = mediator;
aiConfig.Paths = mediaPaths; client.InferenceDataReceived += async (sender, command) =>
client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
while (!detectToken.IsCancellationRequested)
{ {
try try
{ {
var bytes = client.GetBytes(ct: detectToken); if (command.Message == "DONE")
if (bytes == null) {
throw new Exception("Can't get bytes from inference client"); _inferenceCancelTokenSource?.Cancel();
if (bytes.Length == 4 && Encoding.UTF8.GetString(bytes) == "DONE")
return; return;
}
var annotationImage = MessagePackSerializer.Deserialize<AnnotationImage>(bytes, cancellationToken: detectToken); var annImage = MessagePackSerializer.Deserialize<AnnotationImage>(command.Data);
await ProcessDetection(annImage);
await processAnnotation(annotationImage);
} }
catch (Exception e) catch (Exception e)
{ {
logger.LogError(e, e.Message); logger.LogError(e, e.Message);
break;
} }
};
} }
private async Task ProcessDetection(AnnotationImage annotationImage, CancellationToken ct = default)
{
var annotation = await _annotationService.SaveAnnotation(annotationImage, ct);
await _mediator.Publish(new AnnotationAddedEvent(annotation), ct);
}
public async Task RunInference(List<string> mediaPaths, CancellationToken ct = default)
{
_inferenceCancelTokenSource = new CancellationTokenSource();
_client.Send(RemoteCommand.Create(CommandType.Login, _azaionApi.Credentials));
var aiConfig = _aiConfigOptions.Value;
aiConfig.Paths = mediaPaths;
_client.Send(RemoteCommand.Create(CommandType.Inference, aiConfig));
using var combinedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(ct, _inferenceCancelTokenSource.Token);
await combinedTokenSource.Token.AsTask();
} }
public void StopInference() public void StopInference()
{ {
client.Send(RemoteCommand.Create(CommandType.StopInference)); _client.Send(RemoteCommand.Create(CommandType.StopInference));
} }
} }
@@ -8,6 +8,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" /> <PackageReference Include="LazyCache.AspNetCore" Version="2.4.0" />
<PackageReference Include="MediatR" Version="12.4.1" />
<PackageReference Include="MessagePack" Version="3.1.0" /> <PackageReference Include="MessagePack" Version="3.1.0" />
<PackageReference Include="MessagePack.Annotations" Version="3.1.0" /> <PackageReference Include="MessagePack.Annotations" Version="3.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
@@ -3,7 +3,7 @@
namespace Azaion.CommonSecurity.DTO.Commands; namespace Azaion.CommonSecurity.DTO.Commands;
[MessagePackObject] [MessagePackObject]
public class RemoteCommand(CommandType commandType, byte[]? data = null) public class RemoteCommand(CommandType commandType, byte[]? data = null, string? message = null)
{ {
[Key("CommandType")] [Key("CommandType")]
public CommandType CommandType { get; set; } = commandType; public CommandType CommandType { get; set; } = commandType;
@@ -11,11 +11,14 @@ public class RemoteCommand(CommandType commandType, byte[]? data = null)
[Key("Data")] [Key("Data")]
public byte[]? Data { get; set; } = data; public byte[]? Data { get; set; } = data;
[Key("Message")]
public string? Message { get; set; } = message;
public static RemoteCommand Create(CommandType commandType) => public static RemoteCommand Create(CommandType commandType) =>
new(commandType); new(commandType);
public static RemoteCommand Create<T>(CommandType commandType, T data) where T : class => public static RemoteCommand Create<T>(CommandType commandType, T data, string? message = null) where T : class =>
new(commandType, MessagePackSerializer.Serialize(data)); new(commandType, MessagePackSerializer.Serialize(data), message);
} }
[MessagePackObject] [MessagePackObject]
@@ -34,7 +37,12 @@ public enum CommandType
None = 0, None = 0,
Login = 10, Login = 10,
Load = 20, Load = 20,
DataBytes = 25,
Inference = 30, Inference = 30,
InferenceData = 35,
StopInference = 40, StopInference = 40,
Exit = 100 AIAvailabilityCheck = 80,
AIAvailabilityResult = 85,
Error = 90,
Exit = 100,
} }
@@ -10,7 +10,7 @@ public abstract class ExternalClientConfig
public class InferenceClientConfig : ExternalClientConfig public class InferenceClientConfig : ExternalClientConfig
{ {
public string ApiUrl { get; set; } public string ApiUrl { get; set; } = null!;
} }
public class GpsDeniedClientConfig : ExternalClientConfig public class GpsDeniedClientConfig : ExternalClientConfig
@@ -1,4 +1,4 @@
namespace Azaion.Common.Extensions; namespace Azaion.CommonSecurity.DTO;
public static class EnumerableExtensions public static class EnumerableExtensions
{ {
+1 -3
View File
@@ -1,6 +1,4 @@
using Azaion.Common.Extensions; namespace Azaion.CommonSecurity.DTO;
namespace Azaion.CommonSecurity.DTO;
public enum RoleEnum public enum RoleEnum
{ {
@@ -0,0 +1,3 @@
namespace Azaion.CommonSecurity.Exceptions;
public class BusinessException(string message) : Exception(message);
@@ -1,104 +0,0 @@
using System.Diagnostics;
using System.Text;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands;
using MessagePack;
using Microsoft.Extensions.Options;
using NetMQ;
using NetMQ.Sockets;
namespace Azaion.CommonSecurity.Services;
public interface IInferenceClient
{
void Send(RemoteCommand create);
T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class;
byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default);
void Stop();
}
public class InferenceClient : IInferenceClient
{
private readonly DealerSocket _dealer = new();
private readonly Guid _clientId = Guid.NewGuid();
private readonly InferenceClientConfig _inferenceClientConfig;
public InferenceClient(IOptions<InferenceClientConfig> config)
{
_inferenceClientConfig = config.Value;
Start();
_ = Task.Run(ProcessClientCommands);
}
private void Start()
{
try
{
using var process = new Process();
process.StartInfo = new ProcessStartInfo
{
FileName = SecurityConstants.EXTERNAL_INFERENCE_PATH,
Arguments = $"--port {_inferenceClientConfig.ZeroMqPort} --api {_inferenceClientConfig.ApiUrl}",
//RedirectStandardOutput = true,
//RedirectStandardError = true,
//CreateNoWindow = true
};
process.OutputDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.ErrorDataReceived += (_, e) => { if (e.Data != null) Console.WriteLine(e.Data); };
process.Start();
}
catch (Exception e)
{
Console.WriteLine(e);
//throw;
}
_dealer.Options.Identity = Encoding.UTF8.GetBytes(_clientId.ToString("N"));
_dealer.Connect($"tcp://{_inferenceClientConfig.ZeroMqHost}:{_inferenceClientConfig.ZeroMqPort}");
}
private async Task ProcessClientCommands()
{
//TODO: implement always on ready to client's requests. Utilize RemoteCommand
await Task.CompletedTask;
}
public void Stop()
{
if (!_dealer.IsDisposed)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(new RemoteCommand(CommandType.Exit)));
_dealer.Close();
}
}
public void Send(RemoteCommand command)
{
_dealer.SendFrame(MessagePackSerializer.Serialize(command));
}
public T? Get<T>(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default) where T : class
{
var bytes = GetBytes(retries, tryTimeoutSeconds, ct);
return bytes != null ? MessagePackSerializer.Deserialize<T>(bytes, cancellationToken: ct) : null;
}
public byte[]? GetBytes(int retries = 24, int tryTimeoutSeconds = 5, CancellationToken ct = default)
{
var tryNum = 0;
while (!ct.IsCancellationRequested && tryNum < retries)
{
tryNum++;
if (!_dealer.TryReceiveFrameBytes(TimeSpan.FromSeconds(tryTimeoutSeconds), out var bytes))
continue;
return bytes;
}
if (!ct.IsCancellationRequested)
throw new Exception($"Unable to get bytes after {tryNum - 1} retries, {tryTimeoutSeconds} seconds each");
return null;
}
}
@@ -1,22 +1,8 @@
using Azaion.CommonSecurity.DTO.Commands; using Azaion.CommonSecurity.DTO.Commands;
using Microsoft.Extensions.DependencyInjection;
namespace Azaion.CommonSecurity.Services; namespace Azaion.CommonSecurity.Services;
public interface IResourceLoader public interface IResourceLoader
{ {
MemoryStream LoadFile(string fileName, string? folder = null); MemoryStream LoadFile(string fileName, string? folder, TimeSpan? timeout = null);
}
public class ResourceLoader([FromKeyedServices(SecurityConstants.EXTERNAL_INFERENCE_PATH)] IInferenceClient inferenceClient) : IResourceLoader
{
public MemoryStream LoadFile(string fileName, string? folder = null)
{
inferenceClient.Send(RemoteCommand.Create(CommandType.Load, new LoadFileData(fileName, folder)));
var bytes = inferenceClient.GetBytes(2, 3);
if (bytes == null)
throw new Exception($"Unable to receive {fileName}");
return new MemoryStream(bytes);
}
} }
+13 -1
View File
@@ -30,7 +30,8 @@
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition> <RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="32"></RowDefinition> <RowDefinition Height="18"></RowDefinition>
<RowDefinition Height="14"></RowDefinition>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Image <Image
Grid.Row="0" Grid.Row="0"
@@ -42,6 +43,17 @@
Grid.Row="1" Grid.Row="1"
Foreground="LightGray" Foreground="LightGray"
Text="{Binding ImageName}" /> Text="{Binding ImageName}" />
<TextBlock
Grid.Row="2"
HorizontalAlignment="Right"
Foreground="Gray">
<TextBlock.Text>
<MultiBinding StringFormat="{}{0}: {1}">
<Binding Mode="OneWay" Path="CreatedDate"></Binding>
<Binding Mode="OneWay" Path="CreatedEmail"></Binding>
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</Grid> </Grid>
</Border> </Border>
</DataTemplate> </DataTemplate>
+19 -11
View File
@@ -8,6 +8,7 @@ using Azaion.Common.DTO.Config;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO; using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services;
using LinqToDB; using LinqToDB;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -36,6 +37,7 @@ public partial class DatasetExplorer
private readonly IMediator _mediator; private readonly IMediator _mediator;
public readonly List<DetectionClass> AnnotationsClasses; public readonly List<DetectionClass> AnnotationsClasses;
private IAzaionApi _azaionApi;
public bool ThumbnailLoading { get; set; } public bool ThumbnailLoading { get; set; }
@@ -49,7 +51,8 @@ public partial class DatasetExplorer
IGalleryService galleryService, IGalleryService galleryService,
FormState formState, FormState formState,
IDbFactory dbFactory, IDbFactory dbFactory,
IMediator mediator) IMediator mediator,
IAzaionApi azaionApi)
{ {
InitializeComponent(); InitializeComponent();
@@ -59,6 +62,7 @@ public partial class DatasetExplorer
_galleryService = galleryService; _galleryService = galleryService;
_dbFactory = dbFactory; _dbFactory = dbFactory;
_mediator = mediator; _mediator = mediator;
_azaionApi = azaionApi;
var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList(); var photoModes = Enum.GetValues(typeof(PhotoMode)).Cast<PhotoMode>().ToList();
_annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id)) _annotationsDict = _annotationConfig.DetectionClasses.SelectMany(cls => photoModes.Select(mode => (int)mode + cls.Id))
@@ -87,6 +91,7 @@ public partial class DatasetExplorer
ThumbnailsView.SelectionChanged += (_, _) => ThumbnailsView.SelectionChanged += (_, _) =>
{ {
StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}"; StatusText.Text = $"Обрано: {ThumbnailsView.SelectedItems.Count} | {ThumbnailsView.SelectedIndex} / {SelectedAnnotations.Count}";
ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Any(x => x.IsSeed) ValidateBtn.Visibility = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Any(x => x.IsSeed)
? Visibility.Visible ? Visibility.Visible
: Visibility.Hidden; : Visibility.Hidden;
@@ -273,10 +278,9 @@ public partial class DatasetExplorer
if (result != MessageBoxResult.Yes) if (result != MessageBoxResult.Yes)
return; return;
var annotations = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Select(x => x.Annotation) var annotationNames = ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>().Select(x => x.Annotation.Name).ToList();
.ToList();
await _mediator.Publish(new AnnotationsDeletedEvent(annotations)); await _mediator.Publish(new AnnotationsDeletedEvent(annotationNames));
ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected); ThumbnailsView.SelectedIndex = Math.Min(SelectedAnnotations.Count, tempSelected);
} }
@@ -284,14 +288,18 @@ public partial class DatasetExplorer
{ {
SelectedAnnotations.Clear(); SelectedAnnotations.Clear();
SelectedAnnotationDict.Clear(); SelectedAnnotationDict.Clear();
var annotations = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId]; var annThumbnails = _annotationsDict[ExplorerEditor.CurrentAnnClass.YoloId]
foreach (var ann in annotations .Select(x => new AnnotationThumbnail(x.Value, _azaionApi.CurrentUser.Role.IsValidator()))
.OrderBy(x => x.Value.AnnotationStatus) .OrderBy(x => !x.IsSeed)
.ThenByDescending(x => x.Value.CreatedDate)) .ThenByDescending(x =>x.Annotation.CreatedDate);
//var dict = annThumbnails.Take(20).ToDictionary(x => x.Annotation.Name, x => x.IsSeed);
foreach (var thumb in annThumbnails)
{ {
var annThumb = new AnnotationThumbnail(ann.Value); SelectedAnnotations.Add(thumb);
SelectedAnnotations.Add(annThumb); SelectedAnnotationDict.Add(thumb.Annotation.Name, thumb);
SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
} }
await Task.CompletedTask; await Task.CompletedTask;
} }
+28 -12
View File
@@ -1,19 +1,20 @@
using System.IO; using System.Windows.Input;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using Azaion.Common.Database; using Azaion.Common.Database;
using Azaion.Common.DTO; using Azaion.Common.DTO;
using Azaion.Common.DTO.Queue;
using Azaion.Common.Events; using Azaion.Common.Events;
using Azaion.Common.Services; using Azaion.Common.Services;
using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.Services;
using MediatR; using MediatR;
using Microsoft.Extensions.Logging;
namespace Azaion.Dataset; namespace Azaion.Dataset;
public class DatasetExplorerEventHandler( public class DatasetExplorerEventHandler(
ILogger<DatasetExplorerEventHandler> logger,
DatasetExplorer datasetExplorer, DatasetExplorer datasetExplorer,
AnnotationService annotationService) : IAnnotationService annotationService,
IAzaionApi azaionApi) :
INotificationHandler<KeyEvent>, INotificationHandler<KeyEvent>,
INotificationHandler<DatasetExplorerControlEvent>, INotificationHandler<DatasetExplorerControlEvent>,
INotificationHandler<AnnotationCreatedEvent>, INotificationHandler<AnnotationCreatedEvent>,
@@ -26,7 +27,9 @@ public class DatasetExplorerEventHandler(
{ Key.X, PlaybackControlEnum.RemoveAllAnns }, { Key.X, PlaybackControlEnum.RemoveAllAnns },
{ Key.Escape, PlaybackControlEnum.Close }, { Key.Escape, PlaybackControlEnum.Close },
{ Key.Down, PlaybackControlEnum.Next }, { Key.Down, PlaybackControlEnum.Next },
{ Key.PageDown, PlaybackControlEnum.Next },
{ Key.Up, PlaybackControlEnum.Previous }, { Key.Up, PlaybackControlEnum.Previous },
{ Key.PageUp, PlaybackControlEnum.Previous },
{ Key.V, PlaybackControlEnum.ValidateAnnotations}, { Key.V, PlaybackControlEnum.ValidateAnnotations},
}; };
@@ -97,7 +100,7 @@ public class DatasetExplorerEventHandler(
var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>() var annotations = datasetExplorer.ThumbnailsView.SelectedItems.Cast<AnnotationThumbnail>()
.Select(x => x.Annotation) .Select(x => x.Annotation)
.ToList(); .ToList();
await annotationService.ValidateAnnotations(annotations, cancellationToken); await annotationService.ValidateAnnotations(annotations.Select(x => x.Name).ToList(), token: cancellationToken);
foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation))) foreach (var ann in datasetExplorer.SelectedAnnotations.Where(x => annotations.Contains(x.Annotation)))
{ {
ann.Annotation.AnnotationStatus = AnnotationStatus.Validated; ann.Annotation.AnnotationStatus = AnnotationStatus.Validated;
@@ -116,20 +119,23 @@ public class DatasetExplorerEventHandler(
var annotation = notification.Annotation; var annotation = notification.Annotation;
var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber; var selectedClass = datasetExplorer.LvClasses.CurrentClassNumber;
//TODO: For editing existing need to handle updates
datasetExplorer.AddAnnotationToDict(annotation); datasetExplorer.AddAnnotationToDict(annotation);
if (annotation.Classes.Contains(selectedClass) || selectedClass == -1) if (annotation.Classes.Contains(selectedClass) || selectedClass == -1)
{ {
var annThumb = new AnnotationThumbnail(annotation); var index = 0;
var annThumb = new AnnotationThumbnail(annotation, azaionApi.CurrentUser.Role.IsValidator());
if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name)) if (datasetExplorer.SelectedAnnotationDict.ContainsKey(annThumb.Annotation.Name))
{ {
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name); var ann = datasetExplorer.SelectedAnnotations.FirstOrDefault(x => x.Annotation.Name == annThumb.Annotation.Name);
if (ann != null) if (ann != null)
{
index = datasetExplorer.SelectedAnnotations.IndexOf(ann);
datasetExplorer.SelectedAnnotations.Remove(ann); datasetExplorer.SelectedAnnotations.Remove(ann);
} }
}
datasetExplorer.SelectedAnnotations.Insert(0, annThumb); datasetExplorer.SelectedAnnotations.Insert(index, annThumb);
datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb); datasetExplorer.SelectedAnnotationDict.Add(annThumb.Annotation.Name, annThumb);
} }
}); });
@@ -138,9 +144,12 @@ public class DatasetExplorerEventHandler(
public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken) public async Task Handle(AnnotationsDeletedEvent notification, CancellationToken cancellationToken)
{ {
var names = notification.Annotations.Select(x => x.Name).ToList(); try
{
datasetExplorer.Dispatcher.Invoke(() =>
{
var annThumbs = datasetExplorer.SelectedAnnotationDict var annThumbs = datasetExplorer.SelectedAnnotationDict
.Where(x => names.Contains(x.Key)) .Where(x => notification.AnnotationNames.Contains(x.Key))
.Select(x => x.Value) .Select(x => x.Value)
.ToList(); .ToList();
foreach (var annThumb in annThumbs) foreach (var annThumb in annThumbs)
@@ -148,6 +157,13 @@ public class DatasetExplorerEventHandler(
datasetExplorer.SelectedAnnotations.Remove(annThumb); datasetExplorer.SelectedAnnotations.Remove(annThumb);
datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name); datasetExplorer.SelectedAnnotationDict.Remove(annThumb.Annotation.Name);
} }
});
}
catch (Exception e)
{
logger.LogError(e, e.Message);
throw;
}
await Task.CompletedTask; await Task.CompletedTask;
} }
} }
+26 -4
View File
@@ -1,4 +1,5 @@
import json import json
import os
from http import HTTPStatus from http import HTTPStatus
from os import path from os import path
from uuid import UUID from uuid import UUID
@@ -6,6 +7,7 @@ import jwt
import requests import requests
cimport constants cimport constants
import yaml import yaml
from requests import HTTPError
from cdn_manager cimport CDNManager, CDNCredentials from cdn_manager cimport CDNManager, CDNCredentials
from hardware_service cimport HardwareService from hardware_service cimport HardwareService
@@ -23,6 +25,9 @@ cdef class ApiClient:
cdef set_credentials(self, Credentials credentials): cdef set_credentials(self, Credentials credentials):
self.credentials = credentials self.credentials = credentials
if self.cdn_manager is not None:
return
yaml_bytes = self.load_bytes(constants.CDN_CONFIG, <str>'') yaml_bytes = self.load_bytes(constants.CDN_CONFIG, <str>'')
yaml_config = yaml.safe_load(yaml_bytes) yaml_config = yaml.safe_load(yaml_bytes)
creds = CDNCredentials(yaml_config["host"], creds = CDNCredentials(yaml_config["host"],
@@ -34,11 +39,18 @@ cdef class ApiClient:
self.cdn_manager = CDNManager(creds) self.cdn_manager = CDNManager(creds)
cdef login(self): cdef login(self):
response = None
try:
response = requests.post(f"{self.api_url}/login", response = requests.post(f"{self.api_url}/login",
json={"email": self.credentials.email, "password": self.credentials.password}) json={"email": self.credentials.email, "password": self.credentials.password})
response.raise_for_status() response.raise_for_status()
token = response.json()["token"] token = response.json()["token"]
self.set_token(token) self.set_token(token)
except HTTPError as e:
print(response.json())
if response.status_code == HTTPStatus.CONFLICT:
res = response.json()
raise Exception(res['Message'])
cdef set_token(self, str token): cdef set_token(self, str token):
@@ -123,11 +135,21 @@ cdef class ApiClient:
return data return data
cdef load_big_small_resource(self, str resource_name, str folder, str key): cdef load_big_small_resource(self, str resource_name, str folder, str key):
cdef str big_part = path.join(<str>folder, f'{resource_name}.big') cdef str big_part = f'{resource_name}.big'
cdef str small_part = f'{resource_name}.small' cdef str small_part = f'{resource_name}.small'
with open(<str>big_part, 'rb') as binary_file: print(f'checking on existence for {folder}\\{big_part}')
if os.path.exists(os.path.join(<str> folder, big_part)):
with open(path.join(<str> folder, big_part), 'rb') as binary_file:
encrypted_bytes_big = binary_file.read() encrypted_bytes_big = binary_file.read()
print(f'local file {folder}\\{big_part} is found!')
else:
print(f'downloading file {folder}\\{big_part} from cdn...')
if self.cdn_manager.download(folder, big_part):
with open(path.join(<str> folder, big_part), 'rb') as binary_file:
encrypted_bytes_big = binary_file.read()
else:
return None
encrypted_bytes_small = self.load_bytes(small_part, folder) encrypted_bytes_small = self.load_bytes(small_part, folder)
@@ -145,7 +167,7 @@ cdef class ApiClient:
part_big = resource_encrypted[part_small_size:] part_big = resource_encrypted[part_small_size:]
self.cdn_manager.upload(<str>constants.MODELS_FOLDER, <str>big_part_name, part_big) self.cdn_manager.upload(folder, <str>big_part_name, part_big)
with open(path.join(<str>folder, <str>big_part_name), 'wb') as f: with open(path.join(<str>folder, <str>big_part_name), 'wb') as f:
f.write(part_big) f.write(part_big)
self.upload_file(small_part_name, part_small, constants.MODELS_FOLDER) self.upload_file(small_part_name, part_small, folder)
+3 -3
View File
@@ -31,10 +31,10 @@ cdef class CDNManager:
print(e) print(e)
return False return False
cdef download(self, str bucket, str filename): cdef download(self, str folder, str filename):
try: try:
self.download_client.download_file(bucket, filename, filename) self.download_client.download_file(folder, filename, f'{folder}\\{filename}')
print(f'downloaded {filename} from the {bucket} to current folder') print(f'downloaded {filename} from the {folder} to current folder')
return True return True
except Exception as e: except Exception as e:
print(e) print(e)
-2
View File
@@ -13,7 +13,5 @@ cdef str MODELS_FOLDER
cdef int SMALL_SIZE_KB cdef int SMALL_SIZE_KB
cdef bytes DONE_SIGNAL
cdef log(str log_message, bytes client_id=*) cdef log(str log_message, bytes client_id=*)
+1 -1
View File
@@ -16,7 +16,7 @@ cdef class Inference:
cdef int model_width cdef int model_width
cdef int model_height cdef int model_height
cdef build_tensor_engine(self) cdef build_tensor_engine(self, object updater_callback)
cdef init_ai(self) cdef init_ai(self)
cdef bint is_building_engine cdef bint is_building_engine
cdef bint is_video(self, str filepath) cdef bint is_video(self, str filepath)
+12 -9
View File
@@ -32,7 +32,7 @@ cdef class Inference:
self.engine = None self.engine = None
self.is_building_engine = False self.is_building_engine = False
cdef build_tensor_engine(self): cdef build_tensor_engine(self, object updater_callback):
is_nvidia = HardwareService.has_nvidia_gpu() is_nvidia = HardwareService.has_nvidia_gpu()
if not is_nvidia: if not is_nvidia:
return return
@@ -40,18 +40,22 @@ cdef class Inference:
engine_filename = TensorRTEngine.get_engine_filename(0) engine_filename = TensorRTEngine.get_engine_filename(0)
key = Security.get_model_encryption_key() key = Security.get_model_encryption_key()
models_dir = constants.MODELS_FOLDER models_dir = constants.MODELS_FOLDER
if not os.path.exists(os.path.join(<str> models_dir, f'{engine_filename}.big')):
#TODO: Check cdn on engine exists, if there is, download
self.is_building_engine = True self.is_building_engine = True
time.sleep(8) # prevent simultaneously loading dll and models updater_callback('downloading')
if self.api_client.load_big_small_resource(engine_filename, models_dir, key):
print('tensor rt engine is here, no need to build')
self.is_building_engine = False
return
# time.sleep(8) # prevent simultaneously loading dll and models
updater_callback('converting')
onnx_model = self.api_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir, key) onnx_model = self.api_client.load_big_small_resource(constants.AI_ONNX_MODEL_FILE, models_dir, key)
model_bytes = TensorRTEngine.convert_from_onnx(onnx_model) model_bytes = TensorRTEngine.convert_from_onnx(onnx_model)
updater_callback('uploading')
self.api_client.upload_big_small_resource(model_bytes, <str> engine_filename, models_dir, key) self.api_client.upload_big_small_resource(model_bytes, <str> engine_filename, models_dir, key)
print('uploaded ') print(f'uploaded {engine_filename} to CDN and API')
self.is_building_engine = False self.is_building_engine = False
else:
print('tensor rt engine is here, no need to build')
cdef init_ai(self): cdef init_ai(self):
if self.engine is not None: if self.engine is not None:
@@ -161,7 +165,6 @@ cdef class Inference:
self.stop_signal = False self.stop_signal = False
self.init_ai() self.init_ai()
print(ai_config.paths)
for m in ai_config.paths: for m in ai_config.paths:
if self.is_video(m): if self.is_video(m):
videos.append(m) videos.append(m)
+23 -10
View File
@@ -34,7 +34,8 @@ cdef class CommandProcessor:
try: try:
command = self.inference_queue.get(timeout=0.5) command = self.inference_queue.get(timeout=0.5)
self.inference.run_inference(command) self.inference.run_inference(command)
self.remote_handler.send(command.client_id, <bytes>'DONE'.encode('utf-8')) end_inference_command = RemoteCommand(CommandType.INFERENCE_DATA, None, 'DONE')
self.remote_handler.send(command.client_id, end_inference_command.serialize())
except queue.Empty: except queue.Empty:
continue continue
except Exception as e: except Exception as e:
@@ -44,11 +45,13 @@ cdef class CommandProcessor:
cdef on_command(self, RemoteCommand command): cdef on_command(self, RemoteCommand command):
try: try:
if command.command_type == CommandType.LOGIN: if command.command_type == CommandType.LOGIN:
self.login(command) self.api_client.set_credentials(Credentials.from_msgpack(command.data))
elif command.command_type == CommandType.LOAD: elif command.command_type == CommandType.LOAD:
self.load_file(command) self.load_file(command)
elif command.command_type == CommandType.INFERENCE: elif command.command_type == CommandType.INFERENCE:
self.inference_queue.put(command) self.inference_queue.put(command)
elif command.command_type == CommandType.AI_AVAILABILITY_CHECK:
self.build_tensor_engine(command.client_id)
elif command.command_type == CommandType.STOP_INFERENCE: elif command.command_type == CommandType.STOP_INFERENCE:
self.inference.stop() self.inference.stop()
elif command.command_type == CommandType.EXIT: elif command.command_type == CommandType.EXIT:
@@ -59,18 +62,28 @@ cdef class CommandProcessor:
except Exception as e: except Exception as e:
print(f"Error handling client: {e}") print(f"Error handling client: {e}")
cdef login(self, RemoteCommand command): cdef build_tensor_engine(self, client_id):
self.api_client.set_credentials(Credentials.from_msgpack(command.data)) self.inference.build_tensor_engine(lambda status: self.build_tensor_status_updater(client_id, status))
Thread(target=self.inference.build_tensor_engine).start() # build AI engine in non-blocking thread self.remote_handler.send(client_id, RemoteCommand(CommandType.AI_AVAILABILITY_RESULT, None, 'enabled').serialize())
cdef build_tensor_status_updater(self, bytes client_id, str status):
self.remote_handler.send(client_id, RemoteCommand(CommandType.AI_AVAILABILITY_RESULT, None, status).serialize())
cdef load_file(self, RemoteCommand command): cdef load_file(self, RemoteCommand command):
cdef FileData file_data = FileData.from_msgpack(command.data) cdef RemoteCommand response
response = self.api_client.load_bytes(file_data.filename, file_data.folder) cdef FileData file_data
self.remote_handler.send(command.client_id, response) cdef bytes file_bytes
try:
file_data = FileData.from_msgpack(command.data)
file_bytes = self.api_client.load_bytes(file_data.filename, file_data.folder)
response = RemoteCommand(CommandType.DATA_BYTES, file_bytes)
except Exception as e:
response = RemoteCommand(CommandType.DATA_BYTES, None, str(e))
self.remote_handler.send(command.client_id, response.serialize())
cdef on_annotation(self, RemoteCommand cmd, Annotation annotation): cdef on_annotation(self, RemoteCommand cmd, Annotation annotation):
data = annotation.serialize() cdef RemoteCommand response = RemoteCommand(CommandType.INFERENCE_DATA, annotation.serialize())
self.remote_handler.send(cmd.client_id, data) self.remote_handler.send(cmd.client_id, response.serialize())
def stop(self): def stop(self):
self.inference.stop() self.inference.stop()
+6
View File
@@ -1,13 +1,19 @@
cdef enum CommandType: cdef enum CommandType:
LOGIN = 10 LOGIN = 10
LOAD = 20 LOAD = 20
DATA_BYTES = 25
INFERENCE = 30 INFERENCE = 30
INFERENCE_DATA = 35
STOP_INFERENCE = 40 STOP_INFERENCE = 40
AI_AVAILABILITY_CHECK = 80
AI_AVAILABILITY_RESULT = 85
ERROR = 90
EXIT = 100 EXIT = 100
cdef class RemoteCommand: cdef class RemoteCommand:
cdef public bytes client_id cdef public bytes client_id
cdef CommandType command_type cdef CommandType command_type
cdef str message
cdef bytes data cdef bytes data
@staticmethod @staticmethod
+10 -3
View File
@@ -1,16 +1,22 @@
import msgpack import msgpack
cdef class RemoteCommand: cdef class RemoteCommand:
def __init__(self, CommandType command_type, bytes data): def __init__(self, CommandType command_type, bytes data, str message=None):
self.command_type = command_type self.command_type = command_type
self.data = data self.data = data
self.message = message
def __str__(self): def __str__(self):
command_type_names = { command_type_names = {
10: "LOGIN", 10: "LOGIN",
20: "LOAD", 20: "LOAD",
25: "DATA_BYTES",
30: "INFERENCE", 30: "INFERENCE",
35: "INFERENCE_DATA",
40: "STOP_INFERENCE", 40: "STOP_INFERENCE",
80: "AI_AVAILABILITY_CHECK",
85: "AI_AVAILABILITY_RESULT",
90: "ERROR",
100: "EXIT" 100: "EXIT"
} }
data_str = f'{len(self.data)} bytes' if self.data else '' data_str = f'{len(self.data)} bytes' if self.data else ''
@@ -19,10 +25,11 @@ cdef class RemoteCommand:
@staticmethod @staticmethod
cdef from_msgpack(bytes data): cdef from_msgpack(bytes data):
unpacked = msgpack.unpackb(data, strict_map_key=False) unpacked = msgpack.unpackb(data, strict_map_key=False)
return RemoteCommand(unpacked.get("CommandType"), unpacked.get("Data")) return RemoteCommand(unpacked.get("CommandType"), unpacked.get("Data"), unpacked.get("Message"))
cdef bytes serialize(self): cdef bytes serialize(self):
return msgpack.packb({ return msgpack.packb({
"CommandType": self.command_type, "CommandType": self.command_type,
"Data": self.data "Data": self.data,
"Message": self.message
}) })
+6 -4
View File
@@ -70,10 +70,12 @@ cdef class RemoteCommandHandler:
worker_socket.close() worker_socket.close()
cdef send(self, bytes client_id, bytes data): cdef send(self, bytes client_id, bytes data):
with self._context.socket(zmq.DEALER) as socket: self._router.send_multipart([client_id, data])
socket.connect("inproc://backend")
socket.send_multipart([client_id, data]) # with self._context.socket(zmq.DEALER) as socket:
# constants.log(<str>f'Sent {len(data)} bytes.', client_id) # socket.connect("inproc://backend")
# socket.send_multipart([client_id, data])
# # constants.log(<str>f'Sent {len(data)} bytes.', client_id)
cdef stop(self): cdef stop(self):
self._shutdown_event.set() self._shutdown_event.set()
+1
View File
@@ -35,6 +35,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Build", "Build", "{CF141A48
build\publish.cmd = build\publish.cmd build\publish.cmd = build\publish.cmd
build\installer.full.iss = build\installer.full.iss build\installer.full.iss = build\installer.full.iss
build\installer.iterative.iss = build\installer.iterative.iss build\installer.iterative.iss = build\installer.iterative.iss
build\publish-full.cmd = build\publish-full.cmd
EndProjectSection EndProjectSection
EndProject EndProject
Global Global
+22 -14
View File
@@ -2,7 +2,6 @@
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Text; using System.Text;
using System.Text.Unicode;
using System.Windows; using System.Windows;
using System.Windows.Threading; using System.Windows.Threading;
using Azaion.Annotator; using Azaion.Annotator;
@@ -18,7 +17,6 @@ using Azaion.CommonSecurity.DTO;
using Azaion.CommonSecurity.DTO.Commands; using Azaion.CommonSecurity.DTO.Commands;
using Azaion.CommonSecurity.Services; using Azaion.CommonSecurity.Services;
using Azaion.Dataset; using Azaion.Dataset;
using LazyCache;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using MediatR; using MediatR;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@@ -40,13 +38,15 @@ public partial class App
private FormState _formState = null!; private FormState _formState = null!;
private IInferenceClient _inferenceClient = null!; private IInferenceClient _inferenceClient = null!;
private IResourceLoader _resourceLoader = null!;
private Stream _securedConfig = null!; private Stream _securedConfig = null!;
private Stream _systemConfig = null!; private Stream _systemConfig = null!;
private static readonly Guid KeyPressTaskId = Guid.NewGuid(); private static readonly Guid KeyPressTaskId = Guid.NewGuid();
private string _loadErrors = "";
private readonly ICache _cache = new MemoryCache(); private readonly ICache _cache = new MemoryCache();
private IAzaionApi _azaionApi = null!; private IAzaionApi _azaionApi = null!;
private CancellationTokenSource _mainCTokenSource = new();
private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e) private void OnDispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
{ {
@@ -89,10 +89,10 @@ public partial class App
new ConfigUpdater().CheckConfig(); new ConfigUpdater().CheckConfig();
var secureAppConfig = ReadSecureAppConfig(); var secureAppConfig = ReadSecureAppConfig();
var apiDir = secureAppConfig.DirectoriesConfig.ApiResourcesDirectory; var apiDir = secureAppConfig.DirectoriesConfig.ApiResourcesDirectory;
_inferenceClient = new InferenceClient(new OptionsWrapper<InferenceClientConfig>(secureAppConfig.InferenceClientConfig)); _inferenceClient = new InferenceClient(new OptionsWrapper<InferenceClientConfig>(secureAppConfig.InferenceClientConfig), _mainCTokenSource.Token);
_resourceLoader = new ResourceLoader(_inferenceClient);
var login = new Login(); var login = new Login();
var loader = (IResourceLoader)_inferenceClient;
login.CredentialsEntered += (_, credentials) => login.CredentialsEntered += (_, credentials) =>
{ {
_inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials)); _inferenceClient.Send(RemoteCommand.Create(CommandType.Login, credentials));
@@ -100,8 +100,8 @@ public partial class App
try try
{ {
_securedConfig = _resourceLoader.LoadFile("config.secured.json", apiDir); _securedConfig = loader.LoadFile("config.secured.json", apiDir);
_systemConfig = _resourceLoader.LoadFile("config.system.json", apiDir); _systemConfig = loader.LoadFile("config.system.json", apiDir);
} }
catch (Exception e) catch (Exception e)
{ {
@@ -123,12 +123,13 @@ public partial class App
{ {
try try
{ {
var stream = _resourceLoader.LoadFile($"{assemblyName}.dll", apiDir); var stream = loader.LoadFile($"{assemblyName}.dll", apiDir);
return Assembly.Load(stream.ToArray()); return Assembly.Load(stream.ToArray());
} }
catch (Exception e) catch (Exception e)
{ {
Console.WriteLine(e); Log.Logger.Error(e, $"Failed to load assembly {assemblyName}");
_loadErrors += $"{e.Message}{Environment.NewLine}{Environment.NewLine}";
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!; var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var dllPath = Path.Combine(currentLocation, SecurityConstants.DUMMY_DIR, $"{assemblyName}.dll"); var dllPath = Path.Combine(currentLocation, SecurityConstants.DUMMY_DIR, $"{assemblyName}.dll");
return Assembly.LoadFile(dllPath); return Assembly.LoadFile(dllPath);
@@ -143,13 +144,13 @@ public partial class App
StartMain(); StartMain();
_host.Start(); _host.Start();
EventManager.RegisterClassHandler(typeof(UIElement), UIElement.KeyDownEvent, new RoutedEventHandler(GlobalClick)); EventManager.RegisterClassHandler(typeof(UIElement), UIElement.PreviewKeyDownEvent, new RoutedEventHandler(GlobalKeyHandler));
_host.Services.GetRequiredService<MainSuite>().Show(); _host.Services.GetRequiredService<MainSuite>().Show();
}; };
login.Closed += (sender, args) => login.Closed += (sender, args) =>
{ {
if (!login.MainSuiteOpened) if (!login.MainSuiteOpened)
_inferenceClient.Stop(); _inferenceClient.Dispose();
}; };
login.ShowDialog(); login.ShowDialog();
} }
@@ -218,7 +219,7 @@ public partial class App
services.AddSingleton<FailsafeAnnotationsProducer>(); services.AddSingleton<FailsafeAnnotationsProducer>();
services.AddSingleton<AnnotationService>(); services.AddSingleton<IAnnotationService, AnnotationService>();
services.AddSingleton<DatasetExplorer>(); services.AddSingleton<DatasetExplorer>();
services.AddSingleton<IGalleryService, GalleryService>(); services.AddSingleton<IGalleryService, GalleryService>();
@@ -229,18 +230,25 @@ public partial class App
.Build(); .Build();
Annotation.InitializeDirs(_host.Services.GetRequiredService<IOptions<DirectoriesConfig>>().Value); Annotation.InitializeDirs(_host.Services.GetRequiredService<IOptions<DirectoriesConfig>>().Value);
var datasetExplorer = _host.Services.GetRequiredService<DatasetExplorer>();
datasetExplorer.Show();
datasetExplorer.Hide();
_mediator = _host.Services.GetRequiredService<IMediator>(); _mediator = _host.Services.GetRequiredService<IMediator>();
if (!string.IsNullOrEmpty(_loadErrors))
_mediator.Publish(new LoadErrorEvent(_loadErrors));
_logger = _host.Services.GetRequiredService<ILogger<App>>(); _logger = _host.Services.GetRequiredService<ILogger<App>>();
_formState = _host.Services.GetRequiredService<FormState>(); _formState = _host.Services.GetRequiredService<FormState>();
DispatcherUnhandledException += OnDispatcherUnhandledException; DispatcherUnhandledException += OnDispatcherUnhandledException;
} }
private void GlobalClick(object sender, RoutedEventArgs e) private void GlobalKeyHandler(object sender, RoutedEventArgs e)
{ {
var args = (KeyEventArgs)e; var args = (KeyEventArgs)e;
var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow); var keyEvent = new KeyEvent(sender, args, _formState.ActiveWindow);
ThrottleExt.Throttle(() => _mediator.Publish(keyEvent), KeyPressTaskId, TimeSpan.FromMilliseconds(50)); ThrottleExt.Throttle(() => _mediator.Publish(keyEvent, _mainCTokenSource.Token), KeyPressTaskId, TimeSpan.FromMilliseconds(50));
//e.Handled = true;
} }
protected override async void OnExit(ExitEventArgs e) protected override async void OnExit(ExitEventArgs e)
+1 -1
View File
@@ -143,7 +143,7 @@ public partial class MainSuite
foreach (var window in _openedWindows) foreach (var window in _openedWindows)
window.Value.Close(); window.Value.Close();
_inferenceClient.Stop(); _inferenceClient.Dispose();
_gpsMatcherClient.Stop(); _gpsMatcherClient.Stop();
Application.Current.Shutdown(); Application.Current.Shutdown();
} }
+1 -1
View File
@@ -27,6 +27,6 @@
"LeftPanelWidth": 220.0, "LeftPanelWidth": 220.0,
"RightPanelWidth": 230.0, "RightPanelWidth": 230.0,
"GenerateAnnotatedImage": true, "GenerateAnnotatedImage": true,
"SilentDetection": true "SilentDetection": false
} }
} }
+10 -9
View File
@@ -1,22 +1,23 @@
{ {
"AnnotationConfig": { "AnnotationConfig": {
"DetectionClasses": [ "DetectionClasses": [
{ "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#FF0000" }, { "Id": 0, "Name": "ArmorVehicle", "ShortName": "Броня", "Color": "#ff0000" },
{ "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00FF00" }, { "Id": 1, "Name": "Truck", "ShortName": "Вантаж.", "Color": "#00ff00" },
{ "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000FF" }, { "Id": 2, "Name": "Vehicle", "ShortName": "Машина", "Color": "#0000ff" },
{ "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#FFFF00" }, { "Id": 3, "Name": "Atillery", "ShortName": "Арта", "Color": "#ffff00" },
{ "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#FF00FF" }, { "Id": 4, "Name": "Shadow", "ShortName": "Тінь", "Color": "#ff00ff" },
{ "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00FFFF" }, { "Id": 5, "Name": "Trenches", "ShortName": "Окопи", "Color": "#00ffff" },
{ "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021" }, { "Id": 6, "Name": "MilitaryMan", "ShortName": "Військов", "Color": "#188021" },
{ "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000" }, { "Id": 7, "Name": "TyreTracks", "ShortName": "Накати", "Color": "#800000" },
{ "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000" }, { "Id": 8, "Name": "AdditArmoredTank", "ShortName": "Танк.захист", "Color": "#008000" },
{ "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" }, { "Id": 9, "Name": "Smoke", "ShortName": "Дим", "Color": "#000080" },
{ "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#000080" }, { "Id": 10, "Name": "Plane", "ShortName": "Літак", "Color": "#a52a2a" },
{ "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" }, { "Id": 11, "Name": "Moto", "ShortName": "Мото", "Color": "#808000" },
{ "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#800080" }, { "Id": 12, "Name": "CamouflageNet", "ShortName": "Сітка", "Color": "#87ceeb" },
{ "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" }, { "Id": 13, "Name": "CamouflageBranches", "ShortName": "Гілки", "Color": "#2f4f4f" },
{ "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" }, { "Id": 14, "Name": "Roof", "ShortName": "Дах", "Color": "#1e90ff" },
{ "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" } { "Id": 15, "Name": "Building", "ShortName": "Будівля", "Color": "#ffb6c1" },
{ "Id": 16, "Name": "Caponier", "ShortName": "Капонір", "Color": "#ffa500" }
], ],
"VideoFormats": [ ".mp4", ".mov", ".avi" ], "VideoFormats": [ ".mp4", ".mov", ".avi" ],
"ImageFormats": [ ".jpg", ".jpeg", ".png", ".bmp" ], "ImageFormats": [ ".jpg", ".jpeg", ".png", ".bmp" ],
+57
View File
@@ -0,0 +1,57 @@
using Azaion.Common.Extensions;
using FluentAssertions;
using Xunit;
namespace Azaion.Annotator.Test;
public class ThrottleTest
{
private readonly Guid _testTaskId = Guid.NewGuid();
[Fact]
public async Task TestScheduleAfterCooldown()
{
var calls = new List<DateTime>();
Console.WriteLine($"Start time: {DateTime.Now}");
for (int i = 0; i < 10; i++)
{
ThrottleExt.Throttle(() =>
{
calls.Add(DateTime.Now);
return Task.CompletedTask;
}, _testTaskId, TimeSpan.FromSeconds(1), scheduleCallAfterCooldown: true);
}
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(string.Join(',', calls));
calls.Count.Should().Be(2);
}
[Fact]
public async Task TestScheduleAfterCooldown2()
{
var calls = new List<DateTime>();
Console.WriteLine($"Start time: {DateTime.Now}");
ThrottleExt.Throttle(() =>
{
calls.Add(DateTime.Now);
return Task.CompletedTask;
}, _testTaskId, TimeSpan.FromSeconds(1), scheduleCallAfterCooldown: true);
await Task.Delay(TimeSpan.FromSeconds(2));
ThrottleExt.Throttle(() =>
{
calls.Add(DateTime.Now);
return Task.CompletedTask;
}, _testTaskId, TimeSpan.FromSeconds(1), scheduleCallAfterCooldown: true);
await Task.Delay(TimeSpan.FromSeconds(2));
Console.WriteLine(string.Join(',', calls));
calls.Count.Should().Be(2);
}
}
+31 -4
View File
@@ -7,13 +7,39 @@
Title="Azaion Annotator" Height="800" Width="1100" Title="Azaion Annotator" Height="800" Width="1100"
WindowState="Maximized" WindowState="Maximized"
> >
<Grid Background="Black"
<TextBlock Padding="20 80" ShowGridLines="False">
Background="Black" <Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<TextBlock
Grid.Row="0"
Padding="20 20"
Foreground="Brown" Foreground="Brown"
TextWrapping="Wrap" TextWrapping="Wrap"
FontSize="32" FontSize="30"
TextAlignment="Center"> TextAlignment="Center">
Під час запуску виникла помилка!
<LineBreak /><LineBreak />
Error happened during the launch!
</TextBlock>
<TextBlock
Grid.Row="1"
Padding="10"
TextAlignment="Center"
Foreground="Red"
TextWrapping="Wrap"
FontSize="20"
Name="TbError"
Text="{Binding ErrorMessage}"/>
<TextBlock
Grid.Row="2"
TextAlignment="Center"
Foreground="Brown"
TextWrapping="Wrap"
FontSize="20">
Будь ласка перевірте правильність email чи паролю! <LineBreak /> Будь ласка перевірте правильність email чи паролю! <LineBreak />
Також зауважте, що запуск можливий лише з одного конкретного компьютера, копіювання заборонене! <LineBreak/> <LineBreak/> Також зауважте, що запуск можливий лише з одного конкретного компьютера, копіювання заборонене! <LineBreak/> <LineBreak/>
Для подальшого вирішення проблеми ви можете зв'язатися з нами: hi@azaion.com Для подальшого вирішення проблеми ви можете зв'язатися з нами: hi@azaion.com
@@ -22,4 +48,5 @@
The program is restricted to start only from particular hardware, copying is forbidden! <LineBreak/> <LineBreak/> The program is restricted to start only from particular hardware, copying is forbidden! <LineBreak/> <LineBreak/>
For the further guidance, please feel free to contact us: hi@azaion.com For the further guidance, please feel free to contact us: hi@azaion.com
</TextBlock> </TextBlock>
</Grid>
</Window> </Window>
@@ -1,3 +1,13 @@
namespace Azaion.Annotator; using Azaion.Common.Events;
using MediatR;
public class AnnotatorEventHandler; namespace Azaion.Annotator;
public class AnnotatorEventHandler(Annotator annotator) : INotificationHandler<LoadErrorEvent>
{
public Task Handle(LoadErrorEvent notification, CancellationToken cancellationToken)
{
annotator.Dispatcher.Invoke(() => annotator.TbError.Text = notification.Error);
return Task.CompletedTask;
}
}
+7 -6
View File
@@ -34,24 +34,25 @@ class CDNManager:
print(e) print(e)
return False return False
def download(self, bucket: str, filename: str): def download(self, folder: str, filename: str):
try: try:
if filename is not None: if filename is not None:
self.download_client.download_file(bucket, filename, os.path.join(bucket, filename)) self.download_client.download_file(folder, filename, f'{folder}\\{filename}')
print(f'downloaded {filename} from the {bucket} to current folder') print(f'downloaded {filename} from the {folder} to current folder')
return True
else: else:
response = self.download_client.list_objects_v2(Bucket=bucket) response = self.download_client.list_objects_v2(Bucket=folder)
if 'Contents' in response: if 'Contents' in response:
for obj in response['Contents']: for obj in response['Contents']:
object_key = obj['Key'] object_key = obj['Key']
local_filepath = os.path.join(bucket, object_key) local_filepath = os.path.join(folder, object_key)
local_dir = os.path.dirname(local_filepath) local_dir = os.path.dirname(local_filepath)
if local_dir: if local_dir:
os.makedirs(local_dir, exist_ok=True) os.makedirs(local_dir, exist_ok=True)
if not object_key.endswith('/'): if not object_key.endswith('/'):
try: try:
self.download_client.download_file(bucket, object_key, local_filepath) self.download_client.download_file(folder, object_key, local_filepath)
except Exception as e_file: except Exception as e_file:
all_successful = False # Mark as failed if any file fails all_successful = False # Mark as failed if any file fails
return True return True
+2 -2
View File
@@ -1,12 +1,12 @@
[Setup] [Setup]
AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}}
AppName=Azaion Suite AppName=Azaion Suite
AppVersion=1.4.5 AppVersion=1.4.6
AppPublisher=Azaion Ukraine AppPublisher=Azaion Ukraine
DefaultDirName={localappdata}\Azaion\Azaion Suite DefaultDirName={localappdata}\Azaion\Azaion Suite
DefaultGroupName=Azaion Suite DefaultGroupName=Azaion Suite
OutputDir=..\ OutputDir=..\
OutputBaseFilename=AzaionSuite.Full.1.4.5 OutputBaseFilename=AzaionSuite.Full.1.4.6
SetupIconFile=..\dist-azaion\logo.ico SetupIconFile=..\dist-azaion\logo.ico
UninstallDisplayName=Azaion Suite UninstallDisplayName=Azaion Suite
UninstallDisplayIcon={app}\Azaion.Suite.exe UninstallDisplayIcon={app}\Azaion.Suite.exe
+2 -2
View File
@@ -1,12 +1,12 @@
[Setup] [Setup]
AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}} AppId={{CCFEC8E2-0FCC-4B03-8EEA-00AF20D265E5}}
AppName=Azaion Suite AppName=Azaion Suite
AppVersion=1.4.5 AppVersion=1.4.6
AppPublisher=Azaion Ukraine AppPublisher=Azaion Ukraine
DefaultDirName={localappdata}\Azaion\Azaion Suite DefaultDirName={localappdata}\Azaion\Azaion Suite
DefaultGroupName=Azaion Suite DefaultGroupName=Azaion Suite
OutputDir=..\ OutputDir=..\
OutputBaseFilename=AzaionSuite.Iterative.1.4.5 OutputBaseFilename=AzaionSuite.Iterative.1.4.6
SetupIconFile=..\dist-azaion\logo.ico SetupIconFile=..\dist-azaion\logo.ico
UninstallDisplayName=Azaion Suite UninstallDisplayName=Azaion Suite
UninstallDisplayIcon={app}\Azaion.Suite.exe UninstallDisplayIcon={app}\Azaion.Suite.exe
-1
View File
@@ -12,7 +12,6 @@ call ..\gps-denied\image-matcher\build_gps
call build\download_models call build\download_models
echo building installer... echo building installer...
iscc build\installer.full.iss
iscc build\installer.iterative.iss iscc build\installer.iterative.iss
popd popd