mirror of
https://github.com/azaion/annotations.git
synced 2026-04-22 12:26:30 +00:00
queue + local sqlite WIP
This commit is contained in:
@@ -7,12 +7,19 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="linq2db.SQLite" Version="5.4.1" />
|
||||
<PackageReference Include="MediatR" Version="12.4.1" />
|
||||
<PackageReference Include="MessagePack" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1" />
|
||||
<PackageReference Include="RabbitMQ.Stream.Client" Version="1.8.9" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="4.7.3" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Azaion.CommonSecurity\Azaion.CommonSecurity.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+19
-22
@@ -5,20 +5,7 @@ namespace Azaion.Common;
|
||||
|
||||
public class Constants
|
||||
{
|
||||
public const string CONFIG_PATH = "config.json";
|
||||
public const string DEFAULT_DLL_CACHE_DIR = "DllCache";
|
||||
|
||||
#region ApiConfig
|
||||
|
||||
public const string DEFAULT_API_URL = "https://api.azaion.com/";
|
||||
public const int DEFAULT_API_RETRY_COUNT = 3;
|
||||
public const int DEFAULT_API_TIMEOUT_SECONDS = 40;
|
||||
|
||||
public const string CLAIM_NAME_ID = "nameid";
|
||||
public const string CLAIM_EMAIL = "unique_name";
|
||||
public const string CLAIM_ROLE = "role";
|
||||
|
||||
#endregion ApiConfig
|
||||
public const string SECURE_RESOURCE_CACHE = "SecureResourceCache";
|
||||
|
||||
#region DirectoriesConfig
|
||||
|
||||
@@ -44,12 +31,18 @@ public class Constants
|
||||
new() { Id = 7, Name = "Накати", ShortName = "Накати" },
|
||||
new() { Id = 8, Name = "Танк з захистом", ShortName = "Танк захист" },
|
||||
new() { Id = 9, Name = "Дим", ShortName = "Дим" },
|
||||
new() { Id = 10, Name = "Літак", ShortName = "Літак" }
|
||||
new() { Id = 10, Name = "Літак", ShortName = "Літак" },
|
||||
new() { Id = 11, Name = "Мотоцикл", ShortName = "Мото" }
|
||||
];
|
||||
|
||||
public static readonly List<string> DefaultVideoFormats = ["mp4", "mov", "avi"];
|
||||
public static readonly List<string> DefaultImageFormats = ["jpg", "jpeg", "png", "bmp"];
|
||||
|
||||
public static int DEFAULT_LEFT_PANEL_WIDTH = 250;
|
||||
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
|
||||
|
||||
public const string DEFAULT_ANNOTATIONS_DB_FILE = "annotations.db";
|
||||
|
||||
# endregion AnnotatorConfig
|
||||
|
||||
# region AIRecognitionConfig
|
||||
@@ -62,13 +55,6 @@ public class Constants
|
||||
|
||||
# endregion AIRecognitionConfig
|
||||
|
||||
# region AnnotatorWindowConfig
|
||||
|
||||
public static int DEFAULT_LEFT_PANEL_WIDTH = 250;
|
||||
public static int DEFAULT_RIGHT_PANEL_WIDTH = 250;
|
||||
|
||||
#endregion
|
||||
|
||||
#region Thumbnails
|
||||
|
||||
public static readonly Size DefaultThumbnailSize = new(240, 135);
|
||||
@@ -98,4 +84,15 @@ public class Constants
|
||||
return new TimeSpan(0, hours, minutes, seconds, milliseconds * 100);
|
||||
}
|
||||
|
||||
#region Queue
|
||||
|
||||
public const string MQ_DIRECT_TYPE = "direct";
|
||||
public const string MQ_ANNOTATIONS_QUEUE = "azaion-annotations";
|
||||
public const string MQ_ANNOTATIONS_CONFIRM_QUEUE = "azaion-annotations-confirm";
|
||||
public const string ANNOTATION_PRODUCER = "AnnotationsProducer";
|
||||
public const string ANNOTATION_CONFIRM_PRODUCER = "AnnotationsConfirmProducer";
|
||||
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.IO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.DTO.Queue;
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class Annotation
|
||||
{
|
||||
private static string _labelsDir = null!;
|
||||
private static string _imagesDir = null!;
|
||||
|
||||
public static void InitializeDirs(DirectoriesConfig config)
|
||||
{
|
||||
_labelsDir = config.LabelsDirectory;
|
||||
_imagesDir = config.ImagesDirectory;
|
||||
}
|
||||
|
||||
public string Name { get; set; } = null!;
|
||||
public DateTime CreatedDate { get; set; }
|
||||
public List<int> Classes { get; set; } = null!;
|
||||
public string CreatedEmail { get; set; } = null!;
|
||||
public RoleEnum CreatedRole { get; set; }
|
||||
public SourceEnum Source { get; set; }
|
||||
public AnnotationStatus AnnotationStatus { get; set; }
|
||||
|
||||
public string ImagePath => Path.Combine(_imagesDir, $"{Name}.jpg");
|
||||
public string LabelPath => Path.Combine(_labelsDir, $"{Name}.txt");
|
||||
|
||||
}
|
||||
|
||||
public enum AnnotationStatus
|
||||
{
|
||||
None = 0,
|
||||
Created = 10,
|
||||
Validated = 20
|
||||
}
|
||||
|
||||
public class AnnotationName
|
||||
{
|
||||
public string Name { get; set; } = null!;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class ApiCredentials(string email, string password) : EventArgs
|
||||
{
|
||||
public string Email { get; set; } = email;
|
||||
public string Password { get; set; } = password;
|
||||
}
|
||||
@@ -15,4 +15,9 @@ public class AnnotationConfig
|
||||
|
||||
public List<string> VideoFormats { get; set; } = null!;
|
||||
public List<string> ImageFormats { get; set; } = null!;
|
||||
|
||||
public string AnnotationsDbFile { get; set; } = null!;
|
||||
|
||||
public double LeftPanelWidth { get; set; }
|
||||
public double RightPanelWidth { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class AnnotatorWindowConfig
|
||||
{
|
||||
public double LeftPanelWidth { get; set; }
|
||||
public double RightPanelWidth { get; set; }
|
||||
public bool ShowHelpOnStart { get; set; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class ApiConfig
|
||||
{
|
||||
public string Url { get; set; } = null!;
|
||||
public int RetryCount {get;set;}
|
||||
public double TimeoutSeconds { get; set; }
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using Azaion.CommonSecurity;
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
@@ -8,12 +10,12 @@ public class AppConfig
|
||||
{
|
||||
public ApiConfig ApiConfig { get; set; } = null!;
|
||||
|
||||
public QueueConfig QueueConfig { get; set; } = null!;
|
||||
|
||||
public DirectoriesConfig DirectoriesConfig { get; set; } = null!;
|
||||
|
||||
public AnnotationConfig AnnotationConfig { get; set; } = null!;
|
||||
|
||||
public AnnotatorWindowConfig AnnotatorWindowConfig { get; set; } = null!;
|
||||
|
||||
public AIRecognitionConfig AIRecognitionConfig { get; set; } = null!;
|
||||
|
||||
public ThumbnailConfig ThumbnailConfig { get; set; } = null!;
|
||||
@@ -30,7 +32,7 @@ public class ConfigUpdater : IConfigUpdater
|
||||
public void CheckConfig()
|
||||
{
|
||||
var exePath = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory)!;
|
||||
var configFilePath = Path.Combine(exePath, Constants.CONFIG_PATH);
|
||||
var configFilePath = Path.Combine(exePath, SecurityConstants.CONFIG_PATH);
|
||||
|
||||
if (File.Exists(configFilePath))
|
||||
return;
|
||||
@@ -39,9 +41,9 @@ public class ConfigUpdater : IConfigUpdater
|
||||
{
|
||||
ApiConfig = new ApiConfig
|
||||
{
|
||||
Url = Constants.DEFAULT_API_URL,
|
||||
RetryCount = Constants.DEFAULT_API_RETRY_COUNT,
|
||||
TimeoutSeconds = Constants.DEFAULT_API_TIMEOUT_SECONDS
|
||||
Url = SecurityConstants.DEFAULT_API_URL,
|
||||
RetryCount = SecurityConstants.DEFAULT_API_RETRY_COUNT,
|
||||
TimeoutSeconds = SecurityConstants.DEFAULT_API_TIMEOUT_SECONDS
|
||||
},
|
||||
|
||||
AnnotationConfig = new AnnotationConfig
|
||||
@@ -49,12 +51,11 @@ public class ConfigUpdater : IConfigUpdater
|
||||
AnnotationClasses = Constants.DefaultAnnotationClasses,
|
||||
VideoFormats = Constants.DefaultVideoFormats,
|
||||
ImageFormats = Constants.DefaultImageFormats,
|
||||
},
|
||||
|
||||
AnnotatorWindowConfig = new AnnotatorWindowConfig
|
||||
{
|
||||
LeftPanelWidth = Constants.DEFAULT_LEFT_PANEL_WIDTH,
|
||||
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH
|
||||
RightPanelWidth = Constants.DEFAULT_RIGHT_PANEL_WIDTH,
|
||||
|
||||
AnnotationsDbFile = Constants.DEFAULT_ANNOTATIONS_DB_FILE
|
||||
},
|
||||
|
||||
DirectoriesConfig = new DirectoriesConfig
|
||||
@@ -86,6 +87,6 @@ public class ConfigUpdater : IConfigUpdater
|
||||
|
||||
public void Save(AppConfig config)
|
||||
{
|
||||
File.WriteAllText(Constants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
|
||||
File.WriteAllText(SecurityConstants.CONFIG_PATH, JsonConvert.SerializeObject(config, Formatting.Indented), Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Azaion.Common.DTO.Config;
|
||||
|
||||
public class QueueConfig
|
||||
{
|
||||
public string Host { get; set; } = null!;
|
||||
public int Port { get; set; }
|
||||
|
||||
public string ProducerUsername { get; set; } = null!;
|
||||
public string ProducerPassword { get; set; } = null!;
|
||||
|
||||
public string ConsumerUsername { get; set; } = null!;
|
||||
public string ConsumerPassword { get; set; } = null!;
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class HardwareInfo
|
||||
{
|
||||
public string CPU { get; set; } = null!;
|
||||
public string GPU { get; set; } = null!;
|
||||
public string MacAddress { get; set; } = null!;
|
||||
public string Memory { get; set; } = null!;
|
||||
|
||||
public string Hash { get; set; } = null!;
|
||||
}
|
||||
@@ -158,19 +158,25 @@ public class YoloLabel : Label
|
||||
public static async Task<List<YoloLabel>> ReadFromFile(string filename, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var str = await File.ReadAllTextAsync(filename, cancellationToken);
|
||||
|
||||
return str.Split('\n')
|
||||
.Select(Parse)
|
||||
.Where(ann => ann != null)
|
||||
.ToList()!;
|
||||
return Deserialize(str);
|
||||
}
|
||||
|
||||
public static async Task WriteToFile(IEnumerable<YoloLabel> labels, string filename, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var labelsStr = string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
|
||||
var labelsStr = Serialize(labels);
|
||||
await File.WriteAllTextAsync(filename, labelsStr, cancellationToken);
|
||||
}
|
||||
|
||||
public static string Serialize(IEnumerable<YoloLabel> labels) =>
|
||||
string.Join(Environment.NewLine, labels.Select(x => x.ToString()));
|
||||
|
||||
public static List<YoloLabel> Deserialize(string str) =>
|
||||
str.Split('\n')
|
||||
.Select(Parse)
|
||||
.Where(ann => ann != null)
|
||||
.ToList()!;
|
||||
|
||||
|
||||
public override string ToString() => $"{ClassNumber} {CenterX:F5} {CenterY:F5} {Width:F5} {Height:F5}".Replace(',', '.');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public string Token { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
|
||||
namespace Azaion.Common.DTO.Queue;
|
||||
using MessagePack;
|
||||
|
||||
[MessagePackObject]
|
||||
public class AnnotationCreatedMessage
|
||||
{
|
||||
[Key(0)] public DateTime CreatedDate { get; set; }
|
||||
[Key(1)] public string Name { get; set; } = null!;
|
||||
[Key(2)] public string Label { get; set; } = null!;
|
||||
[Key(3)] public byte[] Image { get; set; } = null!;
|
||||
[Key(4)] public RoleEnum CreatedRole { get; set; }
|
||||
[Key(5)] public string CreatedEmail { get; set; } = null!;
|
||||
[Key(6)] public SourceEnum Source { get; set; }
|
||||
[Key(7)] public AnnotationStatus Status { get; set; }
|
||||
}
|
||||
|
||||
[MessagePackObject]
|
||||
public class AnnotationValidatedMessage
|
||||
{
|
||||
[Key(0)] public string Name { get; set; } = null!;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Azaion.Common.DTO.Queue;
|
||||
|
||||
public enum SourceEnum
|
||||
{
|
||||
AI,
|
||||
Manual
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public enum RoleEnum
|
||||
{
|
||||
None = 0,
|
||||
Operator = 10, //only annotator is available. Could send annotations to queue.
|
||||
Validator = 20, //annotator + dataset explorer. This role allows to receive annotations from the queue.
|
||||
CompanionPC = 30,
|
||||
Admin = 40, //
|
||||
ResourceUploader = 50, //Uploading dll and ai models
|
||||
ApiAdmin = 1000 //everything
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace Azaion.Common.DTO;
|
||||
|
||||
public class User
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public string Email { get; set; }
|
||||
public RoleEnum Role { get; set; }
|
||||
|
||||
public User(IEnumerable<Claim> claims)
|
||||
{
|
||||
var claimDict = claims.ToDictionary(x => x.Type, x => x.Value);
|
||||
|
||||
Id = Guid.Parse(claimDict[Constants.CLAIM_NAME_ID]);
|
||||
Email = claimDict[Constants.CLAIM_EMAIL];
|
||||
if (!Enum.TryParse(claimDict[Constants.CLAIM_ROLE], out RoleEnum role))
|
||||
role = RoleEnum.None;
|
||||
Role = role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using Azaion.Common.DTO;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Data;
|
||||
|
||||
namespace Azaion.Common.Database;
|
||||
|
||||
public class AnnotationsDb(DataOptions dataOptions) : DataConnection(dataOptions)
|
||||
{
|
||||
public ITable<Annotation> Annotations => this.GetTable<Annotation>();
|
||||
public ITable<AnnotationName> AnnotationsQueue => this.GetTable<AnnotationName>();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using System.Diagnostics;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using LinqToDB;
|
||||
using LinqToDB.Mapping;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Azaion.Common.Database;
|
||||
|
||||
public interface IDbFactory
|
||||
{
|
||||
Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func);
|
||||
Task Run(Func<AnnotationsDb, Task> func);
|
||||
}
|
||||
|
||||
public class DbFactory : IDbFactory
|
||||
{
|
||||
private readonly DataOptions _dataOptions;
|
||||
|
||||
public DbFactory(IOptions<AnnotationConfig> annConfig)
|
||||
{
|
||||
_dataOptions = LoadOptions(annConfig.Value.AnnotationsDbFile);
|
||||
}
|
||||
|
||||
private DataOptions LoadOptions(string dbFile)
|
||||
{
|
||||
if (string.IsNullOrEmpty(dbFile))
|
||||
throw new ArgumentException($"Empty AnnotationsDbFile in config!");
|
||||
|
||||
var dataOptions = new DataOptions()
|
||||
.UseSQLiteOfficial($"Data Source={dbFile}")
|
||||
.UseMappingSchema(AnnotationsDbSchemaHolder.MappingSchema);
|
||||
|
||||
_ = dataOptions.UseTracing(TraceLevel.Info, t => Console.WriteLine(t.SqlText));
|
||||
return dataOptions;
|
||||
}
|
||||
|
||||
|
||||
public async Task<T> Run<T>(Func<AnnotationsDb, Task<T>> func)
|
||||
{
|
||||
await using var db = new AnnotationsDb(_dataOptions);
|
||||
return await func(db);
|
||||
}
|
||||
|
||||
public async Task Run(Func<AnnotationsDb, Task> func)
|
||||
{
|
||||
await using var db = new AnnotationsDb(_dataOptions);
|
||||
await func(db);
|
||||
}
|
||||
}
|
||||
|
||||
public static class AnnotationsDbSchemaHolder
|
||||
{
|
||||
public static readonly MappingSchema MappingSchema;
|
||||
|
||||
static AnnotationsDbSchemaHolder()
|
||||
{
|
||||
MappingSchema = new MappingSchema();
|
||||
var builder = new FluentMappingBuilder(MappingSchema);
|
||||
|
||||
builder.Entity<AnnotationName>().HasTableName("annotations_queue");
|
||||
|
||||
builder.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Azaion.Common.Extensions;
|
||||
|
||||
public static class EnumerableExtensions
|
||||
{
|
||||
public static bool In<T>(this T obj, params T[] objects) =>
|
||||
objects.Contains(obj);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
namespace Azaion.Annotator.Extensions;
|
||||
namespace Azaion.Common.Extensions;
|
||||
|
||||
public static class ThrottleExt
|
||||
{
|
||||
private static bool _throttleOn;
|
||||
public static async Task Throttle(Func<Task> func, TimeSpan? throttleTime = null)
|
||||
public static async Task Throttle(this Func<Task> func, TimeSpan? throttleTime = null, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_throttleOn)
|
||||
return;
|
||||
@@ -12,8 +12,8 @@ public static class ThrottleExt
|
||||
await func();
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500));
|
||||
await Task.Delay(throttleTime ?? TimeSpan.FromMilliseconds(500), cancellationToken);
|
||||
_throttleOn = false;
|
||||
});
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.DTO.Queue;
|
||||
using Azaion.CommonSecurity.DTO;
|
||||
using Azaion.CommonSecurity.Services;
|
||||
using LinqToDB;
|
||||
using MessagePack;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RabbitMQ.Stream.Client;
|
||||
using RabbitMQ.Stream.Client.Reliable;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public class AnnotationService
|
||||
{
|
||||
private readonly AzaionApiClient _apiClient;
|
||||
private readonly IDbFactory _dbFactory;
|
||||
private readonly FailsafeAnnotationsProducer _producer;
|
||||
private readonly QueueConfig _queueConfig;
|
||||
private Consumer _consumer = null!;
|
||||
|
||||
public AnnotationService(AzaionApiClient apiClient,
|
||||
IDbFactory dbFactory,
|
||||
FailsafeAnnotationsProducer producer,
|
||||
IOptions<QueueConfig> queueConfig)
|
||||
{
|
||||
_apiClient = apiClient;
|
||||
_dbFactory = dbFactory;
|
||||
_producer = producer;
|
||||
_queueConfig = queueConfig.Value;
|
||||
|
||||
Task.Run(async () => await Init()).Wait();
|
||||
}
|
||||
|
||||
private async Task Init()
|
||||
{
|
||||
var consumerSystem = await StreamSystem.Create(new StreamSystemConfig
|
||||
{
|
||||
Endpoints = new List<EndPoint>{new DnsEndPoint(_queueConfig.Host, _queueConfig.Port)},
|
||||
UserName = _queueConfig.ConsumerUsername,
|
||||
Password = _queueConfig.ConsumerPassword
|
||||
});
|
||||
_consumer = await Consumer.Create(new ConsumerConfig(consumerSystem, Constants.MQ_ANNOTATIONS_QUEUE)
|
||||
{
|
||||
OffsetSpec = new OffsetTypeFirst(),
|
||||
MessageHandler = async (stream, _, _, message) =>
|
||||
await Consume(MessagePackSerializer.Deserialize<AnnotationCreatedMessage>(message.Data.Contents)),
|
||||
});
|
||||
}
|
||||
|
||||
//AI / Manual
|
||||
public async Task SaveAnnotation(string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream = null, CancellationToken token = default) =>
|
||||
await SaveAnnotationInner(DateTime.UtcNow, fName, labels, source, stream, _apiClient.User.Role, _apiClient.User.Email, token);
|
||||
|
||||
//Queue (only from operators)
|
||||
public async Task Consume(AnnotationCreatedMessage message, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (message.CreatedRole == RoleEnum.Validator) //Don't proceed our own messages (or from another Validator)
|
||||
return;
|
||||
|
||||
await SaveAnnotationInner(
|
||||
message.CreatedDate,
|
||||
message.Name,
|
||||
YoloLabel.Deserialize(message.Label),
|
||||
message.Source,
|
||||
new MemoryStream(message.Image),
|
||||
message.CreatedRole,
|
||||
message.CreatedEmail,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private async Task SaveAnnotationInner(DateTime createdDate, string fName, List<YoloLabel>? labels, SourceEnum source, MemoryStream? stream,
|
||||
RoleEnum createdRole,
|
||||
string createdEmail,
|
||||
CancellationToken token = default)
|
||||
{
|
||||
//Flow for roles:
|
||||
// Operator:
|
||||
// sourceEnum: (manual, ai) <AnnotationCreatedMessage>
|
||||
// Validator:
|
||||
// sourceEnum: (manual) if was in received.json then <AnnotationValidatedMessage> else <AnnotationCreatedMessage>
|
||||
// sourceEnum: (queue, AI) if queue CreatedMessage with the same user - do nothing Add to received.json
|
||||
|
||||
var classes = labels?.Select(x => x.ClassNumber).Distinct().ToList() ?? [];
|
||||
AnnotationStatus status;
|
||||
|
||||
var annotation = await _dbFactory.Run(async db =>
|
||||
{
|
||||
var ann = await db.Annotations.FirstOrDefaultAsync(x => x.Name == fName, token: token);
|
||||
status = ann?.AnnotationStatus == AnnotationStatus.Created && createdRole == RoleEnum.Validator
|
||||
? AnnotationStatus.Validated
|
||||
: AnnotationStatus.Created;
|
||||
|
||||
if (ann != null)
|
||||
await db.Annotations
|
||||
.Where(x => x.Name == fName)
|
||||
.Set(x => x.Classes, classes)
|
||||
.Set(x => x.Source, source)
|
||||
.Set(x => x.AnnotationStatus, status)
|
||||
.UpdateAsync(token: token);
|
||||
else
|
||||
{
|
||||
ann = new Annotation
|
||||
{
|
||||
CreatedDate = createdDate,
|
||||
Name = fName,
|
||||
Classes = classes,
|
||||
CreatedEmail = createdEmail,
|
||||
CreatedRole = createdRole,
|
||||
AnnotationStatus = status,
|
||||
Source = source
|
||||
};
|
||||
await db.InsertAsync(ann, token: token);
|
||||
}
|
||||
return ann;
|
||||
});
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
var img = System.Drawing.Image.FromStream(stream);
|
||||
img.Save(annotation.ImagePath, ImageFormat.Jpeg); //todo: check png images coming from queue
|
||||
}
|
||||
if (labels != null)
|
||||
await YoloLabel.WriteToFile(labels, annotation.LabelPath, token);
|
||||
|
||||
await _producer.SendToQueue(annotation, token);
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Security;
|
||||
using System.Text;
|
||||
using Azaion.Common.DTO;
|
||||
using Newtonsoft.Json;
|
||||
using System.IdentityModel.Tokens.Jwt;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public class AzaionApiClient(HttpClient httpClient) : IDisposable
|
||||
{
|
||||
const string JSON_MEDIA = "application/json";
|
||||
|
||||
private string Email { get; set; } = null!;
|
||||
private SecureString Password { get; set; } = new();
|
||||
|
||||
private string JwtToken { get; set; } = null!;
|
||||
public User User { get; set; } = null!;
|
||||
|
||||
public void EnterCredentials(ApiCredentials credentials)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(credentials.Email) || string.IsNullOrWhiteSpace(credentials.Password))
|
||||
throw new Exception("Email or password is empty!");
|
||||
|
||||
Email = credentials.Email;
|
||||
Password = credentials.Password.ToSecureString();
|
||||
}
|
||||
|
||||
public async Task<Stream> GetResource(string fileName, string password, HardwareInfo hardware)
|
||||
{
|
||||
var response = await Send(httpClient, new HttpRequestMessage(HttpMethod.Post, "/resources/get")
|
||||
{
|
||||
Content = new StringContent(JsonConvert.SerializeObject(new { fileName, password, hardware }), Encoding.UTF8, JSON_MEDIA)
|
||||
});
|
||||
return await response.Content.ReadAsStreamAsync();
|
||||
}
|
||||
|
||||
private async Task Authorize()
|
||||
{
|
||||
if (string.IsNullOrEmpty(Email) || Password.Length == 0)
|
||||
throw new Exception("Email or password is empty! Please do EnterCredentials first!");
|
||||
|
||||
var payload = new
|
||||
{
|
||||
email = Email,
|
||||
password = Password.ToRealString()
|
||||
};
|
||||
var response = await httpClient.PostAsync(
|
||||
"login",
|
||||
new StringContent(JsonConvert.SerializeObject(payload), Encoding.UTF8, JSON_MEDIA));
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
throw new Exception($"EnterCredentials failed: {response.StatusCode}");
|
||||
|
||||
var responseData = await response.Content.ReadAsStringAsync();
|
||||
|
||||
var result = JsonConvert.DeserializeObject<LoginResponse>(responseData);
|
||||
|
||||
if (string.IsNullOrEmpty(result?.Token))
|
||||
throw new Exception("JWT Token not found in response");
|
||||
|
||||
var handler = new JwtSecurityTokenHandler();
|
||||
var token = handler.ReadJwtToken(result.Token);
|
||||
|
||||
User = new User(token.Claims);
|
||||
JwtToken = result.Token;
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> Send(HttpClient client, HttpRequestMessage request)
|
||||
{
|
||||
if (string.IsNullOrEmpty(JwtToken))
|
||||
await Authorize();
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (response.StatusCode == HttpStatusCode.Unauthorized)
|
||||
{
|
||||
await Authorize();
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", JwtToken);
|
||||
response = await client.SendAsync(request);
|
||||
}
|
||||
|
||||
if (response.IsSuccessStatusCode)
|
||||
return response;
|
||||
|
||||
var result = await response.Content.ReadAsStringAsync();
|
||||
throw new Exception($"Failed: {response.StatusCode}! Result: {result}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
httpClient.Dispose();
|
||||
Password.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using Azaion.Common.Database;
|
||||
using Azaion.Common.DTO;
|
||||
using Azaion.Common.DTO.Config;
|
||||
using Azaion.Common.DTO.Queue;
|
||||
using LinqToDB;
|
||||
using MessagePack;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using RabbitMQ.Stream.Client;
|
||||
using RabbitMQ.Stream.Client.Reliable;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public class FailsafeAnnotationsProducer
|
||||
{
|
||||
private readonly ILogger<FailsafeAnnotationsProducer> _logger;
|
||||
private readonly IDbFactory _dbFactory;
|
||||
private readonly QueueConfig _queueConfig;
|
||||
|
||||
private Producer _annotationProducer = null!;
|
||||
private Producer _annotationConfirmProducer = null!;
|
||||
|
||||
|
||||
public FailsafeAnnotationsProducer(ILogger<FailsafeAnnotationsProducer> logger, IDbFactory dbFactory, IOptions<QueueConfig> queueConfig)
|
||||
{
|
||||
_logger = logger;
|
||||
_dbFactory = dbFactory;
|
||||
_queueConfig = queueConfig.Value;
|
||||
Task.Run(async () => await ProcessQueue()).Wait();
|
||||
}
|
||||
|
||||
private async Task<StreamSystem> GetProducerQueueConfig()
|
||||
{
|
||||
return await StreamSystem.Create(new StreamSystemConfig
|
||||
{
|
||||
|
||||
Endpoints = new List<EndPoint> { new IPEndPoint(IPAddress.Parse(_queueConfig.Host), _queueConfig.Port) },
|
||||
UserName = _queueConfig.ProducerUsername,
|
||||
Password = _queueConfig.ProducerPassword
|
||||
});
|
||||
}
|
||||
|
||||
private async Task Init(CancellationToken cancellationToken = default)
|
||||
{
|
||||
_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));
|
||||
}
|
||||
|
||||
private async Task ProcessQueue(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await Init(cancellationToken);
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
var messages = await GetFromQueue(cancellationToken);
|
||||
foreach (var messagesChunk in messages.Chunk(10)) //Sending by 10
|
||||
{
|
||||
var sent = false;
|
||||
while (!sent || cancellationToken.IsCancellationRequested) //Waiting for send
|
||||
{
|
||||
try
|
||||
{
|
||||
var createdMessages = messagesChunk
|
||||
.Where(x => x.Status == AnnotationStatus.Created)
|
||||
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
||||
.ToList();
|
||||
await _annotationProducer.Send(createdMessages, CompressionType.Gzip);
|
||||
|
||||
var validatedMessages = messagesChunk
|
||||
.Where(x => x.Status == AnnotationStatus.Validated)
|
||||
.Select(x => new Message(MessagePackSerializer.Serialize(x)))
|
||||
.ToList();
|
||||
await _annotationConfirmProducer.Send(validatedMessages, CompressionType.Gzip);
|
||||
|
||||
await _dbFactory.Run(async db =>
|
||||
await db.AnnotationsQueue.DeleteAsync(aq => messagesChunk.Any(x => aq.Name == x.Name), token: cancellationToken));
|
||||
sent = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e, e.Message);
|
||||
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<AnnotationCreatedMessage>> GetFromQueue(CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await _dbFactory.Run(async db =>
|
||||
{
|
||||
var annotations = await db.AnnotationsQueue.Join(db.Annotations, aq => aq.Name, a => a.Name, (aq, a) => a)
|
||||
.ToListAsync(token: cancellationToken);
|
||||
|
||||
var messages = new List<AnnotationCreatedMessage>();
|
||||
foreach (var annotation in annotations)
|
||||
{
|
||||
var image = await File.ReadAllBytesAsync(annotation.ImagePath, cancellationToken);
|
||||
var label = await File.ReadAllTextAsync(annotation.LabelPath, cancellationToken);
|
||||
var annCreateMessage = new AnnotationCreatedMessage
|
||||
{
|
||||
Name = annotation.Name,
|
||||
|
||||
CreatedRole = annotation.CreatedRole,
|
||||
CreatedEmail = annotation.CreatedEmail,
|
||||
CreatedDate = annotation.CreatedDate,
|
||||
|
||||
Image = image,
|
||||
Label = label,
|
||||
Source = annotation.Source
|
||||
};
|
||||
messages.Add(annCreateMessage);
|
||||
}
|
||||
return messages;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task SendToQueue(Annotation annotation, CancellationToken cancellationToken = default)
|
||||
{
|
||||
await _dbFactory.Run(async db =>
|
||||
await db.InsertAsync(new AnnotationName { Name = annotation.Name }, token: cancellationToken));
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Azaion.Common.DTO;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public interface IHardwareService
|
||||
{
|
||||
HardwareInfo GetHardware();
|
||||
}
|
||||
|
||||
public class HardwareService : IHardwareService
|
||||
{
|
||||
private const string WIN32_GET_HARDWARE_COMMAND =
|
||||
"wmic OS get TotalVisibleMemorySize /Value && " +
|
||||
"wmic CPU get Name /Value && " +
|
||||
"wmic path Win32_VideoController get Name /Value";
|
||||
|
||||
private const string UNIX_GET_HARDWARE_COMMAND =
|
||||
"/bin/bash -c \"free -g | grep Mem: | awk '{print $2}' && " +
|
||||
"lscpu | grep 'Model name:' | cut -d':' -f2 && " +
|
||||
"lspci | grep VGA | cut -d':' -f3\"";
|
||||
|
||||
public HardwareInfo GetHardware()
|
||||
{
|
||||
try
|
||||
{
|
||||
var output = RunCommand(Environment.OSVersion.Platform == PlatformID.Win32NT
|
||||
? WIN32_GET_HARDWARE_COMMAND
|
||||
: UNIX_GET_HARDWARE_COMMAND);
|
||||
|
||||
var lines = output
|
||||
.Replace("TotalVisibleMemorySize=", "")
|
||||
.Replace("Name=", "")
|
||||
.Replace(" ", " ")
|
||||
.Trim()
|
||||
.Split(['\n', '\r'], StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
var memoryStr = "Unknown RAM";
|
||||
if (lines.Length > 0)
|
||||
{
|
||||
memoryStr = lines[0];
|
||||
if (int.TryParse(memoryStr, out var memKb))
|
||||
memoryStr = $"{Math.Round(memKb / 1024.0 / 1024.0)} Gb";
|
||||
}
|
||||
|
||||
var hardwareInfo = new HardwareInfo
|
||||
{
|
||||
Memory = memoryStr,
|
||||
CPU = lines.Length > 1 && string.IsNullOrEmpty(lines[1])
|
||||
? "Unknown RAM"
|
||||
: lines[1],
|
||||
GPU = lines.Length > 2 && string.IsNullOrEmpty(lines[2])
|
||||
? "Unknown GPU"
|
||||
: lines[2]
|
||||
};
|
||||
hardwareInfo.Hash = ToHash($"Azaion_{MacAddress()}_{hardwareInfo.CPU}_{hardwareInfo.GPU}");
|
||||
return hardwareInfo;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private string MacAddress()
|
||||
{
|
||||
var macAddress = NetworkInterface
|
||||
.GetAllNetworkInterfaces()
|
||||
.Where(nic => nic.OperationalStatus == OperationalStatus.Up)
|
||||
.Select(nic => nic.GetPhysicalAddress().ToString())
|
||||
.FirstOrDefault();
|
||||
|
||||
return macAddress ?? string.Empty;
|
||||
}
|
||||
|
||||
private string RunCommand(string command)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var process = new Process();
|
||||
process.StartInfo.FileName = Environment.OSVersion.Platform == PlatformID.Unix ? "/bin/bash" : "cmd.exe";
|
||||
process.StartInfo.Arguments = Environment.OSVersion.Platform == PlatformID.Unix
|
||||
? $"-c \"{command}\""
|
||||
: $"/c {command}";
|
||||
process.StartInfo.RedirectStandardOutput = true;
|
||||
process.StartInfo.UseShellExecute = false;
|
||||
process.StartInfo.CreateNoWindow = true;
|
||||
|
||||
process.Start();
|
||||
var result = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
|
||||
return result.Trim();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToHash(string str) =>
|
||||
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
|
||||
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Azaion.Common.DTO;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public interface IResourceLoader
|
||||
{
|
||||
Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default);
|
||||
Assembly? LoadAssembly(string asmName);
|
||||
}
|
||||
|
||||
public class ResourceLoader(AzaionApiClient api, ApiCredentials credentials) : IResourceLoader
|
||||
{
|
||||
private static readonly List<string> EncryptedResources =
|
||||
[
|
||||
"Azaion.Annotator",
|
||||
"Azaion.Dataset"
|
||||
];
|
||||
|
||||
public Assembly? LoadAssembly(string resourceName)
|
||||
{
|
||||
var assemblyName = resourceName.Split(',').First();
|
||||
if (EncryptedResources.Contains(assemblyName))
|
||||
{
|
||||
try
|
||||
{
|
||||
var stream = Load($"{assemblyName}.dll").GetAwaiter().GetResult();
|
||||
return Assembly.Load(stream.ToArray());
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
var currentLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
|
||||
var dllPath = Path.Combine(currentLocation, "dummy", $"{assemblyName}.dll");
|
||||
return Assembly.LoadFile(dllPath);
|
||||
}
|
||||
}
|
||||
|
||||
var loadedAssembly = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.FirstOrDefault(a => a.GetName().Name == assemblyName);
|
||||
|
||||
return loadedAssembly;
|
||||
}
|
||||
|
||||
public async Task<MemoryStream> Load(string fileName, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var hardwareService = new HardwareService();
|
||||
var hardwareInfo = hardwareService.GetHardware();
|
||||
|
||||
var encryptedStream = Task.Run(() => api.GetResource(fileName, credentials.Password, hardwareInfo), cancellationToken).Result;
|
||||
|
||||
var key = Security.MakeEncryptionKey(credentials.Email, credentials.Password, hardwareInfo.Hash);
|
||||
var stream = new MemoryStream();
|
||||
await encryptedStream.DecryptTo(stream, key, cancellationToken);
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
|
||||
namespace Azaion.Common.Services;
|
||||
|
||||
public static class Security
|
||||
{
|
||||
private const int BUFFER_SIZE = 524288; // 512 KB buffer size
|
||||
|
||||
public static string ToHash(this string str) =>
|
||||
Convert.ToBase64String(SHA384.HashData(Encoding.UTF8.GetBytes(str)));
|
||||
|
||||
public static string MakeEncryptionKey(string email, string password, string? hardwareHash) =>
|
||||
$"{email}-{password}-{hardwareHash}-#%@AzaionKey@%#---".ToHash();
|
||||
|
||||
public static SecureString ToSecureString(this string str)
|
||||
{
|
||||
var secureString = new SecureString();
|
||||
foreach (var c in str.ToCharArray())
|
||||
secureString.AppendChar(c);
|
||||
|
||||
return secureString;
|
||||
}
|
||||
|
||||
public static string? ToRealString(this SecureString value)
|
||||
{
|
||||
var valuePtr = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value);
|
||||
return Marshal.PtrToStringUni(valuePtr);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.ZeroFreeGlobalAllocUnicode(valuePtr);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async Task EncryptTo(this Stream stream, Stream toStream, string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (stream is { CanRead: false }) throw new ArgumentNullException(nameof(stream));
|
||||
if (key is not { Length: > 0 }) throw new ArgumentNullException(nameof(key));
|
||||
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
aes.GenerateIV();
|
||||
|
||||
using var encryptor = aes.CreateEncryptor(aes.Key, aes.IV);
|
||||
await using var cs = new CryptoStream(toStream, encryptor, CryptoStreamMode.Write, leaveOpen: true);
|
||||
|
||||
// Prepend IV to the encrypted data
|
||||
await toStream.WriteAsync(aes.IV.AsMemory(0, aes.IV.Length), cancellationToken);
|
||||
|
||||
var buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await stream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
await cs.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
||||
}
|
||||
|
||||
public static async Task DecryptTo(this Stream encryptedStream, Stream toStream, string key, CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var aes = Aes.Create();
|
||||
aes.Key = SHA256.HashData(Encoding.UTF8.GetBytes(key));
|
||||
|
||||
// Read the IV from the start of the input stream
|
||||
var iv = new byte[aes.BlockSize / 8];
|
||||
_ = await encryptedStream.ReadAsync(iv, cancellationToken);
|
||||
aes.IV = iv;
|
||||
|
||||
using var decryptor = aes.CreateDecryptor(aes.Key, aes.IV);
|
||||
await using var cryptoStream = new CryptoStream(encryptedStream, decryptor, CryptoStreamMode.Read, leaveOpen: true);
|
||||
|
||||
// Read and write in chunks
|
||||
var buffer = new byte[BUFFER_SIZE];
|
||||
int bytesRead;
|
||||
while ((bytesRead = await cryptoStream.ReadAsync(buffer, cancellationToken)) > 0)
|
||||
await toStream.WriteAsync(buffer.AsMemory(0, bytesRead), cancellationToken);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user