mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 11:16:30 +00:00
Errors sending to UI
notifying client of AI model conversion
This commit is contained in:
@@ -1,6 +1,9 @@
|
|||||||
.idea
|
.idea
|
||||||
bin
|
bin
|
||||||
obj
|
obj
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.log
|
||||||
.vs
|
.vs
|
||||||
*.DotSettings*
|
*.DotSettings*
|
||||||
*.user
|
*.user
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -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,8 @@ 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.ToDictionary(x => x.FName);
|
||||||
LvFiles.ItemsSource = AllMediaFiles;
|
LvFiles.ItemsSource = AllMediaFiles;
|
||||||
|
|
||||||
BlinkHelp(AllMediaFiles.Count == 0
|
|
||||||
? HelpTexts.HelpTextsDict[HelpTextEnum.Initial]
|
|
||||||
: HelpTexts.HelpTextsDict[HelpTextEnum.PlayVideo]);
|
|
||||||
DataContext = this;
|
DataContext = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,14 +478,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 +515,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 +540,22 @@ 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;
|
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)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using System.Windows.Input;
|
|||||||
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 +22,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 +84,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 +131,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 +224,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);
|
||||||
@@ -301,4 +299,28 @@ public class AnnotatorEventHandler(
|
|||||||
}
|
}
|
||||||
await Task.CompletedTask;
|
await 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 =>
|
||||||
|
$"Розпізнавання: {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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -7,3 +7,8 @@ public class AnnotationsDeletedEvent(List<Annotation> annotations) : INotificati
|
|||||||
{
|
{
|
||||||
public List<Annotation> Annotations { get; set; } = annotations;
|
public List<Annotation> Annotations { get; set; } = annotations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class AnnotationAddedEvent(Annotation annotation) : INotification
|
||||||
|
{
|
||||||
|
public Annotation Annotation { get; set; } = annotation;
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ 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;
|
||||||
@@ -124,11 +124,17 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
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
|
||||||
|
.LoadWith(x => x.Detections)
|
||||||
|
.FirstOrDefaultAsync(x => x.Name == fName, token: token);
|
||||||
|
|
||||||
status = userRole.IsValidator() && source == SourceEnum.Manual
|
status = userRole.IsValidator() && source == SourceEnum.Manual
|
||||||
? AnnotationStatus.Validated
|
? AnnotationStatus.Validated
|
||||||
: AnnotationStatus.Created;
|
: AnnotationStatus.Created;
|
||||||
|
|
||||||
|
if (fromQueue && ann is { AnnotationStatus: AnnotationStatus.Validated })
|
||||||
|
return ann;
|
||||||
|
|
||||||
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)
|
||||||
@@ -164,6 +170,9 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
return ann;
|
return ann;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (fromQueue && annotation is { AnnotationStatus: AnnotationStatus.Validated })
|
||||||
|
return annotation;
|
||||||
|
|
||||||
if (stream != null)
|
if (stream != null)
|
||||||
{
|
{
|
||||||
var img = System.Drawing.Image.FromStream(stream);
|
var img = System.Drawing.Image.FromStream(stream);
|
||||||
@@ -220,3 +229,11 @@ public class AnnotationService : INotificationHandler<AnnotationsDeletedEvent>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Annotation> annotations, CancellationToken token = default);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ public class GpsMatcherService(IGpsMatcherClient gpsMatcherClient, ISatelliteDow
|
|||||||
var indexOffset = 0;
|
var indexOffset = 0;
|
||||||
while (routeFiles.Any())
|
while (routeFiles.Any())
|
||||||
{
|
{
|
||||||
await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
|
//await satelliteTileDownloader.GetTiles(currentLat, currentLon, SATELLITE_RADIUS_M, ZOOM_LEVEL, detectToken);
|
||||||
gpsMatcherClient.StartMatching(new StartMatchingEvent
|
gpsMatcherClient.StartMatching(new StartMatchingEvent
|
||||||
{
|
{
|
||||||
ImagesCount = POINTS_COUNT,
|
ImagesCount = POINTS_COUNT,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,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,7 +13,7 @@ namespace Azaion.Dataset;
|
|||||||
|
|
||||||
public class DatasetExplorerEventHandler(
|
public class DatasetExplorerEventHandler(
|
||||||
DatasetExplorer datasetExplorer,
|
DatasetExplorer datasetExplorer,
|
||||||
AnnotationService annotationService) :
|
IAnnotationService annotationService) :
|
||||||
INotificationHandler<KeyEvent>,
|
INotificationHandler<KeyEvent>,
|
||||||
INotificationHandler<DatasetExplorerControlEvent>,
|
INotificationHandler<DatasetExplorerControlEvent>,
|
||||||
INotificationHandler<AnnotationCreatedEvent>,
|
INotificationHandler<AnnotationCreatedEvent>,
|
||||||
@@ -26,7 +26,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},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -116,20 +118,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 index = 0;
|
||||||
var annThumb = new AnnotationThumbnail(annotation);
|
var annThumb = new AnnotationThumbnail(annotation);
|
||||||
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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=*)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
+18
-13
@@ -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 _mainCancelTokenSource = 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), _mainCancelTokenSource.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>();
|
||||||
@@ -231,16 +232,20 @@ public partial class App
|
|||||||
Annotation.InitializeDirs(_host.Services.GetRequiredService<IOptions<DirectoriesConfig>>().Value);
|
Annotation.InitializeDirs(_host.Services.GetRequiredService<IOptions<DirectoriesConfig>>().Value);
|
||||||
|
|
||||||
_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), KeyPressTaskId, TimeSpan.FromMilliseconds(50));
|
||||||
|
//e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnExit(ExitEventArgs e)
|
protected override async void OnExit(ExitEventArgs e)
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
|
<ProjectReference Include="..\Azaion.Common\Azaion.Common.csproj" />
|
||||||
<ProjectReference Include="..\dummy\Azaion.Annotator\Azaion.Annotator.csproj" />
|
<ProjectReference Include="..\Azaion.Annotator\Azaion.Annotator.csproj" />
|
||||||
<ProjectReference Include="..\dummy\Azaion.Dataset\Azaion.Dataset.csproj" />
|
<ProjectReference Include="..\Azaion.Dataset\Azaion.Dataset.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -62,8 +62,8 @@
|
|||||||
|
|
||||||
<Target Name="PostBuild" AfterTargets="Build">
|
<Target Name="PostBuild" AfterTargets="Build">
|
||||||
<MakeDir Directories="$(TargetDir)dummy" />
|
<MakeDir Directories="$(TargetDir)dummy" />
|
||||||
<Move SourceFiles="$(TargetDir)Azaion.Annotator.dll" DestinationFolder="$(TargetDir)dummy" />
|
<Copy SourceFiles="$(TargetDir)Azaion.Annotator.dll" DestinationFolder="$(TargetDir)dummy" />
|
||||||
<Move SourceFiles="$(TargetDir)Azaion.Dataset.dll" DestinationFolder="$(TargetDir)dummy" />
|
<Copy SourceFiles="$(TargetDir)Azaion.Dataset.dll" DestinationFolder="$(TargetDir)dummy" />
|
||||||
<Exec Command="postbuild.cmd $(ConfigurationName) stage" />
|
<Exec Command="postbuild.cmd $(ConfigurationName) stage" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ UninstallDisplayName=Azaion Suite
|
|||||||
UninstallDisplayIcon={app}\Azaion.Suite.exe
|
UninstallDisplayIcon={app}\Azaion.Suite.exe
|
||||||
Compression=lzma2/fast
|
Compression=lzma2/fast
|
||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
DiskSpanning=yes
|
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|||||||
Reference in New Issue
Block a user