mirror of https://github.com/Squidex/squidex.git
14 changed files with 384 additions and 104 deletions
@ -0,0 +1,29 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.ComponentModel.DataAnnotations; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules; |
|||
|
|||
namespace Squidex.Extensions.Actions.DeepDetect; |
|||
|
|||
[RuleAction( |
|||
Title = "DeepDetect", |
|||
IconImage = "<svg viewBox='0 0 28 28' xmlns='http://www.w3.org/2000/svg'><g style='stroke-width:1.24962' fill='none'><path fill='#ff5252' d='M13 21.92H0v-8.032h9.386V10.92h3.57v11zm-9.386-4.889v1.702H9.43v-1.702z' style='stroke-width:1.24962' transform='matrix(.78667 0 0 .81405 2.529 2.668)'/><path fill='#fff' d='M29.164 21.92h-13V14.028H25.7V5.92h3.464zm-9.536-4.804v1.673H25.7v-1.673z' style='stroke-width:1.24962' transform='matrix(.78667 0 0 .81405 2.529 2.668)'/></g></svg>", |
|||
IconColor = "#526a75", |
|||
Display = "Annotate image", |
|||
Description = "Annotate an image using deep detect.")] |
|||
public sealed record DeepDetectAction : RuleAction |
|||
{ |
|||
[Display(Name = "Min Probability", Description = "The minimum probability for objects to be recognized (0 - 100).")] |
|||
[Editor(RuleFieldEditor.Number)] |
|||
public long MinimumProbability { get; set; } |
|||
|
|||
[Display(Name = "Max Tags", Description = "The maximum number of tags to use.")] |
|||
[Editor(RuleFieldEditor.Number)] |
|||
public long MaximumTags { get; set; } |
|||
} |
|||
@ -0,0 +1,197 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Net.Http.Json; |
|||
using System.Text.RegularExpressions; |
|||
using Squidex.Domain.Apps.Core; |
|||
using Squidex.Domain.Apps.Core.Assets; |
|||
using Squidex.Domain.Apps.Core.HandleRules; |
|||
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents; |
|||
using Squidex.Domain.Apps.Entities; |
|||
using Squidex.Domain.Apps.Entities.Assets; |
|||
using Squidex.Domain.Apps.Entities.Assets.Commands; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Json; |
|||
using Squidex.Text; |
|||
|
|||
namespace Squidex.Extensions.Actions.DeepDetect; |
|||
|
|||
#pragma warning disable MA0048 // File name must match type name
|
|||
|
|||
internal partial class DeepDetectActionHandler : RuleActionHandler<DeepDetectAction, DeepDetectJob> |
|||
{ |
|||
private const string Description = "Analyze Image"; |
|||
private readonly IHttpClientFactory httpClientFactory; |
|||
private readonly IJsonSerializer jsonSerializer; |
|||
private readonly IAppProvider appProvider; |
|||
private readonly IAssetQueryService assetQuery; |
|||
private readonly ICommandBus commandBus; |
|||
private readonly IUrlGenerator urlGenerator; |
|||
|
|||
public DeepDetectActionHandler(RuleEventFormatter formatter, IHttpClientFactory httpClientFactory, |
|||
IJsonSerializer jsonSerializer, |
|||
IAppProvider appProvider, |
|||
IAssetQueryService assetQuery, |
|||
ICommandBus commandBus, |
|||
IUrlGenerator urlGenerator) |
|||
: base(formatter) |
|||
{ |
|||
this.httpClientFactory = httpClientFactory; |
|||
this.jsonSerializer = jsonSerializer; |
|||
this.appProvider = appProvider; |
|||
this.assetQuery = assetQuery; |
|||
this.commandBus = commandBus; |
|||
this.urlGenerator = urlGenerator; |
|||
} |
|||
|
|||
protected override Task<(string Description, DeepDetectJob Data)> CreateJobAsync(EnrichedEvent @event, DeepDetectAction action) |
|||
{ |
|||
if (@event is not EnrichedAssetEvent assetEvent) |
|||
{ |
|||
return Task.FromResult(("Ignore", new DeepDetectJob())); |
|||
} |
|||
|
|||
if (assetEvent.AssetType != AssetType.Image) |
|||
{ |
|||
return Task.FromResult(("Ignore", new DeepDetectJob())); |
|||
} |
|||
|
|||
var ruleJob = new DeepDetectJob |
|||
{ |
|||
Actor = assetEvent.Actor, |
|||
AppId = assetEvent.AppId.Id, |
|||
AssetId = assetEvent.Id, |
|||
MaximumTags = action.MaximumTags, |
|||
MinimumPropability = action.MinimumProbability, |
|||
Url = urlGenerator.AssetContent(assetEvent.AppId, assetEvent.Id.ToString(), assetEvent.FileVersion) |
|||
}; |
|||
|
|||
return Task.FromResult((Description, ruleJob)); |
|||
} |
|||
|
|||
protected override async Task<Result> ExecuteJobAsync(DeepDetectJob job, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(job.Url)) |
|||
{ |
|||
return Result.Ignored(); |
|||
} |
|||
|
|||
var httpClient = httpClientFactory.CreateClient("DeepDetect"); |
|||
|
|||
var response = await httpClient.PostAsJsonAsync("predict", new |
|||
{ |
|||
service = "squidexdetector", |
|||
output = new |
|||
{ |
|||
best = job.MaximumTags, |
|||
confidence_threshold = job.MinimumPropability / 100d, |
|||
}, |
|||
data = new[] |
|||
{ |
|||
job.Url, |
|||
} |
|||
}, ct); |
|||
|
|||
var body = await response.Content.ReadAsStringAsync(ct); |
|||
|
|||
if (!response.IsSuccessStatusCode) |
|||
{ |
|||
return Result.Failed(new InvalidOperationException($"Failed with status code {response.StatusCode}\n\n{body}")); |
|||
} |
|||
|
|||
var responseJson = jsonSerializer.Deserialize<DetectResponse>(body); |
|||
|
|||
var tags = responseJson!.Body.Predictions.SelectMany(x => x.Classes); |
|||
|
|||
if (!tags.Any()) |
|||
{ |
|||
return Result.Success(body); |
|||
} |
|||
|
|||
var app = await appProvider.GetAppAsync(job.AppId, true, ct); |
|||
if (app == null) |
|||
{ |
|||
return Result.Failed(new InvalidOperationException("App not found.")); |
|||
} |
|||
|
|||
var context = Context.Admin(app); |
|||
|
|||
var asset = await assetQuery.FindAsync(context, job.AssetId, ct: ct); |
|||
if (asset == null) |
|||
{ |
|||
return Result.Failed(new InvalidOperationException("Asset not found.")); |
|||
} |
|||
|
|||
var command = new AnnotateAsset |
|||
{ |
|||
Tags = asset.TagNames, |
|||
AssetId = asset.AssetId, |
|||
AppId = asset.AppId, |
|||
Actor = job.Actor, |
|||
FromRule = true |
|||
}; |
|||
|
|||
foreach (var tag in tags) |
|||
{ |
|||
var tagParts = tag.Cat.Split(',')[0].Split(' ', StringSplitOptions.RemoveEmptyEntries); |
|||
|
|||
if (IdRegex().IsMatch(tagParts[0])) |
|||
{ |
|||
tagParts = tagParts.Skip(1).ToArray(); |
|||
} |
|||
|
|||
var tagName = string.Join('_', tagParts.Select(x => x.Slugify())); |
|||
|
|||
command.Tags.Add($"ai/{tagName}"); |
|||
} |
|||
|
|||
await commandBus.PublishAsync(command, ct); |
|||
return Result.Success(body); |
|||
} |
|||
|
|||
private sealed class DetectResponse |
|||
{ |
|||
public DetectBody Body { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectBody |
|||
{ |
|||
public DetectPredications[] Predictions { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectPredications |
|||
{ |
|||
public DetectClass[] Classes { get; set; } |
|||
} |
|||
|
|||
private sealed class DetectClass |
|||
{ |
|||
public double Prob { get; set; } |
|||
|
|||
public string Cat { get; set; } |
|||
} |
|||
|
|||
[GeneratedRegex("^n[0-9]+$")]
|
|||
private static partial Regex IdRegex(); |
|||
} |
|||
|
|||
public sealed class DeepDetectJob |
|||
{ |
|||
public DomainId AppId { get; set; } |
|||
|
|||
public DomainId AssetId { get; set; } |
|||
|
|||
public RefToken Actor { get; set; } |
|||
|
|||
public long MaximumTags { get; set; } |
|||
|
|||
public long MinimumPropability { get; set; } |
|||
|
|||
public string? Url { get; set; } |
|||
} |
|||
@ -0,0 +1,32 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Configuration; |
|||
using Microsoft.Extensions.DependencyInjection; |
|||
using Squidex.Infrastructure.Plugins; |
|||
|
|||
namespace Squidex.Extensions.Actions.DeepDetect; |
|||
|
|||
internal class DeepDetectPlugin : IPlugin |
|||
{ |
|||
public void ConfigureServices(IServiceCollection services, IConfiguration config) |
|||
{ |
|||
var url = config.GetValue<string>("deepdetect:url"); |
|||
|
|||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
services.AddHttpClient("DeepDetect", client => |
|||
{ |
|||
client.BaseAddress = uri; |
|||
}); |
|||
|
|||
services.AddRuleAction<DeepDetectAction, DeepDetectActionHandler>(); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue